Lesson 42 of 45 · Web pages

Media, animation and other events

Sound, video, animations and transitions all report what they are doing through events, and so do a few special parts of the page: slots, canvases and the security policy. This lesson covers the 33 events for them, builds a small player, and says exactly when the browser fires the ones you cannot trigger by hand.

Media elements and a sound to play

The audio and video elements play a file and report what happens to it. Their attributes are source (the file), controls (the browser's own buttons), preload (how much to fetch before the user presses play: none, metadata or auto), loop and muted. Their events follow the life of the file: it starts loading, it knows its length, it can play, it plays, it ends. Event attributes work as in the previous lesson: a function name, a short lambda, or a JavaScript string.

The examples of this lesson have no network, so they make their own sound. The function clip returns a WAV file as a data: address: a 54 byte header, written in base64, and a short square wave tone that is added 400 times, which makes three seconds of beeping. Every example sets the src of the audio element from a button, so you see the load events in their real order. Browsers only play sound after the user clicked something on the page, so the Play buttons are real clicks, not code that runs on load.

The life of a media file

This page logs the events of loading and playing. Press Load, then Play, and compare the log with the list below. The log is what Chrome produces; the order of neighbours such as suspend and loaded data can differ in other browsers.

web-mediaev-log.salam
import dom

mut lines := ""
mut count := 0 as i64

func say(what: str):
    if count == 12: lines = "" count = 0 end
    lines = lines + what + "\n"
    count += 1
    dom.SetText(dom.ById("log"), lines)
end

func clip(): str:
    mut sound := "data:audio/wav;base64,UklGRu5dAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgASlVOSwIAAAAAAGRhdGHAXQAA"
    repeat 400: sound += "oKCgoKCgoKCgoGBgYGBgYGBgYGCgoKCgoKCgoKCgYGBgYGBgYGBgYKCgoKCgoKCgoKBgYGBgYGBgYGBg" end
    ret sound
end

func load_clip():
    dom.SetAttr(dom.ById("sound"), "src", clip())
end

func play_clip():
    _ := dom.Call0(dom.ById("sound"), "play")
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    audio: id = "sound" controls = true preload = "none"
        load start = (): say("load start") end
        suspend = (): say("suspend") end
        loaded metadata = (): say("loaded metadata") end
        loaded data = (): say("loaded data") end
        can play = (): say("can play") end
        can play through = (): say("can play through") end
        play = (): say("play") end
        playing = (): say("playing") end
        pause = (): say("pause") end
        ended = (): say("ended") end
    end
    box: display = flex gap = 8
        button: click = load_clip content = "Load the sound" end
        button: click = play_clip content = "Play" end
        button: click = (): _ := dom.Call0(dom.ById("sound"), "pause") end content = "Pause" end
    end
    paragraph: id = "log" white space = "pre-wrap" content = "Events appear here." end
end
  • load start: the browser begins to fetch the file.
  • suspend: it stopped fetching on purpose. With preload = "none" it waits for the first play, and later it also stops when its buffer is full.
  • loaded metadata: the length (and, for video, the size) is known. Read duration here.
  • loaded data: the first moment can be shown.
  • can play: enough data to start. can play through: enough to reach the end at the current speed.
  • play: playing was requested. playing: it is really running. pause and ended close the story.

One thing deserves a name: play and playing are not the same. play fires the moment the button is pressed, even when no data has arrived. playing fires when sound or picture is moving, and again after every pause for buffering. A button that must show Pause should follow playing and pause, not the click.

Do not set src after the page asked for play() and expect the old sound to go on. Changing the source resets the element, and the browser fires emptied and starts the whole life cycle again.

A small custom player

The browser's controls are fine, but a page often wants its own. The example has a button, a progress bar and a volume slider, and every part reacts to an event instead of to the click that caused it. play and pause change the label of the button, so it is right even when something else pauses the sound. time update fires about four times a second and moves the bar with the share currentTime / duration. volume change writes the new volume next to the slider. The slider itself sets volume from its input event.

The state of the element is read with dom.GetNum and dom.GetBool (currentTime, duration, paused) and changed with dom.SetNum and dom.Call0, which calls methods such as play and pause.

web-mediaev-player.salam
import dom

func clip(): str:
    mut sound := "data:audio/wav;base64,UklGRu5dAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgASlVOSwIAAAAAAGRhdGHAXQAA"
    repeat 400: sound += "oKCgoKCgoKCgoGBgYGBgYGBgYGCgoKCgoKCgoKCgYGBgYGBgYGBgYKCgoKCgoKCgoKBgYGBgYGBgYGBg" end
    ret sound
end

func play_pause():
    sound := dom.ById("sound")
    if dom.GetAttr(sound, "src") == "":
        dom.SetAttr(sound, "src", clip())
    end
    if dom.GetBool(sound, "paused"):
        _ := dom.Call0(sound, "play")
    else:
        _ := dom.Call0(sound, "pause")
    end
end

func move_bar():
    sound := dom.ById("sound")
    now := dom.GetNum(sound, "currentTime")
    dom.SetStyle(dom.ById("fill"), "width", now * 100.0 / dom.GetNum(sound, "duration") + "%")
    dom.SetText(dom.ById("time"), dom.Call1(dom.Global("Math"), "floor", now) + " s of 3 s")
end

func set_volume():
    dom.SetNum(dom.ById("sound"), "volume", dom.GetNum(dom.ById("level"), "valueAsNumber") / 100.0)
end

func show_volume():
    percent := dom.Call1(dom.Global("Math"), "round", dom.GetNum(dom.ById("sound"), "volume") * 100.0)
    dom.SetText(dom.ById("say"), "Volume " + percent + "%")
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 max width = 360 end
    audio: id = "sound" preload = "none" play = (): dom.SetText(dom.ById("go"), "Pause") end
        pause = (): dom.SetText(dom.ById("go"), "Play") end time update = move_bar volume change = show_volume
    end
    box: display = flex gap = 12 align items = center
        button: id = "go" click = play_pause content = "Play" width = 70 end
        box: flex = 1 height = 8 background = "#e2e8f0" border radius = 4
            box: id = "fill" width = "0%" height = 8 background = "#7c3aed" border radius = 4 end
        end
    end
    paragraph: id = "time" content = "0 s of 3 s" end
    box: display = flex gap = 12 align items = center
        input: id = "level" type = "range" min = 0 max = 100 value = 100 input = set_volume end
        span: id = "say" content = "Volume 100%" end
    end
end

Seeking, speed and length

seeking fires when a jump to another time starts and seeked when it is finished. Setting currentTime causes both, and so does the user dragging the slider of the browser controls. rate change fires when playbackRate changes: 1 is normal, 0.5 is half speed. duration change fires when the length becomes known or changes, and emptied when the element is reset, as the Reset button does by calling load().

web-mediaev-seek.salam
import dom

mut lines := ""

func say(what: str):
    lines = lines + what + "\n"
    dom.SetText(dom.ById("log"), lines)
end

func clip(): str:
    mut sound := "data:audio/wav;base64,UklGRu5dAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgASlVOSwIAAAAAAGRhdGHAXQAA"
    repeat 400: sound += "oKCgoKCgoKCgoGBgYGBgYGBgYGCgoKCgoKCgoKCgYGBgYGBgYGBgYKCgoKCgoKCgoKBgYGBgYGBgYGBg" end
    ret sound
end

func load_clip():
    dom.SetAttr(dom.ById("sound"), "src", clip())
end

func skip():
    dom.SetNum(dom.ById("sound"), "currentTime", 2.0)
end

func set_speed(rate: f64):
    dom.SetNum(dom.ById("sound"), "playbackRate", rate)
end

func restart():
    _ := dom.Call0(dom.ById("sound"), "load")
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    audio: id = "sound" preload = "auto"
        duration change = (): say("duration change: " + dom.GetNum(dom.ById("sound"), "duration") + " s") end
        seeking = (): say("seeking") end seeked = (): say("seeked") end
        rate change = (): say("rate change: " + dom.GetNum(dom.ById("sound"), "playbackRate")) end
        emptied = (): say("emptied") end
    end
    box: display = flex gap = 8 flex wrap = wrap
        button: click = load_clip content = "Load" end
        button: click = skip content = "Jump to 2 s" end
        button: click = (): set_speed(0.5) end content = "Half speed" end
        button: click = (): set_speed(1.0) end content = "Normal speed" end
        button: click = restart content = "Reset" end
    end
    paragraph: id = "log" white space = "pre-wrap" content = "Press Load first." end
end

Captions and cue change

A track element inside an audio or video element holds captions in the WebVTT format, a plain text file with time ranges. Its cue change event fires whenever a caption begins or ends. The text of the current one is in track.activeCues: the example reads the list with dom.GetProp, checks its length, and shows the first cue's text in a heading. The track needs default = true (or a script that sets its mode) or no cues become active. The page language accepts only ordinary addresses for the file, so the example gives the track a placeholder name and sets the real src, a data: address, from code when Play is pressed.

web-mediaev-cues.salam
import dom

const WORDS := "data:text/vtt,WEBVTT%0A%0A00:00.000 --> 00:01.000%0ASalam%0A%0A00:01.000 --> 00:02.000%0Aeveryone%0A%0A00:02.000 --> 00:03.000%0Awelcome"

func show_cue():
    cues := dom.GetProp(dom.GetProp(dom.ById("words"), "track"), "activeCues")
    if dom.GetNum(cues, "length") > 0.0:
        dom.SetText(dom.ById("line"), dom.GetStr(dom.ItemAt(cues, 0), "text"))
    else:
        dom.SetText(dom.ById("line"), "...")
    end
end

func clip(): str:
    mut sound := "data:audio/wav;base64,UklGRu5dAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgASlVOSwIAAAAAAGRhdGHAXQAA"
    repeat 400: sound += "oKCgoKCgoKCgoGBgYGBgYGBgYGCgoKCgoKCgoKCgYGBgYGBgYGBgYKCgoKCgoKCgoKBgYGBgYGBgYGBg" end
    ret sound
end

func play_captioned():
    sound := dom.ById("sound")
    dom.SetAttr(dom.ById("words"), "src", WORDS)
    dom.SetAttr(sound, "src", clip())
    _ := dom.Call0(sound, "play")
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    audio: id = "sound" controls = true
        track: id = "words" kind = "captions" default = true cue change = show_cue source = "captions.vtt"
        end
    end
    button: click = play_captioned content = "Play with captions" end
    heading: size = 2 id = "line" content = "Press play." end
end

Animation events

A CSS animation moves an element by itself, and four events tell what stage it is at. animation start fires when it begins, animation iteration at the end of each round except the last, animation end after the last round, and animation cancel if it is cut off: for instance when the element is hidden with display: none. The animation here is defined in a style: element with @keyframes, and a class switches it on, three rounds of half a second each. So the log of an uninterrupted run is one start, two iterations and one end, and a cancelled run is a start and a cancel.

The end handler removes the class, which lets the button start it again. That is the usual job of animation end. An animation that never ends (infinite) never fires it.

web-mediaev-animation.salam
import dom

mut lines := ""

func say(what: str):
    lines = lines + what + "\n"
    dom.SetText(dom.ById("log"), lines)
end

func run_ball():
    lines = ""
    dom.AddClass(dom.ById("ball"), "run")
end

func run_over():
    dom.RemoveClass(dom.ById("ball"), "run")
    say("animation end")
end

func restore():
    dom.RemoveClass(dom.ById("ball"), "run")
    dom.SetStyle(dom.ById("ball"), "display", "")
end

func cancel_ball():
    dom.SetStyle(dom.ById("ball"), "display", "none")
    dom.SetTimeout((): restore() end, 300)
end

page:
    style: content = "@keyframes slide { to { transform: translateX(200px) } } .run { animation: slide 0.5s ease-in-out 3 }" end
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: width = 260 padding = "10px 0"
        box: id = "ball" width = 40 height = 40 border radius = "50%" background = "#e11d48"
            animation start = (): say("animation start") end
            animation iteration = (): say("animation iteration") end
            animation end = run_over
            animation cancel = (): say("animation cancel") end
        end
    end
    box: display = flex gap = 8
        button: click = run_ball content = "Run 3 times" end
        button: click = cancel_ball content = "Cancel it" end
    end
    paragraph: id = "log" white space = "pre-wrap" content = "" end
end

Transition events

A transition is the smooth change of a property after it was set, and it has four events. transition run fires when the transition is created, before its delay. transition start fires when the delay is over and the value begins to move. transition end fires when it arrives. transition cancel fires when something interrupts it. All four carry propertyName, so a transition of two properties fires each event twice. The example gives the move a delay of 0.3 seconds, so run and start are visible apart, and the second button flips the class twice at once to cut the transition off.

There is one trap that is not in your code. In our tests Chromium, the engine of Chrome and Edge, read transition end from an HTML attribute but not transition run, transition start or transition cancel; Firefox reads all four. Writing them as attributes compiles and works in Firefox, and silently does nothing in Chrome. The reliable way is dom.OnEventE(element, "transitionrun", ...), which the example uses for those three, and it also shows the plain attribute for transition end.

web-mediaev-transition.salam
import dom

mut lines := ""
mut wired := false

func record(kind: str, e: i64):
    lines = lines + kind + ": " + dom.GetStr(e, "propertyName") + "\n"
    dom.SetText(dom.ById("log"), lines)
end

func transition_ended(e: i64):
    record("end", e)
end

func wire_events():
    if not wired:
        wired = true
        chip := dom.ById("chip")
        dom.OnEventE(chip, "transitionrun", (e: i64): record("run", e) end)
        dom.OnEventE(chip, "transitionstart", (e: i64): record("start", e) end)
        dom.OnEventE(chip, "transitioncancel", (e: i64): record("cancel", e) end)
    end
end

func move_chip():
    wire_events()
    lines = ""
    dom.ToggleClass(dom.ById("chip"), "moved")
end

func move_and_undo():
    move_chip()
    dom.SetTimeout((): dom.ToggleClass(dom.ById("chip"), "moved") end, 150)
end

page:
    style: content = ".moved { transform: translateX(180px) }" end
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: width = 240 padding = "10px 0"
        box: id = "chip" width = 50 height = 40 border radius = 8 background = "#2563eb"
            transition = "transform 0.4s 0.3s" transition end = transition_ended
        end
    end
    box: display = flex gap = 8
        button: click = move_chip content = "Move it" end
        button: click = move_and_undo content = "Move and undo" end
    end
    paragraph: id = "log" white space = "pre-wrap" content = "" end
end

Slots and slot change

A template with shadow root mode = "open" turns its parent into a shadow host: the content of the template becomes a separate little tree, and the original children of the host are shown wherever a slot is. slot change fires on the slot when the nodes that fill it change, so it is how a component notices that someone added a child. assignedNodes(), called with dom.Call0, lists them. Page styles do not reach inside a shadow tree, which is why the example puts the colours on the host.

web-mediaev-slot.salam
import dom

mut members := 0 as i64

func slot_changed(e: i64):
    nodes := dom.Call0(dom.EventTarget(e), "assignedNodes")
    dom.SetText(dom.ById("log"), "slot change: " + dom.GetNum(nodes, "length") + " in the slot")
end

func add_member():
    members += 1
    person := dom.Create("p")
    dom.SetText(person, "Member " + members)
    dom.Append(dom.ById("team"), person)
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: id = "team" padding = "4px 16px" background = "#f5f3ff" border = "2px solid #7c3aed" border radius = 8
        template: shadow root mode = "open"
            heading: size = 3 content = "Team" end
            slot: slot change = slot_changed end
        end
    end
    button: click = add_member content = "Add a member" margin top = 12 end
    paragraph: id = "log" content = "Nothing in the slot yet." end
end

Security policy violations

A Content Security Policy is a list of things a page promises not to do, set by a meta element with http equiv = "Content-Security-Policy" or by a header. When the page breaks its own promise the browser blocks the act and fires security policy violation, with the blocked address in blockedURI and the rule in violatedDirective. The example forbids style attributes. Setting one with dom.SetAttr(el, "style", ...) is blocked and reported on the element itself, while dom.SetStyle works because it changes the style object, not the attribute. Some violations, such as a blocked image, are reported on the document and not on the element.

web-mediaev-policy.salam
import dom

func blocked(e: i64):
    what := dom.GetStr(e, "blockedURI")
    rule := dom.GetStr(e, "violatedDirective")
    dom.SetText(dom.ById("log"), "Blocked: " + what + " (" + rule + ")")
end

func by_attribute():
    dom.SetAttr(dom.ById("card"), "style", "background: gold")
end

func by_property():
    dom.SetStyle(dom.ById("card"), "background", "lightgreen")
    dom.SetText(dom.ById("log"), "SetStyle worked.")
end

page:
    meta: http equiv = "Content-Security-Policy" content = "style-src-attr 'none'" end
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: id = "card" security policy violation = blocked padding = 20 border radius = 8 background = "#e2e8f0"
        content = "This card's style attribute is forbidden by the page's policy."
    end
    box: display = flex gap = 8 margin top = 12
        button: click = by_attribute content = "Set the style attribute" end
        button: click = by_property content = "Use SetStyle" end
    end
    paragraph: id = "log" content = "No violation yet." end
end

Events you cannot trigger here

A few events need a network that fails or a graphics system that breaks, so no example can show them. This is when the browser fires them.

  • stalled: it wants more data and none has come for a few seconds, for example on a slow mobile connection. It may recover on its own.
  • waiting: playback stopped because the buffer ran dry. playing follows when data arrives again. Show a spinner between them.
  • progress: the browser downloaded another chunk of a remote file. A short data: address does not produce it.
  • context lost and context restored: a 2D canvas loses its memory when the graphics driver resets or the system switches graphics cards, and gets an empty drawing back when it recovers. Everything on it has to be drawn again in context restored. A page cannot cause it.

Reference: every event of this lesson

All of these are global attributes, but each one only fires on the elements named in its description. The examples are compiled by the checker; where an example needs a media file it uses an address that does not exist, so the event is described and not run. Three events of this lesson end in end or ended: ended, animation end and transition end. In Persian their names start with اتمام and not پایان, because پایان on its own would end the element.

Loading

AttributeWhen it firesExample
load start
شروع بارگذاری
onloadstart
Fires on an audio or video element when the browser starts to load its file, right after src is set or load() is called. It is the first event in the life of a media file.audio: controls = true source = "/sounds/bell.mp3" load start = (): dom.SetText(dom.ById("out"), "Loading started") end end paragraph: id = "out" content = "-" end
progress
پیشرفت
onprogress
Fires every so often while the browser is downloading the file. Read buffered to see which time ranges are already there. Do not confuse it with time update, which is about playing.audio: controls = true source = "/sounds/bell.mp3" progress = (): dom.SetText(dom.ById("out"), "More data downloaded") end end paragraph: id = "out" content = "-" end
suspend
تعلیق
onsuspend
Fires when the browser stops downloading on purpose and without an error: the buffer is full, or preload = none says to wait for the user. It is not a problem; downloading starts again when needed.audio: controls = true source = "/sounds/bell.mp3" suspend = (): dom.SetText(dom.ById("out"), "Download paused") end end paragraph: id = "out" content = "-" end
stalled
متوقف شده
onstalled
Fires when the browser wants more data but none has come for some seconds, for example when the connection is bad. It may recover by itself. You cannot cause it without a slow network, so a page usually only shows a hint when it fires.audio: controls = true source = "/sounds/bell.mp3" stalled = (): dom.SetText(dom.ById("out"), "The data stopped coming") end end paragraph: id = "out" content = "-" end
loaded metadata
بارگذاری ابرداده
onloadedmetadata
Fires when the browser knows the duration and, for video, the size, but may not have any sound or picture yet. Read duration here, for example to size a progress bar.audio: controls = true source = "/sounds/bell.mp3" loaded metadata = (): dom.SetText(dom.ById("out"), "Duration is known") end end paragraph: id = "out" content = "-" end
duration change
تغییر مدت
ondurationchange
Fires when duration changes: when metadata arrives (from not a number to a real length), when the source changes, and for streams whose length grows.audio: controls = true source = "/sounds/bell.mp3" duration change = (): dom.SetText(dom.ById("out"), "The length changed") end end paragraph: id = "out" content = "-" end
loaded data
بارگذاری داده
onloadeddata
Fires when the first moment of the media can be shown, the first video frame or the first sound. It comes after loaded metadata.audio: controls = true source = "/sounds/bell.mp3" loaded data = (): dom.SetText(dom.ById("out"), "The first data arrived") end end paragraph: id = "out" content = "-" end
can play
قابل پخش
oncanplay
Fires when enough data has arrived to start playing from the current position, although more may have to be loaded while it plays. It is the moment to enable a Play button.audio: controls = true source = "/sounds/bell.mp3" can play = (): dom.SetText(dom.ById("out"), "Ready to play") end end paragraph: id = "out" content = "-" end
can play through
قابل پخش کامل
oncanplaythrough
Fires when the browser estimates that, at the current download speed, it can play to the end without stopping to buffer. For a short local file it comes right after can play.audio: controls = true source = "/sounds/bell.mp3" can play through = (): dom.SetText(dom.ById("out"), "Can play to the end") end end paragraph: id = "out" content = "-" end
emptied
خالی شدن
onemptied
Fires when the media is reset to its start, because load() was called or the source changed after something had loaded. The old duration and position are gone.audio: controls = true source = "/sounds/bell.mp3" emptied = (): dom.SetText(dom.ById("out"), "The media was reset") end end paragraph: id = "out" content = "-" end

Playing

AttributeWhen it firesExample
play
پخش
onplay
Fires when playback is asked for: the user pressed play or the code called play(). Data may not have arrived yet, so sound can start a moment later; playing says when it really does.audio: controls = true source = "/sounds/bell.mp3" play = (): dom.SetText(dom.ById("out"), "Play was requested") end end paragraph: id = "out" content = "-" end
playing
در حال پخش
onplaying
Fires when playback is really running, after play and also after a pause for buffering ended (waiting). Use it, not play, for a button that shows Pause.audio: controls = true source = "/sounds/bell.mp3" playing = (): dom.SetText(dom.ById("out"), "It is playing") end end paragraph: id = "out" content = "-" end
pause
مکث
onpause
Fires when playback is paused, by the user, by pause(), or by the browser just before ended. A paused element keeps its position.audio: controls = true source = "/sounds/bell.mp3" pause = (): dom.SetText(dom.ById("out"), "Paused") end end paragraph: id = "out" content = "-" end
ended
اتمام پخش
onended
Fires when playback reaches the end and the element does not loop. A pause event comes just before it. Use it to show a Play again button or start the next track.audio: controls = true source = "/sounds/bell.mp3" ended = (): dom.SetText(dom.ById("out"), "Playback finished") end end paragraph: id = "out" content = "-" end
time update
به‌روزرسانی زمان
ontimeupdate
Fires when currentTime changes, about four times a second while playing and also after a jump. It is the event that moves a progress bar.audio: controls = true source = "/sounds/bell.mp3" time update = (): dom.SetText(dom.ById("out"), "The time moved") end end paragraph: id = "out" content = "-" end
seeking
در حال جابجایی
onseeking
Fires when a jump to another time begins, because the user dragged the slider or the code set currentTime. The picture may not be there yet.audio: controls = true source = "/sounds/bell.mp3" seeking = (): dom.SetText(dom.ById("out"), "Seeking") end end paragraph: id = "out" content = "-" end
seeked
جابجا شده
onseeked
Fires when the jump has finished and the media can play from the new time. It is the partner of seeking.audio: controls = true source = "/sounds/bell.mp3" seeked = (): dom.SetText(dom.ById("out"), "The jump is done") end end paragraph: id = "out" content = "-" end
waiting
در انتظار
onwaiting
Fires when playback stops because the next data has not arrived, and will go on by itself when it does; playing follows. Show a spinner between the two.audio: controls = true source = "/sounds/bell.mp3" waiting = (): dom.SetText(dom.ById("out"), "Waiting for data") end end paragraph: id = "out" content = "-" end
volume change
تغییر صدا
onvolumechange
Fires when volume or muted changes. The value is from 0 (silent) to 1 (full); read it to update a slider or an icon.audio: controls = true source = "/sounds/bell.mp3" volume change = (): dom.SetText(dom.ById("out"), "The volume changed") end end paragraph: id = "out" content = "-" end
rate change
تغییر سرعت
onratechange
Fires when playbackRate or defaultPlaybackRate changes. 1 is normal speed, 0.5 half speed, 2 double speed.audio: controls = true source = "/sounds/bell.mp3" rate change = (): dom.SetText(dom.ById("out"), "The speed changed") end end paragraph: id = "out" content = "-" end
cue change
تغییر نشانه
oncuechange
Fires on a track element when the set of active cues changes: a caption starts or ends. Read track.activeCues to get the text. The track must be showing or hidden, which default = true on a captions track does.audio: controls = true source = "/sounds/talk.mp3" track: kind = "captions" source = "/captions/talk.vtt" default = true cue change = (): dom.SetText(dom.ById("out"), "A caption changed") end end end paragraph: id = "out" content = "-" end

Animations and transitions

AttributeWhen it firesExample
animation start
شروع پویانمایی
onanimationstart
Fires on an element when a CSS animation starts, after any animation-delay. event.animationName says which animation. It does not fire when the animation has a duration of zero.box: animation = "spin 2s linear 3" animation start = (): dom.SetText(dom.ById("out"), "The animation started") end content = "Spin" padding = 20 end paragraph: id = "out" content = "-" end
animation iteration
تکرار پویانمایی
onanimationiteration
Fires at the end of each round of an animation that repeats, except the last one (that fires animation end). An animation that runs three times fires it twice.box: animation = "spin 2s linear 3" animation iteration = (): dom.SetText(dom.ById("out"), "Another round") end content = "Spin" padding = 20 end paragraph: id = "out" content = "-" end
animation end
اتمام پویانمایی
onanimationend
Fires when a CSS animation has finished all its rounds. An animation that repeats forever (infinite) never fires it. Use it to remove a class that started the animation.box: animation = "fade 1s" animation end = (): dom.SetText(dom.ById("out"), "The animation ended") end content = "Fade" padding = 20 end paragraph: id = "out" content = "-" end
animation cancel
لغو پویانمایی
onanimationcancel
Fires when a running animation is cut short without finishing: its element is hidden with display: none, removed from the page, or the animation property is taken away. No animation end follows.box: animation = "spin 2s linear infinite" animation cancel = (): dom.SetText(dom.ById("out"), "The animation was cancelled") end content = "Spin" padding = 20 end paragraph: id = "out" content = "-" end
transition run
اجرای گذار
ontransitionrun
Fires when a CSS transition is created, before its delay. event.propertyName says which property moves. In our tests Chromium (the engine of Chrome and Edge) did not read this one as an HTML attribute, while Firefox did; attach it with dom.OnEventE(el, "transitionrun", ...) to be safe.box: transition = "transform 0.4s 0.3s" hover transform = "scale(1.2)" transition run = (): dom.SetText(dom.ById("out"), "The transition was created") end content = "Hover me" padding = 20 end paragraph: id = "out" content = "-" end
transition start
شروع گذار
ontransitionstart
Fires when the transition really begins to move, after its delay is over. Without a delay it comes right after transition run. In our tests Chromium (the engine of Chrome and Edge) did not read it as an HTML attribute, while Firefox did; attach it with dom.OnEventE(el, "transitionstart", ...).box: transition = "transform 0.4s 0.3s" hover transform = "scale(1.2)" transition start = (): dom.SetText(dom.ById("out"), "The movement began") end content = "Hover me" padding = 20 end paragraph: id = "out" content = "-" end
transition end
اتمام گذار
ontransitionend
Fires when a transition has finished moving, once for every property that changed. event.propertyName tells them apart. It does not fire if the transition was cut short (that is transition cancel). Browsers read it as an attribute everywhere.box: transition = "transform 0.4s" hover transform = "scale(1.2)" transition end = (): dom.SetText(dom.ById("out"), "The transition ended") end content = "Hover me" padding = 20 end paragraph: id = "out" content = "-" end
transition cancel
لغو گذار
ontransitioncancel
Fires when a transition is interrupted before it ends, for example the mouse leaves during a hover transition and the value starts to go back, or the element is hidden. In our tests Chromium (the engine of Chrome and Edge) did not read it as an HTML attribute, while Firefox did; attach it with dom.OnEventE(el, "transitioncancel", ...).box: transition = "transform 0.4s 0.3s" hover transform = "scale(1.2)" transition cancel = (): dom.SetText(dom.ById("out"), "The transition was interrupted") end content = "Hover me" padding = 20 end paragraph: id = "out" content = "-" end

Slots, canvas and security

AttributeWhen it firesExample
context lost
از دست رفتن زمینه
oncontextlost
Fires on a canvas when the browser loses the memory behind its 2D drawing context, for example after a graphics driver reset or when the GPU is switched. Everything drawn is gone. It cannot be provoked from a page; browsers fire it only when the graphics system really fails. (A WebGL canvas has its own webglcontextlost.)canvas: id = "pad" width = 200 height = 100 context lost = (): dom.SetText(dom.ById("out"), "The drawing was lost") end end paragraph: id = "out" content = "-" end
context restored
بازیابی زمینه
oncontextrestored
Fires on a canvas after a lost context became usable again, with an empty drawing. Draw everything again here. Like context lost it only happens when the graphics system recovers from a failure.canvas: id = "pad" width = 200 height = 100 context restored = (): dom.SetText(dom.ById("out"), "Draw it again") end end paragraph: id = "out" content = "-" end
security policy violation
نقض سیاست امنیتی
onsecuritypolicyviolation
Fires when the page breaks its own Content Security Policy, the rules set by a meta element or a header, for example by writing a style attribute when style-src-attr forbids it. The event holds blockedURI and violatedDirective. It bubbles from the element to the document, but a blocked file such as an image is reported on the document only.box: security policy violation = (): dom.SetText(dom.ById("out"), "The policy blocked something") end content = "Guarded by a policy" end paragraph: id = "out" content = "-" end
slot change
تغییر شکاف
onslotchange
Fires on a slot element inside a shadow tree when the nodes that fill the slot change: one was added, removed or moved. assignedNodes() lists the current ones. The shadow tree comes from a template with shadow root mode.box: id = "team" template: shadow root mode = "open" slot: slot change = (): dom.SetText(dom.ById("out"), "The slot content changed") end end end end paragraph: id = "out" content = "-" end

Try it yourself

  • Add a button to the player that skips 1 second ahead by setting currentTime, and show in the log that seeking and seeked fire.
  • Make a button that changes the speed to 2, and a label that follows rate change.
  • Run the transition example with a transition of one property only, and compare the log with the two property version.
  • Add a second slot with a name to the slot example, and add members to it with the slot attribute.