User:Glodenox/Userscript Guidelines View history

Revision as of 21:34, 20 September 2017 by Glodenox (talk | contribs) (Added MutationObserver example)

Warning: this page is very technical of nature. The information detailed here is not needed to make use of userscripts created by the Waze Community.

What is a userscript technically?

In order to write a proper userscript, you'll need at least some basic knowledge of JavaScript. This goes beyond the scope of this page, but there are several resources available online that help you with learning the language.

A userscript consists of two parts:

  • A block of meta data that details the script's name, its namespace, where it should be executed, when it should be executed and which permissions it needs to run (amongst others). This looks like the code below:
// ==UserScript==
// @propertyName propertyValue
// ==/UserScript==
  • A block of JavaScript code that contains how the userscript works.

UserScripts are executed through add-ons to your browser. The most well-known add-ons are GreaseMonkey and TamperMonkey. It is generally adviced to verify your script works in both of them (luckily that is usually the case). These add-ons make the block of JavaScript code execute at a certain moment during the loading of the page. There are certain restrictions put on this code for security reasons. This code is restricted in how it can perform cross-domain requests, for example.

General JavaScript remarks

It is usually advised to follow the guidelines like those laid out at the JavaScript Toolbox or this blog by Sarfraz Ahmed. Two remarks in particular are important for userscripts in the editor: make sure you use the var keyword when declaring variables (otherwise they get put in the global scope where they may conflict with other scripts) and when you add an event listener to the map somewhere, make sure your code doesn't break (null pointers, for example) as this will swallow the event and will prevent other code (including the editor itself) from reacting to that event. Use a try ... catch if you want to play safe.

You don't need to encapsulate your code into a self-executing anonymous function any more. While early versions of the userscript add-ons used to simply insert the userscript at the bottom of the page, nowadays these are executed in a separate context that still allows access to the global scope on the page, but doesn't expose the variables you create with the var keyword.

Also it is best not to rely too much on setTimeout() or setInterval() for the inner workings of your script. If too many of these are being executed in userscripts, the user experience can suffer a lot due to performance issues. If you want to monitor changes to a certain element, consider using a MutationObserver instead. As an example, below is shown how to execute code whenever the settings tab is opened:

var settingsObserver = new MutationObserver(function(mutationRecords) {
  // do stuff, this is triggered whenever the class of the settings tab is changed
});
selectionObserver.observe(document.getElementById('#sidepanel-prefs').parentNode, {
  attributes: true,
  attributeFilter: ['class']
});

It also helps to run your code through JSHint and JSLint before submitting it. Those tools tend to find bugs you may otherwise overlook.

Components of the Waze Map Editor

When you want to write a userscript, it is of course necessary that you understand what you are trying to adjust. The Waze Map Editor is an application that is made up of several libraries and tools. Luckily, most of it is relatively exposed, so there is a lot we can play with ;)

The most prominent components used are OpenLayers, Bootstrap, jQuery and FontAwesome. The main access point for data within the editor is the Waze object. For a detailed information on the technological components of the Waze Map Editor, visit the Technological overview of the Waze Map Editor page.

Userscript Template (bootstrap)

While a lot depends on the purpose of the userscript, a rough template that can be used for most userscripts can be found below. Userscripts may start executing when the map editor hasn't fully loaded yet or when the user hasn't logged in yet, so they need to be able to cope with this. Also, if userscripts adjust something in the side panel, they will need to adjust this again whenever the user leaves the 'Events' mode and enters the 'Default' mode again. The code below provides the framework for that.

// ==UserScript==
// @name        Your script name here
// @namespace   http://domainnameofyourchoicehere.something/
// @description A succinct description of what your userscript does
// @include     /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor.*$/
// @version     0.0.1
// @grant       none
// ==/UserScript==

// Initialisation of the script, this will only run completely one time
function init(e) {
  if (e && e.user == null) {
    return;
  }
  // if you require certain features to be loaded, you can add them here
  if (typeof I18n === 'undefined' || typeof Waze === 'undefined' ||  typeof Waze.loginManager === 'undefined') {
    setTimeout(init, 200);
    return;
  }
  if (!Waze.loginManager.hasUser()) {
    Waze.loginManager.events.register("login", null, init);
    Waze.loginManager.events.register("loginStatus", null, init);
    if (!Waze.loginManager.hasUser()) {
      return;
    }
  }
  setModeChangeListener();
  performScript();
}

// Attempt to hook into the controller that can notify us whenever the editor's mode changes
function setModeChangeListener() {
  if (!Waze.app || !Waze.app.modeController) {
    setTimeout(setModeChangeListener, 400);
    return;
  }
  Waze.app.modeController.model.bind('change:mode', function(model, modeId) {
    if (modeId == 0) { // 0 = Default, 1 = Events
      performScript();
    }
  });
}

function performScript() {
  // Your userscript logic can go here. The Waze editor has been loaded and seems to be ready for your userscript.
}

Instead of executing the whole script again when the mode changes, you could also store your generated elements as a whole in a variable at the top of your script and just insert them again when you recover from 'Events' mode.

Several common problems and tasks you will encounter while writing a userscript have already been solved. You may consider adding a dependency to a library like WazeWrap in your userscript so you don't need to investigate these yourself any more.