Lesson 41 of 45 · Web pages

Drag, touch, scroll and window

A page often has to follow something that moves: a card carried across the screen, a finger on a phone, the scroll bar, the size of the window. This lesson covers the 27 events for that, shows each idea in a page you can run, and explains the traps that make drag and drop fail on the first try.

Events that move

An event attribute takes a function. You can give it the name of a function declared above the page: block, a short lambda such as (): dom.SetText(dom.ById("out"), "hi") end, or a plain JavaScript string. A function that needs the event itself takes one parameter, e: i64, and passes it to the helpers of the dom package: dom.EventX(e), dom.PreventDefault(e), dom.GetProp(e, "dataTransfer") and so on. The lesson on mouse events explains the model in full. Here you will use it for events that carry more data than a click: where the finger is, what is being dragged, how far the page has scrolled.

Drag and drop

HTML has its own drag and drop, and it needs three parts. The element you pick up has draggable = true and a drag start handler that puts something in dataTransfer, the small package the browser carries during the drag. The place you drop on has a drag over handler that calls dom.PreventDefault, because the browser refuses every drop unless it is told that this place accepts one. And that place has a drop handler that takes the package out again.

In the example the package is the text parcel, set with dom.CallS2(dt, "setData", ...). On drop, getData gives it back and the handler uses it to find the element and move it into the zone. dom.GetProp(e, "dataTransfer") returns the package as an object, and dom.CallS1 and dom.CallS2 call its methods with one or two strings.

web-drag-basic.salam
import dom

func lift(e: i64):
    dt := dom.GetProp(e, "dataTransfer")
    _ := dom.CallS2(dt, "setData", "text/plain", "parcel")
end

func allow(e: i64):
    dom.PreventDefault(e)
end

func land(e: i64):
    dom.PreventDefault(e)
    dt := dom.GetProp(e, "dataTransfer")
    id := dom.CallS1(dt, "getData", "text/plain")
    dom.Append(dom.ById("zone"), dom.ById("" + id))
    dom.SetText(dom.ById("status"), "Dropped: " + id)
end

page:
    title = "Drag and drop"
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: id = "parcel" draggable = true drag start = lift
        content = "Parcel" display = "inline-block" padding = "10px 20px" cursor = grab
        background = "#fde68a" border radius = 6
    end
    box: id = "zone" drag over = allow drop = land
        margin top = 20 padding = 30 min height = 40 border = "2px dashed #94a3b8" border radius = 8
        content = "Drop zone"
    end
    paragraph: id = "status" content = "Drag the parcel onto the zone." end
end

The most common reason a drop does nothing is a missing drag over handler. The second most common is a missing setData in drag start: Firefox does not start a drag at all without it. Write both, even when you do not need the data.

Feedback while dragging

Four more events tell you what is going on. drag fires on the dragged element again and again, drag end fires once when the drag is over, and drag enter and drag leave fire on the places the item passes. A drop zone usually uses the last two to add and remove a highlight: a class that the style: element defines, switched with dom.AddClass and dom.RemoveClass.

The example also reads dataTransfer.dropEffect in drag end. It is none when the item was dropped on a place that did not accept it, so you can tell a successful drop from a cancelled one. Other values (copy, move) depend on the browser, so only compare with none.

web-drag-feedback.salam
import dom

mut ticks := 0

func begin(e: i64):
    dt := dom.GetProp(e, "dataTransfer")
    _ := dom.CallS2(dt, "setData", "text/plain", "note")
    ticks = 0
    dom.SetText(dom.ById("log"), "drag start")
end

func moving():
    ticks += 1
    dom.SetText(dom.ById("log"), "drag fired " + ticks + " times")
end

func over(e: i64):
    dom.PreventDefault(e)
end

func leave():
    dom.RemoveClass(dom.ById("zone"), "hot")
end

func drop(e: i64):
    dom.PreventDefault(e)
    leave()
end

func finish(e: i64):
    effect := dom.GetStr(dom.GetProp(e, "dataTransfer"), "dropEffect")
    dom.SetText(dom.ById("log"), "drag end, dropEffect = " + effect)
end

page:
    style: content = ".hot { background: #dcfce7; border-color: #16a34a }" end
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: id = "note" draggable = true drag start = begin drag = moving drag end = finish
        content = "Sticky note" display = "inline-block" padding = "10px 20px" background = "#fde68a"
    end
    box: id = "zone" drag over = over
        drag enter = (): dom.AddClass(dom.ById("zone"), "hot") end
        drag leave = leave drop = drop
        content = "Drop here" margin top = 20 padding = 30 border = "2px dashed #94a3b8" border radius = 8
    end
    paragraph: id = "log" content = "Nothing yet." end
end

Moving items between lists

A board with columns is the same recipe with one change: the handlers are shared. drag start stores the id of the card in the package, and every column's drop reads the id, finds the card with dom.ById and appends it to the column that received the drop. dom.GetProp(e, "currentTarget") is the element the handler is attached to, so one function serves all columns even when the drop lands on a card inside the column. Each card and column here is a component, which keeps the page short.

web-drag-board.salam
import dom

component Task(key, label):
    box: id = key draggable = true drag start = lift content = label
        padding = "8px 12px" margin bottom = 8 background = "white" border radius = 6 cursor = grab
    end
end

component Column(key, label):
    box: id = key drag over = over drag leave = leave drop = land
        background = "#e2e8f0" border radius = 8 padding = 12 min height = 140 width = 150
        heading: size = 3 content = label margin = "0 0 10px" end
        content: end
    end
end

func lift(e: i64):
    dt := dom.GetProp(e, "dataTransfer")
    _ := dom.CallS2(dt, "setData", "text/plain", dom.GetStr(dom.EventTarget(e), "id"))
end

func over(e: i64):
    dom.PreventDefault(e)
    dom.AddClass(dom.GetProp(e, "currentTarget"), "hot")
end

func leave(e: i64):
    dom.RemoveClass(dom.GetProp(e, "currentTarget"), "hot")
end

func land(e: i64):
    dom.PreventDefault(e)
    column := dom.GetProp(e, "currentTarget")
    dom.RemoveClass(column, "hot")
    id := dom.CallS1(dom.GetProp(e, "dataTransfer"), "getData", "text/plain")
    dom.Append(column, dom.ById("" + id))
end

page:
    style: content = ".hot { outline: 2px solid #2563eb }" end
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: display = flex gap = 16
        Column: key = "todo" label = "To do" Task: key = "t1" label = "Write" end Task: key = "t2" label = "Test" end end
        Column: key = "doing" label = "Doing" Task: key = "t3" label = "Review" end end
        Column: key = "done" label = "Done" end
    end
end

Sorting inside one list needs a decision: before or after the row you dropped on? The example compares the pointer's height with the middle of that row, taken from getBoundingClientRect, and then puts the rows back in order with dom.Append, which moves an element that is already in the page.

web-drag-sort.salam
import dom

mut held := 0 as i64

component Row(key, label):
    box: id = key draggable = true drag start = pick drag over = over drop = land
        content = label padding = "10px 14px" margin bottom = 6 background = "#f1f5f9" border radius = 6 cursor = grab
    end
end

func pick(e: i64):
    held = dom.EventTarget(e)
    _ := dom.CallS2(dom.GetProp(e, "dataTransfer"), "setData", "text/plain", "row")
end

func over(e: i64):
    dom.PreventDefault(e)
end

func land(e: i64):
    dom.PreventDefault(e)
    target := dom.GetProp(e, "currentTarget")
    rect := dom.Call0(target, "getBoundingClientRect")
    middle := dom.GetNum(rect, "top") + dom.GetNum(rect, "height") / 2.0
    above := dom.EventY(e) < middle
    list := dom.ById("list")
    rows := dom.QueryAll("#list > div")
    repeat dom.CountOf(rows) in i:
        row := dom.ItemAt(rows, i)
        if row == target:
            if above: dom.Append(list, held) end
            dom.Append(list, row)
            if not above: dom.Append(list, held) end
        else row != held:
            dom.Append(list, row)
        end
    end
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 max width = 320 end
    box: id = "list"
        Row: key = "a" label = "Wake up" end
        Row: key = "b" label = "Make tea" end
        Row: key = "c" label = "Read the lesson" end
        Row: key = "d" label = "Write a page" end
    end
    paragraph: id = "hint" content = "Drag a row to a new place." end
end

Pointer events

A mouse, a pen and a finger are all pointers, and pointer events cover them with one set of names: pointer down, pointer move and pointer up. New code uses these and leaves mouse down and touch start to old pages, because one handler then works on every device. event.pointerType tells which device it was.

The example drags a ball by hand, without HTML drag and drop. On pointer down it remembers where the pointer and the ball were. On each pointer move it adds the distance travelled to the ball's position with dom.SetStyle. The important call is setPointerCapture: it tells the browser to send all later events of this pointer to the ball, even when the pointer runs ahead of it or leaves it. The events pointer capture start and pointer capture end report when that begins and ends. The touch action = none property keeps a finger from scrolling the page instead.

web-drag-pointer.salam
import dom

mut dragging := false
mut startX := 0.0
mut startY := 0.0
mut baseX := 0.0
mut baseY := 0.0

func grab(e: i64):
    ball := dom.EventTarget(e)
    _ := dom.Call1(ball, "setPointerCapture", dom.GetNum(e, "pointerId"))
    dragging = true
    startX = dom.EventX(e)
    startY = dom.EventY(e)
    baseX = dom.GetNum(ball, "offsetLeft")
    baseY = dom.GetNum(ball, "offsetTop")
    dom.SetText(dom.ById("info"), "pointer down (" + dom.GetStr(e, "pointerType") + ")")
end

func move(e: i64):
    if dragging:
        ball := dom.EventTarget(e)
        dom.SetStyle(ball, "left", (baseX + dom.EventX(e) - startX) + "px")
        dom.SetStyle(ball, "top", (baseY + dom.EventY(e) - startY) + "px")
    end
end

func release():
    dragging = false
    dom.SetText(dom.ById("info"), "pointer up")
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: position = relative width = 320 height = 180 background = "#e0f2fe" border radius = 10
        box: id = "ball" position = absolute left = 20 top = 20 width = 60 height = 60 background = "#0284c7"
            border radius = "50%" cursor = grab touch action = none
            pointer down = grab pointer move = move pointer up = release
            pointer capture start = (): dom.SetText(dom.ById("cap"), "captured") end
            pointer capture end = (): dom.SetText(dom.ById("cap"), "released") end
        end
    end
    paragraph: id = "info" content = "Drag the ball." end
    paragraph: id = "cap" content = "-" end
end

Hand made dragging is the better choice when the item must follow the pointer exactly, as in a slider, a drawing tool or a game. HTML drag and drop is the better choice when something is carried to a drop target, and it comes with the browser's drag image and the keyboard escape for free.

Entering and leaving

Four events tell where the pointer is. pointer enter and pointer leave fire once as the pointer comes and goes, and ignore the children of the element. pointer over and pointer out bubble, so they also fire when the pointer crosses between the element and a child. Pick enter and leave for hover effects. The fifth event, pointer cancel, means the browser took the pointer away, most often because a touch turned into a page scroll; end your gesture there.

web-drag-states.salam
import dom

mut trail := ""

func note(what: str):
    if trail.len() > 110: trail = "" end
    trail = trail + what + "  "
    dom.SetText(dom.ById("trail"), trail)
end

func sees(e: i64):
    note("over:" + dom.GetStr(dom.EventTarget(e), "id"))
end

func leaves(e: i64):
    note("out:" + dom.GetStr(dom.EventTarget(e), "id"))
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: id = "outer" width = 280 padding = 24 background = "#ede9fe" border radius = 10
        content = "outer"
        pointer enter = (): note("enter") end pointer leave = (): note("leave") end
        pointer over = sees pointer out = leaves
        pointer cancel = (): note("cancel") end
        box: id = "inner" margin top = 12 padding = 20 background = "#c4b5fd" border radius = 8 content = "inner" end
    end
    paragraph: id = "trail" white space = "pre-wrap" content = "Move a mouse, pen or finger over the boxes." end
end

In the log, a mouse that moves from the outer box into the inner one gives out:outer and then over:inner, with no enter or leave, because it never left the outer box.

Touch events

Touch events came first, and they still exist: touch start, touch move, touch end and touch cancel. They give you every finger, which pointer events do one at a time. dom.TouchX(e) and dom.TouchY(e) read the position of the finger that changed, and touches counts all fingers on the screen, as the example shows. On touch end the finger is already gone from touches, so the last position comes from the changed finger.

The CSS property touch action belongs with these events. By default a finger that moves on the page may scroll or zoom it, and the browser then cancels your gesture. touch action = none on your element says that you handle it yourself. Use pan-y instead if the page may still scroll vertically.

web-drag-touch.salam
import dom

func place(e: i64):
    rect := dom.Call0(dom.ById("pad"), "getBoundingClientRect")
    mark := dom.ById("mark")
    dom.SetStyle(mark, "left", (dom.TouchX(e) - dom.GetNum(rect, "left") - 20.0) + "px")
    dom.SetStyle(mark, "top", (dom.TouchY(e) - dom.GetNum(rect, "top") - 20.0) + "px")
    dom.SetStyle(mark, "display", "block")
    fingers := dom.GetNum(dom.GetProp(e, "touches"), "length")
    dom.SetText(dom.ById("info"), "fingers on the pad: " + fingers)
end

func lift(what: str):
    dom.SetStyle(dom.ById("mark"), "display", "none")
    dom.SetText(dom.ById("info"), what)
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: id = "pad" position = relative width = 320 height = 180 background = "#fef3c7" border radius = 10
        touch action = none user select = none
        touch start = place touch move = place
        touch end = (): lift("touch end") end
        touch cancel = (): lift("touch cancel") end
        content = "Touch here with a finger"
        box: id = "mark" position = absolute display = none width = 40 height = 40 border radius = "50%" background = "#f59e0b" end
    end
    paragraph: id = "info" content = "Waiting for a touch." end
end

Scroll and scroll end

scroll fires on a scrollable element every time its position changes, and a single flick causes dozens of events. Read scrollTop, scrollHeight and clientHeight with dom.GetNum: the first is how far you are, and the last two give the room there is. The example turns that into a progress bar. scroll end fires once when the movement has come to rest, which is the moment to save a position or load a picture. The button sets scrollTop back to zero with dom.SetNum, and scroll behavior = smooth makes that an animation.

web-drag-scroll-bar.salam
import dom

func fill_bar():
    area := dom.ById("text")
    room := dom.GetNum(area, "scrollHeight") - dom.GetNum(area, "clientHeight")
    percent := dom.GetNum(area, "scrollTop") * 100.0 / room
    dom.SetStyle(dom.ById("bar"), "width", percent + "%")
end

func settled():
    where := dom.GetNum(dom.ById("text"), "scrollTop")
    dom.SetText(dom.ById("note"), "Scrolling stopped at " + where + " px.")
end

func to_top():
    dom.SetNum(dom.ById("text"), "scrollTop", 0.0)
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 max width = 420 end
    box: height = 6 background = "#e2e8f0" border radius = 3
        box: id = "bar" height = 6 width = "0%" background = "#2563eb" border radius = 3 end
    end
    box: id = "text" height = 150 overflow y = auto padding = "0 12px" border = "1px solid #cbd5e1"
        scroll behavior = smooth scroll = fill_bar scroll end = settled
        paragraph: repeat = 12 content = "Scrolling changes scrollTop, and every change fires a scroll event." end
    end
    button: click = to_top content = "Back to top" margin top = 10 end
    paragraph: id = "note" content = "Scroll the text above." end
end

An endless list is a scroll handler that checks how much room is left and adds items when it gets small. This one adds five paragraphs with dom.Create and dom.Append whenever the reader is within 30 pixels of the end.

web-drag-scroll-more.salam
import dom

mut total := 6 as i64

func load_more():
    feed := dom.ById("feed")
    left := dom.GetNum(feed, "scrollHeight") - dom.GetNum(feed, "scrollTop") - dom.GetNum(feed, "clientHeight")
    if left < 30.0:
        repeat 5 in i:
            item := dom.Create("p")
            dom.SetText(item, "Loaded item " + (total + i + 1))
            dom.Append(feed, item)
        end
        total += 5
        dom.SetText(dom.ById("count"), total + " items so far")
    end
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 max width = 360 end
    box: id = "feed" height = 130 overflow y = auto padding = "0 12px" background = "#f8fafc" border = "1px solid #cbd5e1" scroll = load_more
        paragraph: repeat = 6 content = "A first item" end
    end
    paragraph: id = "count" content = "6 items so far" end
end

Keep scroll handlers cheap, because they run very often. The example above does arithmetic and one style change. Work that is slow, such as a network request, belongs in scroll end, which runs once.

The window, and images

Two events belong to the window and not to an element. scroll on the whole page and resize are fired at the window, and the page language cannot attach code to the window: the HTML attribute exists only on the body element, which a page does not write. The way out is to register them with dom.OnWindow("scroll", ...) and dom.OnResize(...), and to run that registration from the load event of a one pixel image, which the browser fires as soon as the page is ready. The example shows the size of the window, and a progress bar for the whole page.

web-drag-window.salam
import dom

func show_size():
    text := "Window: " + dom.WinWidth() + " x " + dom.WinHeight() + " px"
    dom.SetText(dom.ById("size"), text)
end

func move_bar():
    room := dom.DocHeight() - dom.WinHeight()
    dom.SetStyle(dom.ById("bar"), "width", dom.ScrollY() * 100.0 / room + "%")
end

func listen():
    dom.OnResize((): show_size() end)
    dom.OnWindow("scroll", (): move_bar() end)
    show_size()
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 0 end
    image: source = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt = "" width = 1 height = 1 load = listen end
    box: position = fixed top = 0 left = 0 right = 0 height = 6 background = "#e2e8f0"
        box: id = "bar" height = 6 width = "0%" background = "#16a34a" end
    end
    box: padding = "24px 24px 600px"
        paragraph: id = "size" content = "Waiting for the page to load." end
        paragraph: content = "Resize the window and scroll the page. The bar at the top shows how far you have gone." end
    end
end

The load event also does its normal job on images, and so do error and abort. error fires when the file cannot be loaded or decoded, and the handler can put a spare picture in its place with dom.SetAttr. abort fires when loading is stopped half way, for example because the page changed the source. In the example the second picture is not an image at all, so it fails and the page swaps it for a spare. Check that the spare differs from the current source, or a broken spare would start an endless loop.

web-drag-image.salam
import dom

const SPARE := "data:image/gif;base64,R0lGODlhAQABAIAAAMvV4QAAACwAAAAAAQABAAACAkQBADs="

mut lines := ""

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

func broken():
    img := dom.ById("second")
    if dom.GetAttr(img, "src") != SPARE:
        say("error: the second image is broken, using the spare")
        dom.SetAttr(img, "src", SPARE)
    end
end

page:
    global: selector = "body" font family = "system-ui, sans-serif" margin = 24 end
    box: display = flex gap = 16
        image: id = "first" alt = "A blue square" width = 80 height = 80 load = (): say("load: the first image arrived") end
            source = "data:image/gif;base64,R0lGODlhAQABAIAAAAKExwAAACwAAAAAAQABAAACAkQBADs="
        end
        image: id = "second" alt = "A picture" width = 80 height = 80 error = broken
            source = "data:image/png;base64,not-an-image"
            load = (): say("load: the second image is showing") end
        end
    end
    paragraph: id = "log" white space = "pre-wrap" content = "" end
end

Reference: every event of this lesson

Each event is a global attribute, so it can go on any element, although some only make sense on certain ones. The examples are checked by the compiler. They use lambdas or short JavaScript strings; in a real page a function declared above the page is easier to read. Four events of this lesson end in end: drag end, pointer capture end, touch end and scroll end. In Persian their names start with اتمام and not پایان, because پایان on its own would end the element.

Drag and drop

AttributeWhen it firesExample
drag start
شروع کشیدن
ondragstart
Fires on the element you start dragging, once, when the drag begins. Put the data you want to carry into dataTransfer here. The element must have draggable = true (images and links already are). Firefox only starts a drag if you call setData in this event.box: draggable = true drag start = "event.dataTransfer.setData('text/plain', 'card')" content = "Drag me" end
drag
کشیدن
ondrag
Fires on the dragged element again and again while the drag goes on, every few hundred milliseconds even if the mouse stands still. Use it for a live counter or a preview. For a drop target use drag over instead.box: draggable = true drag = (): dom.SetText(dom.ById("out"), "Dragging...") end content = "Drag me" end paragraph: id = "out" content = "-" end
drag end
اتمام کشیدن
ondragend
Fires once on the dragged element when the drag is over, whether it was dropped or cancelled with Escape. Read dataTransfer.dropEffect here: none means nobody accepted the drop. Use it to clean up what drag start changed.box: draggable = true drag end = (): dom.SetText(dom.ById("out"), "The drag is over") end content = "Drag me" end paragraph: id = "out" content = "-" end
drag enter
ورود کشیدن
ondragenter
Fires on an element when something being dragged moves onto it. It also fires for the children of the element and bubbles, so a highlight that is removed in drag leave may flicker; a second check on relatedTarget or a counter solves that. To allow dropping, you still need drag over.box: id = "zone" drag enter = (): dom.SetText(dom.ById("zone"), "Something arrived") end content = "Drop zone" padding = 30 end
drag leave
خروج کشیدن
ondragleave
Fires on an element when the dragged item leaves it, and also when it moves onto a child of the element. It fires after a drop is cancelled too. Pair it with drag enter to add and remove a highlight.box: id = "zone" drag leave = (): dom.SetText(dom.ById("zone"), "Left the zone") end content = "Drop zone" padding = 30 end
drag over
عبور کشیدن
ondragover
Fires on the element under the dragged item every few hundred milliseconds. The browser refuses a drop unless this event is cancelled, so every drop target needs a handler that calls event.preventDefault() (dom.PreventDefault(e) in a function). The short JavaScript string below does exactly that.box: drag over = "event.preventDefault()" content = "Drop target" padding = 30 end
drop
رها کردن
ondrop
Fires on the target when the user releases the dragged item over it, but only if drag over was cancelled on that target. Read what the item carries with dataTransfer.getData, and call preventDefault so the browser does not also open a dropped link or file.box: id = "zone" drag over = "event.preventDefault()" drop = (): dom.SetText(dom.ById("zone"), "Dropped!") end content = "Drop here" padding = 30 end

Pointer

AttributeWhen it firesExample
pointer down
فشردن اشاره‌گر
onpointerdown
Fires when a mouse button goes down, a pen touches the screen or a finger touches it, one event for all three. pointerType tells which one it was. It replaces mouse down and touch start in new code.box: pointer down = (): dom.SetText(dom.ById("out"), "Pointer down") end content = "Touch here" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
pointer up
رها کردن اشاره‌گر
onpointerup
Fires when the button is released or the finger or pen lifts, on the element under the pointer (or on the element that captured it). It does not fire after pointer cancel.box: pointer up = (): dom.SetText(dom.ById("out"), "Pointer up") end content = "Press and release" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
pointer move
حرکت اشاره‌گر
onpointermove
Fires whenever the pointer moves over the element, very often. Read the position with dom.EventX and dom.EventY. To follow a pointer that has left the element, capture it with setPointerCapture in pointer down.box: pointer move = (): dom.SetText(dom.ById("out"), "Moving") end content = "Move over me" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
pointer enter
ورود اشاره‌گر
onpointerenter
Fires once when the pointer moves onto the element or onto one of its children. It does not bubble, and moving between the element and its children does not fire it again, which makes it the right event for hover effects. Touch fires it at the start of a touch.box: pointer enter = (): dom.SetText(dom.ById("out"), "Entered") end content = "Hover me" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
pointer leave
خروج اشاره‌گر
onpointerleave
Fires once when the pointer leaves the element and all of its children. It does not bubble. It is the partner of pointer enter. After a touch ends it fires too.box: pointer leave = (): dom.SetText(dom.ById("out"), "Left") end content = "Hover me" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
pointer over
عبور اشاره‌گر
onpointerover
Fires when the pointer moves onto the element or onto a child. Unlike pointer enter it bubbles and fires again each time the pointer crosses into a child, so event.target tells which element it is over.box: pointer over = (): dom.SetText(dom.ById("out"), "Over") end content = "Hover me" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
pointer out
بیرون رفتن اشاره‌گر
onpointerout
Fires when the pointer leaves the element or moves onto a child of it. It bubbles. It is the partner of pointer over.box: pointer out = (): dom.SetText(dom.ById("out"), "Out") end content = "Hover me" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
pointer cancel
لغو اشاره‌گر
onpointercancel
Fires when the browser takes the pointer away from you, most often because a touch turned into a scroll or a zoom. No pointer up follows, so end your gesture here. The CSS property touch action tells the browser which touches you handle yourself.box: pointer cancel = (): dom.SetText(dom.ById("out"), "Cancelled") end content = "Touch and drag away" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
pointer capture start
شروع ضبط اشاره‌گر
ongotpointercapture
Fires on the element after it captured the pointer with setPointerCapture (HTML name ongotpointercapture). From then on all pointer events of that pointer go to this element, even when the pointer is far outside it. That is what makes a drag by hand reliable.box: pointer down = "event.target.setPointerCapture(event.pointerId)" pointer capture start = (): dom.SetText(dom.ById("out"), "Captured") end content = "Press here" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
pointer capture end
اتمام ضبط اشاره‌گر
onlostpointercapture
Fires on the element when it loses the pointer capture (HTML name onlostpointercapture): after pointer up or pointer cancel, or when you call releasePointerCapture. Clean up the drag here.box: pointer down = "event.target.setPointerCapture(event.pointerId)" pointer capture end = (): dom.SetText(dom.ById("out"), "Released") end content = "Press here" padding = 30 touch action = none end paragraph: id = "out" content = "-" end

Touch

AttributeWhen it firesExample
touch start
شروع لمس
ontouchstart
Fires when a finger touches the element. dom.TouchX and dom.TouchY read the position of the touch that changed, and touches lists all fingers on the screen. For one handler that also works with a mouse prefer pointer down.box: touch start = (): dom.SetText(dom.ById("out"), "A finger touched") end content = "Touch here" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
touch move
حرکت لمس
ontouchmove
Fires over and over while a finger that started on this element moves. A page scrolls with such moves unless you set touch action = none on the element, so set it when you want to handle the gesture yourself.box: touch move = (): dom.SetText(dom.ById("out"), "The finger is moving") end content = "Slide here" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
touch end
اتمام لمس
ontouchend
Fires when a finger lifts from the screen. The finger is no longer in touches; its last position is in changedTouches, which is what dom.TouchX reads.box: touch end = (): dom.SetText(dom.ById("out"), "The finger lifted") end content = "Touch here" padding = 30 touch action = none end paragraph: id = "out" content = "-" end
touch cancel
لغو لمس
ontouchcancel
Fires when the system interrupts a touch, for example a call arrives, the browser takes the gesture for scrolling, or too many fingers are down. Treat it like touch end without a result: drop what the gesture started.box: touch cancel = (): dom.SetText(dom.ById("out"), "The touch was interrupted") end content = "Touch here" padding = 30 touch action = none end paragraph: id = "out" content = "-" end

Scroll, window and loading

AttributeWhen it firesExample
abort
لغو
onabort
Fires on an image or a media element when loading is stopped before it finished and it was not an error, for example because the page changed the src while the first file was still arriving. A file that cannot be loaded fires error instead.image: source = "/photos/large.jpg" alt = "A large photo" abort = (): dom.SetText(dom.ById("out"), "Loading was stopped") end end paragraph: id = "out" content = "-" end
error
خطا
onerror
Fires on an image, script, media element or style link when its file cannot be loaded or decoded. The usual use is to swap in a spare picture. Set the new src only if it differs from the current one, or a broken spare makes an endless loop.image: source = "/photos/missing.png" alt = "Logo" error = (): dom.SetText(dom.ById("out"), "The logo could not be loaded") end end paragraph: id = "out" content = "-" end
load
بار شد
onload
Fires on an image, frame or similar element when its file has finished loading. It is also the way to run code at the start of a page: the page language cannot attach code to the whole window, but a tiny image that loads at once can, as this snippet does to listen to window resizes.image: source = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt = "" width = 1 height = 1 load = (): dom.OnResize((): dom.SetText(dom.ById("out"), "Width " + dom.WinWidth()) end) end end paragraph: id = "out" content = "-" end
window resize
تغییر اندازه پنجره
onresize
The browser fires resize only at the window, and HTML only lets you write it on the body element, which a page cannot reach. On an ordinary element this attribute never fires. To react to a window resize use dom.OnResize (registered from a load handler, as here) or, for layout changes, a media query.image: source = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" alt = "" width = 1 height = 1 load = (): dom.OnResize((): dom.SetText(dom.ById("size"), "" + dom.WinWidth()) end) end end paragraph: id = "size" content = "-" end
scroll
پیمایش
onscroll
Fires on a scrollable element every time its scroll position changes, many times during one flick. Keep the handler small. It is the event for progress bars, a back-to-top button that appears late, and loading more items near the bottom.box: height = 80 overflow y = auto scroll = (): dom.SetText(dom.ById("out"), "Scrolled") end paragraph: repeat = 10 content = "A line of text" end end paragraph: id = "out" content = "-" end
scroll end
اتمام پیمایش
onscrollend
Fires once when scrolling has come to rest, after the finger lifts and any momentum or smooth scrolling is over. It is newer than scroll, and Safari added it last, so check the browsers you must support. Use it for work that should wait until the movement stops, such as saving the position.box: height = 80 overflow y = auto scroll end = (): dom.SetText(dom.ById("out"), "Scrolling stopped") end paragraph: repeat = 10 content = "A line of text" end end paragraph: id = "out" content = "-" end

Try it yourself

  • Turn the sortable list into a to-do list: a second list called Done, and a drop on it moves the row there and strikes the text through.
  • Add a slider to the pointer example: a track and a handle that you drag only sideways, and a paragraph that shows the value from 0 to 100.
  • Make a gallery of three images in which the second one has a wrong file name. Show a grey spare for it with the error event.
  • Combine the scroll progress bar with scroll end so that the position of the text is saved in dom.StoreSetInt when the reader stops.