Events

Every element can respond to some events and execute what we call event listeners. Event listeners are functions that are called when a specific event happens (ex: click).

There are some common events that every element can respond to (click, mousedown, keydown...) and there are other events that are specific to the type of view, layout and composite (ex: a specific key is pressed). When creating a type, you can define what are these other events that the type will be able to respond.

Signals

Before explaining how to create events, you have to understand what are signals and how they work.

Every time that an action occurs on the user interface, the UI will send a signal to the element calling the .signal(signal) method of the element. There are different types of actions that can occur and these are:

  • mousedown: The mouse button is pressed inside the user interface.
  • mouseup: The mouse button is released inside the user interface.
  • mousemove: The mouse is moved inside the user interface.
  • mouseenter: The mouse enters the user interface.
  • mouseleave: The mouse leaves the user interface.
  • keydown: A key is pressed.
  • keyup: A key is released.

When calling the .signal(signal) method on the element, the UI will pass as argument an object with these properties:

  • type: It is the type of action that occurred.
  • data: It is the data about the action (mouse coords, key pressed...).

When calling the .signal(signal) method on the element, the element will check if there are events that were triggered, and if that's the case, it will execute its corresponding event listeners.

Then, if the element is a layout, it will call the .signal(signal) method on each child, and if it is a composite, it will call the .signal(signal) method on its element.

Events

To create the events that the element type has to respond to, you have to use the .events.set(name, func) method of the element type, like so:

elementType.events.set("clickOutside", function (element, signal, state) {
  // Code
})

The first argument is the name of the event, and the second argument is a function that will be used to check whether the event was triggered or not.

This function will be called every time that the .signal(signal) method of the element is called and it has three parameters. The first one is the element itself and the second one is the object that was passed in the .signal(signal) method. The third parameter will be explained later.

The function needs to return an object with these properties:

  • event: It is a boolean that defines about whether the event was triggered or not. If it is true it means it was and if it is false it means it wasn't.
  • data: It may be used when event is true, and it defines data about the event.

The following code shows how to create an event that is triggered when we click outside the element:

elementType.events.set("clickOutside", function (element, signal, state) {
  if (signal.type === "mousedown") {
    const coords = signal.data
    if (outRectangle(coords, element.coords, element.size))
      return { event: true, data: coords }
    else return { event: false }
  }
  return { event: false }
})

const outRectangle = function (coords, rectangleCoords, rectangleSize) {
  return (
    (coords.x < rectangleCoords.x) |
    (coords.y < rectangleCoords.y) |
    (coords.x > rectangleCoords.x + rectangleSize.width) |
    (coords.y > rectangleCoords.y + rectangleSize.height)
  )
}

When creating an event, it may be needed to store some data for later use. To do that, we can use the state parameter. This parameter is used to store some data about the state of the element in relation to the event. It has these methods:

  • set(key, value): To set a key with a value.
  • get(key, value): To get the value of a key (second argument if not exists).
  • del(key): To delete a key.
  • has(key): To check whether the key exists or not.

The following code shows how to use the state parameter to create an event that is triggered when the same key is pressed twice:

elementType.events.set("keyPressedTwice", function (element, signal, state) {
  if (signal.type === "keydown") {
    const key = signal.data
    const lastKey = state.get("lastKey", null)
    if (key === lastKey) {
      state.del("lastKey")
      return { event: true, data: key }
    }
    if (lastKey === null) state.set("lastKey", key)
    else state.del("lastKey")
  }
  return { event: false }
})

Custom signals

You can use the .signal(signal) method to create your custom signals and create events that are triggered when this signal takes place. This technique is very useful when creating events for the composite types, like so:

const twoTextsType = canvasUI.composite.newType("twoTexts")

twoTextsType.lifecycle.set("getElement", function (composite) {
  const linear = canvasUI.layout.new("linear", "linear")
  linear.set("size", { width: "auto", height: "auto" })
  linear.set("gap", 20)

  const textL = canvasUI.view.new("textL", "text")
  const textR = canvasUI.view.new("textR", "text")

  textL.set("text", "LEFT")
  textR.set("text", "RIGHT")

  linear.insert(textL)
  linear.insert(textR)

  const signal = (text) => {
    composite.signal({
      type: "clickText",
      data: text.get("text"),
    })
  }

  textL.listeners.add("click", signal)
  textR.listeners.add("click", signal)

  return linear
})

twoTextsType.events.set("clickText", function (text, signal, state) {
  if (signal.type === "clickText") return { event: true, data: signal.data }
  return { event: false }
})

In this code, we have created a composite that is composed of a linear layout and two text views. When a text view is clicked, it sends a signal to the composite. The composite has an event that is triggered every time that it receives this signal.