Как интегрировать пользовательские контекстные меню в Mithril

Я пытаюсь добавить пользовательские контекстные меню для некоторых элементов на странице и сделал это так в представлении, которое содержит таблицу. Контекстное меню прикреплено к заголовку таблицы с именем "S":

list.view = function(ctrl, args) {

var contextMenuSelection =      
    m("div", {
    id : "context-menu-bkg-01",
    class : ctrl.isContextMenuVisible() === ctrl.contextMenuId ? "context-menu" : "hide",
    style : ctrl.contextMenuPosition(),
}, [ m("#select.menu-item.allow-hover", {
    onclick : function(e) {
        args.model.callMenu({
            cmdName : this.id
        })
    }
}, "Select all"), m("#deselect.menu-item.allow-hover", {
    onclick : function(e) {
        args.model.callMenu({
            cmdName : this.id
        })
    }
}, "Deselect all"), m("#invertSel.menu-item.allow-hover", {
    onclick : function(e) {
        args.model.callMenu({
            cmdName : this.id
        })
    }
}, "Invert selection") ]);

var table = m("table", [
    m("tr", [ m("th", {
        id : ctrl.contextMenuId,
        config : ctrl.configContextMenu(),
        oncontextmenu : function(e) {
            console.log("2021 contextMenuShow")
            e.preventDefault()
            var coords = utils.getCoords(e)
            var pos = {}
            pos.left = coords[0] + "px"
            pos.top = coords[1] + "px"
            ctrl.contextMenuPosition(pos)
            var id = e.currentTarget.id
            ctrl.isContextMenuVisible(id)
        }
        }, "S"),
            m("th[data-sort-by=pName]", "Name"),
            m("th[data-sort-by=pSize]", "Size"),
            m("th[data-sort-by=pPath]", "Path"),
            m("th[data-sort-by=pMedia]", "Media") ]),
        ctrl.items().map(
           function(item, idx) {
              return m("tr", ctrl.initRow(item, idx), {
              key : item.guid
              }, [ m("input[type=checkbox]", {
                id : item.guid,
                checked : ctrl.isSelected(item.guid)
                }),
                                    m("td", item.pName),
                m("td",  utils.numberWithDots(item.pSize)),
                m("td", item.pPath), m("td", item.pMedia) ])
            }) ])

return m("div", [contextMenuSelection, table])              
}

Чтобы закрыть контекстное меню после нажатия клавиши escape или когда пользователь щелкает мышью где-нибудь на странице, эта функция присоединяется к элементу через атрибут config:

ctrl.configContextMenu = function() { 
    return function(element, isInitialized, context) {
        console.log("1220 isInitialized=" + isInitialized)
        if(!isInitialized) {
           console.log("1225")
           document.addEventListener('click', function() {
              m.startComputation()
              ctrl.contextMenuVisibility(0)
              m.endComputation()
           }, false);
               document.addEventListener('keydown', function() {
              console.log("1235")
              m.startComputation()
                          ctrl.contextMenuVisibility(0)
              m.endComputation()
           }, false)
        }
    };  
};

Поведение непредсказуемо: если таблица пуста, пользовательское контекстное меню отображается и скрывается, как и ожидалось. Если таблица заполнена, вместо нее отображается контекстное меню по умолчанию.

Использование отладчика и некоторых точек останова не дало мне никакой информации о том, что происходит, за исключением того, что иногда запуск отладчика шаг за шагом вызывал пользовательское контекстное меню. Поэтому я предполагаю, что это как-то связано с состоянием гонки между eventListener и системой Mithrils Draw.

Кто-нибудь имеет опыт работы с пользовательскими контекстными меню и может дать мне несколько примеров?

Большое спасибо, Стефан

РЕДАКТИРОВАТЬ: Что касается комментария Барни относительно m.startComputation(), я изменил код на следующее:

var table = m("table", ctrl.sorts(ctrl.items()), [
m("tr", [ m("th", {
    oncontextmenu : ctrl.onContextMenu(ctrl.contextMenuId, "context-menu context-menu-bkg", "hide" )
}, "S"), m("th[data-sort-by=pName]", "Name"),
m("th[data-sort-by=pSize]", "Size"), 
m("th[data-sort-by=pPath]", "Path"), 
m("th[data-sort-by=pMedia]", "Media") ]), 
ctrl.items().map(function(item, idx) {
    return m("tr", ctrl.initRow(item, idx), {
        key : item.guid
    }, [ m("input[type=checkbox]", {
        id : item.guid,
        checked : ctrl.isSelected(item.guid),
        onclick : function(e) {
            console.log("1000")
            ctrl.setSelected(this.id);
        }
    }), m("td", item.pName), m("td", utils.numberWithDots(item.pSize)), 
    m("td", item.pPath), m("td", item.pMedia) ])
}) ])

И реализующая функция onContextMenu:

// open a context menu
// @elementId   the id of the element which resembles the context menu.
//              Usually this is a div.
// @classShow   the name of the css class for showing the menu
// @classHide   the name of the css class for hiding the menu
vmc.onContextMenu = function(elementId, classShow, classHide) {
    var callback = function(e) {
        console.log("3010" + this)
        var contextmenudiv = document.getElementById(elementId);
        contextmenudiv.className = classHide;
        document.removeEventListener("click", callback, false);
        document.removeEventListener("keydown", callback, false);
    }
    return function(e) {
        console.log("3000" + this)
        var contextmenudiv = document.getElementById(elementId);
        // Prevent the browser from opening the default context menu
        e.preventDefault();
        var coords = utils.getCoords(e);
        contextmenudiv.style.left = coords[0] + "px";
        contextmenudiv.style.top = coords[1] + "px";
        // Display it
        contextmenudiv.className = classShow;
        // When you click somewhere else, hide it
        document.addEventListener("click", callback, false);
        document.addEventListener("keydown", callback, false);
    }
};

Теперь это работает без проблем. Барни, если бы ты был так любезен, чтобы подтвердить это как жизнеспособный способ, я отправлю это как ответ.

Спасибо Стефан

1 ответ

Решение

Это рабочее решение:

var table = m("table", ctrl.sorts(ctrl.items()), [
    m("tr", [ m("th", {
        oncontextmenu : ctrl.onContextMenu(ctrl.contextMenuId, "context-menu  context-menu-bkg", "hide" )
    }, "S"), m("th[data-sort-by=pName]", "Name"),
    m("th[data-sort-by=pSize]", "Size"), 
    m("th[data-sort-by=pPath]", "Path"), 
    m("th[data-sort-by=pMedia]", "Media") ]), 
    ctrl.items().map(function(item, idx) {
        return m("tr", ctrl.initRow(item, idx), {
            key : item.guid
    }, [ m("input[type=checkbox]", {
            id : item.guid,
            checked : ctrl.isSelected(item.guid),
            onclick : function(e) {
                console.log("1000")
            ctrl.setSelected(this.id);
        }
    }), m("td", item.pName), m("td", utils.numberWithDots(item.pSize)), 
    m("td", item.pPath), m("td", item.pMedia) ])
}) ])

И реализующая функция onContextMenu:

// open a context menu
// @elementId   the id of the element which resembles the context menu.
//              Usually this is a div.
// @classShow   the name of the css class for showing the menu
// @classHide   the name of the css class for hiding the menu
vmc.onContextMenu = function(elementId, classShow, classHide) {
    var callback = function(e) {
        console.log("3010" + this)
        var contextmenudiv = document.getElementById(elementId);
        contextmenudiv.className = classHide;
        document.removeEventListener("click", callback, false);
        document.removeEventListener("keydown", callback, false);
    }
    return function(e) {
        console.log("3000" + this)
        var contextmenudiv = document.getElementById(elementId);
        // Prevent the browser from opening the default context menu
        e.preventDefault();
        var coords = utils.getCoords(e);
        contextmenudiv.style.left = coords[0] + "px";
        contextmenudiv.style.top = coords[1] + "px";
        // Display it
        contextmenudiv.className = classShow;
        // When you click somewhere else, hide it
        document.addEventListener("click", callback, false);
        document.addEventListener("keydown", callback, false);
    }
};

Теперь контекстное меню находится вне цикла рендеринга мифрилов, и больше нет условий гонки. Кроме того, событие щелчка для скрытия меню прикрепляется к документу и не вступает в конфликт с обработчиком щелчка, прикрепленным мифрилом.

Протестировано с Firefox 38.01

Другие вопросы по тегам