Project

General

Profile

Actions

Support #14626

open

Best practices for encapsulating third-party JS components

Added by Manuel Villalba 19 days ago.

Status:
New
Priority:
Normal
Assignee:
-
Target version:
-
Start date:
06/29/2026
Due date:
% Done:

0%

Estimated time:

Description

Hello Wt Team!

I am reaching out to ask for some guidance regarding the integration of third-party JavaScript libraries within Wt.
Recently, I upgraded a project from Wt 4.11.0 to 4.12.6 (with the last version 4.13.3 in mind). In this project, I have a custom widget that integrates Ace Editor. Everything worked perfectly in 4.11.0, but after the upgrade, the editor's internal event handling seems to clash with Wt's environment.
While left and right arrow keys work fine, vertical navigation (up/down arrows) and the Backspace key (especially when trying to delete empty lines) have stopped working properly.

I'm not expecting a fix for Ace Editor itself, I am looking for a way to properly isolate JS code from Wt's internals.

The web we built with Wt is just wonderful, it's a shame that we got stuck on 4.11.0 just because this problem.

Here is some of my code in order to recreate the problem, in case you need it:

I have this Ace Editor version: https://github.com/ajaxorg/ace/releases/tag/v1.5.0
App.cpp

    app->require("ace/src/ace.js");
    app->useStyleSheet("ace/css/ace.css");
    app->require("ace/src/ext-language_tools.js");

AceText.h

#ifndef ACETEXT_H_
#define ACETEXT_H_

#include <Wt/WContainerWidget.h>

using namespace Wt;

class AceText : public Wt::WContainerWidget
{
    private:
        WContainerWidget* ace_;
        WPushButton* syntax_btn_;
        WWidget* editor_;
        std::string current_text_;
        JSignal<std::string> text_changed_;
    public:
        enum class Mode
        {
            Read,
            Write
        };
        enum class Size
        {
            Line,
            TextArea,
            Free
        };
        enum class Syntax
        {
            Custom,
            Json,
            Html,
            Diff,
            None
        };
        AceText ( const std::string& text, AceText::Mode, AceText::Size, AceText::Syntax );
        void add_change_syntax();
        void set_mode( AceText::Mode );
        void set_syntax( AceText::Syntax );
        void propagateSetEnabled( bool enabled );
        void set_text( const std::string& text );
        std::string get_text();
        void set_size( AceText::Size );
        void set_wrap_limit( const int& min, const int& max );
        void update();

        JSignal<std::string>& text_changed();
};
#endif

AceText.cpp

#include "AceText.h"
#include <Wt/WApplication.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WEnvironment.h>
#include <Wt/Http/Cookie.h>
#include <Wt/Http/Cookie.h>
#include <Wt/WPushButton.h>
#include <Wt/WPopupMenu.h>
#include <string>

using namespace Wt;

AceText::AceText( const std::string& text, AceText::Mode mode, AceText::Size size, AceText::Syntax syntax ) : text_changed_(this, "text_changed")
{
    WApplication *app = WApplication::instance();
    const std::string* dark_mode_cookie_value = app->environment().getCookie("dark_mode");
    std::string theme = ( dark_mode_cookie_value && *dark_mode_cookie_value == "true" ) ? "ace/theme/monokai" : "ace/theme/chrome";

    current_text_ = text;

    text_changed_.connect([this](std::string new_text)
    {
        this->current_text_ = std::move(new_text);
    });

    ace_ = this->addNew<WContainerWidget>();

    ace_->doJavaScript(
        ace_->jsRef() + ".style.width = '100%';" +
        ace_->jsRef() + ".editor = ace.edit(" + ace_->jsRef() + ");" +
        ace_->jsRef() + ".editor.on('change', function(e) {" +
        "   var text = " + ace_->jsRef() + ".editor.getValue();" +
        text_changed_.createCall({"text"}) +
        "});" +
        ace_->jsRef() + ".editor.setTheme('" + theme + "');" +
        ace_->jsRef() + ".editor.setOptions({ " +
        "  fontSize: '16px'," +
        "  tabSize: 4," +
        "  showLineNumbers: false," +
        "  showGutter: false," +
        "  selectionStyle: 'text'," +
        "  highlightActiveLine: false," +
        "  printMargin: false," +
        "  vScrollBarAlwaysVisible: false," +
        "  hScrollBarAlwaysVisible: false," +
        "  cursorStyle: 'slim'," +
        "  indentedSoftWrap: false,"
        "});" +
        ace_->jsRef() + ".editor.session.setUseWrapMode(true); " +
        ace_->jsRef() + ".editor.resize();"
    );

    set_text( text );
    set_syntax( syntax );
    set_mode( mode );
    set_size( size );
    if ( mode == AceText::Mode::Write && size != AceText::Size::Line )
    {
        add_change_syntax();
        if ( size == AceText::Size::Free )
        {
            syntax_btn_->removeStyleClass("mb-3");
            syntax_btn_->addStyleClass("me-2");
        }
    }
};

void AceText::add_change_syntax()
{
    this->addStyleClass("position-relative");
    syntax_btn_ = this->addNew<WPushButton>("Syntax");
    syntax_btn_->addStyleClass("position-absolute bottom-100 end-0 mb-3 btn-outline-secondary btn-sm ");
    syntax_btn_->setAttributeValue("style", "--bs-btn-padding-y: .15rem; --bs-btn-font-size: .75rem;");
    syntax_btn_->doJavaScript(syntax_btn_->jsRef() + ".innerHTML = '<span class=\"bi bi-code-slash me-2\"></span>' + " + syntax_btn_->jsRef() + ".innerHTML;");

    auto popup_syntax = std::make_unique<WPopupMenu>();
    auto popup_syntax_ptr = popup_syntax.get();

    auto html_item = popup_syntax_ptr->addItem("HTML");
    auto html_icon = html_item->anchor()->insertNew<WContainerWidget>(0);
    html_item->anchor()->addStyleClass("d-flex align-items-center");
    html_icon->addStyleClass("bi bi-filetype-html me-2");
    html_item->triggered().connect([=] {
        set_syntax( AceText::Syntax::Html );
    });

    auto json_item = popup_syntax_ptr->addItem("JSON");
    auto json_icon = json_item->anchor()->insertNew<WContainerWidget>(0);
    json_item->anchor()->addStyleClass("d-flex align-items-center");
    json_icon->addStyleClass("bi bi-filetype-json me-2");
    json_item->triggered().connect([=] {
        set_syntax( AceText::Syntax::Json );
    });

    auto inja_item = popup_syntax_ptr->addItem("Inja");
    auto inja_icon = inja_item->anchor()->insertNew<WContainerWidget>(0);
    inja_item->anchor()->addStyleClass("d-flex align-items-center");
    inja_icon->addStyleClass("bi bi-file-earmark-code me-2");
    inja_item->triggered().connect([=] {
        set_syntax( AceText::Syntax::Custom );
    });

    syntax_btn_->setMenu(std::move(popup_syntax));
}

void AceText::set_mode( AceText::Mode mode )
{
    switch ( mode )
    {
        case AceText::Mode::Write:
            ace_->doJavaScript(
                ace_->jsRef() + ".editor.renderer.$cursorLayer.element.style.opacity=1;" +
                ace_->jsRef() + ".editor.setOptions({ " +
                "  readOnly: false," +
                "  highlightGutterLine: true," +
                "  showLineNumbers: true," +
                "  showGutter: true," +
                "  enableBasicAutocompletion: true," +
                "  enableLiveAutocompletion: true"
                "});"
            );
            break;
        case AceText::Mode::Read:
            ace_->doJavaScript(
                ace_->jsRef() + ".editor.renderer.$cursorLayer.element.style.opacity=0;" +
                ace_->jsRef() + ".editor.setOptions({ " +
                "  readOnly: true," +
                "  highlightGutterLine: false" +
                "});"
            );
            break;
    }
}

void AceText::set_syntax( AceText::Syntax syntax )
{
    switch ( syntax )
    {
        case AceText::Syntax::Custom:
            ace_->doJavaScript(
                ace_->jsRef() + ".editor.session.setMode('ace/mode/custom');"
            );
            break;
        case AceText::Syntax::Json:
            ace_->doJavaScript(
                ace_->jsRef() + ".editor.session.setMode('ace/mode/json');" 
            );
            break;
        case AceText::Syntax::Html:
            ace_->doJavaScript(
                ace_->jsRef() + ".editor.session.setMode('ace/mode/html');" 
            );
            break;
        case AceText::Syntax::Diff:
            ace_->doJavaScript(
                ace_->jsRef() + ".editor.session.setMode('ace/mode/diff');" 
            );
            break;
        case AceText::Syntax::None:
            break;
    }
}

void AceText::set_size( AceText::Size size )
{
    switch ( size )
    {
        case AceText::Size::Line:
            ace_->doJavaScript(
                ace_->jsRef() + ".editor.setOptions({ " +
                    "  minLines: 1," +
                    "  maxLines: 1," +
                    "  showGutter: false" +
                "});" +
                ace_->jsRef() + ".editor.commands.bindKey('Enter|Shift-Enter', 'null');"
            );
            break;
        case AceText::Size::TextArea:
            ace_->doJavaScript(
                ace_->jsRef() + ".style.minHeight = '54px';" +
                ace_->jsRef() + ".editor.setOptions({ " +
                    "  minLines: 3," +
                    "  maxLines: 50," +
                "});"
            );
            break;
        case AceText::Size::Free:
            this->addStyleClass("d-flex flex-column flex-grow-1");
            ace_->addStyleClass("flex-grow-1");
            ace_->doJavaScript(
                ace_->jsRef() + ".editor.setOptions({ " +
                    "  minLines: null," +
                    "  maxLines: null," +
                "});"
            );
            break;
    }
}

void AceText::set_wrap_limit( const int& min, const int& max )
{
    ace_->doJavaScript( ace_->jsRef() + ".editor.session.setWrapLimitRange(" + std::to_string(min) + "," + std::to_string(max) + "); ");
}

void AceText::propagateSetEnabled( bool enabled )
{
    if ( enabled )
    {
        ace_->doJavaScript(
            ace_->jsRef() + ".editor.renderer.$cursorLayer.element.style.opacity=1;" +
            ace_->jsRef() + ".editor.textInput.getElement().disabled=false;" +
            ace_->jsRef() + ".editor.commands.commandKeyBinding={};" +
            ace_->jsRef() + ".editor.container.style.pointerEvents='auto';" +
            ace_->jsRef() + ".editor.container.style.opacity=1;" +
            ace_->jsRef() + ".editor.renderer.setStyle('disabled', false);" +
            ace_->jsRef() + ".editor.focus();"
        ); 
    }
    else
    {
        ace_->doJavaScript(
            ace_->jsRef() + ".editor.renderer.$cursorLayer.element.style.opacity=0;" +
            ace_->jsRef() + ".editor.textInput.getElement().disabled=true;" +
            ace_->jsRef() + ".editor.commands.commandKeyBinding={};" +
            ace_->jsRef() + ".editor.container.style.pointerEvents='none';" +
            ace_->jsRef() + ".editor.container.style.opacity=0.5;" +
            ace_->jsRef() + ".editor.renderer.setStyle('disabled', true);" +
            ace_->jsRef() + ".editor.blur();"
        );
    }
}

void AceText::set_text( const std::string& text )
{
    current_text_ = text;

    WString wtext = WString(text);
    ace_->doJavaScript(
        ace_->jsRef() + ".editor.setValue(" + wtext.jsStringLiteral() + ", -1);" +
        ace_->jsRef() + ".editor.navigateFileEnd();"
    );
}

std::string AceText::get_text()
{
    return current_text_;
}

void AceText::update()
{
    auto app = WApplication::instance();

    app->log("debug") << "AceText update!";

    ace_->doJavaScript(
        ace_->jsRef() + ".editor.container.style.pointerEvents = 'auto';" +
        ace_->jsRef() + ".editor.renderer.updateFull();" +
        ace_->jsRef() + ".editor.focus();"
    );
}

JSignal<std::string>& AceText::text_changed() { return text_changed_; }

Files

clipboard-202606291153-utzju.png (45.4 KB) clipboard-202606291153-utzju.png Manuel Villalba, 06/29/2026 11:53 AM

No data to display

Actions

Also available in: Atom PDF