diff --git a/src/web/DomElement.C b/src/web/DomElement.C index cbed472a..87f58f7c 100644 --- a/src/web/DomElement.C +++ b/src/web/DomElement.C @@ -904,6 +904,101 @@ void DomElement::setJavaScriptEvent(EscapeOStream& out, // events on the dom root container are events received by the whole // document when no element has focus + // Fast path: most exposed-signal handlers are produced by setEvent() from a + // small set of deterministic templates. Recognize the three common shapes + // (simple update, guarded update, anchor-guarded update) and emit a call + // into a shared closure factory in Wt.js instead of a unique per-element + // `function fNNN(event) { ... }` declaration. The factory closures + // reproduce the inlined handler body exactly; this is a payload-size + // optimization, not a behavior change. Other handler shapes fall through + // to the unchanged per-element function emission below. + if (!globalUnfocused_ && !handler.signalName.empty()) { + static const std::string prelude = "var e=event||window.event,o=this;"; + + const std::string& body = handler.jsCode; + if (body.size() > prelude.size() + && body.compare(0, prelude.size(), prelude) == 0) { + static const std::string anchorOpen = + "if(e.ctrlKey||e.metaKey||e.shiftKey||(" WT_CLASS ".button(e) > 1))" + "return true;else{"; + + std::size_t bodyStart = prelude.size(); + std::size_t bodyEnd = body.size(); + bool anchor = false; + if (body.size() >= bodyStart + anchorOpen.size() + 1 + && body.compare(bodyStart, anchorOpen.size(), anchorOpen) == 0 + && body.back() == '}') { + anchor = true; + bodyStart += anchorOpen.size(); + --bodyEnd; + } + + // Keep the "Wt-no-default" string and the cancelEvent/preventDefault + // structure in sync with the HandlerChecks built by + // WInteractWidget::updateSignalConnection() and with the same string + // in the _mkgu / _mkau factories in src/web/skeleton/Wt.{,min.}js. + // Detection deliberately uses the literal here rather than the + // WInteractWidget::noDefault constant so that a future rename of the + // constant fails closed (handlers fall through to per-element + // function emission, payload regresses) rather than silently + // running a factory whose Wt.js body still checks the old class. + const std::string handlerChecks = + "if(o.classList.contains('" + app->theme()->disabledClass() + "')){" + WT_CLASS ".cancelEvent(e);return;}" + "if(o.classList.contains('Wt-no-default')){e.preventDefault();}"; + + bool guarded = false; + if (bodyEnd >= bodyStart + handlerChecks.size() + && body.compare(bodyStart, handlerChecks.size(), handlerChecks) == 0) { + guarded = true; + bodyStart += handlerChecks.size(); + } + + // The anchor-without-guard shape is reachable only from direct + // DomElement::setEvent callers on a raw element (stock Wt anchor + // clicks always go through WInteractWidget, which injects HandlerChecks + // before setEvent runs). Skip optimizing that case; the AnchorGuarded + // factory adds a disabled-class check the original handler did not run, + // which would be observable behavior change. + if (anchor && !guarded) { + // fall through to per-element function emission + } else { + const std::string updateCall = + app->javaScriptClass() + "._p_.update(o,'" + handler.signalName + + "',e,true);"; + + if (bodyEnd - bodyStart == updateCall.size() + && body.compare(bodyStart, updateCall.size(), updateCall) == 0) { + // Factory contract: _mksu(app, sig) takes two arguments; + // _mkgu(app, dc, sig) and _mkau(app, dc, sig) take three. Keep + // emission below in sync with these signatures. + const char *factory = + anchor ? "._mkau(" : + guarded ? "._mkgu(" : + "._mksu("; + const bool wheelOnIE = + (eventName == WInteractWidget::WHEEL_SIGNAL && + app->environment().agentIsIE() && + static_cast(app->environment().agent()) >= + static_cast(UserAgent::IE9)); + declare(out); + if (wheelOnIE) + out << var_ << ".addEventListener('wheel',"; + else + out << var_ << ".on" << eventName << '='; + out << WT_CLASS << factory << app->javaScriptClass(); + if (anchor || guarded) + out << ",'" << app->theme()->disabledClass() << '\''; + out << ",'" << handler.signalName << "')"; + if (wheelOnIE) + out << ",false)"; + out << ";\n"; + return; + } + } + } + } + unsigned fid = nextId_++; out << "function f" << fid << "(event) { "; diff --git a/src/web/skeleton/Wt.js b/src/web/skeleton/Wt.js index ddccc773..f5eb211c 100644 --- a/src/web/skeleton/Wt.js +++ b/src/web/skeleton/Wt.js @@ -740,6 +740,50 @@ if (!window._$_WT_CLASS_$_) { } }; + // Closure factories used by DomElement::setJavaScriptEvent() to avoid + // emitting a unique `function fNNN(event){...}` declaration for every + // exposed-signal event handler. Each factory returns a per-element + // closure whose body reproduces, byte-for-byte at runtime, the inlined + // handler that DomElement::setEvent() would otherwise have built. + // + // _mksu — simple update: just the signal dispatch. + // _mkgu — guarded update: disabled-class + no-default-class checks, + // then signal dispatch (non-anchor click pattern). + // _mkau — anchor-guarded update: ctrl/meta/shift / non-primary-button + // bypass, then guarded update (anchor click pattern). + // + // The literal "Wt-no-default" below must match WInteractWidget::noDefault + // (src/Wt/WInteractWidget.C) and the matching literal in + // DomElement::setJavaScriptEvent(). All three sites must be updated + // together if that class name ever changes. + this._mksu = function(app, sig) { + return function(event) { + const e = event || window.event, o = this; + app._p_.update(o, sig, e, true); + }; + }; + + this._mkgu = function(app, dc, sig) { + return function(event) { + const e = event || window.event, o = this; + if (o.classList.contains(dc)) { WT.cancelEvent(e); return; } + if (o.classList.contains("Wt-no-default")) { e.preventDefault(); } + app._p_.update(o, sig, e, true); + }; + }; + + this._mkau = function(app, dc, sig) { + return function(event) { + const e = event || window.event, o = this; + if (e.ctrlKey || e.metaKey || e.shiftKey || (WT.button(e) > 1)) { + return true; + } + if (o.classList.contains(dc)) { WT.cancelEvent(e); return; } + if (o.classList.contains("Wt-no-default")) { e.preventDefault(); } + app._p_.update(o, sig, e, true); + }; + }; + this.getElement = function(id) { let el = document.getElementById(id); if (!el) {