diff options
author | bptato <nincsnevem662@gmail.com> | 2024-07-22 21:07:54 +0200 |
---|---|---|
committer | bptato <nincsnevem662@gmail.com> | 2024-07-22 21:07:54 +0200 |
commit | 98c6b86b3f2d66a67d5fc25ff78d8a5a228f28a6 (patch) | |
tree | 2a8de456757cfe238341e7d7c5723d8d7acf1eb6 | |
parent | 814b7b000af414696da6f5d8613fd21a1e104ef4 (diff) | |
download | chawan-98c6b86b3f2d66a67d5fc25ff78d8a5a228f28a6.tar.gz |
buffer, dom, event: JS binding for dispatchEvent
* move dispatchEvent to event, add a JS binding * only reshape if the document was actually invalidated after event dispatch/interval call/etc.
-rw-r--r-- | src/html/catom.nim | 3 | ||||
-rw-r--r-- | src/html/dom.nim | 144 | ||||
-rw-r--r-- | src/html/event.nim | 52 | ||||
-rw-r--r-- | src/html/script.nim | 17 | ||||
-rw-r--r-- | src/html/xmlhttprequest.nim | 4 | ||||
-rw-r--r-- | src/server/buffer.nim | 48 |
6 files changed, 152 insertions, 116 deletions
diff --git a/src/html/catom.nim b/src/html/catom.nim index b6dced60..f22d74ef 100644 --- a/src/html/catom.nim +++ b/src/html/catom.nim @@ -197,8 +197,7 @@ func toStaticAtom*(factory: CAtomFactory; atom: CAtom): StaticAtom = return StaticAtom(i) return atUnknown -var getFactory* {.compileTime.}: proc(ctx: JSContext): CAtomFactory {.nimcall, - noSideEffect.} +var getFactory*: proc(ctx: JSContext): CAtomFactory {.nimcall, noSideEffect.} proc toAtom*(ctx: JSContext; atom: StaticAtom): CAtom = return ctx.getFactory().toAtom(atom) diff --git a/src/html/dom.nim b/src/html/dom.nim index 43e078a6..d1b8d249 100644 --- a/src/html/dom.nim +++ b/src/html/dom.nim @@ -187,6 +187,7 @@ type renderBlockingElements: seq[Element] invalidCollections: HashSet[pointer] # pointers to Collection objects + invalid*: bool # whether the document must be rendered again cachedAll: HTMLAllCollection cachedSheets: seq[CSSStylesheet] @@ -2437,62 +2438,36 @@ func findAutoFocus*(document: Document): Element = return nil # Forward declaration hack -isDefaultPassive = func(eventTarget: EventTarget): bool = - if eventTarget of Window: +isDefaultPassive = func(target: EventTarget): bool = + if target of Window: return true - if not (eventTarget of Node): + if not (target of Node): return false - let node = Node(eventTarget) - return EventTarget(node.document) == eventTarget or - EventTarget(node.document.html) == eventTarget or - EventTarget(node.document.body) == eventTarget + let node = Node(target) + return EventTarget(node.document) == target or + EventTarget(node.document.html) == target or + EventTarget(node.document.body) == target + +getParent = proc(ctx: JSContext; eventTarget: EventTarget; event: Event): + EventTarget = + if eventTarget of Node: + if eventTarget of Document: + if event.ctype == ctx.toAtom(satLoad): + return nil + # if no browsing context, then window will be nil anyway + return Document(eventTarget).window + return Node(eventTarget).parentNode + return nil getFactory = proc(ctx: JSContext): CAtomFactory = return ctx.getGlobal().factory -type DispatchEventResult = tuple - called: bool - canceled: bool - -proc dispatchEvent0(window: Window; event: Event; currentTarget: EventTarget; - called, stop, canceled: var bool) = - let ctx = window.jsctx - event.currentTarget = currentTarget - var els = currentTarget.eventListeners # copy intentionally - for el in els: - if JS_IsUndefined(el.callback): - continue # removed, presumably by a previous handler - if el.ctype == event.ctype: - let e = ctx.invoke(el, event) - called = true - if JS_IsException(e): - window.console.error(ctx.getExceptionMsg()) - JS_FreeValue(ctx, e) - if efStopImmediatePropagation in event.flags: - stop = true - break - if efStopPropagation in event.flags: - stop = true - if efCanceled in event.flags: - canceled = true - -proc dispatchEvent*(window: Window; event: Event; target: EventTarget): - DispatchEventResult = - #TODO this is far from being compliant - var called = false - var canceled = false - var stop = false - window.dispatchEvent0(event, target, called, stop, canceled) - if not stop and target of Node: - for a in Node(target).nodeAncestors: - window.dispatchEvent0(event, a, called, stop, canceled) - if stop: - break - return (called, canceled) +windowConsoleError = proc(ctx: JSContext; ss: varargs[string]) = + ctx.getGlobal().console.error(ss) proc fireEvent*(window: Window; name: StaticAtom; target: EventTarget) = let event = newEvent(window.toAtom(name), target) - discard window.dispatchEvent(event, target) + discard window.jsctx.dispatchEvent(target, event) proc parseColor(element: Element; s: string): ARGBColor = let cval = parseComponentValue(s) @@ -2841,6 +2816,11 @@ proc invalidateCollections(node: Node) = for id in node.liveCollections: node.document.invalidCollections.incl(id) +proc setInvalid*(element: Element) = + element.invalid = true + if element.document != nil: + element.document.invalid = true + proc delAttr(element: Element; i: int; keep = false) = let map = element.attributesInternal let name = element.attrs[i].qualifiedName @@ -2866,7 +2846,7 @@ proc delAttr(element: Element; i: int; keep = false) = map.attrlist.del(j) # ordering does not matter element.reflectAttr(name, none(string)) element.invalidateCollections() - element.invalid = true + element.setInvalid() proc newCSSStyleDeclaration(element: Element; value: string): CSSStyleDeclaration = @@ -3084,7 +3064,7 @@ proc loadResource(window: Window; image: HTMLImageElement) = cachedURL.bmp = bmp for share in cachedURL.shared: share.bitmap = bmp - image.invalid = true + image.setInvalid() ) ) window.loadingResourcePromises.add(p) @@ -3224,7 +3204,7 @@ proc attr*(element: Element; name: CAtom; value: string) = if i >= 0: element.attrs[i].value = value element.invalidateCollections() - element.invalid = true + element.setInvalid() else: element.attrs.insert(AttrData( qualifiedName: name, @@ -3255,7 +3235,7 @@ proc attrns*(element: Element; localName: CAtom; prefix: NamespacePrefix; element.attrs[i].qualifiedName = qualifiedName element.attrs[i].value = value element.invalidateCollections() - element.invalid = true + element.setInvalid() else: element.attrs.insert(AttrData( prefix: prefixAtom, @@ -3399,7 +3379,9 @@ proc remove*(node: Node; suppressObservers: bool) = parent.childList[i] = parent.childList[i + 1] parent.childList[i].index = i parent.childList.setLen(parent.childList.len - 1) - node.parentNode.invalidateCollections() + parent.invalidateCollections() + if parent of Element: + Element(parent).setInvalid() node.parentNode = nil node.index = -1 if node.document != nil and (node of HTMLStyleElement or @@ -3438,7 +3420,7 @@ proc resetElement*(element: Element) = input.file = nil else: input.value = input.attr(satValue) - input.invalid = true + input.setInvalid() of TAG_SELECT: let select = HTMLSelectElement(element) if not select.attrb(satMultiple): @@ -3464,7 +3446,7 @@ proc resetElement*(element: Element) = of TAG_TEXTAREA: let textarea = HTMLTextAreaElement(element) textarea.value = textarea.childTextContent() - textarea.invalid = true + textarea.setInvalid() else: discard proc setForm*(element: FormAssociatedElement; form: HTMLFormElement) = @@ -3632,7 +3614,7 @@ proc insert*(parent, node, before: Node; suppressObservers = false) = #TODO live ranges discard if parent of Element: - Element(parent).invalid = true + Element(parent).setInvalid() for node in nodes: insertNode(parent, node, before) @@ -3744,14 +3726,14 @@ proc textContent*(node: Node; data: Option[string]) {.jsfset.} = proc reset*(form: HTMLFormElement) = for control in form.controls: control.resetElement() - control.invalid = true + control.setInvalid() proc renderBlocking*(element: Element): bool = if "render" in element.attr(satBlocking).split(AsciiWhitespace): return true if element of HTMLScriptElement: let element = HTMLScriptElement(element) - if element.ctype == CLASSIC and element.parserDocument != nil and + if element.ctype == stClassic and element.parserDocument != nil and not element.attrb(satAsync) and not element.attrb(satDefer): return true return false @@ -3786,14 +3768,14 @@ proc fetchClassicScript(element: HTMLScriptElement; url: URL; onComplete: OnCompleteProc) = let window = element.document.window if not element.scriptingEnabled: - element.onComplete(ScriptResult(t: RESULT_NULL)) + element.onComplete(ScriptResult(t: srtNull)) return let request = createPotentialCORSRequest(url, rdScript, cors) request.client = some(window.settings) #TODO make this non-blocking somehow let response = window.loader.doRequest(request.request) if response.res != 0: - element.onComplete(ScriptResult(t: RESULT_NULL)) + element.onComplete(ScriptResult(t: srtNull)) return window.loader.resume(response.outputId) let s = response.body.recvAll() @@ -3801,7 +3783,7 @@ proc fetchClassicScript(element: HTMLScriptElement; url: URL; let source = s.decodeAll(cs) response.body.sclose() let script = window.jsctx.createClassicScript(source, url, options, false) - element.onComplete(ScriptResult(t: RESULT_SCRIPT, script: script)) + element.onComplete(ScriptResult(t: srtScript, script: script)) #TODO settings object proc fetchDescendantsAndLink(element: HTMLScriptElement; script: Script; @@ -3815,7 +3797,7 @@ proc fetchExternalModuleGraph(element: HTMLScriptElement; url: URL; options: ScriptOptions; onComplete: OnCompleteProc) = let window = element.document.window if not element.scriptingEnabled: - element.onComplete(ScriptResult(t: RESULT_NULL)) + element.onComplete(ScriptResult(t: srtNull)) return window.importMapsAllowed = false element.fetchSingleModule( @@ -3825,7 +3807,7 @@ proc fetchExternalModuleGraph(element: HTMLScriptElement; url: URL; parseURL("about:client").get, isTopLevel = true, onComplete = proc(element: HTMLScriptElement; res: ScriptResult) = - if res.t == RESULT_NULL: + if res.t == srtNull: element.onComplete(res) else: element.fetchDescendantsAndLink(res.script, rdScript, onComplete) @@ -3846,7 +3828,7 @@ proc fetchSingleModule(element: HTMLScriptElement; url: URL; let settings = element.document.window.settings let i = settings.moduleMap.find(url, moduleType) if i != -1: - if settings.moduleMap[i].value.t == RESULT_FETCHING: + if settings.moduleMap[i].value.t == srtFetching: #TODO await value assert false element.onComplete(settings.moduleMap[i].value) @@ -3878,14 +3860,14 @@ proc execute*(element: HTMLScriptElement) = #assert element.scriptResult != nil if element.scriptResult == nil: return - if element.scriptResult.t == RESULT_NULL: + if element.scriptResult.t == srtNull: #TODO fire error event return - let needsInc = element.external or element.ctype == MODULE + let needsInc = element.external or element.ctype == stModule if needsInc: inc document.ignoreDestructiveWrites case element.ctype - of CLASSIC: + of stClassic: let oldCurrentScript = document.currentScript #TODO not if shadow root document.currentScript = element @@ -3929,11 +3911,11 @@ proc prepare*(element: HTMLScriptElement) = else: "text/javascript" if typeString.isJavaScriptType(): - element.ctype = CLASSIC + element.ctype = stClassic elif typeString == "module": - element.ctype = MODULE + element.ctype = stModule elif typeString == "importmap": - element.ctype = IMPORTMAP + element.ctype = stImportMap else: return if parserDocument != nil: @@ -3946,10 +3928,10 @@ proc prepare*(element: HTMLScriptElement) = return if not element.scriptingEnabled: return - if element.attrb(satNomodule) and element.ctype == CLASSIC: + if element.attrb(satNomodule) and element.ctype == stClassic: return #TODO content security policy - if element.ctype == CLASSIC and element.attrb(satEvent) and + if element.ctype == stClassic and element.attrb(satEvent) and element.attrb(satFor): let f = element.attr(satFor).strip(chars = AsciiWhitespace) let event = element.attr(satEvent).strip(chars = AsciiWhitespace) @@ -3973,7 +3955,7 @@ proc prepare*(element: HTMLScriptElement) = ) #TODO settings object if element.attrb(satSrc): - if element.ctype == IMPORTMAP: + if element.ctype == stImportMap: #TODO fire error event return let src = element.attr(satSrc) @@ -3990,22 +3972,22 @@ proc prepare*(element: HTMLScriptElement) = element.delayingTheLoadEvent = true if element in element.document.renderBlockingElements: options.renderBlocking = true - if element.ctype == CLASSIC: + if element.ctype == stClassic: element.fetchClassicScript(url.get, options, classicCORS, encoding, markAsReady) else: element.fetchExternalModuleGraph(url.get, options, markAsReady) else: let baseURL = element.document.baseURL - if element.ctype == CLASSIC: + if element.ctype == stClassic: let ctx = element.document.window.jsctx let script = ctx.createClassicScript(sourceText, baseURL, options) - element.markAsReady(ScriptResult(t: RESULT_SCRIPT, script: script)) + element.markAsReady(ScriptResult(t: srtScript, script: script)) else: - #TODO MODULE, IMPORTMAP - element.markAsReady(ScriptResult(t: RESULT_NULL)) - if element.ctype == CLASSIC and element.attrb(satSrc) or - element.ctype == MODULE: + #TODO stModule, stImportMap + element.markAsReady(ScriptResult(t: srtNull)) + if element.ctype == stClassic and element.attrb(satSrc) or + element.ctype == stModule: let prepdoc = element.preparationTimeDocument if element.attrb(satAsync): prepdoc.scriptsToExecSoon.add(element) @@ -4026,7 +4008,7 @@ proc prepare*(element: HTMLScriptElement) = script.execute() prepdoc.scriptsToExecInOrder.shrink(1) ) - elif element.ctype == MODULE or element.attrb(satDefer): + elif element.ctype == stModule or element.attrb(satDefer): element.parserDocument.scriptsToExecOnLoad.addFirst(element) element.onReady = (proc() = element.readyForParserExec = true @@ -4038,7 +4020,7 @@ proc prepare*(element: HTMLScriptElement) = element.readyForParserExec = true ) else: - #TODO if CLASSIC, parserDocument != nil, parserDocument has a style sheet + #TODO if stClassic, parserDocument != nil, parserDocument has a style sheet # that is blocking scripts, either the parser is an XML parser or a HTML # parser with a script level <= 1 element.execute() diff --git a/src/html/event.nim b/src/html/event.nim index d396f3ba..04b7fb67 100644 --- a/src/html/event.nim +++ b/src/html/event.nim @@ -3,6 +3,8 @@ import std/options import std/times import html/catom +import html/script +import js/domexception import monoucha/fromjs import monoucha/javascript import monoucha/jserror @@ -64,7 +66,10 @@ jsDestructor(CustomEvent) jsDestructor(EventTarget) # Forward declaration hack -var isDefaultPassive*: proc(eventTarget: EventTarget): bool {.nimcall.} = nil +var isDefaultPassive*: proc(target: EventTarget): bool {.nimcall, + noSideEffect.} = nil +var getParent*: proc(ctx: JSContext; target: EventTarget, event: Event): + EventTarget {.nimcall.} type EventInit* = object of JSDict @@ -192,8 +197,7 @@ proc findEventListener(eventTarget: EventTarget; ctype: CAtom; return -1 # EventListener -proc invoke*(ctx: JSContext; listener: EventListener; event: Event): - JSValue = +proc invoke(ctx: JSContext; listener: EventListener; event: Event): JSValue = #TODO make this standards compliant if JS_IsNull(listener.callback): return JS_UNDEFINED @@ -290,6 +294,48 @@ proc removeEventListener(ctx: JSContext; eventTarget: EventTarget; if i != -1: eventTarget.removeAnEventListener(ctx, i) +proc dispatchEvent0(ctx: JSContext; event: Event; currentTarget: EventTarget; + stop, canceled: var bool) = + event.currentTarget = currentTarget + var els = currentTarget.eventListeners # copy intentionally + for el in els: + if JS_IsUndefined(el.callback): + continue # removed, presumably by a previous handler + if el.ctype == event.ctype: + let e = ctx.invoke(el, event) + if JS_IsException(e): + ctx.logException() + JS_FreeValue(ctx, e) + if efStopImmediatePropagation in event.flags: + stop = true + break + if efStopPropagation in event.flags: + stop = true + if efCanceled in event.flags: + canceled = true + +proc dispatch*(ctx: JSContext; target: EventTarget; event: Event): bool = + #TODO this is far from being compliant + var canceled = false + var stop = false + event.flags.incl(efDispatch) + var target = target + while target != nil and not stop: + ctx.dispatchEvent0(event, target, stop, canceled) + target = ctx.getParent(target, event) + event.flags.excl(efDispatch) + return canceled + +proc dispatchEvent*(ctx: JSContext; this: EventTarget; event: Event): + DOMResult[bool] {.jsfunc.} = + if efDispatch in event.flags: + return errDOMException("Event's dispatch flag is already set", + "InvalidStateError") + if efInitialized notin event.flags: + return errDOMException("Event is not initialized", "InvalidStateError") + event.isTrusted = false + return ok(ctx.dispatch(this, event)) + proc addEventModule*(ctx: JSContext) = let eventCID = ctx.registerType(Event) ctx.registerType(CustomEvent, parent = eventCID) diff --git a/src/html/script.nim b/src/html/script.nim index 59ada2f0..ab445516 100644 --- a/src/html/script.nim +++ b/src/html/script.nim @@ -7,10 +7,10 @@ type pmParserInserted, pmNotParserInserted ScriptType* = enum - NO_SCRIPTTYPE, CLASSIC, MODULE, IMPORTMAP + stNone, stClassic, stModule, stImportMap ScriptResultType* = enum - RESULT_NULL, RESULT_SCRIPT, RESULT_IMPORT_MAP_PARSE, RESULT_FETCHING + srtNull, srtScript, srtImportMapParse, srtFetching RequestDestination* = enum rdNone = "" @@ -64,13 +64,11 @@ type ScriptResult* = ref object case t*: ScriptResultType - of RESULT_NULL: + of srtNull, srtFetching: discard - of RESULT_SCRIPT: + of srtScript: script*: Script - of RESULT_FETCHING: - discard - of RESULT_IMPORT_MAP_PARSE: + of srtImportMapParse: discard #TODO ModuleMapEntry = object @@ -93,3 +91,8 @@ func fetchDestinationFromModuleType*(default: RequestDestination; if moduleType == "css": return rdStyle return default + +var windowConsoleError*: proc(ctx: JSContext; ss: varargs[string]) {.nimcall.} + +proc logException*(ctx: JSContext) = + windowConsoleError(ctx, ctx.getExceptionMsg()) diff --git a/src/html/xmlhttprequest.nim b/src/html/xmlhttprequest.nim index 2ab6b88d..1fc2e8d7 100644 --- a/src/html/xmlhttprequest.nim +++ b/src/html/xmlhttprequest.nim @@ -164,10 +164,10 @@ proc fireProgressEvent(window: Window; target: EventTarget; name: StaticAtom; total: length, lengthComputable: length != 0 )) - discard window.dispatchEvent(event, target) + discard window.jsctx.dispatch(target, event) # Forward declaration hack -var windowFetch* {.compileTime.}: proc(window: Window; input: JSRequest; +var windowFetch*: proc(window: Window; input: JSRequest; init = none(RequestInit)): JSResult[FetchPromise] {.nimcall.} = nil proc errorSteps(window: Window; this: XMLHttpRequest; name: StaticAtom) = diff --git a/src/server/buffer.nim b/src/server/buffer.nim index 3864d9c6..984ed826 100644 --- a/src/server/buffer.nim +++ b/src/server/buffer.nim @@ -719,6 +719,11 @@ proc do_reshape(buffer: Buffer) = buffer.images) buffer.prevStyled = styledRoot +proc maybeReshape(buffer: Buffer) = + if buffer.document != nil and buffer.document.invalid: + buffer.do_reshape() + buffer.document.invalid = false + proc processData0(buffer: Buffer; data: UnsafeSlice): bool = if buffer.ishtml: if buffer.htmlParser.parseBuffer(data.toOpenArray()) == PRES_STOP: @@ -736,7 +741,7 @@ proc processData0(buffer: Buffer; data: UnsafeSlice): bool = Text(lastChild).data &= data else: plaintext.insert(buffer.document.createTextNode($data), nil) - plaintext.invalid = true + plaintext.setInvalid() true func canSwitch(buffer: Buffer): bool {.inline.} = @@ -990,16 +995,14 @@ proc clone*(buffer: Buffer; newurl: URL): int {.proxy.} = proc dispatchDOMContentLoadedEvent(buffer: Buffer) = let window = buffer.window let event = newEvent(window.toAtom(satDOMContentLoaded), buffer.document) - let (called, _) = window.dispatchEvent(event, buffer.document) - if called: - buffer.do_reshape() + discard window.jsctx.dispatch(buffer.document, event) + buffer.maybeReshape() proc dispatchLoadEvent(buffer: Buffer) = let window = buffer.window let event = newEvent(window.toAtom(satLoad), window) - let (called, _) = window.dispatchEvent(event, window) - if called: - buffer.do_reshape() + discard window.jsctx.dispatch(window, event) + buffer.maybeReshape() proc finishLoad(buffer: Buffer): EmptyPromise = if buffer.state != bsLoadingPage: @@ -1320,20 +1323,20 @@ proc readSuccess*(buffer: Buffer; s: string; hasFd: bool): ReadSuccessResult case input.inputType of itFile: input.file = newWebFile(s, fd) - input.invalid = true + input.setInvalid() buffer.do_reshape() res.repaint = true res.open = buffer.implicitSubmit(input) else: input.value = s - input.invalid = true + input.setInvalid() buffer.do_reshape() res.repaint = true res.open = buffer.implicitSubmit(input) of TAG_TEXTAREA: let textarea = HTMLTextAreaElement(buffer.document.focus) textarea.value = s - textarea.invalid = true + textarea.setInvalid() buffer.do_reshape() res.repaint = true else: discard @@ -1473,15 +1476,15 @@ proc click(buffer: Buffer; input: HTMLInputElement): ClickResult = ) of itCheckbox: input.setChecked(not input.checked) - input.invalid = true + input.setInvalid() buffer.do_reshape() return ClickResult(repaint: true) of itRadio: for radio in input.radiogroup: radio.setChecked(false) - radio.invalid = true + radio.setInvalid() input.setChecked(true) - input.invalid = true + input.setInvalid() buffer.do_reshape() return ClickResult(repaint: true) of itReset: @@ -1531,24 +1534,27 @@ proc click(buffer: Buffer; clickable: Element): ClickResult = return ClickResult(repaint: buffer.restoreFocus()) proc click*(buffer: Buffer; cursorx, cursory: int): ClickResult {.proxy.} = - if buffer.lines.len <= cursory: return - var called = false + if buffer.lines.len <= cursory: return ClickResult() + var repaint = false var canceled = false let clickable = buffer.getCursorClickable(cursorx, cursory) if buffer.config.scripting: let element = buffer.getCursorElement(cursorx, cursory) if element != nil: - let event = newEvent(buffer.window.toAtom(satClick), element) - (called, canceled) = buffer.window.dispatchEvent(event, element) - if called: + let window = buffer.window + let event = newEvent(window.toAtom(satClick), element) + canceled = window.jsctx.dispatch(element, event) + if buffer.document.invalid: buffer.do_reshape() + buffer.document.invalid = false + repaint = true if not canceled: if clickable != nil: var res = buffer.click(clickable) - if called: # override repaint + if repaint: # override res.repaint = true return res - return ClickResult(repaint: called) + return ClickResult(repaint: repaint) proc select*(buffer: Buffer; selected: seq[int]): ClickResult {.proxy.} = if buffer.document.focus != nil and @@ -1795,7 +1801,7 @@ proc runBuffer(buffer: Buffer) = let r = buffer.window.timeouts.runTimeoutFd(event.fd) assert r buffer.window.runJSJobs() - buffer.do_reshape() + buffer.maybeReshape() buffer.loader.unregistered.setLen(0) proc cleanup(buffer: Buffer) = |