├── .gitignore ├── osmtags-128.png ├── osmtags-48.png ├── osmtags-96.png ├── osmtags-editor-screen.png ├── land.html ├── land.js ├── sidebar-listener.js ├── CHANGELOG.md ├── README.md ├── manifest3.json ├── manifest.json ├── LICENSE ├── osm-auth.iife.min.js └── osmorg-editor.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | *.swp 3 | oauth-keys.js 4 | -------------------------------------------------------------------------------- /osmtags-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/osmtags-editor/HEAD/osmtags-128.png -------------------------------------------------------------------------------- /osmtags-48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/osmtags-editor/HEAD/osmtags-48.png -------------------------------------------------------------------------------- /osmtags-96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/osmtags-editor/HEAD/osmtags-96.png -------------------------------------------------------------------------------- /osmtags-editor-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/osmtags-editor/HEAD/osmtags-editor-screen.png -------------------------------------------------------------------------------- /land.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Please wait. 7 | 8 | -------------------------------------------------------------------------------- /land.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('load', function(e) { 2 | opener.authComplete(window.location.href); 3 | window.close(); 4 | }); 5 | -------------------------------------------------------------------------------- /sidebar-listener.js: -------------------------------------------------------------------------------- 1 | chrome.webNavigation.onHistoryStateUpdated.addListener(details => { 2 | const url = details.url; 3 | if (url.indexOf('//www.openstreetmap.org/') < 0) return; 4 | if (url.indexOf('/node/') > 0 || url.indexOf('/way/') > 0 || url.indexOf('/relation/') > 0) { 5 | chrome.tabs.sendMessage(details.tabId, {type: 'osm_url', url: url}); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Tags Editor Changes 2 | 3 | ## 1.3 (2024-03-16) 4 | 5 | * Changeset comments now split for tag modification types (thanks @Henry00572) 6 | 7 | ## 1.2 (2024-02-25) 8 | 9 | * Fixed saving changes with a `%` symbol inside tags. 10 | * Newline character is replaced with `\\` for editing. 11 | * Change tags ordering from Z-A to A-Z (thanks @Dimitar5555). 12 | * Cancel button is red (thanks @Dimitar5555). 13 | 14 | ## 1.1 (2022-07-25) 15 | 16 | * Made adding the button more robust. 17 | * Fixed the issue with wrong tags on history pages. 18 | 19 | ## 1.0 (2022-07-20) 20 | 21 | * Initial version. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenStreetMap Tags Editor 2 | 3 | This is a WebExtension that adds an "Edit Tags" button to all node, way, 4 | and relation pages on the [osm.org](https://www.openstreetmap.org) website. 5 | The button opens a text area for editing raw tags (in form `key=value`) with 6 | a "Save" button to upload changes. 7 | 8 | ![Screenshot of the editor in action](osmtags-editor-screen.png) 9 | 10 | ## How to Install 11 | 12 | Either open the development console and use the source code, or head over 13 | to your browser's extension store: 14 | 15 | * [Mozilla Firefox](https://addons.mozilla.org/firefox/addon/openstreetmap-tags-editor/) 16 | * [Google Chrome](https://chrome.google.com/webstore/detail/openstreetmap-tags-editor/gcbcbndjajojkneicbfdaegcghgbdjnj) 17 | 18 | ## Author and License 19 | 20 | Written by Ilya Zverev, published under MIT license. 21 | -------------------------------------------------------------------------------- /manifest3.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "OpenStreetMap Tags Editor", 4 | "version": "1.4", 5 | 6 | "description": "Adds an \"edit tags\" button to every object on osm.org.", 7 | 8 | "icons": { 9 | "48": "osmtags-48.png", 10 | "96": "osmtags-96.png", 11 | "128": "osmtags-128.png" 12 | }, 13 | 14 | "content_scripts": [ 15 | { 16 | "matches": [ 17 | "https://www.openstreetmap.org/*" 18 | ], 19 | "js": [ 20 | "osmorg-editor.js", 21 | "osm-auth.iife.min.js" 22 | ] 23 | } 24 | ], 25 | 26 | "background": { 27 | "service_worker": "sidebar-listener.js" 28 | }, 29 | 30 | "web_accessible_resources": [ 31 | { 32 | "resources": ["land.html", "land.js"], 33 | "matches": ["https://www.openstreetmap.org/*"] 34 | } 35 | ], 36 | 37 | "permissions": [ 38 | "storage", 39 | "webNavigation" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "OpenStreetMap Tags Editor", 4 | "version": "1.4", 5 | 6 | "description": "Adds an \"edit tags\" button to every object on osm.org.", 7 | 8 | "icons": { 9 | "48": "osmtags-48.png", 10 | "96": "osmtags-96.png", 11 | "128": "osmtags-128.png" 12 | }, 13 | 14 | "content_scripts": [ 15 | { 16 | "matches": [ 17 | "https://www.openstreetmap.org/*" 18 | ], 19 | "js": [ 20 | "osmorg-editor.js", 21 | "osm-auth.iife.min.js" 22 | ] 23 | } 24 | ], 25 | 26 | "background": { 27 | "scripts": ["sidebar-listener.js"], 28 | "persistent": false 29 | }, 30 | 31 | "web_accessible_resources": ["land.html", "land.js"], 32 | 33 | "permissions": [ 34 | "storage", 35 | "webNavigation" 36 | ], 37 | 38 | "browser_specific_settings": { 39 | "gecko": { 40 | "id": "osmorg-editor@ilya.zverev.info" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Ilya Zverev 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /osm-auth.iife.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Skipped minification because the original files appears to be already minified. 3 | * Original file: /npm/osm-auth@2.0.0/dist/osm-auth.iife.js 4 | * 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files 6 | */ 7 | var osmAuth=(function(){var Se=Object.create;var q=Object.defineProperty;var ye=Object.getOwnPropertyDescriptor;var Oe=Object.getOwnPropertyNames;var Ae=Object.getPrototypeOf,Pe=Object.prototype.hasOwnProperty;var d=function(e,t){return function(){return t||e((t={exports:{}}).exports,t),t.exports}},je=function(e,t){for(var r in t)q(e,r,{get:t[r],enumerable:!0})},U=function(e,t,r,a){if(t&&typeof t=="object"||typeof t=="function")for(var i=Oe(t),o=0,l=i.length,u;o=0;t--){var r=S().key(t);e(W(r),r)}}function Ze(e){return S().removeItem(e)}function Ye(){return S().clear()}});var Y=d(function(Lt,Z){var Ve=_(),et=Ve.Global;Z.exports={name:"oldFF-globalStorage",read:tt,write:rt,each:Q,remove:nt,clearAll:it};var w=et.globalStorage;function tt(e){return w[e]}function rt(e,t){w[e]=t}function Q(e){for(var t=w.length-1;t>=0;t--){var r=w.key(t);e(w[r],r)}}function nt(e){return w.removeItem(e)}function it(){Q(function(e,t){delete w[e]})}});var te=d(function(zt,ee){var at=_(),M=at.Global;ee.exports={name:"oldIE-userDataStorage",write:ot,read:ut,each:ct,remove:st,clearAll:ft};var A="storejs",O=M.document,P=pt(),V=(M.navigator?M.navigator.userAgent:"").match(/ (MSIE 8|MSIE 9|MSIE 10)\./);function ot(e,t){if(!V){var r=L(e);P(function(a){a.setAttribute(r,t),a.save(A)})}}function ut(e){if(!V){var t=L(e),r=null;return P(function(a){r=a.getAttribute(t)}),r}}function ct(e){P(function(t){for(var r=t.XMLDocument.documentElement.attributes,a=r.length-1;a>=0;a--){var i=r[a];e(t.getAttribute(i.name),i.name)}})}function st(e){var t=L(e);P(function(r){r.removeAttribute(t),r.save(A)})}function ft(){P(function(e){var t=e.XMLDocument.documentElement.attributes;e.load(A);for(var r=t.length-1;r>=0;r--)e.removeAttribute(t[r].name);e.save(A)})}var lt=new RegExp("[!\"#$%&'()*+,/\\\\:;<=>?@[\\]^`{|}~]","g");function L(e){return e.replace(/^\d/,"___$&").replace(lt,"___")}function pt(){if(!O||!O.documentElement||!O.documentElement.addBehavior)return null;var e="script",t,r,a;try{r=new ActiveXObject("htmlfile"),r.open(),r.write("<"+e+">document.w=window'),r.close(),t=r.w.frames[0].document,a=t.createElement("div")}catch(i){a=O.createElement("div"),t=O.body}return function(i){var o=[].slice.call(arguments,0);o.unshift(a),t.appendChild(a),a.addBehavior("#default#userData"),a.load(A),i.apply(this,o),t.removeChild(a)}}});var ue=d(function(Ut,oe){var re=_(),gt=re.Global,dt=re.trim;oe.exports={name:"cookieStorage",read:vt,write:ht,each:ne,remove:ie,clearAll:mt};var F=gt.document;function vt(e){if(!e||!ae(e))return null;var t="(?:^|.*;\\s*)"+escape(e).replace(/[\-\.\+\*]/g,"\\$&")+"\\s*\\=\\s*((?:[^;](?!;))*[^;]?).*";return unescape(F.cookie.replace(new RegExp(t),"$1"))}function ne(e){for(var t=F.cookie.split(/; ?/g),r=t.length-1;r>=0;r--)if(!!dt(t[r])){var a=t[r].split("="),i=unescape(a[0]),o=unescape(a[1]);e(o,i)}}function ht(e,t){!e||(F.cookie=escape(e)+"="+escape(t)+"; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/")}function ie(e){!e||!ae(e)||(F.cookie=escape(e)+"=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/")}function mt(){ne(function(e,t){ie(t)})}function ae(e){return new RegExp("(?:^|;\\s*)"+escape(e).replace(/[\-\.\+\*]/g,"\\$&")+"\\s*\\=").test(F.cookie)}});var fe=d(function(Dt,se){var _t=_(),wt=_t.Global;se.exports={name:"sessionStorage",read:ce,write:bt,each:xt,remove:St,clearAll:yt};function y(){return wt.sessionStorage}function ce(e){return y().getItem(e)}function bt(e,t){return y().setItem(e,t)}function xt(e){for(var t=y().length-1;t>=0;t--){var r=y().key(t);e(ce(r),r)}}function St(e){return y().removeItem(e)}function yt(){return y().clear()}});var pe=d(function($t,le){le.exports={name:"memoryStorage",read:Ot,write:At,each:Pt,remove:jt,clearAll:Ft};var b={};function Ot(e){return b[e]}function At(e,t){b[e]=t}function Pt(e){for(var t in b)b.hasOwnProperty(t)&&e(b[t],t)}function jt(e){delete b[e]}function Ft(e){b={}}});var de=d(function(Xt,ge){ge.exports=[H(),Y(),te(),ue(),fe(),pe()]});var ve=d(function(exports,module){typeof JSON!="object"&&(JSON={});(function(){"use strict";var rx_one=/^[\],:{}\s]*$/,rx_two=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,rx_three=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,rx_four=/(?:^|:|,)(?:\s*\[)+/g,rx_escapable=/[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,rx_dangerous=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;function f(e){return e<10?"0"+e:e}function this_value(){return this.valueOf()}typeof Date.prototype.toJSON!="function"&&(Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},Boolean.prototype.toJSON=this_value,Number.prototype.toJSON=this_value,String.prototype.toJSON=this_value);var gap,indent,meta,rep;function quote(e){return rx_escapable.lastIndex=0,rx_escapable.test(e)?'"'+e.replace(rx_escapable,function(t){var r=meta[t];return typeof r=="string"?r:"\\u"+("0000"+t.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+e+'"'}function str(e,t){var r,a,i,o,l=gap,u,n=t[e];switch(n&&typeof n=="object"&&typeof n.toJSON=="function"&&(n=n.toJSON(e)),typeof rep=="function"&&(n=rep.call(t,e,n)),typeof n){case"string":return quote(n);case"number":return isFinite(n)?String(n):"null";case"boolean":case"null":return String(n);case"object":if(!n)return"null";if(gap+=indent,u=[],Object.prototype.toString.apply(n)==="[object Array]"){for(o=n.length,r=0;r= baseParts.length) return null; 31 | return { 32 | type: baseParts[i], 33 | ref: baseParts[i + 1], 34 | part: baseParts[i] + '/' + baseParts[i + 1] 35 | }; 36 | } 37 | 38 | // Queries OSM API for the element data and populates the text area. 39 | function queryForTags() { 40 | const typeRef = getTypeAndRef(); 41 | if (!typeRef) { 42 | closeEditor(); 43 | return; 44 | } 45 | const url = apiBase + typeRef.part + '.json'; 46 | fetch(url).then(response => response.json()).then(response => { 47 | // If the element was deleted, or there have been an error, close the editor. 48 | if (!response.elements) { 49 | closeEditor(); 50 | return; 51 | } 52 | 53 | // Store the original object for uploading it later. 54 | originalObject = response.elements[0]; 55 | let tags = []; 56 | if (originalObject.tags) { 57 | for (const [k, v] of Object.entries(originalObject.tags)) { 58 | tags.push(k + ' = ' + v.replaceAll('\n', '\\\\')); 59 | } 60 | } 61 | textArea.value = tags.join('\n');; 62 | textArea.focus(); 63 | // We built the editor with a disabled saving button. 64 | saveButton.disabled = null; 65 | }).catch(err => { 66 | // On error we just close the editor without an error message. 67 | console.log({dataFetchingError: err}); 68 | closeEditor(); 69 | }); 70 | } 71 | 72 | // Reads new tags off the text area and returns a json object.. 73 | function buildTags() { 74 | const lines = textArea.value.split('\n'); 75 | let json = {}; 76 | for (let i = 0; i < lines.length; i++) { 77 | const line = lines[i]; 78 | const eqPos = line.indexOf('='); 79 | if (eqPos <= 0 || eqPos == line.length - 1) continue; 80 | const k = line.substring(0, eqPos).trim(); 81 | const v = line.substring(eqPos + 1).trim(); 82 | if (v == '' || k == '') continue; 83 | json[k] = v.replaceAll('\\\\', '\n'); 84 | } 85 | return json; 86 | } 87 | 88 | // Returns an object with three lists of keys that were added/changed/removed between newTags and oldTags. 89 | function getModifiedKeys(newTags, oldTags) { 90 | let keys = {added: [], 91 | changed: [], 92 | removed: []}; 93 | for (const [k, v] of Object.entries(newTags)) { 94 | if (!oldTags.hasOwnProperty(k)) { 95 | keys.added.push(k); 96 | } else if (oldTags[k] != v) { 97 | keys.changed.push(k); 98 | } 99 | } 100 | for (const [k, v] of Object.entries(oldTags)) { 101 | if (!newTags.hasOwnProperty(k)) 102 | keys.removed.push(k); 103 | } 104 | return keys; 105 | } 106 | 107 | // Converts an object to an XML string. 108 | function tagsToXml(doc, node, tags) { 109 | for (const [k, v] of Object.entries(tags)) { 110 | let tag = doc.createElement('tag'); 111 | tag.setAttribute('k', k); 112 | tag.setAttribute('v', v); 113 | node.appendChild(tag); 114 | } 115 | } 116 | 117 | // Builds and returns a version of the object to upload with new tags. 118 | function buildObject(doc, tags) { 119 | let elem = doc.createElement(originalObject.type); 120 | elem.setAttribute('version', originalObject.version); 121 | elem.setAttribute('id', originalObject.id); 122 | elem.setAttribute('visible', 'true'); 123 | if (originalObject.lat && originalObject.lon) { 124 | elem.setAttribute('lat', originalObject.lat); 125 | elem.setAttribute('lon', originalObject.lon); 126 | } 127 | if (originalObject.nodes) { 128 | for (let i = 0; i < originalObject.nodes.length; i++) { 129 | let nd = doc.createElement('nd'); 130 | nd.setAttribute('ref', originalObject.nodes[i]); 131 | elem.appendChild(nd); 132 | } 133 | } else if (originalObject.members) { 134 | for (let i = 0; i < originalObject.members.length; i++) { 135 | let member = doc.createElement('member'); 136 | member.setAttribute('type', originalObject.members[i].type); 137 | member.setAttribute('ref', originalObject.members[i].ref); 138 | member.setAttribute('role', originalObject.members[i].role); 139 | elem.appendChild(member); 140 | } 141 | } 142 | tagsToXml(doc, elem, tags); 143 | return elem; 144 | } 145 | 146 | // Creates an OSM-Auth object instance. 147 | function makeAuth() { 148 | return osmAuth.osmAuth({ 149 | // Put your own credentials here. 150 | client_id: "FwA", 151 | client_secret: "ZUq", 152 | // Hopefully this page is never used. 153 | redirect_uri: chrome.runtime.getURL('land.html'), 154 | scope: "write_api", 155 | auto: true 156 | }); 157 | } 158 | 159 | // Uploads changes made in the text area, if any. 160 | function uploadTags() { 161 | setError(); 162 | if (!originalObject) return; 163 | const xmlHeader = ''; 164 | const newTags = buildTags(); 165 | const modifiedKeys = getModifiedKeys(newTags, originalObject['tags'] || {}); 166 | if (modifiedKeys.added.length + modifiedKeys.changed.length + modifiedKeys.removed.length == 0) { 167 | // If the tags are intact, just close the editor. 168 | closeEditor(); 169 | return; 170 | } 171 | // Iterate over possible actions and build the changeset comment 172 | // For example 'Added smoothness; Changed surface, ref of way 12345'. 173 | const possibleActions = [['added', 'to'], ['changed', 'of'], ['removed', 'from']]; 174 | let changesetComment = ''; 175 | let lastPreposition = ''; 176 | for (const [action, preposition] of possibleActions) { 177 | if (modifiedKeys[action].length > 0) { 178 | // actionComment might be 'Changed surface, ref' 179 | const actionComment = action.charAt(0).toUpperCase() + action.slice(1) + ' ' + 180 | modifiedKeys[action].join(', '); 181 | lastPreposition = preposition; 182 | // Append the action comment to the changeset comment 183 | changesetComment += (changesetComment.length > 0 ? '; ' : '') + actionComment; 184 | } 185 | } 186 | const typeRef = getTypeAndRef(); 187 | changesetComment += ' ' + lastPreposition + ' ' + typeRef.type + ' ' + typeRef.ref + '.'; 188 | // Meet the 255 character limit 189 | if (changesetComment.length > 255) 190 | changesetComment = 'Changed tags of ' + typeRef.type + ' ' + typeRef.ref + '.'; 191 | 192 | // Prepare changeset payload. 193 | const changesetTags = { 194 | 'created_by': 'Osm.Org Tags Editor', 195 | 'comment': changesetComment 196 | }; 197 | let changesetPayload = document.implementation.createDocument(null, 'osm'); 198 | let cs = changesetPayload.createElement('changeset'); 199 | changesetPayload.documentElement.appendChild(cs); 200 | tagsToXml(changesetPayload, cs, changesetTags); 201 | const chPayloadStr = xmlHeader + new XMLSerializer().serializeToString(changesetPayload); 202 | 203 | // Open changeset. 204 | const auth = makeAuth(); 205 | auth.xhr({ 206 | method: 'PUT', 207 | path: apiBase + 'changeset/create', 208 | prefix: false, // not relying on the default prefix. 209 | headers: { 'Content-Type': 'application/xml' }, 210 | content: chPayloadStr 211 | }, function(err, result) { 212 | if (err) { 213 | console.log({changesetError: err}); 214 | if (err.type) 215 | setError('Could not create a changeset because of a network error'); 216 | else 217 | setError('Could not create a changeset. Error ' + err.status + ': ' + err.responseText); 218 | return; 219 | } 220 | const changesetId = result; 221 | 222 | // Create XML with element payload. 223 | let elemPayload = document.implementation.createDocument(null, 'osm'); 224 | let elem = buildObject(elemPayload, newTags); 225 | elem.setAttribute('changeset', changesetId); 226 | elemPayload.documentElement.appendChild(elem); 227 | const elemPayloadStr = xmlHeader + new XMLSerializer().serializeToString(elemPayload); 228 | 229 | // Upload the new element. 230 | auth.xhr({ 231 | method: 'PUT', 232 | path: apiBase + typeRef.part, 233 | prefix: false, 234 | headers: { 'Content-Type': 'application/xml' }, 235 | content: elemPayloadStr 236 | }, function(err, result) { 237 | // Close the changeset regardless. 238 | auth.xhr({ 239 | method: 'PUT', 240 | path: apiBase + 'changeset/' + changesetId + '/close', 241 | prefix: false 242 | }, function(err1, result) { 243 | // Only after closing the changeset reload the page. 244 | // Otherwise the request gets cancelled, and the changeset is not closed. 245 | if (!err) { 246 | closeEditor(); 247 | window.location.reload(); 248 | } 249 | }); 250 | 251 | if (err) { 252 | console.log({uploadError: err}); 253 | if (err.type) 254 | setError('Could not upload data because of a network error'); 255 | else 256 | setError('Could not upload data. Error ' + err.status + ': ' + err.responseText); 257 | } 258 | }); 259 | }); 260 | } 261 | 262 | // Build the editor panel with all the fields. 263 | function createEditorPanel() { 264 | // The text area is copied from the "create note" action. 265 | textArea = document.createElement('textarea'); 266 | textArea.className = 'form-control'; 267 | textArea.rows = 10; 268 | textArea.cols = 40; 269 | textArea.style.fontFamily = 'monospace'; 270 | 271 | // Again, this is the common styling of osm.org's buttons. 272 | saveButton = document.createElement('input'); 273 | saveButton.type = 'submit'; 274 | saveButton.name = 'save'; 275 | saveButton.className = 'btn btn-primary'; 276 | saveButton.value = 'Save'; 277 | // Disabled until we load the text area contents. 278 | saveButton.disabled = '1'; 279 | 280 | let cancelButton = document.createElement('input'); 281 | cancelButton.type = 'submit'; 282 | cancelButton.name = 'cancel'; 283 | cancelButton.className = 'btn btn-danger'; 284 | cancelButton.value = 'Cancel'; 285 | cancelButton.addEventListener('click', function(e) { 286 | // Nothing to save, just return the original panel. 287 | closeEditor(); 288 | e.preventDefault(); 289 | }); 290 | 291 | // As the rest of the website, it uses a form to catch the submit event. 292 | editorArea = document.createElement('form'); 293 | editorArea.action = '#'; 294 | editorArea.addEventListener('submit', function(e) { 295 | uploadTags(); 296 | e.preventDefault(); 297 | return false; 298 | }); 299 | 300 | // Area 1 is the text area, area 2 is the button row. 301 | let editorArea1 = document.createElement('div'); 302 | editorArea1.className = 'form-group'; 303 | editorArea1.append(textArea); 304 | let editorArea2 = document.createElement('div'); 305 | editorArea2.className = 'btn-wrapper'; 306 | editorArea2.append(saveButton); 307 | editorArea2.append(' '); 308 | editorArea2.append(cancelButton); 309 | 310 | errorPane = document.createElement('div'); 311 | errorPane.style.color = 'darkred'; 312 | errorPane.style.paddingBottom = '20px'; 313 | 314 | editorArea.append(editorArea1); 315 | editorArea.append(errorPane); 316 | editorArea.append(editorArea2); 317 | return editorArea; 318 | } 319 | 320 | // Checks for the authentication, and replaces the info panel with the editor. 321 | function openEditor() { 322 | // Do not open the editor twice. 323 | if (editorArea) return; 324 | 325 | // Check for authentication (snatched from the iD editor). 326 | const auth = makeAuth(); 327 | if (!auth.authenticated()) { 328 | if (document.getElementById('open-id-editor-panel')) return; 329 | let idPanel = document.createElement('div'); 330 | idPanel.id = 'open-id-editor-panel'; 331 | idPanel.style.color = 'darkred'; 332 | idPanel.style.paddingBottom = '20px'; 333 | idPanel.innerHTML = 'Please open iD editor first to generate an authentication token.'; 335 | 336 | const actions = document.querySelector('.secondary-actions'); 337 | actions.parentNode.insertBefore(idPanel, actions); 338 | return; 339 | } 340 | 341 | // If authenticated, replace the info block with the editor panel. 342 | originalPanel.replaceWith(createEditorPanel()); 343 | 344 | // And send a query to download tags. 345 | queryForTags(); 346 | } 347 | 348 | // Adds the "Edit Tags" button if there is a place for it, and it's not already there. 349 | function addTheButton() { 350 | // Prevent duplicate button 351 | if (document.querySelector('.edit_tags_class')) return true; 352 | 353 | const actions = document.querySelector('#sidebar_content > .secondary-actions'); 354 | originalPanel = document.querySelector('#sidebar_content > div.border-bottom.border-secondary-subtle'); 355 | if (!actions || !originalPanel) return false; 356 | 357 | let atag = document.createElement('a'); 358 | atag.className = 'edit_tags_class'; 359 | atag.href = '#'; 360 | atag.append('Edit Tags'); 361 | atag.addEventListener('click', function(e) { 362 | openEditor(); 363 | e.preventDefault(); 364 | return false; 365 | }) 366 | 367 | actions.append(' · '); 368 | actions.append(atag); 369 | return true; 370 | } 371 | 372 | // Called from the background service, adds the "Edit Tags" button. 373 | function updateButton(data, sender, sendResponse) { 374 | // Calling multiple times in case the sidebar doesn't load. 375 | if (!addTheButton()) { 376 | window.setTimeout(function() { 377 | if (!addTheButton()) { 378 | window.setTimeout(addTheButton, 1000); 379 | } 380 | }, 300); 381 | } 382 | } 383 | 384 | // When opening the element page directly, the background process does not run. 385 | if (location.pathname.includes('/node/') || location.pathname.includes('/way/') || location.pathname.includes('/relation/')) { 386 | updateButton(); 387 | } 388 | 389 | // Listen to messages from the background script. 390 | chrome.runtime.onMessage.addListener(updateButton); 391 | --------------------------------------------------------------------------------