Advance Fields - sizzlemctwizzle/GM_config GitHub Wiki

This page details more complex field usage, including implementing your own custom field types.

Remove and Set fields in the GM_config panel

You can actually remove a field from the config panel when it's open (if its value was changed it won't be saved):

configInstance.fields['fieldId'].remove();

You can also change the value of a field when the config panel is open the same way you would when it's closed:

configInstance.set('fieldId', 'Some value');

Prevent a field's value from being saved

You may want to achieve some type of advanced behavior using a combination of existing field types, rather than going the whole hog and creating your own custom type. For example, you may want to have a text field were users can enter their own CSS (for some purpose) that needs to be validated before it can be saved.

To accomplish this you set the "save" property on the text field to false. This means that this field is purely visual, but its value won't be saved if altered. You can then attach an onchange event listener to this field in the onOpen callback function. Your listener could then validate the field and if it's valid set a hidden field's value, which will be saved. Here is the code for this:

let configInstance = GM_config(
{
  'id': 'MyConfig', // The id used for this instance of GM_config
  'fields': // Fields object
  {
    'customCSS': 
    {
      'label': 'Enter CSS',
      'type': 'text',
      'save': false, // This field's value will NOT be saved
      'default': ''
    },
    'validCSS': 
    {
      'type': 'hidden',
      'default': '',
    }
  },
  'events': 
  {
    'init': function() {
      // Set the value of the dummy field to the saved value
      this.set('customCSS', GM_config.get('validCSS'));
    },
    'open': function(doc) {
      // Use a listener to update the hidden field when the dummy field passes validation
      this.fields['customCSS'].node.addEventListener('change', function () {
        // get the current value of the visible field
        var customCSS = GM_config.get('customCSS', true);

        // Only save valid CSS
        if (/\w+\s*\{\s*\w+\s*:\s*\w+[\s|\S]*\}/.test(customCSS)) {
          GM_config.set('validCSS', customCSS);
        }
      }, false);
    },
    'save': function(forgotten) {
      // If the values don't match then customCSS wasn't valid
      if (this.isOpen && forgotten['customCSS'] !== this.get('validCSS')) {
        alert('CSS is invalid!');
      }
    }
  }
});

Store an unsaved value separately

Sometimes you need to store a value separately from others managed by GM_config so you can get at it without having to call init(). The best example of this is providing translations of your settings panel. Here is an example of how to get this done:

(async () => {
  // Retrieve language setting
  // Note: getValue(key, default) is a method that taps directly
  // into storage implementation that GM_config uses, which is async.
  // The difference is that GM_config serializes all
  // the data it manages and saves it as a single value
  // with GM_config.setValue(this.id, data)
  var lang = await GM_config.getValue('lang', 'en');

  // Fields in different languages
  var langDefs = {
    'en': // Fields in English
    {
      'lang':
      {
        'label': 'Choose Language',
        'type': 'select',
        'options': ['en', 'de'],
        'save': false // This field's value will NOT be saved
      }
    },
    'de': // Fields in German
    {
      'lang':
      {
        'label': 'Sprache wählen',
        'type': 'select',
        'options': ['en', 'de'],
        'save': false // This field's value will NOT be saved
      }
    }
  };

  // Use field definitions for the stored language
  var fields = langDefs[lang];

  // The title for the settings panel in different languages
  var titles = {
    'en': 'Translations Dialog',
    'de': 'Übersetzungen Dialog'
  };
  var title = titles[lang];

  // Translations for the buttons and reset link
  var saveButton = {'en': 'Save', 'de': 'Speichern'};
  var closeButton = {'en': 'Close', 'de': 'Schließen'};
  var resetLink = {
    'en': 'Reset fields to default values',
    'de': 'Felder zurücksetzen auf Standardwerte'
  };

  var gmc_trans = new GM_configStruct(
  {
    'id': 'GM_config_trans', // The id used for this instance of GM_config
    'title': title,
    'fields': fields, // Fields object
    'events':
    {
      'init': function()
      {
        // You must manually set an unsaved value
        this.fields['lang'].value = lang;
        this.open();
      },
      'open': function (doc) {
        // translate the buttons
        var config = this;
        doc.getElementById(config.id + '_saveBtn').textContent = saveButton[lang];
        doc.getElementById(config.id + '_closeBtn').textContent = closeButton[lang];
        doc.getElementById(config.id + '_resetLink').textContent = resetLink[lang];
      },
      'save': function(values) { // All unsaved values are passed to save
        for (var i in values) {
          if (i == 'lang' && values[i] != lang) {
            var config = this;
            lang = values[i];

            // Use field definitions for the chosen language
            fields = langDefs[lang];
            config.fields['lang'].value = lang;

            // Use the title for the chose language
            title = titles[lang];

            // Re-initialize GM_config for the language change
            config.init({ 'id': config.id, title: title, 'fields': fields });

            // Refresh the config panel for the new language change
            config.close();
            config.open();

            // Save the chosen language for next time
            config.setValue('lang', lang);
          }
        }
      }
    }
  });
})();

This code will actually translate the config panel immediately if the selected language is changed and saved (other values that have been modified will be saved as well), rather than requiring a page refresh. GM_config can be a very powerful tool, provided you know what you're doing.

Custom field types

GM_config only provides support for graphical input of some basic datatypes. It may not be able to meet all your needs out of the box. You can create you own custom field types and have GM_config store your data along-side all the other fields. To do this you need to provide a default value for the type along with three functions (toNode, toValue, reset) that allow GM_config to handle the custom type. These custom functions replace the default methods on the field object, so you will have access to the same instance variables as the default methods (check the GM_configField constructor for details). Here is some code that implements a field for entering dates:

let gmc = new GM_config(
{
  'id': 'MyConfig',
  'fields': 
  {
    'birthday': {
      'label': 'Date of Birth',
      'type': 'date',
      'format': 'dd/mm/yyyy'
    }
  },
  'types':
  {
    'date': {
      'default': null,
        toNode: function(configId) {
          var field = this.settings,
              value = this.value,
              id = this.id,
              create = this.create,
              format = (field.format || 'mm/dd/yyyy').split('/'),
              slash = null,
              retNode = create('div', { className: 'config_var',
                id: configId + '_' + id + '_var',
                title: field.title || '' });

          // Save the format array to the field object so
          // it's easier to hack externally
          this.format = format;

          // Create the field lable
          retNode.appendChild(create('label', {
            innerHTML: field.label,
            id: configId + '_' + id + '_field_label',
            for: configId + '_field_' + id,
            className: 'field_label'
          }));

          // Create the inputs for each part of the date
          value = value ? value.split('/') : this['default'];
          for (var i = 0, len = format.length; i < len; ++i) {
            var props = {
              id: configId + '_field_' + id + '_' + format[i],
              type: 'text',
              size: format[i].length,
              value: value ? value[i] : '',
              onkeydown: function(e) {
                var input = e.target;
                if (input.value.length >= input.size)
                  input.value = input.value.substr(0, input.size - 1);
              }
            };
            
            // Jump to the next input once one is complete
            // This saves the user a little work
            if (i < format.length - 1) {
              slash = create(' / ');
              props.onkeyup = function(e) {
                var input = e.target,
                    inputs = input.parentNode.getElementsByTagName('input'),
                    num = 0;

                for (; num < inputs.length && input != inputs[num]; ++num);
                if (input.value.length >= input.size)
                  inputs[num + 1].focus();
              };
            } else slash = null;

            // Actually create and append the input element
            retNode.appendChild(create('input', props));
            if (slash) retNode.appendChild(slash);
          }

          return retNode;
        },
        toValue: function() {
          var rval = null;
          if (this.wrapper) {
            var inputs = this.wrapper.getElementsByTagName('input');
            rval = '';

            // Join the field values together seperated by slashes
            for (var i = 0, len = inputs.length; i < len; ++i) {
              // Don't save values that aren't numbers
              if (isNaN(Number(inputs[i].value))) {
                alert('Date is invalid');
                return null;
              }
              rval += inputs[i].value + (i < len - 1 ? '/' : '');
            }
          }

          // We are just returning a string to be saved
          // If you want to use this value you'll want a Date object
          return rval;
        },
        reset: function() {
          // Empty all the input fields
          if (this.wrapper) {
            var inputs = this.wrapper.getElementsByTagName('input');
            for (var i = 0, len = inputs.length; i < len; ++i)
              inputs[i].value = '';
          }
        }
    }
  }
});

The complexity of the above code illustrates exactly why we don't implement a field type for everyone's needs. Not many people would need this functionality and GM_config would quickly become bloated. Instead, GM_config makes it easy for you to implement complex fields that you need. You could easily package code like this together and share the library with others. I might even link to it here.