Implementing a contextmenu with wikitext

Adding a contextmenu with the $eventcatcher widget is entirely possible, the Streams plugin takes this approach.

The tricky thing for the Tiddler Context Menu plugin is that you would need to customize the view template to wrap it in an $eventcatcher widget in order for a wikitext based solution to be possible.

Similarly you would need to wrap each link for an alternative to the way the Links Context Menu plugin is implemented. When we have the parametrized transclusions PR merged into the core, it will be possible to override the $link widget via wikitext and wrap it in an $eventcatcher widget.

1 Like

I have that anyway, to run extra actions on tm-navigate messages. Though I do see that packaging that in a plug-in may be difficult.

Similarly you would need to wrap each link for an alternative to the way the Links Context Menu plugin is implemented.

Couldn’t I just set the selector to the whole tiddler and then check the dom-class variable of the caught event? If it’s a link then it’ll start with tc-tiddlylink, otherwise not. So I could craft different context menus for the tiddler (like @ahanniga) and for links (like you) in one go.

The only drawback I see is that with the eventcatcher I can’t do something like holding Ctrl to show the browser context menu, as I can’t make stopPropagation depend on the modifier.

Have a nice day
Yaisog

Precisely and overall the problem you face with contextmenus is that they make the mobile experience subpar - for example when selecting text - which is why I limited the plugin to links and not the entire tiddler. My wikis are also heavily customized in terms of templates and layout and the advantage of the JavaScript approach (as well as the future possibility of overriding the link widget via wikitext) is that it automatically works in all of them without any extra effort.

I made a proof-of-concept for a flexible context menu that uses the $eventcatcher.

It’s probably not (easily) doable as a plug-in as it touches a few templates. But not everything must be a plug-in and the templates that were changed are all custom in my wikis anyway. So, take it or leave it, but it can be done (and I will definitely use it to clean up all the buttons that litter my wikis).

First, place the $eventcatcher. I tried to put it in the ViewTemplate, but there it was difficult to make the popup appear at the clicked spot. So it went into the PageTemplate (which is already customized with a $messagecatcher to trigger additional actions on navigation messages):

<$eventcatcher $contextmenu={{{ [{$:/mwi/config/ContextMenuEnable}match[yes]then<contextmenu>else[]] }}} selector=".tc-tiddler-frame, [contextmenu], a">

I put it inside the $dropzone. Since with this approach I cannot use Ctrl to alternatively show the browser context menu, I’ll put a toggle somewhere in the UI to switch between wiki and browser popup, controlled by the config setting $:/mwi/config/ContextMenuEnable. The selector attribute is set to allow for context menus on a tiddler itself, any link and any DOM element with a contextmenu attribute (I’ll show below what that is useful for).

The macro contextmenu that called from the $eventcatcher is defined as

\define contextmenu()
<$action-log />
<$action-popup $state="$:/state/popup/contextmenu" $coords={{{ [[(]] [<event-fromcatcher-posx>] [[,]] [<event-fromcatcher-posy>] [[,0,0)]] +[join[]] }}} />
<$action-setfield $tiddler="$:/state/popup/contextmenu/dom-class" $value={{{ [<dom-class>!is[blank]else[]] }}} />
<$action-setfield $tiddler="$:/state/popup/contextmenu/dom-href" $value={{{ [<dom-href>!is[blank]else[]] }}} />
<$action-setfield $tiddler="$:/state/popup/contextmenu/dom-contextmenu" $value={{{ [<dom-contextmenu>!is[blank]else[]] }}} />
<$action-setfield $tiddler="$:/state/popup/contextmenu/dom-contextmenutiddler" $value={{{ [<dom-contextmenutiddler>!is[blank]else<dom-data-tiddler-title>!is[blank]else{$:/HistoryList!!current-tiddler}] }}} />
<$action-setfield $tiddler="$:/state/popup/contextmenu/dom-contextmenuparent" $value={{{ [<dom-contextmenuparent>!is[blank]else[]] }}} />
\end

The only way I found to pass information about the DOM element that the context menu was called upon is by putting into into HTML attributes. class and href are set automatically, contextmenu, contextmenutiddler and contextparent are set in the various templates where I need it. By putting it into e.g. the tc-tabbed-table-of-contents-content element, I can call a different context menu for the contents pane of a tabbed-toc which knows about the tiddler shown in the contents pane as well as the tiddler containing the tabbed-toc if needed. The contextmenu attribute can be used to differentiate between the popup types, to make processing easier.

Since the only way to pass information from this page-level macro to the context menu popup is by storing that information in the wiki, a couple of $action-setfields are called.

The popup template looks like this:

<$reveal type="popup" state="$:/state/popup/contextmenu">
	<div class="tc-drop-down mwi-contextmenu">
		Clicked item dom-classes:<br>
		<$list filter="[enlist{$:/state/popup/contextmenu/dom-class}decodeuricomponent[]]" >
		<$text text=<<currentTiddler>>/><br>
		</$list>
		href is //<$text text={{{ [{$:/state/popup/contextmenu/dom-href}decodeuricomponent[]] }}}/>//<br>
		contextmenu(type) is //<$text text={{$:/state/popup/contextmenu/dom-contextmenu}}/>//<br>
		contextmenutiddler is //<$text text={{$:/state/popup/contextmenu/dom-contextmenutiddler}}/>//<br>
		contextmenuparent is //<$text text={{$:/state/popup/contextmenu/dom-contextmenuparent}}/>//
	</div>
</$reveal>

It’s tagged with $:/tags/PageTemplate and at the moment only shows debugging information. This will be extended along the lines of @saqimtiaz’ tag-based and thus easily configurable collection of items.

Next up I need to move all my buttons into the context menus, to clean up the UI.

Have fun with it
Yaisog

PS: The ability for unique context menus for parts of tiddlers could also come in handy in conjunction with section editors and such.

From a semantic perspective, consider using data- prefixed attributes.

Absolutely, bespoke solutions are great when re-sharing between wikis is not a high priority. The tiddler links context menu plugin actually predates the $eventcatcher and there isn’t much motivation to go back and rework what already serves the necessary purpose. If I was starting from scratch today I may have taken a different path as none of my wikis use any of the default templates.

Another approach that I have been playing around with this year is the ability to import widgets into a template, such that it wraps the template. The goal being to allow customizing templates without modifying shadow tiddlers.

Somewhere or the other I have a branch with a version of $eventcatcher that allows specifying modifier keys for a given event, so that the event is only trapped when those modifier keys are used (or absent). I’ll revisit that when I get the opportunity to see if it might be appropriate for the core.

I’ll revisit that when I get the opportunity to see if it might be appropriate for the core.

I would love that. Though I guess the solution with a switch is not so bad. It’s actually better for mobile devices which have no Ctrl key (though that was not my concern).
I found that really the only functionality I need from the browser context menu in a TW is the Inspect item when having to modify CSS. But I guess Ctrl + Shift + C will do.

[…] the ability to import widgets into a template, such that it wraps the template

You mean as a new core functionality, or something that can be implemented with the current means?

Have a nice day
Yaisog

This almost sounds like an excuse when it was an inspiration!

:+1:

Great work guys! I’ve been trying to integrate the plugin in a more consistent way using your examples as a guide. Doing this in pure wikitext is proving more complex than expected (clearly I need to learn more on wikitext!). I’ll discontinue this approach and instead try improving on the first version:

  • Implement as many existing tm-* message handlers as practical to do
  • Allow the user to select those they want in the context menu, similar to the edit, page and view buttons

In phone/tablet view it currenlty works OK with long-press, but if text is selected I’ll skip triggering the menu.

Cheers, Aidan

@saqimtiaz: I noticed that one cannot call the context menu on another item when a context menu is already open – instead the second right-click just closes the existing popup and one has to click again (same was true for the Streams context menu when I checked).

So I put an $action-popup without the $coords attribute, to close all open popups, before the actual $action-popup call. This sort of worked, except that the second popup appears at the same location where the first was, but not where the second right-click occured. The text in the $state tiddler does update to the new coordinates, just the popup doesn’t move. Is this the intended behavior?

I also tried manually deleting the state tiddler before calling $action-popup, but that didn’t help.

I solved the problem – a bit too hacky for my taste, though – by using a different state tiddler for each right-click (adding the current now value to its title). The associated $reveal will look for all matching state tiddlers and use the one with the newest time code in the title. Seems to work reliably.

Have a nice day
Yaisog

@Yaisog You are on the right track. This is a problem that I resolved in the refactor of Streams that I have been working on and is close to release.

The key is to have the actions in this order:

  • close popup
  • set state tiddlers
  • open popup

That almost worked.

For some reason, the popup position does not update when you right-click on the same event target, but at a different location (e.g. end or beginning of the same link or, more common, different points in a tiddler for a tiddler context menu). This may not be relevant for Streams, where you attach the context menu only to the tiny list item markers, but would definitely drive me crazy at some point in my wiki…

So I’ll stay with the unique state tiddlers for now.

Have a nice day
Yaisog

No reason to change what works for you if you are happy with it. However, you might find the updatePopupPosition attribute of the $reveal widget relevant here.

You really do know all

Alrighty, after spending a few days fiddling with context menus (and definitely too much time playing around with CSS transition effects that were ultimately scrapped), I’m in a spot where I’m quite happy with how they turned out. After some refactoring there is not much code to it, and I even got submenus to work.

The $eventcatcher and macro are pretty much as above.

The template for the popup also is not very complicated and mostly sets all the passed variables:

\whitespace trim

<$let stateTiddlerBase="$:/state/popup/contextmenu"
			contextmenu={{{ [<stateTiddlerBase>addsuffix[/dom-data-contextmenu]get[text]else[Tiddler]] }}}
			contextmenutiddler={{{ [<stateTiddlerBase>addsuffix[/dom-data-contextmenutiddler]get[text]] }}}
			menuItemsTag={{{ [[$:/mwi/tags/ContextMenu]addsuffix<contextmenu>] }}}
			auxData={{{ [<stateTiddlerBase>addsuffix[/dom-data-contextmenuaux]get[text]] }}}
			parentTiddler={{{ [<stateTiddlerBase>addsuffix[/dom-data-contextmenuparent]get[text]] }}}
			tv-config-toolbar-text="yes" >
	<$reveal type="popup" state=<<stateTiddlerBase>> updatePopupPosition="yes">
		<div class="tc-drop-down mwi-contextmenu">
			<$tiddler tiddler=<<contextmenutiddler>> >
				<$reveal tag="button" class="tc-btn-invisible mwi-contextmenu-heading" type="nomatch" text="TOCItem" default=<<contextmenu>> >
					<span class="tc-btn-text">
						<$text text=<<currentTiddler>> />
					</span>
				</$reveal>
				<$list filter="[all[shadows+tiddlers]tag<menuItemsTag>!has[draft.of]]" variable="listItem">
					<$transclude tiddler=<<listItem>>/>
				</$list>
			</$tiddler>
		</div>
	</$reveal>
</$let>

The identifier passed from the DOM attribute data-contextmenu via a state tiddler into the variable contextmenu is used to build the tag name menuItemsTag which controls which buttons appear in the context menu for that identifier.
The buttons don’t have to be very complicated, e.g. an edit-button:

\define button-actions()
<!-- wenn Tiddler nur transcluded ist, muss er erst geöffnet werden -->
<$list filter="[<parentTiddler>!is[blank]]" variable="void">
	<$list filter="[<tv-story-list>!contains<currentTiddler>]" variable="void">
		<$action-listops $tiddler=<<tv-story-list>> $subfilter="+[insertbefore:parentTiddler<currentTiddler>move<currentTiddler>]"/>
	</$list>
	<$action-navigate />
</$list>
<$action-sendmessage $message="tm-edit-tiddler" />
\end

<$button class="tc-btn-invisible mwi-btn-meta-toolbar" actions=<<button-actions>> >
	{{$:/core/images/edit-button}}
	<$list filter="[<tv-config-toolbar-text>match[yes]]">
		<span class="tc-btn-text">
			Tiddler bearbeiten
		</span>
	</$list>
</$button>

Most of the buttons do somewhat more complicated actions unique to this wiki. But, they were already implemented and their buttons were littered around the UI. All it took (mostly) was to assign them the respective tag(s).

If there are relevant tiddlers higher up in the hierarchy, a submenu for those is also shown, implemented as just another context menu button with the respective tag:

<!-- wenn es einen parentTiddler gibt, zeigen wir das Menü für den Haupt-Tiddler an (wenn wir nicht schon in einem Untermenüpunkt sind) -->
<$reveal type="nomatch" text="2" default=<<menuLevel>> tag="button" class="tc-btn-invisible">
	<$tiddler tiddler={{$:/HistoryList!!current-tiddler}} >
		<$let parentTiddler=""
					contextmenu="Tiddler"
					menuItemsTag={{{ [[$:/mwi/tags/ContextMenu]addsuffix<contextmenu>] }}}
					menuLevel="2" >
			{{$:/mwi/images/escalator-warning}}
			<span class="tc-btn-text">
				<$text text=<<currentTiddler>> />
			</span>
			<span class="mwi-hotkey">
				{{$:/core/images/right-arrow}}
			</span>
			<div class="mwi-contextsubmenu">
				<$list filter="[all[shadows+tiddlers]tag<menuItemsTag>!has[draft.of]]" variable="listItem">
					<$transclude tiddler=<<listItem>>/>
				</$list>
			</div>
		</$let>
	</$tiddler>
</$reveal>

To complete the UI cleanup, I will consider if it makes sense to move the whole View Toolbar into the context menu (except for the Close button). Some of it is already there, and the rest is rarely used.

Maybe this will inspire someone to try their own hand at crafting a bespoke context menu (except maybe for @saqimtiaz who probably does stuff like this before breakfast).

Have a nice day
Yaisog

PS: Sorry for all the German text. I don’t have the time right now for a translation. Pretty much all of it should not be relevant to understand the principle of the thing.
PPS: The submenus maybe only make sense when considering the general tiddler structure in the wiki: A major topic can have a tabbed table of contents, and each of those TOC entries may have a list of journal tiddlers which are collected together in a nested tabbed TOC. So, a journal tiddler may have a parent that is a contents pane of a toc-tabbed, which again then has a parent tiddler that is the root tiddler of the zoomin view. Nothing for small screens, but very efficient for finding information quickly.

7 Likes

@Yaisog it is extremely pleasing to see your explorations with wikitext and thank you for sharing back with the wider community. A bit of a crunch week for me at the moment but I’ll reply in more detail when the opportunity allows.

1 Like

Turns out, adding the View Toolbar to the context menu is very simple. It is just another “button” with a submenu, just a few lines of code changed from what I posted above:

\import [[$:/core/ui/ViewTemplate]]
<!-- import is needed for the macros defined there -->
<$reveal type="nomatch" text="2" default=<<menuLevel>> tag="button" class="tc-btn-invisible">
	<$tiddler tiddler={{$:/HistoryList!!current-tiddler}} >
		<$let menuLevel="2" >
			{{$:/core/images/options-button}}
			<span class="tc-btn-text">
				Toolbar Buttons
			</span>
			<span class="mwi-hotkey">
				{{$:/core/images/right-arrow}}
			</span>
			<div class="mwi-contextsubmenu">
				<$list filter="[all[shadows+tiddlers]tag[$:/tags/ViewToolbar]!has[draft.of]] -[[$:/core/ui/Buttons/more-tiddler-actions]]" variable="listItem">
					<$transclude tiddler=<<listItem>>/>
				</$list>
			</div>
		</$let>
	</$tiddler>
</$reveal>

Mostly, it’s only a change of the $list filter expression that was simply copied from $:/core/ui/Buttons/more-tiddler-actions.


Have a nice day
Yaisog

PS: Not all of it works fully. Because the context menu is defined in the page template and not in the view template, e.g. Info that relies on the variable tiddlerInfoState that is computed via qualify in the view template does not work. If it really has to go into the context menu, I think it would have to be passed via the DOM into the context menu. The rest of the items work as far as I can tell.

2 Likes