Enter key within table edit box to submit

I am using a modification of Soren’s Zetellkasten Source viewtemplate table which allows me to edit the content of the items within the table from the viewscreen, rather than opening the editor. I’d like to be able to use the enter key within this edit box to achieve the same thing as clicking the done-button, but none of the angles I’ve approached it from have had much success. Any ideas? Am I missing something obvious?

	<tr>
		<th>
				<$button set="titleEdit" setTo="edit" class="tc-btn-invisible">'' Title'' </$button>
		</th>
		<td>
		<$reveal type="nomatch" state="titleEdit" text="edit">
				<$button set="titleEdit" setTo="edit" class="tc-btn-invisible">
					{{!!caption}}
				</$button>
		</$reveal>
			<$reveal type="match" state="titleEdit" text="edit">
				<$edit-text tiddler=<<currentTiddler>> field="caption" tag="input" default="" class="tc-edit-texteditable"/>
				<$button set="titleEdit" setTo="" class="tc-btn-invisible">{{$:/core/images/done-button}}</$button>
			</$reveal>  <$button class="tc-btn-invisible" message="tm-copy-to-clipboard" param={{!!caption}}>
            {{$:/core/images/copy-clipboard}}
        </$button>
		</td>
	</tr>

You’re looking for the $keyboard widget. Try this out:

\procedure edit() <$action-setfield $tiddler="titleEdit" $field="text" $value="edit" />
\procedure done() <$action-deletefield $tiddler="titleEdit" $field="text" />

<tr>
		<th>
				<$button actions=<<edit>> class="tc-btn-invisible">'' Title'' </$button>
		</th>
		<td>
		<$reveal type="nomatch" state="titleEdit" text="edit">
				<$button actions=<<edit>> class="tc-btn-invisible">
					{{!!caption}}
				</$button>
		</$reveal>
			<$reveal type="match" state="titleEdit" text="edit">
				<$keyboard key="enter" actions=<<done>>>
					<$edit-text tiddler=<<currentTiddler>> field="caption" tag="input" default="" class="tc-edit-texteditable"/>
				</$keyboard>
				<$button actions=<<done>> class="tc-btn-invisible">{{$:/core/images/done-button}}</$button>
			</$reveal> 
		<$button class="tc-btn-invisible" message="tm-copy-to-clipboard" param={{!!caption}}>
            {{$:/core/images/copy-clipboard}}
        </$button>
		</td>
</tr>

I moved both button actions to procedures for concision; this also lets us reuse the <<done>> actions in the $keyboard actions.

If you’re using 5.3.2+, you could also replace the $reveals with conditional syntax:

\procedure edit() <$action-setfield $tiddler=<<state>> $field="text" $value="edit" />
\procedure done() <$action-deletefield $tiddler=<<state>> $field="text" />

\define state() titleEdit

<tr>
		<th>
				<$button actions=<<edit>> class="tc-btn-invisible">''Title''</$button>
		</th>
		<td>
		<% if [<state>!text[edit]] %>
				<$button actions=<<edit>> class="tc-btn-invisible">
					{{!!caption}}
				</$button>
		<% else %>
				<$keyboard key="enter" actions=<<done>>>
					<$edit-text tiddler=<<currentTiddler>> field="caption" tag="input" default="" class="tc-edit-texteditable"/>
				</$keyboard>
				<$button actions=<<done>> class="tc-btn-invisible">{{$:/core/images/done-button}}</$button>
		<% endif %>
		<$button class="tc-btn-invisible" message="tm-copy-to-clipboard" param={{!!caption}}>
            {{$:/core/images/copy-clipboard}}
        </$button>
		</td>
</tr>

Here, I also moved the state tiddler into a variable to make it easier to change if desired. Personally, I’d prefer to make the state a temporary tiddler derived from the <<currentTiddler>> variable — that way you could hypothetically edit the same field in multiple tiddlers.

\function state() [[$:/temp/titleEdit/]] [<currentTiddler>] +[join[]]

If you wanted to turn this into a parameterized procedure that you could reuse for editing other fields, you could turn hardcoded references like caption into variables as well (e.g., use <<field>> and {{{ [<currentTiddler>get<field>] }}} in place of "caption" and {{!!caption}} respectively, and use the named parameter field:"caption"when you call the procedure.

3 Likes

Hey @etardiff, I really appreciate you taking a stab at this! Procedures are relatively new for me, and I seem to run into every obstacle when implementing keyboard shortcuts – since this was a pretty complex template I was running into trouble constantly trying to figure this out, and your solution helped me get to a working solution!

I’ll post the entire thing here, if anyone wants to test it out or implement it themselves
$__sib_Templates_Automatic_Source.tid (11.8 KB)

If conditionals did not work well at all for me, very likely user error :slight_smile:

I ended up using not being able to use make the parameterized procedure work, but individual edit/done procedures for each field worked well – would love to see your code if you’ve got a working version of your suggestion though

\procedure edit-title() <$action-setfield $tiddler=<<qualify "$:/state/titleEdit">> text="edit"/>
\procedure done-title() <$action-deletefield $tiddler=<<qualify "$:/state/titleEdit">> text/>
\procedure edit-author() <$action-setfield $tiddler=<<qualify "$:/state/authorEdit">> text="edit"/>
\procedure done-author() <$action-deletefield $tiddler=<<qualify "$:/state/authorEdit">> text/>

Again, appreciate you setting me on the right path!

Glad you got it working — but oof, that’s quite a stack! I’ll take a look later and see if I can come up with something more concise.

1 Like

Here’s how I’d probably do it with <% if %> and a procedure: $__sib_Templates_Automatic_Source.json (5.4 KB)

I only did a little quick testing on TW-com, but it all seemed to work as well as I’d expect without any of the expected data structure. If the conditionals still aren’t working for you, double-check that you’re using a recent version of TW!

I’ll post the code below, too, in case anyone wants to take a quick look. Let me know if it works for you!

\procedure edit-field(field, label, display:"<<fieldValue>>", copy:"no")
\function useLabel() [<label>!match[]] ~[<field>]
\function baseState() [[$:/state/edit/]] [<field>] +[join[]]
\function fieldValue() [<currentTiddler>get<field>]
\procedure edit-toggle() <$action-listops $tiddler=<<state>> $field="text" $subfilter="+[toggle[edit]]" />
<$qualify title=<<baseState>> name="state">
<tr>
	<th>
		<$button actions=<<edit-toggle>> class="tc-btn-invisible tc-max-width capitalize">
			''<<useLabel>>''
		</$button>
	</th>
	<td>
		<% if [<state>!text[edit]] %>
			<$button actions=<<edit-toggle>> class="tc-btn-invisible">
				<<display>>
			</$button>
		<% else %>
			<$keyboard key="enter" actions=<<edit-toggle>>>
				<$edit-text field=<<field>> tag="input" default="" class="tc-edit-texteditable"/>
			</$keyboard>
			<$button actions=<<edit-toggle>> class="tc-btn-invisible">
				{{$:/core/images/done-button}}
			</$button>
		<% endif %>
		<% if [<copy>match[yes]] %>
		<$button class="tc-btn-invisible" message="tm-copy-to-clipboard" param=<<fieldValue>>>
            		{{$:/core/images/copy-clipboard}}
        </$button>
		<% endif %>
	</td>
</tr>
</$qualify>
\end

\procedure enlist-field(field)
\function last() [enlist<items>count[]compare:number:gt[2]then[, ]] [[ and ]] +[join[]]
\whitespace trim
<$let items={{{ [<currentTiddler>each:list-item<field>sort[]] +[format:titlelist[]join[ ]] }}}>
	<$list filter="[enlist<items>butlast[]]" join=", " />
	<$list filter="[enlist<items>butlast[]limit[1]]"><<last>></$list>
	<$list filter="[enlist<items>last[]]" />
</$let>
\end

<$list filter="[all[current]tag[Source]] ~[all[current]tag[Sink]]">

<table class="sourceDeetsTable">
    <!-- Title Row -->
    <<edit-field caption "Title" copy:"yes">>

    <!-- Author Row -->
    <<edit-field author display:"<<enlist-field author>>">>

    <!-- Year Row -->
    <<edit-field year "Publication year">>

    <!-- Medium Row -->
    <<edit-field medium>>

    <!-- Universe Row (Conditional) -->
<$list filter={{{ [all[current]] :filter[get[universe]!match[nonfiction]] }}}>
    <<edit-field universe display:"<$link to={{!!universe}} />">>
</$list>

    <!-- URL Row -->
    <<edit-field url "URL" display:"""<a href={{!!url}} class="tc-tiddlylink-external">{{!!url}}</a>""">>

    <!-- ISBN Row (Conditional) -->
<$list filter="[all[current]has[isbn]]">
	<<edit-field isbn "ISBN"
		display:"""<$let isbn={{!!isbn}}>
				<a href=<<isbnsearch>> class="tc-tiddlylink-external">{{!!isbn}}</a>
			</$let>"""
	>>
</$list>

    <!-- Bibliographies Row -->
    <tr>
        <th>Bibliographies</th>
        <td>
            <$list filter="[enlist{!!bibliography}]" variable="bibItem" join=", ">
                    <$link to={{{ [tag[Bibliography]bibliography<bibItem>] }}}>
                        <$text text={{{ [tag[Bibliography]bibliography<bibItem>get[title]] }}}/>
                    </$link>
<!--  I was having some trouble imagining how this part was designed to work. In any case, you don't need get[title] - tag[Bibliography] will return titles by default. As an alternative, depending on desired output, you might also consider this:
                    <$link to={{{ [tag[Bibliography]bibliography<bibItem>] }}} />
-->
            </$list>
        </td>
    </tr>

    <!-- Status Row -->
    <tr>
        <th>Status</th>
        <td>
            <$list filter="[all[current]tag[Sink]]">
                <<project-status-selector>>
            </$list>
            <$list filter="[all[current]tag[Source]]">
                <<read-status-selector>>
                <$list filter="[all[current]readstatus[read]]">
                    on <$view field="completed" format="date" template="YYYY-0MM-0DD"/>
                </$list>
                <$list filter="[all[current]readstatus[reread]]">
                    (last read <$view field="completed" format="date" template="YYYY-0MM-0DD"/>)
                </$list>
            </$list>
        </td>
    </tr>
</table>

<!-- Video Embed Section -->
<$list filter="[all[current]tag[WATCH]has[url]]">
<div style="display: flex; justify-content: center; margin-top: 15px;">
    <$wikify name="videoUrl" text={{!!url}}>
        <iframe 
            width="560" 
            height="315" 
            src=<<videoUrl>> 
            frameborder="0" 
            allowfullscreen
        ></iframe>
    </$wikify>
</div>
</$list>

<style>
.sourceDeetsTable {
  width: 100%;
  table-layout: fixed;
}
.sourceDeetsTable tr th {
  text-align: center;
  width: 20%;
}
.sourceDeetsTable tr td {
  width: 80%;
}
.tc-edit-texteditable {
  width: 100%;
}
.embedded-video-container {
  display: flex;
  justify-content: center;
  margin-top: 15px;
}
.capitalize { text-transform: capitalize; }
</style>
</$list>

Incidentally, enlist is one of the few filter operators that’s actually a selection constructor, which means that any operators that precede it in a filter run will be ignored. You don’t need the all[current] in [all[current]enlist{!!field}]; a field transclusion looks at the <<currentTiddler>> by default, unless you specify otherwise (e.g. {{HelloThere!!field}}).

2 Likes

It does seem to work, yes, a far more elegant solution :smiley: I will learn a lot by going over it with a finer comb.

And you were correct, <$link to={{{ [tag[Bibliography]bibliography<bibItem>] }}} /> generates the same result.

I was wondering if we could replace the editing of any field such as the $edit-text widget with a call to a custom macro or widget, which then provides this “Enter key within table edit box to submit” feature.

  • ie a generic solution behind a custom utility for any tiddler/field reference.

Perhaps we could leverage the Genisis widget around the $edit-text widget that is wrapped in the keyboard widget?

Were you thinking of using $genesis to determine the HTML elements (so you could replace <tr> <td> with nested <div>s, for instance) or is there another another application I’m missing? I suppose that would bring it more in line with core macros like <<list-links>>. You’d want to parameterize the classes, too, of course; I was working mostly within @well-noted’s existing framework.

The idea would be using the genesis widget inside a custom widget, to pass all parameters through the edit-text widget so it continues to behave with its behaviours. However the custom widget wrapps the recalling of edit text widget to allow the use of a temp storage location and save on enter, cancel etc…

  • If not using the edit text widget it would be “same, same, but different”.

I declare I have not read the above code.

It sounds like you’re essentially envisioning a “draft mode” that would apply on a per-field basis? I don’t think that’s quite in line with what @well-noted was looking for here (that is, a ViewTemplate that allows you to edit fields in view mode and uses “enter” to toggle from the $edit-text view back to a static transclusion) but I do think you could do it with $keyboard and perhaps $genesis.

I’d be a bit wary of overriding $edit-text globally… it’d be tedious to have to hit enter to truly “save” each field back to the intended tiddler, and I can easily see myself forgetting. You’d need a clear visual indicator that the value was only temporary until “submitted”. And I think you’d probably want a combination of keys rather than “enter” or any other single key, since those might get used for normal input purposes if you’re typing into a textarea.

Agreed, no I was only talking about its invocation for tiddler/fieldname, or the alternate edit fieldname method.

I am thinking of a widget or procedure such as <<edit-temp tiddler fieldname>> that you use to edit a fieldname with the required behaviour. Simple replace the current tables invocation of edit.

  • The will then display the value through transclusion but permit click to edit, a mouse over, edit a temp field and enter to save, optional clear and cancel buttons, even delete fieldname option

All though I now realise, not having a delay makes many lists such as in the sidebar possibly unusable, as it will try and preview every link as you move the mouse over.

That’s a good idea and very doable. I don’t think you’d need $genesis, just an extra $keyboard widget and button in the existing macro… I may play with it later.

Am following, excited to see what you come up with :blush:


Editable fields table.json (9.6 KB) (code reproduced below)

\procedure edit-field(field, label, display:"<$transclude $field=<<field>> />", copy:"no")
	\function useLabel() [<label>!match[]] ~[<field>]
	\function baseState() [[$:/state/edit/]] [<field>] +[join[]]
	\function fieldValue() [<currentTiddler>get<field>]
	\function tempValue() [<temp>get<field>]

	\procedure edit-toggle() <$action-listops $tiddler=<<state>> $field="text" $subfilter="+[toggle[edit]]" />

	\procedure done()
		<$action-deletefield $tiddler=<<state>> $field="text" />
		<$action-deletefield $tiddler=<<temp>> $field=<<field>> />
	\end done

	\procedure save()
		<$action-setfield $tiddler=<<currentTiddler>> $field=<<field>> $value=<<tempValue>> />
		<<done>>
	\end save

	\procedure cancel()
		<$action-deletefield $tiddler=<<temp>> $field=<<field>> />
		<<done>>
	\end cancel

	\procedure delete()
		<$action-confirm $message=`Do you wish to delete the '$(field)$' field?`>
			<$action-deletefield $tiddler=<<currentTiddler>> $field=<<field>> />
			<<done>>
		</$action-confirm>
	\end delete

<$let temp=`$:/temp/volatile/edits/$(currentTiddler)$`>
<$qualify title=<<baseState>> name="state">
<tr>
	<th>
		<$button actions=<<edit-toggle>> class="tc-btn-invisible field-label">
			''<<useLabel>>''
		</$button>
	</th>
	<td class="content">
		<% if [<state>!text[edit]] %>
			<$button actions=<<edit-toggle>> class="tc-btn-invisible">
				<<display>>
			</$button>
		<% else %>
			<% if [<fieldValue>!match[]] %>
				<pre title="Current value"><<display>></pre>
			<% endif %>
			<$keyboard key="enter" actions=<<save>>>
			<$keyboard key="shift+backspace" actions=<<cancel>>>
			<$keyboard key="delete" actions=<<delete>>>
				<$edit-text tiddler=<<temp>> field=<<field>>
					default=<<fieldValue>>
					placeholder=<<field>>
					class="tc-max-width"/>
			</$keyboard>
			</$keyboard>
			</$keyboard>
		<% endif %>
	</td>
	<td class="buttons">
		<% if [<state>text[edit]] %>
			<$button
				actions=<<save>>
				class="tc-btn-invisible"
				tooltip="Save changes - [enter]">
					{{$:/core/images/done-button}}
			</$button>
			<$button
				actions=<<cancel>>
				class="tc-btn-invisible"
				tooltip="Cancel changes - [shift+backspace]">
					{{$:/core/images/cancel-button}}
			</$button>
			<$button
				actions=<<delete>>
				class="tc-btn-invisible"
				tooltip="Remove this field - [delete]">
					{{$:/core/images/delete-button}}
			</$button>
		<% endif %>
		<% if [<fieldValue>] [<tempValue>] +[!match[]then<copy>match[yes]] %>
			<$button
				message="tm-copy-to-clipboard"
				param={{{ [<tempValue>!match[]] ~[<fieldValue>] }}}
				class="tc-btn-invisible copy-button"
				tooltip="Copy field value to clipboard">
 		           	{{$:/core/images/copy-clipboard}}
 	       </$button>
		<% endif %>
	</td>
</tr>
</$qualify>
</$let>
\end edit-field

\procedure enlist-field(field)
\function last() [enlist<items>count[]compare:number:gt[2]then[, ]] [[ and ]] +[join[]]
\whitespace trim
<$let items={{{ [<currentTiddler>each:list-item<field>sort[]] +[format:titlelist[]join[ ]] }}}>
	<$list filter="[enlist<items>butlast[]]" join=", " />
	<$list filter="[enlist<items>butlast[]limit[1]]"><<last>></$list>
	<$list filter="[enlist<items>last[]]" />
</$let>
\end

<style>	
.view-field-edits :is(th, td) {
	padding: 0.15em 0.5em;
}
.view-field-edits th {
	min-width: max-content;
	width: 20%;
	white-space: nowrap;
	text-align: right;
}
.view-field-edits th button.field-label {
	text-transform: capitalize;
	width: 100%;
	text-align: right;
}
.view-field-edits td.content {
	width: 80%;
	padding: 0.15em 0.15em;
}
.view-field-edits td.content pre {
	margin: 0.15em 0;
}
.view-field-edits td.buttons {
	min-width: max-content;
	white-space: nowrap;
	border-left: 0px;
	padding: 0;
}
.view-field-edits td.buttons button { display: inline-block; margin: 0.15em; line-height: 1.5em;}
.capitalize { text-transform: capitalize; }
</style>

<table class="view-field-edits tc-max-width">
    <!-- Title Row -->
    <<edit-field caption "Title" copy:"yes">>

    <!-- Author Row -->
    <<edit-field author display:"<<enlist-field author>>">>

    <!-- Year Row -->
    <<edit-field year "Publication year">>

    <!-- Medium Row -->
    <<edit-field medium>>

    <!-- URL Row -->
    <<edit-field url "URL" display:"""<a href={{!!url}} class="tc-tiddlylink-external">{{!!url}}</a>""">>
</table>

I recycled some of your ViewTemplate table + the <<enlist-field>> procedure I’d written up previously for testing purposes, but all you really need here are the edit-field procedure (and the styles to make it look a little nicer).

Current keyboard settings:

  • [enter] = save changes
  • [shift+backspace] = cancel changes
  • [delete] = delete field

I was originally going to use esc for the cancel key, but it’s already in use as the default ‘exit draft mode?’ key, so I switched to something that shouldn’t conflict if you happen to be editing fields in the draft preview window.

If I were releasing this as a plugin, I’d probably include some config settings to let you remap the keybindings more easily… but honestly, I think I’ve tinkered with this about as much as I care to at the moment. Do feel to remix it however you like, though!

A few more design notes:

I was initially using the <<edit-toggle>> macro to handle both editing and saving; it can do both since it toggles the text of the <<state>> tiddler between “edit” and “”, depending on the current value. But since I’m storing the edits in a temporary tiddler here, there are actually at least two possibilities:

  1. save changes and leave edit mode
  2. ignore changes and leave edit mode (and, potentially, delete the temporary value)

I decided to retain the original behavior of clicking on the label cell to enter/leave edit mode, but not to make any decisions about unsaved changes if you exit this way.

  • Any changes you made in the temporary tiddler will be retained in that tiddler, but will not be saved back to the “real” field until you explicitly choose to do so by hitting “enter” or hitting the save button.
  • Similarly, temporary edits won’t be cleared until you hit “shift+backspace” or the cancel button.

This means we’re up to 3 possible actions — <<edit-toggle>>, <<save>>, and <<cancel>> — where both <<save>> and <<cancel>> also use <<done>> to take you out of edit mode and clean up the draft. I took Tony’s suggestion and added a <<delete>> action as well, which also cleans up after itself.

Please note: I’m currently using temporary tiddlers (prefixed with $:/temp/volatile) to store the field “drafts”. This has two major benefits:

  • Tiddlers prefixed with $:/temp/volatile are automatically subject to refresh throttling, which should cut down on potential lag while typing.
  • $:/temp tiddlers don’t persist between wiki sessions, so they won’t clutter your wiki.

However, this also means that any unsaved field drafts won’t be retained across sessions! If you don’t like this behavior, you can replace the $:/temp/volatile prefix with $:/state.

Have fun!

4 Likes