├── .editorconfig ├── .github └── workflows │ ├── nodejs.yml │ ├── publish.yml │ └── static.yml ├── .gitignore ├── .npmrc ├── README.md ├── docs ├── app.js ├── bundle.js ├── bundle.js.map ├── index.html └── style.css ├── index.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── bookmarks.js ├── formpopup.js ├── leaflet.bookmarks.less ├── leaflet.delegate.js ├── storage.js ├── storage │ ├── global.js │ ├── localstorage.js │ └── xhr.js └── string.js └── test └── bookmarks.test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | indent_style = space 6 | indent_size = 2 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | 10 | [*{.scss,.less}] 11 | indent_style = tab 12 | 13 | [Makefile] 14 | indent_style = tab 15 | indent_size = 4 16 | 17 | [*.json] 18 | indent_style = space 19 | indent_size = 2 20 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [master] 9 | pull_request: 10 | branches: [master] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | strategy: 17 | matrix: 18 | node-version: [18.x] 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | - run: npm ci 27 | - run: npm run build --if-present 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*.*.*" 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 18 15 | - run: npm ci 16 | - run: npm run build 17 | - uses: JS-DevTools/npm-publish@v1 18 | with: 19 | token: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["master"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v3 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v1 38 | with: 39 | # Upload entire repository 40 | path: "./docs" 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v1 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .externalToolBuilders/* 3 | .project 4 | *.properties 5 | .idea/* 6 | .cache 7 | .settings/* 8 | buildlogs 9 | node_modules 10 | bower_components/* 11 | 12 | build.properties 13 | .mocha-puppeteer 14 | 15 | test/lib/ 16 | dist 17 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Leaflet.Bookmarks 2 | ================= 3 | [](http://badge.fury.io/js/leaflet-bookmarks) 4 | [](http://badge.fury.io/bo/leaflet-bookmarks) [](https://circleci.com/gh/w8r/Leaflet.Bookmarks) 5 | 6 | Highly customizable Leaflet plugin for user-generated bookmarks, stored locally or on the server. 7 | 8 | See [demo and documentation](http://w8r.github.io/Leaflet.Bookmarks/) 9 | 10 | ## Description 11 | 12 | This is a highly customizable plugin for leaflet to allow users to drop bookmarks on your map and to store them locally or on server. It uses localstorage by default, but allows you to implement your own storage solution. You can also redefine the addition of bookmarks, their looks and fields. 13 | 14 | Right-click on the map, to add a new bookmark 15 | 16 | ## Usage 17 | 18 | Includes 19 | 20 | ```html 21 | 22 | 23 | 24 | ``` 25 | ## Put control on the map 26 | 27 | ```js 28 | var map = new L.Map(...); 29 | var control = new L.Control.Bookmarks().addTo(map); 30 | ``` 31 | 32 | ## Adding a bookmark 33 | 34 | How you trigger a bookmark addition is your own choice. For the demo I used the beautiful Leaflet.contextmenu plugin by @aratcliffe, but you are free to take any approach you want - it's based on the event bookmark:new dispatched by the map instance: 35 | 36 | ```js 37 | map.fire('bookmark:new', { 38 | latlng: new L.LatLng(..., ...) 39 | }); 40 | ``` 41 | 42 | If you want you can omit the naming step and add a bookmark straight to the list using a bookmark:add event. 43 | 44 | ```js 45 | map.fire('bookmark:add', { 46 | data: { 47 | id: 'XXXX' // make sure it's unique, 48 | name: 'Bookmark name', 49 | latlng: [lat, lng] // important, we're dealing with JSON here, 50 | your_key: 'your value' 51 | } 52 | }); 53 | ``` 54 | 55 | ## Events 56 | 57 | Triggered on **map**: 58 | * `bookmark:removed` - Bookmark has been removed from storage and interface 59 | * `bookmark:show` - bookmark selected from list or just created 60 | 61 | ## `GeoJSON` support 62 | 63 | There are GeoJSON import/export methods provided for convinence and use within the storage methods of your choice 64 | 65 | * `.bookmarkToFeature(bookmark)` 66 | Use it on a single bookmark if you want to convert it into geoJSON `Feature` before send 67 | * `.toGeoJSON()` 68 | Exports the whole list into GeoJSON, uses `.bookmarkToFeature` 69 | * `.fromGeoJSON(geojson)` 70 | Uses properties as the bookmark contents, geometry as the location. GeoJSON `Point` expected, you can change it for a different type of geometry, if you want, then you'll have to take care of the centroid routines. 71 | 72 | ## Customizing 73 | 74 | ### localStorage or variable storage 75 | 76 | The control uses localStorage by default. Your bookmarks will be stored in prefixed key-value pairs. You can customize the prefix if you want to 77 | 78 | ```js 79 | var control = new L.Control.Bookmarks({ 80 | name: 'your-storage-prefix', // defaults to 'leaflet-bookmarks' 81 | localStorage: false // if you want to use local variable for storage 82 | }); 83 | ``` 84 | 85 | P.S. You can access the storage directly through the control: 86 | 87 | ```js 88 | control._storage.getItem(key, callback); 89 | control._storage.setItem(key, value, callback); 90 | control._storage.removeItem(key, callback); 91 | control._storage.getAllItems(callback); 92 | ``` 93 | 94 | ### Custom storage(e.g AJAX) 95 | 96 | I intentionally didn't add an engine for anything else than localStorage and local variable storage, so you could use your own xhr functions. To do that, you have to pass the interface to your storage to the control like this: 97 | 98 | ```js 99 | var control = new L.Control.Bookmarks({ 100 | storage: { 101 | getItem: function(id, callback){ 102 | $.ajax({ 103 | url: '/bookmarks/' + id, 104 | type: 'GET', 105 | dataType: 'json', 106 | success: callback 107 | }); 108 | }, 109 | setItem: function(id, value, callback){ 110 | $.ajax({ 111 | url: '/bookmarks/' + id, 112 | type: 'PUT', 113 | data: value, 114 | dataType: 'json', 115 | success: callback 116 | }); 117 | }, 118 | removeItem: function(id, callback){ 119 | $.ajax({ 120 | url: '/bookmarks/' + id, 121 | type: 'DELETE', 122 | dataType: 'json', 123 | success: callback 124 | }); 125 | }, 126 | getAllItems: function(callback){ 127 | $.ajax({ 128 | url: '/bookmarks/', 129 | type: 'GET', 130 | dataType: 'json', 131 | success: callback 132 | }); 133 | } 134 | } 135 | }).addTo(map); 136 | ``` 137 | 138 | ### Custom templates 139 | 140 | Pass those into the options if you want to customize the popup or list templates. Proceed with caution 141 | 142 | ```js 143 | { 144 | // list item MUST contain `data-id` attribute, 145 | // or provide your own `options.getBookmarkFromListItem(listItem)` method 146 | bookmarkTemplate: '
{{ latlng }}
', 181 | 182 | // here you extract them for the template. 183 | // note - you have access to controls methods and fields here 184 | getPopupContent: function(bookmark) { 185 | return L.Util.template(this.options.popupTemplate, { 186 | latlng: this.formatCoords(bookmark.latlng), 187 | name: bookmark.name 188 | }); 189 | }, 190 | 191 | // here you can filter bookmarks that you get from 192 | // the storage or a user, make sure it returns an Array 193 | filterBookmarks: function(bookmarks){ ... }, 194 | } 195 | ``` 196 | 197 | You can customize the bookmark add form too, pass that to the control options: 198 | 199 | ```js 200 | formPopup: { 201 | className: 'leaflet-bookmarks-form-popup', 202 | templateOptions: { 203 | formClass: 'leaflet-bookmarks-form', 204 | inputClass: 'leaflet-bookmarks-form-input', 205 | coordsClass: 'leaflet-bookmarks-form-coords', 206 | submitClass: 'leaflet-bookmarks-form-submit', 207 | inputPlaceholder: 'Bookmark name', 208 | submitText: '+' 209 | }, 210 | getBookmarkData: function(){ 211 | var input = this._contentNode.querySelector('.' + 212 | this.options.templateOptions.inputClass); 213 | ... 214 | return { 215 | id: 'YOUR_UNIQUE_ID', 216 | name: 'Bookmark name', 217 | your_custom_field: ... // get it from the form inputs 218 | latlng: this._source.getLatLng() // get it from the marker 219 | }; 220 | }, 221 | onRemove: function(bookmark, callback){ 222 | /* use that to add confirmation menus 223 | when removing a bookmark */ 224 | }, 225 | generateNames: true, // generate unique name if it's not provided by the user 226 | generateNamesPrefix: 'Bookmark ', 227 | template: '', 234 | } 235 | ``` 236 | 237 | ### Editing 238 | 239 | You can enable bookmarks editing/removal by putting a flag in the bookmark object 240 | 241 | ```js 242 | { 243 | name: '', 244 | id: 'XXX', 245 | latlng: [lat, lng], 246 | editable: true, 247 | removable: true 248 | } 249 | ``` 250 | 251 | This will enable a menu on popup to remove or edit the bookmark. 252 | Presence of menu items will is defined by those params also 253 | 254 |  255 | 256 | ### Removal 257 | 258 | You can pass a custom confirm function to the control, so you could handle confirmation menus 259 | 260 | ``` 261 | onRemove: function(bookmark, callback){ 262 | if(confirm('Are you really sure?')){ 263 | if(bookmark.name === 'Bamby') { 264 | alert('Keep your hands away!'); 265 | callback(false); // won't be removed 266 | } else { 267 | callback(true); // will be removed 268 | } 269 | } else { 270 | callback(false); 271 | } 272 | } 273 | ``` 274 | 275 | ### `L.Util._template` 276 | 277 | Small template function used by this project. It's a simple implementation of @lodash templates, using mustache interpolation syntax. You get it as a souvenir. 278 | 279 | ```js 280 | L.Util._template('Whereof one cannot {{ data.action }}, thereof one must keep {{ data.instead }}', { data: { action: 'speak', instead: 'silent' }}); 281 | // -> "Whereof one cannot speak, thereof one must keep silent" 282 | ``` 283 | ## Authors and Contributors 284 | 285 | Alexander Milevski 286 | 287 | ## License 288 | 289 | MIT 290 | 291 | ## Changelog 292 | 293 | * **0.4.0** 294 | * Leaflet 1.6 support 295 | * **0.3.0** 296 | * Fixed some bugs 297 | * Migrated to rollup build system and ESM modules 298 | * **0.2.0** 299 | * Editing/removal funtionality 300 | * "Add new" button 301 | * Tests added 302 | * **0.1.5** 303 | * GeoJSON support 304 | * **0.1.3** 305 | * Different layout when in `topleft` position 306 | * Scroll to bookmark on addition 307 | * **0.1.2** 308 | * Remove marker when bookmark is removed 309 | * **0.1.0** 310 | * npm & bower packages published 311 | * **0.0.2** 312 | * Zoom level ztored and used by default 313 | * Remove button flickering fixed 314 | * Add bookmark UX: now you can show the newly created bookmark right away 315 | * **0.0.1** 316 | * Initial release 317 | 318 | -------------------------------------------------------------------------------- /docs/app.js: -------------------------------------------------------------------------------- 1 | import L from 'leaflet'; 2 | import '../index'; 3 | import 'leaflet-contextmenu'; 4 | import 'leaflet-modal'; 5 | 6 | const map = window.map = new L.Map('map', { 7 | contextmenu: true, 8 | contextmenuItems: [{ 9 | text: 'Bookmark this position', 10 | callback: function(evt) { 11 | this.fire('bookmark:new', { latlng: evt.latlng }); 12 | } 13 | }] 14 | }).setView([22.2670, 114.188], 13); 15 | 16 | L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { 17 | attribution: '© ' + 18 | 'OpenStreetMap contributors' 19 | }).addTo(map); 20 | 21 | // var bookmarksControl = global.bookmarksControlRight = new L.Control.Bookmarks({ 22 | // position: 'topright' 23 | // }); 24 | // map.addControl(bookmarksControl); 25 | 26 | const bookmarksControl = new L.Control.Bookmarks({ 27 | position: 'topleft', 28 | onRemove: function(bookmark, callback) { 29 | map.fire('modal', { 30 | title: 'Are you sure?', 31 | content: 'Do you wnat to remove bookmark ' + bookmark.name + '?
', 32 | template: ['{{ latlng }}, {{ zoom }}