Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Range actions #5052

Closed
wants to merge 134 commits into from
Closed

Add Range actions #5052

wants to merge 134 commits into from

Conversation

pmario
Copy link
Contributor

@pmario pmario commented Nov 16, 2020

This is WIP and contains some console.log() functions so you can see, what's going on with F12

There some redundant code to simplify debugging. ... See code-comments!

This PR adds 4 actions:

  • actionsMouseDown
  • actionsMouseUp
  • actionsInput
  • actionsChange

It behaves the same way in modern browsers and contains some lines of code to behave well with IE11.
I did test it with IE11 wondows 10, FF latest, Edge latest. ..

Go to Latest Version:

@pmario
Copy link
Contributor Author

pmario commented Nov 16, 2020

This PR will fix: [IDEA] Add actions parameter to "range widget" #5029

Copy link
Owner

@Jermolene Jermolene left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @pmario

@@ -92,16 +173,30 @@ RangeWidget.prototype.handleInputEvent = function(event) {
Compute the internal state of the widget
*/
RangeWidget.prototype.execute = function() {
// TODO remove the next 2 lines once IE is gone!
this.mouseUp = true; // Needed for IE10
this.isIE = (/msie|trident/i.test(window.navigator.userAgent)) ? true : false;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core already defines $tw.browser.isIE

this.actionsMouseUp = this.getAttribute("actionsMouseUp","");
// Change only fires, if start-value is different to end-value
this.actionsChange = this.getAttribute("actionsChange","");
// input fires very often!!
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a small thing but we do try to capitalise comments as sentence case to keep things neat

// Next 3 only fire once!
this.actionsMouseDown = this.getAttribute("actionsMouseDown","");
this.actionsMouseUp = this.getAttribute("actionsMouseUp","");
// Change only fires, if start-value is different to end-value
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comma isn't necessary here

@@ -111,15 +206,17 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
RangeWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes['min'] || changedAttributes['max'] || changedAttributes['increment'] || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.disabled) {
if( changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes['min'] || changedAttributes['max'] ||
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind the condition being split to multiple lines, but we wouldn't usually do it by adding whitespace between the opening parenthesis and the condition.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using tab characters for alignment (the "if(" line is really "if(→" where → represents a tab character) is a bad idea, because it ensures that the code will only be aligned if someone else uses tab settings that are the same size as yours, thereby removing the one big advantage of tabs for indentation (that people can set their own tab settings). Here you have the right idea by putting a tab after the "if(", so that people with either 4-space or 8-space tab settings will see the code correctly aligned. But if someone has their tab size set to 2 spaces, then this is no longer aligned.

If you are using tabs for indentation, the only correct approach is "tabs for indentation, spaces for alignment". The second line of the if( condition should be indented the same level as the if( (here that's two tab characters for two indentation levels), with three spaces after the two tab characters to line up the changedAttributes text. I.e., the second line should look like →→···changedAttributes['increment'], where → represents a tab character and · represents a space. That is the only correct way to do alignment when you have used tabs for indentation, because it ensures that the aligned lines will stay aligned in everyone's editor no matter what tab settings they use.

And the big problem with "tabs for indentation, spaces for alignment" is that most editors are not smart about it, and will mess it up. Which means that if you've aligned your code, you then have to pay careful attention to your whitespace every time you edit that line. Which is a lot of fiddly work that I, for one, don't care for, and that's why my personal preference is to use space characters, not tabs, for indentation.

There are really only four options:

  1. Use tab characters naively, for both indentation and alignment, and end up with code that looks different when different editor settings are applied.
  2. Use tab characters intelligently, using tabs for indentation and spaces for alignment, and end up paying far more attention to whitespace than it's worth.
  3. Use tab characters for indentation and forget trying to align subsequent lines. The rule for function parameters, second lines of "if()" conditions, and so on, is "just indent one more level with one extra tab character". This will still look reasonably good on everyone's editor, and everyone can use their favorite indentation level by tweaking their editor's tab settings.
  4. Use space characters for indentation. This allows you to align function parameters, "if()" conditions, and so on, but other coders won't get to use their favorite indentation level and must conform to the indentation level the project has chosen.

My personal preference is for 4. 1 is just bad, 2 causes you to pay way more attention to whitespace than it's worth, and 3 sacrifices alignment, which is a useful tool in many situations. So I'd rather choose option 4 and sacrifice the ability to use my preferred indentation, instead using whatever the project has settled on.

Point is, it's up to you whether you choose 2 or 3 here (4 is not an option since the TiddlyWiki project uses tabs for indentation), but please don't choose option 1. To choose option 2, write your continuation lines as →→···condition. To choose option 3, write your continuation lines as →→→condition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Use space characters for indentation. This allows you to align function parameters, "if()" conditions, and so on, but other coders won't get to use their favorite indentation level and must conform to the indentation level the project has chosen.

TW doesn't compress or minify the source code. That makes it easy for every user to start to read the code and start to debug it.

That means the empty.html would contain 4 times more indentation spaces as used with tabs. This would considerably increase the basic file size. So atm TW uses tabs for a reason.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, TW is one of the few projects where the fact that tabs take less storage than spaces is actually relevant. Most of the time compression makes the space savings of tab characters irrelevant, but in TW it actually matters. I wasn't actually arguing that TW should change (I did mention that option 4 is not an option in this case), I was just trying to list all available options for dealing with indentation & alignment. I agree that using tabs for TW makes sense since it isn't minified (though it probably is compressed, given that most servers default to gzipping HTML for delivery).

Though it is actually a little hard for every user to read the code, since when you download TW in HTML form via the big "Download Empty" button, the HTML form has had to store the code in a way that looks like "{\n\t\t\t\tresults.push(title);\n\t\t\t}" and so on. Granted, anyone who knows enough about Javascript to want to modify that code also probably knows how to do a search-and-replace to turn "\n" into a real newline and "\t" into a real tab. But there is a small barrier to entry; it's not quite the ideal situation that you described. :-)

{name:"mousedown", handlerObject:this, handlerMethod:"handleMouseDownEvent"},
{name:"mouseup", handlerObject:this, handlerMethod:"handleMouseUpEvent"},
{name:"change", handlerObject:this, handlerMethod:"handleChangeEvent"},
{name:"input", handlerObject:this, handlerMethod:"handleInputEvent"},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rather see this list built dynamically based on whether there's an attribute specified for that event. Something like this:

var eventList = [];
if(this.actionsMouseDown) {
    eventList.push({name:"mousedown", handlerObject:this, handlerMethod:"handleMouseDownEvent"});
}
// Similar for this.actionsMouseUp and this.actionsChange
if(this.actionsInput) {
    eventList.push({name:"input",     handlerObject:this, handlerMethod:"handleInputEvent"});
}
$tw.utils.addEventListeners(this.inputDomNode, eventList);

The advantage is that if the end user doesn't care about the input event, we don't need to call a dozen times into an event handler that will end up doing nothing. There is a performance cost to the browser firing events, even if the event handler returns quickly. If we can avoid hooking up the event at all, the browser can optimize and not fire the event because it knows nothing is going to handle the event.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point @rmunn

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with input and change event are, that they have to be fired, even if there is no action. Otherwise the target tiddler wouldn't be updated. It doesn't matter if there are actions defined or not. ... Input is fired on all modern browsers and change is fired in IE.

You are right for MouseDown and MouseUp. ATM they are only there for handling actions atm. ... BUT we don't know if this will change in the future.

Events are only fired if user wants interaction. So IMO there is no real performance overhead, without user interaction. So I'd vote for simple code here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot something. MouseUp is needed for IE to trigger the change event. So it doesn't matter if actions are defined.

@pmario
Copy link
Contributor Author

pmario commented Nov 17, 2020

@Jermolene Should I add the prepareAttributs to the basic widget.js? ... It can be made more generic and may be renamed to .prepareActionAttributes(). So we can probably reduce code duplication.

https://github.com/Jermolene/TiddlyWiki5/pull/5052/files#diff-10dbab80ae6e1067219cf5b2d25d69fd68bc382029e3056c37feb20ccf4d565cR83

@saqimtiaz
Copy link
Contributor

saqimtiaz commented Nov 17, 2020

@pmario I think a method on the base widget class makes sense.

Perhaps getActionVariables()?

@pmario
Copy link
Contributor Author

pmario commented Nov 17, 2020

Latest commit 8b25443 uses actionValue instead of attr-value now. .. Will add actionValue for radio widget too.

@@ -111,15 +202,17 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
RangeWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes['min'] || changedAttributes['max'] || changedAttributes['increment'] || changedAttributes["default"] || changedAttributes["class"] || changedAttributes.disabled) {
if(changedAttributes.tiddler || changedAttributes.field || changedAttributes.index || changedAttributes['min'] || changedAttributes['max'] ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmario if we need to call refreshSelf() if any attributes have changed, it is easier to just check the count:

if($tw.utils.count(changedAttributes) > 0) {
    this.refreshSelf();
    return true;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this the case for every widget?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmario We just can't use this trick when we only want to call refreshSelf() on some attributes changing but not for others. So there is probably room for some refactoring and optimization in other core widgets.

// Trigger actions
if(this.actionsInput) {
// "tiddler" parameter may be missing. See .execute() below
var variables = this.prepareAttributes() // TODO this line will go into the function call below.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently handleEvent() does debouncing and only calls wiki.setText if the value of the widget has changed. We should similarly debounce the actions, at least the ones for events like input that fire very frequently:

if(this.getValue() !== this.inputDomNode.value) {
    this.invokeActionString(this.actionsInput,this,event,variables);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

input or change(on IE) are only fired, if the value has been changed. ... IMO this is no debouncing.

The line was introduced with commit e84c422 ... But I don't see a difference, if I remove it. ... @Jermolene Do you remember the problem?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmario according to the spec you should be right. I thought I ran into some issue with this last I worked on this widget. I can do some tests to confirm tomorrow.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmario I can confirm that the debouncing does not seem necessary on the input event. In both Chrome and FF I cannot get the event to fire more than once per change of value.

@saqimtiaz
Copy link
Contributor

@pmario @Jermolene @rmunn I have some concerns regarding usability from the point of view of end users, and a question in this regard please:

@rmunn has given some great examples of why having separate actions for the input and change events would be useful. However, what added benefit is there in making the distinction of having separate actions for mouseUp and mouseDown, in addition to actions associated with the change and input events, and how do we reconcile that with the usability perspective?

I am worried about how accessible this will be to non-technical users who need to distinguish between 4 types of actions:

  • actionsChange
  • actionsInput
  • actionsMouseDown
  • actionsMouseUp

If we are handling those extra events (mousedown and mouseup) internally just for IE compatibility, I suggest we not expose them as widget parameters for distinct actions unless or until there is a specific need to do so.

Furthermore the current attribute names are perhaps too developer centric, especially if there are four of them. These are user facing widget attributes and not internal variables or method names.

@Jermolene
Copy link
Owner

I'm inclined to agree with @saqimtiaz. If we do want to trap mousedown/move/up, then I think we might be better off making a generic event trapping widget that traps events within its content. That widget could be wrapped around any other widget without that widget being aware of the event handling.

@saqimtiaz
Copy link
Contributor

saqimtiaz commented Nov 19, 2020

I'm inclined to agree with @saqimtiaz. If we do want to trap mousedown/move/up, then I think we might be better off making a generic event trapping widget that traps events within its content. That widget could be wrapped around any other widget without that widget being aware of the event handling.

I like this idea! In many ways the button widget does this for the click event, but is overloaded with other features.

We could generalize this to a trap widget that can trap any type of event we specify and associate actions with them. There is a good case for the dblclick and contextmenu events as well, and this would be far more versatile and flexible than adding support for each event to every widget where it might be needed.

@Jermolene
Copy link
Owner

I'm inclined to agree with @saqimtiaz. If we do want to trap mousedown/move/up, then I think we might be better off making a generic event trapping widget that traps events within its content. That widget could be wrapped around any other widget without that widget being aware of the event handling.

I like this idea! In many ways the button widget does this for the click event, but is overloaded with other features.

We could generalize this to a trap widget that can trap any type of event we specify and associate actions with them. There is a good case for the dblclick and contextmenu events as well, and this would be far more versatile and flexible than adding support for each event to every widget where it might be needed.

I've got a very simple prototype <$event> widget for click events, but <$trap> is a better name. I'll post it in a moment.

@Jermolene
Copy link
Owner

I've got a very simple prototype <$event> widget for click events, but <$trap> is a better name. I'll post it in a moment.

Here's the ticket: #5074

@pmario
Copy link
Contributor Author

pmario commented Nov 19, 2020

I was thinking about it too. So I wanted to implement a actionStart, actionStop and action. Where start gets actionValue as the start-value. Stop gets actionValue and actionValueHasChanged info. So the user has easy access to the info if the start- and end-value are different. 'action' is activated on input, which is fired a lot of times. ...

It's made sure, that "start" and "stop" is only called once. and action is called whenever the handle is moved.

@pmario
Copy link
Contributor Author

pmario commented Nov 20, 2020

Go To Latest Version

It has 3 actions now:

  • actionsStart ... fires 1x if range-handle is clicked
  • actionsStop ... fires 1x if handle is released
  • actions ... fires if handle is moved.

actionValue is available in the actions
actionValueHasChanged can be "yes" / "no" and shows, if the value between start and stop has changed. In actions it is always "yes", since that action is only fired if the value has been changed, between 2 calls.

@pmario pmario marked this pull request as ready for review November 20, 2020 10:57
@pmario
Copy link
Contributor Author

pmario commented Nov 22, 2020

Latest Version: range-actions.zip

All console.log() and redundant code is removed. see commit: 0111333

}
}
}
return value;
};

RangeWidget.prototype.prepareAttributes = function(options) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this name is not intuitive. In fact we are processing attributes to prepare variables, rather than preparing attributes.

Perhaps getActionVariables() so that the context is easier to undertand?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getActionVariables would be more consistent with other usages in the core.

saqimtiaz referenced this pull request Nov 25, 2020
I was getting some inconsistencies with filter operators that expect strings.
@twMat
Copy link
Contributor

twMat commented Nov 28, 2020

Bump.
Would be nice if included in 5.1.23

@pmario
Copy link
Contributor Author

pmario commented Nov 30, 2020

Will close this one and create a new and simplified version.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants