Skip to content

Creating Widgets

Nick Briz edited this page Aug 19, 2020 · 16 revisions

Widgets are multi-purpose independent windows. They can be used during tutorials to open up other media types (images, videos, gifs, audio, texts, 3D objects, etc). Widgets can also be their own miscellaneous utilities. For example we could create a widget that explains some concept using interactive graphics. Widgets can also be GUIs that interact with the netitor, for example a CSS generator that outputs to the line where the user's cursor is currently positioned. The sky's the limit!

netnet widgets

the Simplest sort of Widget

The Widget class can be used to instantiate new widgets. A widget is a window styled to match netnet's current theme. This includes shadows that respond to the mouse's position. If the window manager update's netnet's theme all widgets will automatically update their themes to match. (for more info on changing netnet's state including how to dispatch a CHANGE_THEME action see the State Management wiki page)

To instantiate a new widget:

const w = new Widget({
  title: 'settings',
  innerHTML: element
})

Where the value of the innerHTML property can either be an string (including an html string like <h1>Hello World!</h1>) or an instance of HTMLElement (ie. a DOM element, const element = document.createElement('h1')).

Widgets are resizable by default (the users can click and drag the bottom right corner to resize), but if we don't want a widget to be resizable by the user we can set the optional resizable: false. When widgets are loaded into netnet using the LOAD_WIDGETS action (see Instantiating Widgets below) it will automatically be indexed and tracked by the Widgets Menu. If for whatever reason the Widgets Menu should not include this widget in it's list you can set the optional listed: false.

If we'd like to instantiate the widget at a particular location and/or at a particular size we can use these optional properties:

const w = new Widget({
  title: 'settings',  // optional
  innerHTML: element, // optional
  x: 20,              // optional
  y: 20,              // optional
  z: 100,             // optional (keep z index between 100 and 200)
  width: 500,         // optional
  height: 500,        // optional
  resizable: true,    // optional (should user be allowed to resize)
  listed: false       // optional (should it appear in Widgets Menu)
})

We can pass these properties a number or a CSS string, like x: '50vw' and width: '100%' and y: '100px'.

Widget properties

All the properties passed in the constructor's options argument can also be accessed/updated via the instance's properties at any point, for example:

w.title = 'layout settings'
w.innerHTML = '<h1>these are the options</h1>'

w.x = innerWidth / 2
w.y = '50vh'
w.z = 101
w.width = '100%'
w.height = '1080px'
w.resizable = false
w.listed = true

Widget Methods

It's important to note that instantiating the widget creates it in memory, but it does not visually present it to the user. In order to display the widget we need to use the instance's .open() method. There's also the .close() method for hiding the widget (while maintaining it in memory) as well as methods for resizing and positioning:

w.position(x, y, z)     // update position
w.resize(width, height) // update size
w.open()                // display
w.close()               // hide

Instantiating Widgets

In most cases we'll want the StateManager to keep track of the Widget instance, to make that happen we'll need to instantiate it like this:

// the keyname has to be unique among the other globally loaded
// widgets in the window.WIDGETS dictionary/object
STORE.dispatch('LOAD_WIDGETS', {
  keyname: new Widget({ title: 'settings', innerHTML: element })
})

Of course if we instantiate the widget outside of the dispatch call we could also pass the instance into it like this: STORE.dispatch('LOAD_WIDGETS', { name: w }) assuming w is an instance of a Widget.

If you instantiate a widget without loading it into netnet's state by dispatching a 'LOAD_WIDGETS' action, then not only will it not be managed by the StateManager but it also won't show up in the Widgets Menu (regardless of it's .listed value). NOTE: when instantiating a widget in a tutorial and including it in the TUTORIAL.widgets array, the TutorialManager will take care of dispatching the LOAD_WIDGETS action. See the Creating Tutorials wiki for more on that.

Extending a Widget's Functionality

The basic widget class is great for doing things like loading up images or videos, but for more interesting/complicated widgets we can create our own widget classes by extending the base widget class, for example:

class RandomColor extends Widget {
  constructor (opts) {
    super(opts)
    this.title = 'Random Color'
    this.key = 'random-color'
    this.innerHTML = `<button>insert color</button>`
    this.$('button').onclick = () => { this.click() }
  }

  click () {
    const r = Maths.randomInt(255)
    const g = Maths.randomInt(255)
    const b = Maths.randomInt(255)
    const code = `rgb(${r}, ${g}, ${b})`
    NNE.cm.replaceSelection(code)
  }
}

NOTE: Maths is one of netnet's global objects, see the Global Objects section in the Project Architecture wiki

The WindowManager handles loading all of netnet's widget classes when the page loads. In order for the WindowManager to find the widgets they need be saved in the www/widgets directory. The directory also has an ExampleWidget.js file which can be copied and used as a template or starting point. Assuming we want to WindowManager to automatically instantiate (via LOAD_WIDGETS) for us, we need to make sure that the class name matches the name of the file itself and we also need to make sure we define a .key property in our constructor.

Clone this wiki locally