├── .editorconfig ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE.md ├── docs ├── README.md └── images │ ├── application.png │ ├── identity.png │ ├── route.png │ └── webmap-browser.png ├── images ├── Register1.png ├── Register2.png └── maps-app.gif ├── intern.json ├── package.json ├── src ├── Application.ts ├── assets │ ├── favicon.ico │ └── icon.png ├── config.ts ├── index.ejs ├── index.ts ├── mapactions │ ├── .gitkeep │ ├── MapAction.ts │ ├── navRotation.ts │ └── reverseGeocode.ts ├── oauth-callback.html ├── styles │ ├── .gitkeep │ ├── index.scss │ └── main.scss ├── utils │ ├── .gitkeep │ └── dateUtils.ts ├── widgets │ ├── .gitkeep │ ├── Alert.tsx │ ├── Authenticate.tsx │ ├── Authenticate │ │ ├── .gitkeep │ │ ├── AuthenticateViewModel.ts │ │ ├── components │ │ │ ├── .gitkeep │ │ │ └── AuthComponents.tsx │ │ ├── nls │ │ │ ├── .gitkeep │ │ │ └── Authenticate.js │ │ └── styles │ │ │ ├── .gitkeep │ │ │ └── Authenticate.scss │ ├── UserNav.tsx │ ├── UserNav │ │ ├── .gitkeep │ │ ├── components │ │ │ ├── .gitkeep │ │ │ └── UserMenu.tsx │ │ └── nls │ │ │ ├── .gitkeep │ │ │ └── UserMenu.js │ ├── WebMapBrowser.tsx │ └── WebMapBrowser │ │ ├── .gitkeep │ │ ├── WebMapBrowserViewModel.ts │ │ ├── components │ │ └── PortalItem.tsx │ │ └── styles │ │ ├── .gitkeep │ │ └── WebMapBrowser.scss └── worker-config.ts ├── tests ├── tsconfig.json └── unit │ ├── Application.ts │ ├── all.ts │ ├── mapactions │ ├── .gitkeep │ ├── navRotation.ts │ └── reverseGeocode.ts │ ├── utils │ ├── .gitkeep │ └── dateUtils.ts │ └── widgets │ ├── .gitkeep │ ├── Alert.ts │ ├── Authenticate │ ├── AuthenticateViewModel.ts │ └── components │ │ ├── .gitkeep │ │ └── AuthComponents.ts │ ├── UserNav │ ├── .gitkeep │ └── components │ │ ├── .gitkeep │ │ └── UserMenu.ts │ ├── WebMapBrowser.ts │ └── WebMapBrowser │ ├── .gitkeep │ ├── WebMapBrowserViewModel.ts │ └── components │ └── PortalItem.ts ├── tsconfig.json ├── tslint.json ├── typings └── extensions.d.ts ├── webpack.config.js └── webpack.tests.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [{src,scripts}/**.{ts,tsx,json,js}] 4 | end_of_line = crlf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 2 -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Expected Behavior 7 | 8 | 9 | ## Actual Behavior 10 | 11 | 12 | ## Possible Fix 13 | 14 | 15 | ## Steps to Reproduce 16 | 17 | 18 | 1. 19 | 2. 20 | 3. 21 | 4. 22 | 23 | ## Context 24 | 25 | 26 | ## Your Environment 27 | 28 | * Version used: 29 | * Browser Name and version: 30 | * Operating System and version (desktop or mobile): 31 | * Link to your project if available: 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | ## How Has This Been Tested? 16 | 17 | 18 | 19 | 20 | ## Screenshots (if appropriate): 21 | 22 | ## Types of changes 23 | 24 | - [ ] Bug fix (non-breaking change which fixes an issue) 25 | - [ ] New feature (non-breaking change which adds functionality) 26 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 27 | 28 | ## Checklist: 29 | 30 | 31 | - [ ] My code follows the code style of this project. 32 | - [ ] My change requires a change to the documentation. 33 | - [ ] I have updated the documentation accordingly. 34 | - [ ] I have read the **CONTRIBUTING** document. 35 | - [ ] I have added tests to cover my changes. 36 | - [ ] All new and existing tests passed. 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | .vscode/* 3 | build/* 4 | dist/* 5 | html-report/* 6 | ~tmp/* 7 | .baseDir* 8 | .tsdrc 9 | .tscache 10 | npm-debug.log 11 | yarn.lock 12 | package-lock.json 13 | coverage-final.* 14 | .firebaserc 15 | firebase.json 16 | firebase-debug.log -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to maps-app-javascript 2 | ================================= 3 | 4 | Esri welcomes contributions from anyone and everyone. Please see our [guidelines for contributing](https://github.com/esri/contributing). 5 | 6 | 1. [Getting Involved](#getting-involved) 7 | 2. [Reporting Bugs](#reporting-bugs) 8 | 3. [Contributing Code](#contributing-code) 9 | 10 | ## Getting Involved 11 | 12 | Third-party patches are absolutely essential on our quest to create the best maps app with the ArcGIS API for JavaScript. 13 | However, they're not the only way to get involved with the development of maps-app-javascript. 14 | You can help the project tremendously by discovering and [reporting bugs](#reporting-bugs), 15 | [improving documentation](#improving-documentation), 16 | helping others with [GitHub issues](https://github.com/Esri/maps-app-javascript/issues), 17 | tweeting to [@ArcGISJSAPI](https://twitter.com/ArcGISJSAPI), 18 | and spreading the word about mapps-app-javascript and the [ArcGIS API for JavaScript](https://developers.arcgis.com/javascript/) among your colleagues and friends. 19 | 20 | ## Reporting Bugs 21 | 22 | Before reporting a bug on the project's [issues page](https://github.com/Esri/maps-app-javascript/issues), 23 | first make sure that your issue is caused by maps-app-javascript, not your application code. 24 | Second, search through the reported issues for your issue, 25 | and if it's already reported, just add any additional details in the comments. 26 | 27 | Also, please only report issues related to the maps-app-javascript. 28 | If your issue is related to the ArcGIS API for JavaScript, please contact [Esri Tech Support](https://support.esri.com/contact-tech-support) or ask the Esri community on [GeoNet](https://geonet.esri.com/community/developers/web-developers/arcgis-api-for-javascript). 29 | 30 | After you made sure that you've found a new maps-app-javascript bug, 31 | please use the provided issue template when creating your issue. 32 | 33 | ## Contributing Code 34 | 35 | ### Setting up your dev environment 36 | Please read the instructions provided in the [readme](https://github.com/Esri/maps-app-javascript/blob/master/README.md) to set up your development environment. 37 | 38 | #### Fork the repo 39 | If you haven't already, go to https://github.com/Esri/maps-app-javascript and click the [Fork](https://github.com/Esri/maps-app-javascript/fork) button. 40 | 41 | #### Clone the repo 42 | Clone the repo and run `npm install`. 43 | 44 | * _NOTE FOR WINDOWS USERS_ - You will need to install the [Windows-Build-Tools](https://github.com/felixrieseberg/windows-build-tools) to compile npm modules for this project. `npm install --global --production windows-build-tools` 45 | 46 | * `npm start` - compile application and run it in a local server at `http://localhost:8080/`. 47 | * `npm run build` - compile application for deployment. 48 | * `npm test` - run unit tests with local chrome driver. 49 | * `npm run serve` - Run a production build of the application, but serve it up locally to see how the built app will behave. 50 | 51 | Use `npm run serve` to full test that Service Workers are working correctly with `webpack-dev-server` self signed certifcates. Refer to [this article](https://deanhume.com/testing-service-workers-locally-with-self-signed-certificates/) on how to run Chrome with proper flags enabled for development purposes. 52 | 53 | #### Configure remotes 54 | Move into the directory the cloning process just created (should be maps-app-javascript), then make sure your local git knows about all the remotes and branches. 55 | ``` 56 | $ cd maps-app-javascript 57 | # Changes the active directory in the prompt to the newly cloned "maps-app-javascript" directory 58 | $ git remote add upstream https://github.com/Esri/maps-app-javascript.git 59 | # Assigns the original repository to a remote called "upstream" 60 | $ git fetch upstream 61 | # Pulls in changes not present in your local repository, without modifying your files 62 | ``` 63 | 64 | * Ask the Esri community in the Example Apps Group on [GeoNet](https://community.esri.com/groups/arcgis-example-apps). 65 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Esri 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 2 | 3 | # Maps App JavaScipt 4 | 5 | This repo provides an example app called [Maps App](https://developers.arcgis.com/example-apps/maps-app-javascript/?utm_source=github&utm_medium=web&utm_campaign=example_apps_maps_app_javascript) that can be used as as starter for your organizations mapping app built with [ArcGIS API 4 for JavaScript](https://developers.arcgis.com/javascript/). You can use the Maps App as is, or extend it using the ArcGIS API for JavaScript. 6 | 7 | 8 | 9 | - [Features](#features) 10 | - [Detailed Documentation](#detailed-documentation) 11 | - [Usage](#usage) 12 | - [Demo](#demo) 13 | - [Issues](#issues) 14 | - [Contributing](#contributing) 15 | - [MDTOC](#mdtoc) 16 | - [Licensing](#licensing) 17 | 18 | 19 | --- 20 | 21 | ## Features 22 | 23 | * Dynamically switch basemaps 24 | * Place search 25 | * Directions 26 | * Sign into an ArcGIS account 27 | * Service Worker 28 | * AppCache 29 | * `manifest.json` - to add as button to home screen 30 | * default icons 31 | 32 | This application takes advantage of numerous technologies for development purposes. It utlizes [webpack](https://webpack.js.org/) to compile and bundle the application code and other files. It is written in [TypeScript](http://www.typescriptlang.org/) and provides examples on how to create [custom widgets](https://developers.arcgis.com/javascript/latest/guide/custom-widget/index.html) using the [ArcGIS API 4 for JavaScript](https://developers.arcgis.com/javascript/). 33 | 34 | This application also uses [Workbox for Webpack](https://developers.google.com/web/tools/workbox/get-started/webpack) to set up [service workers](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API) for the application to cache application code and files, as well as uses an [appcache fallback](https://developer.mozilla.org/en-US/docs/Web/HTML/Using_the_application_cache) for Internet Explorer, Edge, and Safari. 35 | 36 | [Intern](https://theintern.io/) is used for all unit tests and code coverage. 37 | 38 | Feel free to use this project as a starting point for your own applications! 39 | 40 | ## Detailed Documentation 41 | 42 | Read the [docs](./docs/README.md) for a detailed explanation of the application, including its architecture and how it leverages the ArcGIS platform, as well as how you can begin using the app right away. 43 | 44 | ## Usage 45 | 46 | Clone the repo and run `npm install`. 47 | 48 | * _NOTE FOR WINDOWS USERS_ - You will need to install the [Windows-Build-Tools](https://github.com/felixrieseberg/windows-build-tools) to compile npm modules for this project. `npm install --global --production windows-build-tools` 49 | 50 | * `npm start` - compile application and run it in a local server at `http://localhost:8080/`. 51 | * `npm run build` - compile application for deployment. 52 | * `npm test` - run unit tests with local chrome driver. 53 | * `npm run serve` - Run a production build of the application, but serve it up locally to see how the built app will behave. 54 | 55 | Use `npm run serve` to full test that Service Workers are working correctly with `webpack-dev-server` self signed certifcates. Refer to [this article](https://deanhume.com/testing-service-workers-locally-with-self-signed-certificates/) on how to run Chrome with proper flags enabled for development purposes. 56 | 57 | * Login to [ArcGIS for Developers](https://developers.arcgis.com/) and [register](https://developers.arcgis.com/applications/#/) your app to create an Client ID. 58 | 59 | * You will need to register your application with a Client ID so that you can take advantage of the premium [Directions and Routing](https://developers.arcgis.com/features/directions/) and [Geocoding](https://developers.arcgis.com/features/geocoding/) Services from the ArcGIS Platform. 60 | 61 | ![](images/Register1.png) 62 | * Once you've registered your version of the maps-app, grab a copy of the client id from the registration and set the client id in the applications `src/app/config.ts` file. You will also want to provide the Portal URL for your Organization, such as `"https://.maps.arcgis.com"`. You can also provide your own WebMap or use the default one provided. 63 | 64 | ```js 65 | // src/app/config.ts 66 | /** 67 | * Registered application id. 68 | * This is needed to be able to use premium 69 | * services such as routing and directions. 70 | */ 71 | export const appId = ""; 72 | 73 | /** 74 | * Users Portal URL. 75 | */ 76 | export const portalUrl = "https://arcgis.com"; // default Portal URL 77 | 78 | /** 79 | * WebMap id to use for this application. 80 | * You can update this WebMap id with your own. 81 | */ 82 | export const webMapItem = { 83 | portalItem: { 84 | // shared WebMap with Vector Tile basemap 85 | id: "1aab2defd7444b6790f439a186cd4a23" 86 | } 87 | }; 88 | ``` 89 | 90 | * As part of the registration process, add a redirect uri for your app. Navigate to the Redirect URIs section at the bottom of the registration page and set the redirect uri as shown for development purposes. You will also want to add a redirect uri for where your application will be deployed. This redirect uri is the default redirect for `https://www.arcgis.com`. 91 | 92 | For development purposes, you will want to add the following redirects to your Application ID: 93 | 94 | * `http://127.0.0.1:8080` 95 | 96 | When you deploy your application, do not use the same Application ID for development as production. You want your Application ID to _only redirect to your production website_. 97 | 98 | ![](images/Register2.png) 99 | 100 | ## Demo 101 | 102 | ![application](images/maps-app.gif) 103 | 104 | ## Issues 105 | 106 | Find a bug or want to request a new feature enhancement? Let us know by submitting an issue. 107 | 108 | ## Contributing 109 | 110 | Anyone and everyone is welcome to [contribute](CONTRIBUTING.md). We do accept pull requests. 111 | 112 | 1. Get involved 113 | 2. Report issues 114 | 3. Contribute code 115 | 4. Improve documentation 116 | 117 | ## MDTOC 118 | 119 | Generating table of contents for documents in this repository was performed using the [MDTOC package for Atom](https://atom.io/packages/atom-mdtoc). 120 | 121 | ## Licensing 122 | 123 | Copyright 2018 Esri 124 | 125 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 126 | 127 | http://www.apache.org/licenses/LICENSE-2.0 128 | 129 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 130 | 131 | A copy of the license is available in the repository's [LICENSE](./LICENSE) file 132 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release 1.0.2 2 | 3 | - Adds doc table of contents to root README.md and docs/index.md 4 | - Renames docs/index.md to [docs/README.md](/docs/README.md) 5 | 6 | # Release 1.0.1 7 | 8 | - Adds [app documentation](./docs/README.md) from the ArcGIS for Developers site. 9 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Get your organization's authoritative map data into the hands of your workers with this ArcGIS API for JavaScript app. The application you build can include a custom web map from your [ArcGIS Online organization](https://doc.arcgis.com/en/arcgis-online/reference/what-is-agol.htm). For example, a [web map](http://doc.arcgis.com/en/living-atlas/item/?itemId=26888b0c21a44eb1ba2f26d1eb7981fe) from the Living Atlas can be used as a starting place for your app. The maps-app also includes examples of place search and routing capabilities using either ArcGIS Online's powerful services or your own services. It also leverages your organizations configured basemaps to allow users to switch between the basemap that make sense for them. 4 | 5 | This example application is open source so grab the code and either configure the app for your organization, or just learn how to integrate similar capabilities into your own app! 6 | 7 | 8 | 9 | - [Functionality showcased](#functionality-showcased) 10 | - [Using web maps](#using-web-maps) 11 | - [Accessing your organization's basemaps](#accessing-your-organizations-basemaps) 12 | - [Identity](#identity) 13 | - [Place search](#place-search) 14 | - [Reverse geocoding](#reverse-geocoding) 15 | - [Route](#route) 16 | 17 | 18 | --- 19 | 20 | ## Functionality showcased 21 | 22 | - Switching basemaps 23 | - Loading web maps from an organization 24 | - Searching for an address or place (geocoding) 25 | - Searching for a location on the map (reverse geocoding) 26 | - Routing and turn by turn directions 27 | - Authentication with OAuth2 28 | 29 | ![Application](./images/application.png) 30 | 31 | ## Using web maps 32 | 33 | You can author your own web maps from ArcGIS Online or ArcGIS Pro and share them in your app via your ArcGIS Online organization. This is the central power of the Web GIS model built into ArcGIS. Building an app which uses a web map allows the cartography and map configuration to be completed in ArcGIS Online rather than in code. This then allows the map to change over time, without any code changes or app updates. Learn more about the benefits of developing with web maps [here](https://developers.arcgis.com/web-map-specification/). Also, learn about authoring web maps in [ArcGIS Online](http://doc.arcgis.com/en/arcgis-online/create-maps/make-your-first-map.htm) and [ArcGIS Pro](http://pro.arcgis.com/en/pro-app/help/mapping/map-authoring/author-a-basemap.htm). 34 | 35 | Loading web maps in the code is easy; the maps app loads a web map from a portal (which may require the user to sign in, see the identity section below) with the following code: 36 | 37 | ```ts 38 | // config.ts 39 | export const webMapItem = { 40 | portalItem: { 41 | // shared WebMap 42 | id: "3ff64504498c4e9581a7a754412b6a9e" 43 | } 44 | }; 45 | 46 | // Application.ts 47 | @subclass("app.widgets.Application") 48 | class Application extends declared(Accessor) { 49 | @property({ readOnly: true }) 50 | webmap = new WebMap(webMapItem); 51 | ... 52 | } 53 | ``` 54 | 55 | ![Webmap Browser](./images/webmap-browser.png) 56 | 57 | ## Accessing your organization's basemaps 58 | 59 | As an administrator of an ArcGIS Online organization or Portal you can configure the basemaps that your users can switch between via a [group](http://doc.arcgis.com/en/arcgis-online/share-maps/share-items.htm). Applications can leverage this configuration using the [Portal API](https://developers.arcgis.com/javascript/latest/api-reference/esri-portal-Portal.html). The Maps App uses the [BasemapGallery](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-BasemapGallery.html) widget from the ArcGIS API 4 for JavaScript. 60 | 61 | All you need to do is instantiate it, then when a user signs in, it will switch to the basemaps configured to the user’s organization. 62 | 63 | ```ts 64 | const basemapGallery = new BasemapGallery({ 65 | container: element(), 66 | view 67 | }); 68 | ``` 69 | 70 | ## Identity 71 | 72 | The Maps App leverages the ArcGIS [identity](https://developers.arcgis.com/authentication/) model to provide access to resources via the [named user](https://developers.arcgis.com/authentication/#named-user-login) login pattern. 73 | 74 | To access the [Directions](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Directions.html) widget, you must sign in via the provided Authenticate widget. Once signed in, the Directions widget is available for use as well as the custom WebMapBrowser widget. 75 | 76 | The Directions Widget provides a way to build driving and walking directions using ArcGIS online and custom Network Analysis Route services. By default, it uses the [Esri World Route Service](http://www.arcgis.com/home/item.html?id=1feb41652c5c4bd2ba5c60df2b4ea2c4). 77 | 78 | The WebMapBrowser widget is a custom widget in the Maps App that is used to browse any web maps that are part of your content in your organization. The WebMapBrowser widget allows you to switch the web map used in your application. 79 | 80 | ![Identity](./images/identity.png) 81 | 82 | All you need to do is provide a valid Client ID in the configuration file, along with a Portal URL if it differs from the default, and it will be used by the Authentication workflow of the application. 83 | 84 | ```ts 85 | // src/app/config.ts 86 | /** 87 | * Registered application id. 88 | * This is needed to be able to use premium 89 | * services such as routing and directions. 90 | */ 91 | export const appId = ""; 92 | 93 | /** 94 | * Users Portal URL. 95 | */ 96 | export const portalUrl = "https://arcgis.com"; // default Portal URL 97 | ``` 98 | 99 | ## Place search 100 | 101 | The [Search](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Search.html) widget is utilized to let you transform an address or a place name to a specific geographic location. The reverse lets you use a geographic location to find a description of the location, like a postal address or place name. The Search widget performs geocoding and reverse geocoding functions provided by [Esri's World Geocoding Service](https://developers.arcgis.com/features/geocoding/). 102 | 103 | You can customize the [sources](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Search.html#sources) used by the Search widget to use your own custom Geocode Service or a Feature Service as a source. 104 | 105 | [Suggestions](https://developers.arcgis.com/rest/geocode/api-reference/geocoding-suggest.htm) are supported out-of-the-box with the [Search widget](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Search.html#suggestions). You can customize the [min](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Search.html#minSuggestCharacters) and [max](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Search.html#maxSuggestions) suggestions returned if not defined by custom sources you provide. 106 | 107 | ## Reverse geocoding 108 | 109 | The Map App uses the [`"hold"`](https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html#event:hold) event of the [MapView](https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html) and sends the selected location to the [search](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Search.html#search) method of the of the Search widget to reverse geocode the selected location.. 110 | 111 | ```ts 112 | // src/app/mapactions/reverseGeocode.ts 113 | 114 | @subclass("app.mapactions.ReverseGeocode") 115 | export class ReverseGeocode extends declared(MapAction) { 116 | @property() search: Search; 117 | 118 | constructor(params?: ReverseGeocodeOptions) { 119 | super(params); 120 | this.reverseGeocode = this.reverseGeocode.bind(this); 121 | if (this.view) { 122 | this.addListeners(); 123 | } 124 | this.watch("view", () => { 125 | this.addListeners(); 126 | }); 127 | } 128 | 129 | addListeners() { 130 | const handler = this.view.on("hold", this.reverseGeocode); 131 | this.handlers.push(handler); 132 | } 133 | 134 | async reverseGeocode({ mapPoint }: GeocodeOptions) { 135 | const response = await this.search.search(mapPoint); 136 | this.value = response; 137 | } 138 | } 139 | 140 | ``` 141 | 142 | ## Route 143 | 144 | Getting navigation [directions](https://developers.arcgis.com/features/directions/) in the maps-app is just as easy in the [ArcGIS API for JavaScript](https://developers.arcgis.com/javascript/latest/index.html) as it is on [ArcGIS Online](http://doc.arcgis.com/en/arcgis-online/use-maps/get-directions.htm). You can [customize](http://doc.arcgis.com/en/arcgis-online/administer/configure-services.htm#ESRI_SECTION1_567C344D5DEE444988CA2FE5193F3CAD) your navigation services for your organization, add new travel modes that better reflect your organization’s workflows, or remove travel modes that are not suitable for your organization’s workflows. 145 | 146 | You can update the [default route service URL](https://developers.arcgis.com/javascript/latest/api-reference/esri-widgets-Directions.html#routeServiceUrl) in the Directions widget to that of your organization. 147 | 148 | ![Route](./images/route.png) 149 | 150 | For more samples on using the ArcGIS API for JavaScript, see [the documentation](https://developers.arcgis.com/javascript/latest/sample-code/index.html). 151 | -------------------------------------------------------------------------------- /docs/images/application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/docs/images/application.png -------------------------------------------------------------------------------- /docs/images/identity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/docs/images/identity.png -------------------------------------------------------------------------------- /docs/images/route.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/docs/images/route.png -------------------------------------------------------------------------------- /docs/images/webmap-browser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/docs/images/webmap-browser.png -------------------------------------------------------------------------------- /images/Register1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/images/Register1.png -------------------------------------------------------------------------------- /images/Register2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/images/Register2.png -------------------------------------------------------------------------------- /images/maps-app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/images/maps-app.gif -------------------------------------------------------------------------------- /intern.json: -------------------------------------------------------------------------------- 1 | { 2 | "environments": [ 3 | { 4 | "browserName": "chrome", 5 | "chromeOptions": { "args": [ "headless", "window-size=1024,768"] }, 6 | "fixSessionCapabilities": "no-detect" 7 | } 8 | ], 9 | "suites": "~tmp/tests.js" 10 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "maps-app-javascript", 3 | "version": "1.0.2", 4 | "devDependencies": { 5 | "@types/arcgis-js-api": "~4.11.0", 6 | "@types/sinon": "^7.0.11", 7 | "appcache-webpack-plugin": "^1.4.0", 8 | "clean-webpack-plugin": "^2.0.1", 9 | "copy-webpack-plugin": "5.0.2", 10 | "css-loader": "^2.1.1", 11 | "html-loader": "^0.5.5", 12 | "html-webpack-inline-source-plugin": "^0.0.10", 13 | "html-webpack-plugin": "^3.1.0", 14 | "husky": "^1.3.1", 15 | "intern": "^4.4.2", 16 | "lint-staged": "^8.1.5", 17 | "mini-css-extract-plugin": "^0.6.0", 18 | "node-sass": "4.11.0", 19 | "optimize-css-assets-webpack-plugin": "^5.0.1", 20 | "prettier": "^1.17.0", 21 | "resolve-url-loader": "^2.3.0", 22 | "sass-loader": "^7.1.0", 23 | "sinon": "^7.3.2", 24 | "style-loader": "0.23.1", 25 | "terser-webpack-plugin": "^1.2.2", 26 | "ts-loader": "^5.3.3", 27 | "tslint": "^5.16.0", 28 | "tslint-config-prettier": "^1.18.0", 29 | "tslint-eslint-rules": "^5.4.0", 30 | "tslint-plugin-prettier": "^2.0.1", 31 | "typescript": "^3.4.4", 32 | "webpack": "^4.30.0", 33 | "webpack-cli": "^3.3.0", 34 | "webpack-dev-server": "^3.3.1", 35 | "webpack-pwa-manifest": "^4.0.0", 36 | "workbox-webpack-plugin": "^4.3.0" 37 | }, 38 | "license": "Apache-2.0", 39 | "scripts": { 40 | "precommit": "lint-staged", 41 | "start": "webpack-dev-server --mode development --open", 42 | "build": "webpack --mode production", 43 | "serve": "webpack-dev-server --mode production --open --https --compress", 44 | "prettier": "prettier --write \"src/**/*.ts?(x)\"", 45 | "lint": "tslint --fix \"src/**/*.ts?(x)\"", 46 | "test": "webpack --config=webpack.tests.config.js --mode=development && intern" 47 | }, 48 | "lint-staged": { 49 | "*.{ts,tsx}": [ 50 | "prettier --write", 51 | "tslint --fix", 52 | "git add" 53 | ] 54 | }, 55 | "dependencies": { 56 | "@arcgis/webpack-plugin": "^4.11.0", 57 | "calcite-web": "github:Esri/calcite-web#v1.2.5", 58 | "tslib": "^1.9.2" 59 | }, 60 | "arcgis": { 61 | "type": "jsapi" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Application.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import Accessor from "esri/core/Accessor"; 15 | import Collection from "esri/core/Collection"; 16 | import { watch, whenTrue, whenTrueOnce } from "esri/core/watchUtils"; 17 | 18 | import Viewpoint from "esri/Viewpoint"; 19 | import MapView from "esri/views/MapView"; 20 | import WebMap from "esri/WebMap"; 21 | 22 | // Widgets 23 | import BasemapGallery from "esri/widgets/BasemapGallery"; 24 | import Compass from "esri/widgets/Compass"; 25 | import Directions from "esri/widgets/Directions"; 26 | import Expand from "esri/widgets/Expand"; 27 | import Home from "esri/widgets/Home"; 28 | import Locate from "esri/widgets/Locate"; 29 | import Search from "esri/widgets/Search"; 30 | import Track from "esri/widgets/Track"; 31 | 32 | import Alert from "./widgets/Alert"; 33 | import UserNav from "./widgets/UserNav"; 34 | import WebMapBrowser from "./widgets/WebMapBrowser"; 35 | 36 | import { 37 | declared, 38 | property, 39 | subclass 40 | } from "esri/core/accessorSupport/decorators"; 41 | 42 | import { viewOptions, webMapItem } from "./config"; 43 | 44 | import { applyNavRotationAction } from "./mapactions/navRotation"; 45 | import { applyReverseGeocodeAction } from "./mapactions/reverseGeocode"; 46 | 47 | const element = () => document.createElement("div"); 48 | 49 | export const empty = (el: Element) => (el.innerHTML = ""); 50 | 51 | export const locateOnStart = async (view: MapView, locate: Locate) => { 52 | await view.when(); 53 | locate.goToLocationEnabled = false; 54 | await locate.locate(); 55 | const vp = new Viewpoint({ 56 | scale: viewOptions.scale, 57 | targetGeometry: locate.graphic.geometry 58 | }); 59 | await view.goTo(vp); 60 | locate.goToLocationEnabled = true; 61 | }; 62 | 63 | export const collapseAll = (widgets: Collection) => () => { 64 | widgets.forEach(w => w.collapse()); 65 | }; 66 | 67 | @subclass("app.widgets.Application") 68 | class Application extends declared(Accessor) { 69 | @property({ readOnly: true }) 70 | webmap = new WebMap(webMapItem); 71 | 72 | @property() signedIn = false; 73 | 74 | @property() view: MapView; 75 | 76 | async init() { 77 | // We are going to bind some widgets to pre-existing DOM elements 78 | const navNode: HTMLElement = 79 | document.querySelector("user-nav") || element(); 80 | const alertNode: HTMLElement = document.querySelector("alert") || element(); 81 | 82 | let viewNode = document.querySelector("webmap") as HTMLDivElement; 83 | if (viewNode) { 84 | empty(viewNode); 85 | } else { 86 | viewNode = element(); 87 | } 88 | 89 | const userNav = new UserNav({ 90 | container: navNode 91 | }); 92 | 93 | // sync the signed in status of the UserNav with the application 94 | watch(userNav, "signedIn", signedIn => (this.signedIn = signedIn)); 95 | 96 | this.view = new MapView({ map: this.webmap, container: viewNode }); 97 | const view = this.view; 98 | 99 | userNav.view = view; 100 | 101 | // Alert is used to notify users to login if they want to use 102 | // directions widget 103 | const alertMessage = new Alert({ 104 | container: alertNode, 105 | message: "Please Sign In to use Directions", 106 | color: "red", 107 | isFull: true 108 | }); 109 | 110 | /** 111 | * These widgets are going to be added to 112 | * Expand widgets, so they need a container 113 | * element when initialized. 114 | */ 115 | 116 | const expandWidgets = new Collection(); 117 | 118 | const search = new Search({ view }); 119 | 120 | const basemapGallery = new BasemapGallery({ view }); 121 | 122 | const directionsExpand = new Expand({ 123 | view, 124 | expandIconClass: "esri-icon-directions", 125 | group: "right" 126 | }); 127 | 128 | const basemapExpand = new Expand({ 129 | view, 130 | content: basemapGallery, 131 | expandIconClass: "esri-icon-basemap", 132 | group: "right" 133 | }); 134 | 135 | expandWidgets.addMany([directionsExpand, basemapExpand]); 136 | 137 | const browser = new WebMapBrowser({ view }); 138 | const compass = new Compass({ view }); 139 | const home = new Home({ view }); 140 | const locate = new Locate({ view, scale: 1000 }); 141 | const track = new Track({ view, scale: 1000 }); 142 | 143 | // we want to collaplse all expand widgets when search gets focus 144 | // because the suggestions list will cover the widgets anyway 145 | search.on("search-focus", collapseAll(expandWidgets)); 146 | 147 | // Add a reverse geocode action to MapView 148 | applyReverseGeocodeAction(view, search); 149 | // Add nav rotation check to MapView 150 | // this action will manage adding/remove widget to view 151 | applyNavRotationAction(view, compass); 152 | 153 | // Wait for user to login to add Directions widget 154 | const directionsHandle = whenTrue(directionsExpand, "expanded", () => { 155 | directionsExpand.collapse(); 156 | alertMessage.open(); 157 | }); 158 | 159 | // when user is signed in 160 | // add the Directions widget to the application 161 | whenTrueOnce(this, "signedIn").then(() => { 162 | const directions = new Directions({ 163 | view, 164 | searchProperties: { 165 | popupEnabled: true 166 | } 167 | }); 168 | directionsExpand.content = directions; 169 | directionsHandle.remove(); 170 | alertMessage.close(); 171 | const browserExpand = new Expand({ 172 | view, 173 | content: browser, 174 | expandIconClass: "esri-icon-collection", 175 | group: "right" 176 | }); 177 | view.ui.add(browserExpand, "top-right"); 178 | expandWidgets.add(browserExpand); 179 | browser.viewModel.fetchItems(); 180 | }); 181 | 182 | // Create array of widgets with positions to add to MapView 183 | const widgets = [ 184 | { 185 | component: home, 186 | position: "top-left" 187 | }, 188 | { 189 | component: track, 190 | position: "top-left" 191 | }, 192 | { 193 | component: search, 194 | position: "top-right" 195 | }, 196 | { 197 | component: directionsExpand, 198 | position: "top-right" 199 | }, 200 | { 201 | component: basemapExpand, 202 | position: "top-right" 203 | }, 204 | { 205 | component: locate, 206 | position: "bottom-right" 207 | } 208 | ]; 209 | this.view.ui.add(widgets); 210 | 211 | // Go to users location on startup 212 | return locateOnStart(view, locate); 213 | } 214 | } 215 | 216 | export default new Application(); 217 | -------------------------------------------------------------------------------- /src/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/assets/favicon.ico -------------------------------------------------------------------------------- /src/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/assets/icon.png -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | /** 15 | * Registered application id. 16 | * This is needed to be able to use premium 17 | * services such as routing and directions. 18 | */ 19 | export const appId = ""; 20 | 21 | /** 22 | * Users Portal URL. 23 | */ 24 | export const portalUrl = "https://www.arcgis.com"; // default Portal URL 25 | 26 | /** 27 | * MapView options 28 | * See doc for details 29 | * https://developers.arcgis.com/javascript/latest/api-reference/esri-views-MapView.html 30 | */ 31 | export const viewOptions = { 32 | scale: 5000 33 | }; 34 | 35 | /** 36 | * OPTIONAL 37 | * WebMap id to use for this application. 38 | * You can update this WebMap id with your own. 39 | */ 40 | export const webMapItem = { 41 | portalItem: { 42 | // shared WebMap 43 | id: "3ff64504498c4e9581a7a754412b6a9e" 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | <% if (htmlWebpackPlugin.options.mode === 'production') { %> 9 | 21 | <% } %> 22 | 23 | 24 |
25 | ArcGIS Maps App 26 | 27 |
28 | 29 | 30 | 31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | /** 15 | * We use this `main` file to initialize the app for a couple of reasons. 16 | * 1. Using Webpack, cannot reach exported method to start application. 17 | * 2. Because of issue above, makes testing more difficult when mocks are needed. 18 | */ 19 | import app from "./Application"; 20 | 21 | export default app.init(); 22 | -------------------------------------------------------------------------------- /src/mapactions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/mapactions/.gitkeep -------------------------------------------------------------------------------- /src/mapactions/MapAction.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import Accessor from "esri/core/Accessor"; 15 | import Collection from "esri/core/Collection"; 16 | import MapView from "esri/views/MapView"; 17 | 18 | import { 19 | declared, 20 | property, 21 | subclass 22 | } from "esri/core/accessorSupport/decorators"; 23 | 24 | import esri = __esri; 25 | 26 | @subclass("app.mapactions.MapAction") 27 | export default abstract class MapAction extends declared(Accessor) { 28 | @property() value: T; 29 | 30 | handlers: Collection = new Collection(); 31 | 32 | @property() view: MapView; 33 | 34 | subscribe(callback: (value: T) => void) { 35 | const handler = this.watch("value", callback); 36 | this.handlers.add(handler); 37 | return handler; 38 | } 39 | 40 | unsubscribe() { 41 | this.handlers.map(x => x.remove); 42 | this.handlers.removeAll(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/mapactions/navRotation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import MapView from "esri/views/MapView"; 15 | import Compass from "esri/widgets/Compass"; 16 | 17 | import { 18 | declared, 19 | property, 20 | subclass 21 | } from "esri/core/accessorSupport/decorators"; 22 | 23 | import MapAction from "./MapAction"; 24 | 25 | interface NavigationOptions { 26 | view: MapView; 27 | compass: Compass; 28 | } 29 | 30 | @subclass("app.mapactions.NavRotation") 31 | export class NavRotation extends declared(MapAction) { 32 | @property() compass: Compass; 33 | 34 | constructor(params?: NavigationOptions) { 35 | super(params); 36 | this.handleRotation = this.handleRotation.bind(this); 37 | if (this.view) { 38 | this.addListeners(); 39 | } 40 | this.watch("view", () => { 41 | this.addListeners(); 42 | }); 43 | } 44 | 45 | addListeners() { 46 | const handler = this.view.watch("rotation", this.handleRotation); 47 | this.handlers.push(handler); 48 | } 49 | 50 | handleRotation(rotation: number) { 51 | if (rotation === 0) { 52 | this.view.ui.remove(this.compass); 53 | } else { 54 | this.view.ui.add(this.compass, "top-left"); 55 | } 56 | this.value = rotation; 57 | } 58 | } 59 | 60 | export function applyNavRotationAction(view: MapView, compass: Compass) { 61 | const action = new NavRotation({ view, compass }); 62 | return action; 63 | } 64 | -------------------------------------------------------------------------------- /src/mapactions/reverseGeocode.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import MapView from "esri/views/MapView"; 15 | import Search from "esri/widgets/Search"; 16 | 17 | import { 18 | declared, 19 | property, 20 | subclass 21 | } from "esri/core/accessorSupport/decorators"; 22 | 23 | import MapAction from "./MapAction"; 24 | 25 | import esri = __esri; 26 | 27 | interface GeocodeOptions { 28 | mapPoint: esri.Point; 29 | } 30 | 31 | interface ReverseGeocodeOptions { 32 | view: MapView; 33 | search: Search; 34 | } 35 | 36 | @subclass("app.mapactions.ReverseGeocode") 37 | export class ReverseGeocode extends declared(MapAction) { 38 | @property() search: Search; 39 | 40 | constructor(params?: ReverseGeocodeOptions) { 41 | super(params); 42 | this.reverseGeocode = this.reverseGeocode.bind(this); 43 | if (this.view) { 44 | this.addListeners(); 45 | } 46 | this.watch("view", () => { 47 | this.addListeners(); 48 | }); 49 | } 50 | 51 | addListeners() { 52 | const handler = this.view.on("hold", this.reverseGeocode); 53 | this.handlers.push(handler); 54 | } 55 | 56 | async reverseGeocode({ mapPoint }: GeocodeOptions) { 57 | const response = await this.search.search(mapPoint); 58 | this.value = response; 59 | } 60 | } 61 | 62 | export function applyReverseGeocodeAction(view: MapView, search: Search) { 63 | const action = new ReverseGeocode({ view, search }); 64 | return action; 65 | } 66 | -------------------------------------------------------------------------------- /src/oauth-callback.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/styles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/styles/.gitkeep -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | $theme: #767676; 2 | 3 | html, 4 | body { 5 | padding: 0; 6 | margin: 0; 7 | width: 100%; 8 | height: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | letter-spacing: 0em; 12 | font-family: "Avenir Next", "Helvetica Neue", sans-serif; 13 | font-size: 14px; 14 | font-feature-settings: "liga" 1, "calt" 0; 15 | } 16 | 17 | /* header */ 18 | header.top-nav { 19 | z-index: 1; 20 | background-color: $theme; 21 | } 22 | 23 | @media (max-width: 600px) { 24 | .toolbar-title { 25 | display: none; 26 | } 27 | } 28 | 29 | .top-nav-title, 30 | .top-nav-link { 31 | color: $white; 32 | letter-spacing: 2px; 33 | text-transform: uppercase; 34 | } 35 | 36 | /* map view */ 37 | .main, 38 | webmap { 39 | padding: 0; 40 | margin: 0; 41 | width: 100%; 42 | height: 100%; 43 | display: flex; 44 | align-items: center; 45 | justify-content: center; 46 | } 47 | 48 | .esri-view-root { 49 | width: 100%; 50 | height: 100%; 51 | } 52 | 53 | /* Fix for some calcite styles */ 54 | .esri-widget { 55 | h3 { 56 | font-size: 1rem; 57 | } 58 | li { 59 | margin: 0; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | $image-path: "~calcite-web/dist/img" !default; 2 | $font-path: "~calcite-web/dist/fonts" !default; 3 | 4 | $icomoon-font-path: "~arcgis-js-api/themes/base/icons/fonts" !default; 5 | 6 | // Widgets (sorted alphabetically) 7 | $include_AreaMeasurement3D: false !default; 8 | $include_Attribution: true !default; 9 | $include_BasemapGallery: true !default; 10 | $include_BasemapToggle: false !default; 11 | $include_Bookmarks: false !default; 12 | $include_BrowseItems: false !default; 13 | $include_ColorPicker: false !default; 14 | $include_Compass: false !default; 15 | $include_CoordinateConversion: false !default; 16 | $include_DatePicker: true !default; 17 | $include_Directions: true !default; 18 | $include_Expand: true !default; 19 | $include_Feature: true !default; 20 | $include_HorizontalSlider: false !default; 21 | $include_LayerList: false !default; 22 | $include_Legend: false !default; 23 | $include_NavigationToggle: false !default; 24 | $include_Print: false !default; 25 | $include_DirectLineMeasurement3D: false !default; 26 | $include_Popup: true !default; 27 | $include_RasterSymbologyEditor: false !default; 28 | $include_RendererSlider: false !default; 29 | $include_ScaleBar: false !default; 30 | $include_Search: true !default; 31 | $include_Spinner: true !default; 32 | $include_Tags: false !default; 33 | $include_TimePicker: false !default; 34 | $include_Zoom: true !default; 35 | 36 | @import "~calcite-web/dist/sass/calcite-web-no-fonts"; 37 | @import "~arcgis-js-api/themes/light/main.scss"; 38 | 39 | @import "index"; 40 | @import "./../widgets/Authenticate/styles/Authenticate"; 41 | @import "./../widgets/WebMapBrowser/styles/WebMapBrowser"; 42 | -------------------------------------------------------------------------------- /src/utils/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/utils/.gitkeep -------------------------------------------------------------------------------- /src/utils/dateUtils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import moment from "esri/moment"; 15 | 16 | type Duration = moment.Duration; 17 | type Moment = moment.Moment; 18 | 19 | /** 20 | * Simply returns the current moment 21 | */ 22 | export const now = () => moment(); 23 | 24 | /** 25 | * Calculates the duration between two moments 26 | * @param n 27 | * @param m 28 | */ 29 | export const dateDuration: (n: number, m: Moment) => Duration = (n, m) => 30 | moment.duration(moment(n).diff(m)); 31 | 32 | /** 33 | * Returns a Function provide a string representation of a fixed number value 34 | * @param i 35 | */ 36 | export const fixedN: (i: number) => (n: number) => string = i => n => 37 | n.toFixed(i); 38 | /** 39 | * Returns a string representation of a Number to zero digits 40 | */ 41 | export const fixedZero: (n: number) => string = fixedN(0); 42 | /** 43 | * Returns a string representation of a Number to one digit 44 | */ 45 | export const fixedOne: (n: number) => string = fixedN(1); 46 | 47 | /** 48 | * Calculates the expiration time in Days or Hours between moments 49 | * @param n 50 | * @param m 51 | */ 52 | export const expiration: (n: number, m?: Moment) => string = (n, m = now()) => { 53 | const duration = dateDuration(n, m); 54 | return duration.asDays() > 1 55 | ? `${fixedZero(duration.asDays())} days` 56 | : `${fixedOne(duration.asHours())} hours`; 57 | }; 58 | 59 | /** 60 | * Format the given date in the following format: January 12th, 2018 61 | * @param n 62 | */ 63 | export const asMonthDayYear: (n: Date) => string = n => 64 | moment(n).format("MMMM Do YYYY"); 65 | -------------------------------------------------------------------------------- /src/widgets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/widgets/.gitkeep -------------------------------------------------------------------------------- /src/widgets/Alert.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { 15 | declared, 16 | property, 17 | subclass 18 | } from "esri/core/accessorSupport/decorators"; 19 | 20 | import { renderable, tsx } from "esri/widgets/support/widget"; 21 | 22 | import Widget from "esri/widgets/Widget"; 23 | 24 | import esri = __esri; 25 | 26 | export type COLOR = "red" | "yellow" | "green" | "default"; 27 | 28 | export interface AlertProps extends esri.WidgetProperties { 29 | message?: string; 30 | isActive?: boolean; 31 | isFull?: boolean; 32 | color?: COLOR; 33 | } 34 | 35 | const CSS = { 36 | base: "alert modifier-class fade-in", 37 | close: "alert-close", 38 | active: "is-active", 39 | red: "alert-red", 40 | yellow: "alert-yellow", 41 | green: "alert-green", 42 | full: "alert-full", 43 | icon: "svg-icon" 44 | }; 45 | 46 | @subclass("app.widgets.Alert") 47 | class Alert extends declared(Widget) { 48 | @property() 49 | @renderable() 50 | message = ""; 51 | 52 | @property() 53 | @renderable() 54 | isActive = false; 55 | 56 | @property() 57 | @renderable() 58 | isFull = false; 59 | 60 | @property() 61 | @renderable() 62 | color: COLOR = "default"; 63 | 64 | constructor(params?: AlertProps) { 65 | super(params); 66 | } 67 | 68 | render() { 69 | const dynamicClasses = { 70 | [CSS.active]: this.isActive, 71 | [CSS.full]: this.isFull, 72 | [CSS.red]: this.color === "red", 73 | [CSS.green]: this.color === "green", 74 | [CSS.yellow]: this.color === "yellow" 75 | }; 76 | 77 | return ( 78 |
79 | {this.message} 80 | 81 | 88 | 89 | 90 | 91 |
92 | ); 93 | } 94 | 95 | close(event?: MouseEvent) { 96 | if (event) { 97 | event.preventDefault(); 98 | } 99 | this.isActive = false; 100 | } 101 | 102 | open() { 103 | this.isActive = true; 104 | } 105 | 106 | toggle() { 107 | this.isActive = !this.isActive; 108 | } 109 | } 110 | 111 | export default Alert; 112 | -------------------------------------------------------------------------------- /src/widgets/Authenticate.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { 15 | aliasOf, 16 | declared, 17 | property, 18 | subclass 19 | } from "esri/core/accessorSupport/decorators"; 20 | 21 | import { renderable, tsx } from "esri/widgets/support/widget"; 22 | import Widget from "esri/widgets/Widget"; 23 | 24 | import AuthenticateViewModel from "./Authenticate/AuthenticateViewModel"; 25 | import { 26 | AuthStatus, 27 | SignIn, 28 | SignOut 29 | } from "./Authenticate/components/AuthComponents"; 30 | 31 | import i18n from "dojo/i18n!./Authenticate/nls/Authenticate"; 32 | 33 | import esri = __esri; 34 | 35 | interface AuthenticateProperties extends esri.WidgetProperties { 36 | appId?: string; 37 | portalUrl?: string; 38 | showLabel?: boolean; 39 | viewModel?: AuthenticateViewModel; 40 | } 41 | 42 | const CSS = { 43 | base: "authenticate", 44 | margin: "margin-left-1" 45 | }; 46 | 47 | @subclass("app.widgets.Authenticate") 48 | class Authenticate extends declared(Widget) { 49 | @property({ 50 | type: AuthenticateViewModel 51 | }) 52 | viewModel = new AuthenticateViewModel(); 53 | 54 | @renderable() 55 | @property() 56 | showIcon = false; 57 | 58 | @renderable() 59 | @property() 60 | showLabel = true; 61 | 62 | @aliasOf("viewModel.appId") appId: string; 63 | 64 | @aliasOf("viewModel.portalUrl") portalUrl: string; 65 | 66 | @property({ 67 | readOnly: true, 68 | dependsOn: ["viewModel.credential"] 69 | }) 70 | get isSignedIn(): boolean { 71 | return !!this.viewModel.credential; 72 | } 73 | 74 | constructor(params?: AuthenticateProperties) { 75 | super(params); 76 | } 77 | 78 | render() { 79 | const icon = this.isSignedIn 80 | ? // Sign-in icon 81 | SignIn() 82 | : // Sign-out icon 83 | SignOut(); 84 | 85 | // Determine what the displayed message will be. 86 | const text = this.isSignedIn ? i18n.signout : i18n.signin; 87 | 88 | // Properties for stateless component 89 | const props = { 90 | style: CSS.margin, 91 | showIcon: this.showIcon, 92 | showLabel: this.showLabel, 93 | text, 94 | icon 95 | }; 96 | 97 | return ( 98 |
104 | {AuthStatus(props)} 105 |
106 | ); 107 | } 108 | 109 | /** 110 | * Based on curernt signedIn status, either sign the user in or out. 111 | */ 112 | private onClick() { 113 | this.isSignedIn ? this.viewModel.signOut() : this.viewModel.signIn(); 114 | } 115 | 116 | /** 117 | * Once widget is created, check what the current sign in status is. 118 | */ 119 | private handleAfterCreate() { 120 | this.viewModel.checkStatus(); 121 | } 122 | } 123 | 124 | export default Authenticate; 125 | -------------------------------------------------------------------------------- /src/widgets/Authenticate/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/widgets/Authenticate/.gitkeep -------------------------------------------------------------------------------- /src/widgets/Authenticate/AuthenticateViewModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import Accessor from "esri/core/Accessor"; 15 | import Credential from "esri/identity/Credential"; 16 | import IdentityManager from "esri/identity/IdentityManager"; 17 | import OAuthInfo from "esri/identity/OAuthInfo"; 18 | 19 | import { 20 | aliasOf, 21 | declared, 22 | property, 23 | subclass 24 | } from "esri/core/accessorSupport/decorators"; 25 | import { watch, whenOnce } from "esri/core/watchUtils"; 26 | 27 | type Resolver = (value?: any) => void; 28 | type Rejector = (error?: any) => void; 29 | 30 | interface AuthenticateViewModel { 31 | credential: Credential | null; 32 | signIn(): Promise; 33 | signOut(): void; 34 | } 35 | 36 | export interface AuthenticateParams { 37 | appId: string; 38 | } 39 | 40 | @subclass("app.widgets.Authenticate.AuthenticateViewModel") 41 | class AuthenticateViewModel extends declared(Accessor) { 42 | /** 43 | * Current credential for application. 44 | */ 45 | @property() credential: Credential | null; 46 | 47 | /** 48 | * Registered application ID to use for 49 | * OAuth authentication. 50 | */ 51 | @property() appId: string; 52 | 53 | /** 54 | * Portal URL to use for 55 | * OAuth authentication. 56 | */ 57 | @property() portalUrl: string; 58 | 59 | /** 60 | * OAuthInfo using `appId`. 61 | */ 62 | @property() info: OAuthInfo; 63 | 64 | /** 65 | * Currently logged in user name. 66 | */ 67 | @aliasOf("credential.userId") userName: string; 68 | 69 | constructor(params?: AuthenticateParams) { 70 | super(params); 71 | 72 | // Once an `appId` is provided, we can create our OAuthInfo. 73 | watch(this, "appId", appId => { 74 | this.info = new OAuthInfo({ 75 | appId, 76 | portalUrl: this.portalUrl, 77 | popup: true 78 | }); 79 | }); 80 | } 81 | 82 | /** 83 | * Check the current status of whether a user is signed in or not. 84 | */ 85 | checkStatus() { 86 | return new Promise(async (resolve, reject) => { 87 | if (!this.info) { 88 | const { value: info } = await whenOnce(this, "info"); 89 | return this._checkStatus(resolve); 90 | } 91 | return this._checkStatus(resolve); 92 | }); 93 | } 94 | 95 | /** 96 | * Check if there has a current OAuthInfo and take 97 | * user through the steps of signing in. 98 | */ 99 | signIn() { 100 | return new Promise(async (resolve, reject) => { 101 | if (!this.info) { 102 | const { value: info } = await whenOnce(this, "info"); 103 | return this._login(resolve, reject); 104 | } 105 | return this._login(resolve, reject); 106 | }); 107 | } 108 | 109 | /** 110 | * Destroy credentials and reload application. 111 | */ 112 | signOut() { 113 | IdentityManager.destroyCredentials(); 114 | this.credential = null; 115 | this.pageReload(); 116 | } 117 | 118 | /** 119 | * Check the current signed in status and attempt to 120 | * acquire current Credential. 121 | * @param resolve 122 | */ 123 | private async _checkStatus(resolve: Resolver) { 124 | this.registerOAuth(); 125 | try { 126 | this.credential = await this.currentStatus(this.info); 127 | resolve(this.credential); 128 | } catch (error) {} 129 | } 130 | 131 | /** 132 | * Check for a current credential for a user, if not already 133 | * signed in, go through steps to let user authenticate. 134 | * @param resolve 135 | * @param reject 136 | */ 137 | private async _login(resolve: Resolver, reject: Rejector) { 138 | this.registerOAuth(); 139 | try { 140 | this.credential = await this.currentStatus(this.info); 141 | resolve(this.credential); 142 | } catch (error) { 143 | try { 144 | const credential = await this.fetchCredentials(); 145 | resolve(credential); 146 | } catch (err) { 147 | reject(err); 148 | } 149 | } 150 | } 151 | 152 | /** 153 | * Use `IdentityManager` to check users current signed in status. 154 | * @param info 155 | */ 156 | private currentStatus(info: OAuthInfo) { 157 | return IdentityManager.checkSignInStatus(`${info.portalUrl}/sharing`); 158 | } 159 | 160 | private pageReload() { 161 | location.reload(); 162 | } 163 | 164 | /** 165 | * Use `IdentityManager` to register current OAuthInfos. 166 | */ 167 | private registerOAuth() { 168 | /** 169 | * When installed as a homescreen application, display-mode is standalone 170 | * OAuth redirects don't behave correctly here, so will not register 171 | * OAuth with the IdentityManager and use the old-school modal popup 172 | * in this case. 173 | */ 174 | if (!window.matchMedia("(display-mode: standalone)").matches) { 175 | IdentityManager.registerOAuthInfos([this.info]); 176 | } 177 | } 178 | 179 | /** 180 | * OAuth login process for user. 181 | */ 182 | private async fetchCredentials() { 183 | this.credential = await IdentityManager.getCredential( 184 | `${this.info.portalUrl}/sharing`, 185 | { 186 | error: null as any, 187 | oAuthPopupConfirmation: false, 188 | retry: false, 189 | token: null as any 190 | } 191 | ); 192 | 193 | return this.credential; 194 | } 195 | } 196 | 197 | export default AuthenticateViewModel; 198 | -------------------------------------------------------------------------------- /src/widgets/Authenticate/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/widgets/Authenticate/components/.gitkeep -------------------------------------------------------------------------------- /src/widgets/Authenticate/components/AuthComponents.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { tsx } from "esri/widgets/support/widget"; 15 | 16 | interface AuthStatusProps { 17 | icon: JSX.Element; 18 | text: string; 19 | showIcon: boolean; 20 | showLabel: boolean; 21 | style: string; 22 | } 23 | 24 | /** 25 | * These are simple stateless components used 26 | * with the Authentication Widget. 27 | */ 28 | export const AuthStatus = ({ 29 | style, 30 | icon, 31 | text, 32 | showLabel, 33 | showIcon 34 | }: AuthStatusProps) => { 35 | const dynamicStyles = { 36 | [style]: showIcon 37 | }; 38 | return !showLabel && showIcon ? ( 39 | {icon} 40 | ) : ( 41 | 42 | {showIcon ? {icon} : } 43 | {text} 44 | 45 | ); 46 | }; 47 | 48 | /** 49 | * Returns the SignIn SVG Icon 50 | */ 51 | export const SignIn = () => ( 52 | 59 | 60 | 61 | ); 62 | 63 | /** 64 | * Returns the SignOut SVG Icon 65 | */ 66 | export const SignOut = () => ( 67 | 74 | 75 | 76 | ); 77 | 78 | /** 79 | * Returns the label for a User if provided 80 | * @param name 81 | */ 82 | export const User: (name?: string | undefined) => JSX.Element = name => ( 83 | {name ? `(${name})` : ""} 84 | ); 85 | -------------------------------------------------------------------------------- /src/widgets/Authenticate/nls/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/widgets/Authenticate/nls/.gitkeep -------------------------------------------------------------------------------- /src/widgets/Authenticate/nls/Authenticate.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | define({ 15 | root: { 16 | signout: "Sign Out", 17 | signin: "Sign In" 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/widgets/Authenticate/styles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/widgets/Authenticate/styles/.gitkeep -------------------------------------------------------------------------------- /src/widgets/Authenticate/styles/Authenticate.scss: -------------------------------------------------------------------------------- 1 | .authenticate { 2 | width: 100%; 3 | text-decoration: none; 4 | display: inline-block; 5 | cursor: pointer; 6 | } 7 | -------------------------------------------------------------------------------- /src/widgets/UserNav.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { 15 | aliasOf, 16 | declared, 17 | property, 18 | subclass 19 | } from "esri/core/accessorSupport/decorators"; 20 | 21 | import { renderable, tsx } from "esri/widgets/support/widget"; 22 | 23 | import Authenticate from "./Authenticate"; 24 | 25 | import MapView from "esri/views/MapView"; 26 | import Widget from "esri/widgets/Widget"; 27 | 28 | import { dropdown } from "calcite-web"; 29 | 30 | import { appId, portalUrl } from "../config"; 31 | 32 | import { expiration } from "./../utils/dateUtils"; 33 | 34 | const CSS = { 35 | base: "dropdown js-dropdown right", 36 | toggle: 37 | "top-nav-link js-dropdown-toggle user-icon padding-right-1 padding-left-1", 38 | link: "dropdown-link" 39 | }; 40 | 41 | import { userMenu } from "./UserNav/components/UserMenu"; 42 | 43 | @subclass("app.widgets.UserNav") 44 | class UserNav extends declared(Widget) { 45 | @property() 46 | auth = new Authenticate({ 47 | appId, 48 | portalUrl, 49 | showLabel: true 50 | }); 51 | 52 | @property({ 53 | readOnly: true, 54 | dependsOn: ["auth.viewModel.credential"] 55 | }) 56 | get sessionDuration(): string { 57 | if (this.auth && this.auth.viewModel.credential) { 58 | const { expires } = this.auth.viewModel.credential; 59 | return expiration(expires); 60 | } 61 | return "not available"; 62 | } 63 | 64 | @aliasOf("auth.isSignedIn") signedIn: boolean; 65 | 66 | @renderable() 67 | @aliasOf("auth.viewModel.userName") 68 | userName: string; 69 | 70 | @property() view: MapView; 71 | 72 | constructor(params?: any) { 73 | super(params); 74 | } 75 | 76 | render() { 77 | return ( 78 |
79 | 99 | {userMenu( 100 | { 101 | userName: this.userName, 102 | sessionDuration: this.sessionDuration, 103 | menuItems: [ 104 | 105 |
106 | 107 | ] 108 | }, 109 | this 110 | )} 111 |
112 | ); 113 | } 114 | 115 | private handleMenuCreation() { 116 | dropdown(); 117 | } 118 | 119 | private handleAuthButton(element: HTMLDivElement) { 120 | this.auth.container = element; 121 | } 122 | } 123 | 124 | export default UserNav; 125 | -------------------------------------------------------------------------------- /src/widgets/UserNav/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/widgets/UserNav/.gitkeep -------------------------------------------------------------------------------- /src/widgets/UserNav/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/widgets/UserNav/components/.gitkeep -------------------------------------------------------------------------------- /src/widgets/UserNav/components/UserMenu.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { tsx } from "esri/widgets/support/widget"; 15 | 16 | import i18n from "dojo/i18n!./../nls/UserMenu"; 17 | 18 | export const upper: (s: string) => string = s => s.toUpperCase(); 19 | 20 | export interface UserMenuProps { 21 | userName: string; 22 | sessionDuration: string; 23 | menuItems: JSX.IntrinsicElements[]; 24 | } 25 | 26 | const CSS = { 27 | menu: "dropdown-menu dropdown-right", 28 | title: "dropdown-title", 29 | link: "dropdown-link" 30 | }; 31 | 32 | /** 33 | * Returns the navigation menu item for a user to sign in or out 34 | * @param props 35 | * @param context 36 | */ 37 | export const userMenu = (props: UserMenuProps, context: any) => ( 38 | 50 | ); 51 | -------------------------------------------------------------------------------- /src/widgets/UserNav/nls/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/widgets/UserNav/nls/.gitkeep -------------------------------------------------------------------------------- /src/widgets/UserNav/nls/UserMenu.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | define({ 15 | root: { 16 | session: "Session expires in", 17 | signin: "Please sign in" 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /src/widgets/WebMapBrowser.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { 15 | aliasOf, 16 | declared, 17 | property, 18 | subclass 19 | } from "esri/core/accessorSupport/decorators"; 20 | 21 | import { tsx } from "esri/widgets/support/widget"; 22 | 23 | import MapView from "esri/views/MapView"; 24 | import Widget from "esri/widgets/Widget"; 25 | 26 | import { PortalItem } from "./WebMapBrowser/components/PortalItem"; 27 | import WebMapBrowserViewModel from "./WebMapBrowser/WebMapBrowserViewModel"; 28 | 29 | const CSS = { 30 | base: "browser esri-widget esri-widget--panel", 31 | container: "browser__item-container" 32 | }; 33 | 34 | @subclass("app.widgets.WebMapBrowser") 35 | class WebMapBrowser extends declared(Widget) { 36 | @property({ 37 | type: WebMapBrowserViewModel 38 | }) 39 | viewModel = new WebMapBrowserViewModel(); 40 | 41 | @aliasOf("viewModel.view") view: MapView; 42 | 43 | @aliasOf("viewModel.portalItems") items: any[]; 44 | 45 | constructor(params?: any) { 46 | super(params); 47 | } 48 | 49 | render() { 50 | const items = this.items.map((n, key) => { 51 | n.key = key; 52 | n.onClick = this.onItemClick; 53 | return PortalItem(n, this); 54 | }); 55 | return ( 56 |
57 | 60 |
61 | ); 62 | } 63 | 64 | onItemClick(event: MouseEvent) { 65 | const id = (event.currentTarget as HTMLElement).dataset.id || ""; 66 | this.items.forEach(a => (a.isActive = false)); 67 | const item = this.items.find(a => a.id === id); 68 | item.isActive = true; 69 | this.viewModel.changeWebmap(id); 70 | } 71 | } 72 | 73 | export default WebMapBrowser; 74 | -------------------------------------------------------------------------------- /src/widgets/WebMapBrowser/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/widgets/WebMapBrowser/.gitkeep -------------------------------------------------------------------------------- /src/widgets/WebMapBrowser/WebMapBrowserViewModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import esri = __esri; 15 | 16 | import Accessor from "esri/core/Accessor"; 17 | import Collection from "esri/core/Collection"; 18 | import Portal from "esri/portal/Portal"; 19 | import PortalQueryParams from "esri/portal/PortalQueryParams"; 20 | import MapView from "esri/views/MapView"; 21 | import WebMap from "esri/WebMap"; 22 | 23 | import { 24 | declared, 25 | property, 26 | subclass 27 | } from "esri/core/accessorSupport/decorators"; 28 | 29 | export interface BrowserParams extends esri.WidgetProperties { 30 | appId: string; 31 | } 32 | 33 | const WebMapCollection = Collection.ofType(WebMap); 34 | 35 | const fst: (xs: string[]) => string = xs => xs[0] || "1"; 36 | 37 | @subclass("app.widgets.Browser.BrowserViewModel") 38 | class BrowserViewModel extends declared(Accessor) { 39 | @property() view: MapView; 40 | 41 | @property() portal = new Portal(); 42 | 43 | @property() webmaps = new WebMapCollection(); 44 | 45 | @property() portalItems: any[] = []; 46 | 47 | constructor(params?: BrowserParams) { 48 | super(params); 49 | } 50 | 51 | async changeWebmap(id: string) { 52 | const webmap = this.webmaps.find(({ portalItem }) => portalItem.id === id); 53 | await webmap.load(); 54 | const version = (webmap as any).resourceInfo.version; 55 | const majorVersion = fst(version.split(".")); 56 | if (majorVersion === "1") { 57 | return; 58 | } 59 | this.view.map = webmap; 60 | this.view.extent = webmap.portalItem.extent; 61 | return webmap.when(); 62 | } 63 | 64 | async fetchItems() { 65 | await this.portal.load(); 66 | const queryParams = new PortalQueryParams({ 67 | query: `type: "Web Map" -Application AND owner: ${ 68 | this.portal.user.username 69 | }`, 70 | sortField: "numViews", 71 | sortOrder: "desc", 72 | num: 100 73 | }); 74 | const { results } = await this.portal.queryItems(queryParams); 75 | this.portalItems = results.map(a => { 76 | a.isActive = false; 77 | a.isSelected = false; 78 | return a; 79 | }); 80 | this.webmaps.addMany( 81 | results.map( 82 | ({ id }) => 83 | ({ 84 | portalItem: { id } 85 | } as any) 86 | ) 87 | ); 88 | } 89 | } 90 | 91 | export default BrowserViewModel; 92 | -------------------------------------------------------------------------------- /src/widgets/WebMapBrowser/components/PortalItem.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2018 Esri 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | http://www.apache.org/licenses/LICENSE-2.0 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | */ 13 | 14 | import { tsx } from "esri/widgets/support/widget"; 15 | 16 | import { asMonthDayYear } from "../../../utils/dateUtils"; 17 | 18 | export interface PortalItemProps { 19 | key: number; 20 | thumbnailUrl: string; 21 | title: string; 22 | id: string; 23 | created: Date; 24 | isActive: boolean; 25 | onClick: (e: MouseEvent) => void; 26 | } 27 | 28 | const CSS = { 29 | item: "browser__item", 30 | active: "browser__item__active", 31 | thumbnail: "browser__item-thumbnail", 32 | description: "browser__item-description", 33 | title: "browser__item-title" 34 | }; 35 | 36 | /** 37 | * These are simple stateless components used 38 | * with the Browser Widget. 39 | */ 40 | export const PortalItem = (props: PortalItemProps, context: any) => { 41 | const dynamicStyles = { 42 | [CSS.active]: props.isActive 43 | }; 44 | return ( 45 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /src/widgets/WebMapBrowser/styles/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/src/widgets/WebMapBrowser/styles/.gitkeep -------------------------------------------------------------------------------- /src/widgets/WebMapBrowser/styles/WebMapBrowser.scss: -------------------------------------------------------------------------------- 1 | .browser { 2 | color: #323232; 3 | background-color: #fff; 4 | overflow-y: auto; 5 | position: relative; 6 | } 7 | 8 | .browser__item-container { 9 | display: flex; 10 | flex-flow: column nowrap; 11 | position: relative; 12 | transition: opacity 250ms ease-in-out; 13 | list-style: none; 14 | margin: 0; 15 | padding: 0; 16 | } 17 | 18 | .browser__item { 19 | position: relative; 20 | display: flex; 21 | align-items: center; 22 | padding: 6px 7px; 23 | cursor: pointer; 24 | border-left: 3px solid transparent; 25 | border-right: 3px solid transparent; 26 | animation: esri-fade-in 500ms ease-in-out; 27 | transition: background-color 250ms ease-in-out; 28 | } 29 | 30 | .browser__item__active { 31 | border-color: $blue; 32 | background-color: $lightest-blue; 33 | } 34 | 35 | .browser__item:hover, 36 | .browser__item:focus { 37 | background-color: $lightest-gray; 38 | } 39 | 40 | .browser__item-thumbnail { 41 | height: 64px; 42 | width: auto; 43 | box-shadow: 0 0 0 1px rgba(50, 50, 50, 0.25); 44 | } 45 | 46 | .browser__item-description { 47 | font-size: 12px; 48 | word-break: break-word; 49 | color: #6e6e6e; 50 | padding: 0 7px; 51 | span { 52 | white-space: nowrap; 53 | overflow: hidden; 54 | text-overflow: ellipsis; 55 | max-width: 150px; 56 | } 57 | } 58 | 59 | .browser__item-title { 60 | font-weight: bold; 61 | } 62 | -------------------------------------------------------------------------------- /src/worker-config.ts: -------------------------------------------------------------------------------- 1 | import esriConfig from "esri/config"; 2 | 3 | const DEFAULT_WORKER_URL = "https://js.arcgis.com/4.11/"; 4 | const DEFAULT_LOADER_URL = `${DEFAULT_WORKER_URL}dojo/dojo-lite.js`; 5 | 6 | (esriConfig.workers as any).loaderUrl = DEFAULT_LOADER_URL; 7 | esriConfig.workers.loaderConfig = { 8 | baseUrl: `${DEFAULT_WORKER_URL}dojo`, 9 | packages: [ 10 | { name: "esri", location: DEFAULT_WORKER_URL + "esri" }, 11 | { name: "dojo", location: DEFAULT_WORKER_URL + "dojo" }, 12 | { name: "dojox", location: DEFAULT_WORKER_URL + "dojox" }, 13 | { name: "dijit", location: DEFAULT_WORKER_URL + "dijit" }, 14 | { name: "dstore", location: DEFAULT_WORKER_URL + "dstore" }, 15 | { name: "moment", location: DEFAULT_WORKER_URL + "moment" }, 16 | { name: "@dojo", location: DEFAULT_WORKER_URL + "@dojo" }, 17 | { 18 | name: "cldrjs", 19 | location: DEFAULT_WORKER_URL + "cldrjs", 20 | main: "dist/cldr" 21 | }, 22 | { 23 | name: "globalize", 24 | location: DEFAULT_WORKER_URL + "globalize", 25 | main: "dist/globalize" 26 | }, 27 | { 28 | name: "maquette", 29 | location: DEFAULT_WORKER_URL + "maquette", 30 | main: "dist/maquette.umd" 31 | }, 32 | { 33 | name: "maquette-css-transitions", 34 | location: DEFAULT_WORKER_URL + "maquette-css-transitions", 35 | main: "dist/maquette-css-transitions.umd" 36 | }, 37 | { 38 | name: "maquette-jsx", 39 | location: DEFAULT_WORKER_URL + "maquette-jsx", 40 | main: "dist/maquette-jsx.umd" 41 | }, 42 | { name: "tslib", location: DEFAULT_WORKER_URL + "tslib", main: "tslib" } 43 | ] 44 | } as any; 45 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": "..", 5 | "types": [ "arcgis-js-api", "intern", "sinon" ] 6 | }, 7 | "include": [ 8 | "./**/*.ts" 9 | ] 10 | } -------------------------------------------------------------------------------- /tests/unit/Application.ts: -------------------------------------------------------------------------------- 1 | import app, { collapseAll, empty, locateOnStart } from "../../src/Application"; 2 | 3 | import Collection from "esri/core/Collection"; 4 | import MapView from "esri/views/MapView"; 5 | import Expand from "esri/widgets/Expand"; 6 | import Locate from "esri/widgets/Locate"; 7 | 8 | import sinon from "sinon"; 9 | 10 | const { suite, test } = intern.getInterface("tdd"); 11 | const { assert } = intern.getPlugin("chai"); 12 | 13 | suite("Application", () => { 14 | test("Application will create MapView", () => { 15 | app.init(); 16 | assert.ok(app.view); 17 | }); 18 | 19 | test("Can empty DOM element when needed", () => { 20 | const div = document.createElement("div"); 21 | div.innerHTML = "I am a DOM element"; 22 | empty(div); 23 | assert.isEmpty(div.innerHTML); 24 | }); 25 | 26 | test("can locate on start of application", async () => { 27 | const when = sinon.stub().resolves({}); 28 | const locateFunc = sinon.stub().resolves({}); 29 | const goTo = sinon.stub().resolves({}); 30 | 31 | const container = document.createElement("div"); 32 | const view = new MapView({ container }); 33 | 34 | (view as any).when = when; 35 | (view as any).goTo = goTo; 36 | const location = new Locate(); 37 | location.graphic = { 38 | attributes: {}, 39 | geometry: {} 40 | } as any; 41 | (location as any).locate = locateFunc; 42 | await locateOnStart(view, location); 43 | 44 | assert.isTrue(goTo.called); 45 | assert.isTrue(location.goToLocationEnabled); 46 | }); 47 | 48 | test("can collapse list of widgets", () => { 49 | const collection = new Collection(); 50 | const exp1 = new Expand(); 51 | const exp2 = new Expand(); 52 | exp1.expand(); 53 | exp2.expand(); 54 | collection.addMany([exp1, exp2]); 55 | const collapser = collapseAll(collection); 56 | assert.isTrue(exp1.expanded); 57 | assert.isTrue(exp2.expanded); 58 | collapser(); 59 | assert.isFalse(exp1.expanded); 60 | assert.isFalse(exp2.expanded); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /tests/unit/all.ts: -------------------------------------------------------------------------------- 1 | import "./Application"; 2 | import "./mapactions/navRotation"; 3 | import "./mapactions/reverseGeocode"; 4 | import "./utils/dateUtils"; 5 | import "./widgets/Alert"; 6 | import "./widgets/Authenticate/AuthenticateViewModel"; 7 | import "./widgets/Authenticate/components/AuthComponents"; 8 | import "./widgets/UserNav/components/UserMenu"; 9 | import "./widgets/WebMapBrowser"; 10 | import "./widgets/WebMapBrowser/components/PortalItem"; 11 | import "./widgets/WebMapBrowser/WebMapBrowserViewModel"; 12 | -------------------------------------------------------------------------------- /tests/unit/mapactions/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/tests/unit/mapactions/.gitkeep -------------------------------------------------------------------------------- /tests/unit/mapactions/navRotation.ts: -------------------------------------------------------------------------------- 1 | import { applyNavRotationAction } from "../../../src/mapactions/navRotation"; 2 | 3 | import MapView from "esri/views/MapView"; 4 | import Compass from "esri/widgets/Compass"; 5 | 6 | import sinon from "sinon"; 7 | 8 | const { suite, test } = intern.getInterface("tdd"); 9 | const { assert } = intern.getPlugin("chai"); 10 | 11 | suite("mapactions/navRotation", () => { 12 | const PROP = "rotation"; 13 | 14 | test("view will watch for rotation change", () => { 15 | const watch = sinon.stub().returns({}); 16 | const view = new MapView(); 17 | const compass = new Compass({ view }); 18 | (view as any).watch = watch; 19 | const action = applyNavRotationAction(view, compass); 20 | action.addListeners(); 21 | 22 | assert.isTrue(watch.calledWithMatch(PROP)); 23 | }); 24 | 25 | test("can add/remove compass when rotation changes", () => { 26 | const remove = sinon.stub(); 27 | const add = sinon.stub(); 28 | const view = new MapView(); 29 | const compass = new Compass({ view }); 30 | 31 | const action = applyNavRotationAction(view, compass); 32 | (action.view.ui as any).remove = remove; 33 | (action.view.ui as any).add = add; 34 | 35 | action.handleRotation(2); 36 | assert.isTrue(add.calledWith(sinon.match.any, sinon.match.string)); 37 | 38 | action.handleRotation(0); 39 | assert.isTrue(remove.called); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/unit/mapactions/reverseGeocode.ts: -------------------------------------------------------------------------------- 1 | import { applyReverseGeocodeAction } from "../../../src/mapactions/reverseGeocode"; 2 | 3 | import Point from "esri/geometry/Point"; 4 | import MapView from "esri/views/MapView"; 5 | import Search from "esri/widgets/Search"; 6 | 7 | import sinon from "sinon"; 8 | 9 | const { suite, test } = intern.getInterface("tdd"); 10 | const { assert } = intern.getPlugin("chai"); 11 | 12 | suite("mapactions/reverseGeocode", () => { 13 | const EVENT = "hold"; 14 | 15 | test("listens for MapView hold event", () => { 16 | const on = sinon.stub().returns({}); 17 | const view = new MapView(); 18 | const search = new Search({ view }); 19 | (view as any).on = on; 20 | const action = applyReverseGeocodeAction(view, search); 21 | action.addListeners(); 22 | 23 | assert.isTrue(on.calledWith(EVENT, sinon.match.any)); 24 | }); 25 | 26 | test("will use search function to reverse geocode", async () => { 27 | const searchMethod = sinon.stub(); 28 | const view = new MapView(); 29 | const search = new Search({ view }); 30 | (search as any).search = searchMethod; 31 | const mapPoint = new Point({ x: 1, y: 1 }); 32 | const action = applyReverseGeocodeAction(view, search); 33 | await action.reverseGeocode({ mapPoint }); 34 | 35 | assert.isTrue(searchMethod.calledWith(mapPoint)); 36 | }); 37 | 38 | test("can subscribe to results", () => { 39 | const watchMethod = sinon.stub().returns({}); 40 | const view = new MapView(); 41 | const search = new Search({ view }); 42 | const action = applyReverseGeocodeAction(view, search); 43 | const callback = () => "hello"; 44 | (action as any).watch = watchMethod; 45 | action.subscribe(callback); 46 | 47 | assert.isTrue(watchMethod.calledWith("value", callback)); 48 | }); 49 | 50 | test("can unsubscribe from results", () => { 51 | const remove = { remove() {} }; 52 | const view = new MapView(); 53 | const search = new Search({ view }); 54 | const action = applyReverseGeocodeAction(view, search); 55 | action.handlers.add({ remove } as any); 56 | action.unsubscribe(); 57 | assert.isEmpty(action.handlers.toArray()); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/unit/utils/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/tests/unit/utils/.gitkeep -------------------------------------------------------------------------------- /tests/unit/utils/dateUtils.ts: -------------------------------------------------------------------------------- 1 | import moment from "esri/moment"; 2 | 3 | import { expiration, now } from "../../../src/utils/dateUtils"; 4 | 5 | const { suite, test } = intern.getInterface("tdd"); 6 | const { assert } = intern.getPlugin("chai"); 7 | 8 | suite("utils/dateUtils", () => { 9 | test("expiration: Can return a proper expiration string as days", () => { 10 | const EXPECTED = "14 days"; 11 | const startTime = now(); 12 | const endTime = moment() 13 | .add(2, "weeks") 14 | .valueOf(); 15 | const result = expiration(endTime, startTime); 16 | assert.equal(result, EXPECTED); 17 | }); 18 | 19 | test("expiration: Can return a proper expiration string as hours", () => { 20 | const EXPECTED = "4.0 hours"; 21 | const startTime = now(); 22 | const endTime = moment() 23 | .add(4, "hours") 24 | .valueOf(); 25 | const result = expiration(endTime, startTime); 26 | assert.equal(result, EXPECTED); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /tests/unit/widgets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/tests/unit/widgets/.gitkeep -------------------------------------------------------------------------------- /tests/unit/widgets/Alert.ts: -------------------------------------------------------------------------------- 1 | import Alert from "../../../src/widgets/Alert"; 2 | 3 | const { beforeEach, suite, test } = intern.getInterface("tdd"); 4 | const { assert } = intern.getPlugin("chai"); 5 | 6 | suite("widgets/Alert", () => { 7 | const MESSAGE = "Can you hear me now?"; 8 | let alertWidget: Alert; 9 | 10 | beforeEach(() => { 11 | alertWidget = new Alert({ 12 | message: MESSAGE 13 | }); 14 | }); 15 | 16 | test("Alert can have a message assigned", () => { 17 | assert.strictEqual(alertWidget.message, MESSAGE); 18 | }); 19 | 20 | test("Alert can be opened", () => { 21 | alertWidget.open(); 22 | assert.isTrue(alertWidget.isActive); 23 | }); 24 | 25 | test("Alert can be closed", () => { 26 | alertWidget.close(); 27 | assert.isFalse(alertWidget.isActive); 28 | }); 29 | 30 | test("Alert can be toggled", () => { 31 | alertWidget.close(); 32 | alertWidget.toggle(); 33 | assert.isTrue(alertWidget.isActive); 34 | alertWidget.toggle(); 35 | assert.isFalse(alertWidget.isActive); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/unit/widgets/Authenticate/AuthenticateViewModel.ts: -------------------------------------------------------------------------------- 1 | import Credential from "esri/identity/Credential"; 2 | import AuthenticateViewModel from "../../../../src/widgets/Authenticate/AuthenticateViewModel"; 3 | 4 | import sinon from "sinon"; 5 | 6 | const { beforeEach, suite, test } = intern.getInterface("tdd"); 7 | const { assert } = intern.getPlugin("chai"); 8 | 9 | suite("widgets/Authenticate/AuthenticateViewModel", () => { 10 | let authVM: AuthenticateViewModel; 11 | 12 | beforeEach(() => { 13 | authVM = new AuthenticateViewModel(); 14 | authVM.appId = "1234"; 15 | authVM.portalUrl = "BobsBurger"; 16 | }); 17 | 18 | test("will update OUAthInfo when appId assigned", async () => { 19 | await Promise.resolve(); 20 | assert.isOk(authVM.info); 21 | }); 22 | 23 | test("check authentication status", async () => { 24 | const credential = new Credential({ 25 | expires: Date.now(), 26 | userId: "Sam" 27 | }); 28 | const currentStatus = sinon.stub().resolves(credential); 29 | (authVM as any).currentStatus = currentStatus; 30 | const results = await authVM.checkStatus(); 31 | 32 | assert.equal(results, credential); 33 | assert.isTrue(currentStatus.calledWith(authVM.info)); 34 | }); 35 | 36 | test("can sign in", async () => { 37 | const credential = new Credential({ 38 | expires: Date.now(), 39 | userId: "Sam" 40 | }); 41 | const currentStatus = sinon.stub().resolves(credential); 42 | (authVM as any).currentStatus = currentStatus; 43 | const results = await authVM.signIn(); 44 | 45 | assert.equal(results, credential); 46 | assert.isTrue(currentStatus.calledWith(authVM.info)); 47 | }); 48 | 49 | test("can sign out", () => { 50 | const reload = sinon.stub(); 51 | (authVM as any).pageReload = reload; 52 | authVM.signOut(); 53 | assert.isNull(authVM.credential); 54 | assert.isTrue(reload.called); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/unit/widgets/Authenticate/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/tests/unit/widgets/Authenticate/components/.gitkeep -------------------------------------------------------------------------------- /tests/unit/widgets/Authenticate/components/AuthComponents.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AuthStatus, 3 | SignIn, 4 | SignOut, 5 | User 6 | } from "../../../../../src/widgets/Authenticate/components/AuthComponents"; 7 | 8 | const { suite, test } = intern.getInterface("tdd"); 9 | const { assert } = intern.getPlugin("chai"); 10 | 11 | suite("widgets/Authenticate/components/AuthComponents", () => { 12 | test("Icons can be initialized with properties", () => { 13 | const vnode1 = SignIn(); 14 | const vnode2 = SignOut(); 15 | assert.equal((vnode1 as any).properties.class, "svg-icon"); 16 | assert.equal((vnode2 as any).properties.class, "svg-icon"); 17 | }); 18 | 19 | test("User contains empty name", () => { 20 | const vnode: JSX.IntrinsicElements = User(); 21 | assert.equal(vnode.text, ""); 22 | }); 23 | 24 | test("User contains given name", () => { 25 | const name = "Syd"; 26 | const vnode: JSX.IntrinsicElements = User(name); 27 | assert.equal(vnode.text, `(${name})`); 28 | }); 29 | 30 | test("AuthStatus contains a single element", () => { 31 | const props = { 32 | icon: SignIn(), 33 | text: "Hello", 34 | showLabel: false, 35 | showIcon: false, 36 | style: "test-class" 37 | }; 38 | const vnode: JSX.IntrinsicElements = AuthStatus(props); 39 | const child = vnode.children[0]; 40 | assert.ok(child); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /tests/unit/widgets/UserNav/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/tests/unit/widgets/UserNav/.gitkeep -------------------------------------------------------------------------------- /tests/unit/widgets/UserNav/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/tests/unit/widgets/UserNav/components/.gitkeep -------------------------------------------------------------------------------- /tests/unit/widgets/UserNav/components/UserMenu.ts: -------------------------------------------------------------------------------- 1 | import { 2 | upper, 3 | userMenu, 4 | UserMenuProps 5 | } from "../../../../../src/widgets/UserNav/components/UserMenu"; 6 | 7 | const { suite, test } = intern.getInterface("tdd"); 8 | const { assert } = intern.getPlugin("chai"); 9 | 10 | suite("widgets/UserNav/components/UserMenu", () => { 11 | test("Props can be applied to vnodes", () => { 12 | const props: UserMenuProps = { 13 | userName: "Sam", 14 | sessionDuration: "14 days", 15 | menuItems: [] 16 | }; 17 | const vnode: JSX.IntrinsicElements = userMenu(props, this); 18 | const children: JSX.IntrinsicElements[] = vnode.children[0].children; 19 | const elem1 = children.find( 20 | a => a.vnodeSelector === "" 21 | ) as JSX.IntrinsicElements; 22 | assert.strictEqual(elem1.text, props.userName); 23 | 24 | const small = children.find( 25 | a => a.vnodeSelector === "small" 26 | ) as JSX.IntrinsicElements; 27 | assert.strictEqual( 28 | small.text, 29 | `Session expires in ${upper(props.sessionDuration)}` 30 | ); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/unit/widgets/WebMapBrowser.ts: -------------------------------------------------------------------------------- 1 | import WebMapBrowser from "../../../src/widgets/WebMapBrowser"; 2 | 3 | import sinon from "sinon"; 4 | 5 | const { suite, test } = intern.getInterface("tdd"); 6 | const { assert } = intern.getPlugin("chai"); 7 | 8 | suite("widgets/WebMapBrowser", () => { 9 | test("when webmap selected, will ask view model to change it in view", () => { 10 | const items = [ 11 | { 12 | id: "atom", 13 | isActive: false 14 | }, 15 | { 16 | id: "heart", 17 | isActive: false 18 | }, 19 | { 20 | id: "mother", 21 | isActive: false 22 | } 23 | ]; 24 | const changeWebmap = sinon.stub(); 25 | const browser = new WebMapBrowser({ items }); 26 | (browser.viewModel as any).changeWebmap = changeWebmap; 27 | const mockEvent = { 28 | currentTarget: { 29 | dataset: { 30 | id: "heart" 31 | } 32 | } 33 | }; 34 | browser.onItemClick(mockEvent as any); 35 | assert.isTrue(items[1].isActive); 36 | assert.isTrue(changeWebmap.calledWith(items[1].id)); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/unit/widgets/WebMapBrowser/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Esri/maps-app-javascript/c93a64a694a5ab71ab92a3c67cc91281f0b8b68f/tests/unit/widgets/WebMapBrowser/.gitkeep -------------------------------------------------------------------------------- /tests/unit/widgets/WebMapBrowser/WebMapBrowserViewModel.ts: -------------------------------------------------------------------------------- 1 | import MapView from "esri/views/MapView"; 2 | 3 | import WebMapBrowserViewModel from "../../../../src/widgets/WebMapBrowser/WebMapBrowserViewModel"; 4 | 5 | import sinon from "sinon"; 6 | 7 | const { beforeEach, suite, test } = intern.getInterface("tdd"); 8 | const { assert } = intern.getPlugin("chai"); 9 | 10 | suite("widgets/WebMapBrowser/WebMapBrowserViewModel", () => { 11 | let vm: WebMapBrowserViewModel; 12 | 13 | beforeEach(() => { 14 | vm = new WebMapBrowserViewModel({ 15 | view: new MapView() 16 | } as any); 17 | vm.portal.user = { 18 | username: "Pete Namlook" 19 | } as any; 20 | }); 21 | 22 | test("Portal will attempt to load and fetch webmaps", async () => { 23 | const ids = [ 24 | { 25 | id: "atom" 26 | }, 27 | { 28 | id: "heart" 29 | }, 30 | { 31 | id: "mother" 32 | } 33 | ]; 34 | const load = sinon.stub().resolves({}); 35 | const queryItems = sinon.stub().resolves({ results: ids }); 36 | 37 | (vm.portal as any).load = load; 38 | (vm.portal as any).queryItems = queryItems; 39 | 40 | await vm.fetchItems(); 41 | assert.isTrue(load.called); 42 | assert.isTrue(queryItems.called); 43 | assert.lengthOf(vm.webmaps, 3); 44 | }); 45 | 46 | test("view model can change webmaps of view", async () => { 47 | const when = sinon.stub().resolves(true); 48 | const ids = [ 49 | { 50 | portalItem: { 51 | id: "atom" 52 | }, 53 | resourceInfo: { 54 | version: "2.8" 55 | }, 56 | when 57 | }, 58 | { 59 | portalItem: { 60 | id: "heart" 61 | }, 62 | resourceInfo: { 63 | version: "2.8" 64 | }, 65 | when 66 | }, 67 | { 68 | portalItem: { 69 | id: "mother" 70 | }, 71 | resourceInfo: { 72 | version: "1.2" 73 | }, 74 | when 75 | } 76 | ]; 77 | vm.webmaps.addMany(ids as any); 78 | const load = sinon.stub(); 79 | vm.webmaps.forEach(w => ((w as any).load = load)); 80 | const result1 = await vm.changeWebmap("mother"); 81 | 82 | assert.isTrue(load.called); 83 | assert.isNotTrue(result1); 84 | const result2 = await vm.changeWebmap("atom"); 85 | 86 | assert.isTrue(when.called); 87 | assert.isTrue(result2); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/unit/widgets/WebMapBrowser/components/PortalItem.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PortalItem, 3 | PortalItemProps 4 | } from "../../../../../src/widgets/WebMapBrowser/components/PortalItem"; 5 | 6 | import sinon from "sinon"; 7 | 8 | const { suite, test } = intern.getInterface("tdd"); 9 | const { assert } = intern.getPlugin("chai"); 10 | 11 | suite("widgets/WebMapBrowser/components/PortalItem", () => { 12 | test("Can create a Portal Item card from Portal Item props given", () => { 13 | const props: PortalItemProps = { 14 | key: 42, 15 | thumbnailUrl: "https://www.fillmurray.com/140/200", 16 | title: "You're Awesome", 17 | id: "4815162342", 18 | created: new Date(), 19 | isActive: true, 20 | onClick: sinon.stub() as any 21 | }; 22 | 23 | const vnode: JSX.IntrinsicElements = PortalItem(props, sinon); 24 | assert.equal(vnode.properties["data-id"], props.id); 25 | assert.lengthOf(vnode.children, 2); 26 | const img = vnode.children[0]; 27 | assert.equal(img.properties.src, props.thumbnailUrl); 28 | assert.equal(img.properties.alt, props.title); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "amd", 4 | "target": "es5", 5 | "noImplicitAny": true, 6 | "removeComments": true, 7 | "jsx": "react", 8 | "jsxFactory": "tsx", 9 | "moduleResolution": "node", 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "declaration": false, 13 | "allowJs": true, 14 | 15 | "lib": [ 16 | "dom", 17 | "es5", 18 | "es2015.promise" 19 | ], 20 | 21 | "outDir": "./dist/", 22 | "sourceMap": true, 23 | "noEmitOnError": false, 24 | "noUnusedLocals": true, 25 | "importHelpers": true 26 | }, 27 | "types": ["arcgis-js-api", "moment"], 28 | "include": ["src/*"], 29 | "files": ["./typings/extensions.d.ts"] 30 | } 31 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest", 4 | "tslint-eslint-rules", 5 | "tslint-config-prettier" 6 | ], 7 | "rules": { 8 | "interface-name": [false], 9 | "member-access": [ 10 | true, 11 | "no-public" 12 | ], 13 | "no-empty": false, 14 | "no-unused-expression": false, 15 | "no-submodule-imports": false, 16 | "object-literal-sort-keys": false, 17 | "one-line": [false], 18 | "trailing-comma": [false], 19 | "no-implicit-dependencies": false 20 | } 21 | } -------------------------------------------------------------------------------- /typings/extensions.d.ts: -------------------------------------------------------------------------------- 1 | declare module "calcite-web" { 2 | export const dropdown: () => void; 3 | } 4 | 5 | declare module "esri/moment" { 6 | import moment from "moment"; 7 | export = moment; 8 | } 9 | 10 | // dojo 11 | declare module "dojo/i18n!*" { 12 | const i18n: any; 13 | export = i18n; 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const AppCachePlugin = require("appcache-webpack-plugin"); 2 | const ArcGISPlugin = require("@arcgis/webpack-plugin"); 3 | const CleanWebpackPlugin = require("clean-webpack-plugin"); 4 | const HtmlWebPackPlugin = require("html-webpack-plugin"); 5 | const HtmlWebpackInlineSourcePlugin = require("html-webpack-inline-source-plugin"); 6 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 7 | const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"); 8 | const TerserPlugin = require("terser-webpack-plugin"); 9 | const WebpackPwaManifest = require("webpack-pwa-manifest"); 10 | const WorkboxPlugin = require("workbox-webpack-plugin"); 11 | 12 | const path = require("path"); 13 | 14 | module.exports = function(_, arg) { 15 | const config = { 16 | entry: { 17 | index: [ 18 | "./src/styles/main.scss", 19 | "./src/worker-config.ts", 20 | "./src/index.ts" 21 | ] 22 | }, 23 | output: { 24 | filename: "[name].[chunkhash].js", 25 | publicPath: "" 26 | }, 27 | optimization: { 28 | minimizer: [ 29 | new TerserPlugin({ 30 | cache: true, 31 | parallel: true, 32 | sourceMap: false, 33 | terserOptions: { 34 | output: { 35 | comments: false 36 | } 37 | } 38 | }), 39 | new OptimizeCSSAssetsPlugin({ 40 | cssProcessorOptions: { 41 | discardComments: { 42 | removeAll: true 43 | }, 44 | // Run cssnano in safe mode to avoid 45 | // potentially unsafe transformations. 46 | safe: true 47 | } 48 | }) 49 | ] 50 | }, 51 | module: { 52 | rules: [ 53 | { 54 | test: /\.tsx?$/, 55 | loader: "ts-loader", 56 | options: { 57 | transpileOnly: true 58 | } 59 | }, 60 | { 61 | test: /\.html$/, 62 | use: [ 63 | { 64 | loader: "html-loader", 65 | options: { minimize: false } 66 | } 67 | ], 68 | exclude: /node_modules/ 69 | }, 70 | { 71 | test: /\.scss$/, 72 | use: [ 73 | MiniCssExtractPlugin.loader, 74 | "css-loader", 75 | { 76 | loader: "resolve-url-loader", 77 | options: { includeRoot: true } 78 | }, 79 | "sass-loader?sourceMap" 80 | ] 81 | }, 82 | { 83 | test: /\.(jpe?g|png|gif|webp)$/, 84 | loader: "url-loader", 85 | options: { 86 | // Inline files smaller than 10 kB (10240 bytes) 87 | limit: 10 * 1024 88 | } 89 | }, 90 | { 91 | test: /\.(wsv|ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, 92 | use: [ 93 | { 94 | loader: "file-loader", 95 | options: { 96 | name: "build/[name].[ext]" 97 | } 98 | } 99 | ] 100 | } 101 | ] 102 | }, 103 | plugins: [ 104 | new CleanWebpackPlugin(), 105 | 106 | new ArcGISPlugin({ 107 | // disable provided asset loaders 108 | useDefaultAssetLoaders: false 109 | }), 110 | 111 | new HtmlWebPackPlugin({ 112 | title: "ArcGIS Maps App", 113 | template: "./src/index.ejs", 114 | filename: "./index.html", 115 | favicon: "./src/assets/favicon.ico", 116 | chunksSortMode: "none", 117 | inlineSource: ".(css)$", 118 | mode: arg.mode 119 | }), 120 | 121 | new HtmlWebPackPlugin({ 122 | template: "./src/oauth-callback.html", 123 | filename: "./oauth-callback.html", 124 | chunksSortMode: "none", 125 | inject: false 126 | }), 127 | 128 | new MiniCssExtractPlugin({ 129 | filename: "[name].[chunkhash].css", 130 | chunkFilename: "[id].css" 131 | }), 132 | 133 | new HtmlWebpackInlineSourcePlugin(), 134 | 135 | new WebpackPwaManifest({ 136 | name: "ArcGIS Application Template", 137 | short_name: "ArcGISWebApp", 138 | description: "My ArcGIS Template Application", 139 | background_color: "#0079c1", 140 | theme_color: "#0079c1", 141 | icons: [ 142 | { 143 | src: path.resolve("src/assets/icon.png"), 144 | sizes: [96, 128, 192, 256, 384, 512] // multiple sizes 145 | } 146 | ] 147 | }) 148 | ], 149 | resolve: { 150 | modules: [ 151 | path.resolve(__dirname, "/src"), 152 | path.resolve(__dirname, "node_modules/") 153 | ], 154 | extensions: [".ts", ".tsx", ".js", ".scss", ".css"] 155 | }, 156 | node: { 157 | process: false, 158 | global: false, 159 | fs: "empty" 160 | } 161 | }; 162 | 163 | if (arg.mode === "production") { 164 | config.plugins.push( 165 | new AppCachePlugin({ 166 | network: ["*"], 167 | settings: ["prefer-online"], 168 | output: "manifest.appcache" 169 | }) 170 | ); 171 | config.plugins.push( 172 | new WorkboxPlugin.GenerateSW({ 173 | // Exclude images from the precache 174 | exclude: [/\.(?:png|jpg|jpeg|svg|gif)$/], 175 | 176 | // Define runtime caching rules. 177 | runtimeCaching: [ 178 | { 179 | // Match any request ends with .png, .jpg, .jpeg or .svg. 180 | urlPattern: /\.(?:png|jpg|jpeg|svg|gif)$/, 181 | // Apply a cache-first strategy. 182 | handler: "cacheFirst" 183 | }, 184 | { 185 | // Match any fonts 186 | urlPattern: /\.(?:eot|ttf|jpeg|woff|woff2)$/, 187 | // Apply a cache-first strategy. 188 | handler: "cacheFirst" 189 | }, 190 | { 191 | urlPattern: new RegExp("https://js.arcgis.com"), 192 | handler: "staleWhileRevalidate" 193 | }, 194 | { 195 | urlPattern: new RegExp("https://basemaps.arcgis.com"), 196 | handler: "staleWhileRevalidate" 197 | }, 198 | { 199 | urlPattern: new RegExp("https://arcgis.com/sharing"), 200 | handler: "staleWhileRevalidate" 201 | } 202 | ] 203 | }) 204 | ); 205 | } 206 | 207 | return config; 208 | }; 209 | -------------------------------------------------------------------------------- /webpack.tests.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const ArcGISPlugin = require("@arcgis/webpack-plugin"); 4 | const CleanWebpackPlugin = require("clean-webpack-plugin"); 5 | 6 | module.exports = { 7 | entry: { 8 | init: "./src/index.ts", 9 | tests: "./tests/unit/all.ts" 10 | }, 11 | output: { 12 | filename: "[name].js", 13 | path: path.resolve(__dirname, "./~tmp") 14 | }, 15 | 16 | resolve: { 17 | modules: [ 18 | path.resolve(__dirname, "./src"), 19 | path.resolve(__dirname, "./tests"), 20 | "node_modules/" 21 | ], 22 | extensions: [".ts", ".tsx", ".js", ".css", ".scss"] 23 | }, 24 | 25 | module: { 26 | rules: [ 27 | { 28 | test: /\.tsx?$/, 29 | use: [ 30 | { 31 | loader: "ts-loader", 32 | options: { 33 | transpileOnly: true 34 | } 35 | } 36 | ] 37 | } 38 | ] 39 | }, 40 | 41 | plugins: [new CleanWebpackPlugin(), new ArcGISPlugin()], 42 | 43 | node: { 44 | process: false, 45 | global: false, 46 | fs: "empty" 47 | } 48 | }; 49 | --------------------------------------------------------------------------------