Skip to content
This repository has been archived by the owner on Sep 6, 2021. It is now read-only.

Fix for #6742: Handle invalid timing function params #6757

Closed
wants to merge 30 commits into from

Conversation

marcelgerber
Copy link
Contributor

This PR is for #6742 @redmunds

Todo:

  • Set values for an empty cubic-bezier() (according to @redmunds, we should use ease-in-out here)
  • Set values for an invalid cubic-bezier(-5, 0.7, 8, "invalid") (only replace the invalid ones with default values)
  • Set values for a cubic-bezier(0, 0, 0) with an invalid number of params (only replace the missing/invalid ones with default values)
  • Set values for an empty steps()
  • Set values for an invalid steps("invalid", 2) (only replace the invalid ones with default values)
  • Set values for an steps(1, 2, 3) with an invalid number of params (only replace the missing/invalid ones with default values)
  • Add unit tests
  • Display hint to inform the user about the invalid syntax in the editor and that it will be changed (see Fix for #6742: Handle invalid timing function params #6757 (comment) and following comments)
  • Hide the hint when changes are reflected in the document

* @param {Array} def The default params to replace invalid values with
* @return { isNumber: boolean, value: number }
*/
function getValidBezierParams(match, def) {
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 think we can do this easier, but how to?
Any ideas are very appreciated.

Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of having "easy" and "complex" regexes, rename them to be "strict" and "lax". Then follow the pattern of "strict" versus "lax" used by the other regexes. "lax" is used when initially parsing user data in page and "strict" is used by editor reparsing strings it already knows to be valid (to get offsets for replacing in doc).

For the getValidBezierParams(match, def) function, I think match[1] will contain the parameter string, so you'll need to do something like the following to separate them into an array:

    var params = match[1].split(",");

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'll rename the regexes, but we need to do a little bit more repositioning to do that.

Even if we do .split(), we still need for loop to loop through the entries, don't we?
Btw, match[0] contains the parameter string, but the cubic-bezier( prefix is included, too.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, you have to loop through the parameters. Use match[1] because you want only parameters.

@redmunds redmunds self-assigned this Feb 4, 2014
} else {
return TimingFunctionUtils.getValidBezierParams(match, [ ".42", "0", ".58", "1" ]);
// take ease-in-out as default value in case there are no params yet (or they are invalid)
}
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

You shouldn't need to do any of this here because this only gets called once we're already in editor, so data should already be valid.

The initial validation gets done in main.js in prepareEditorForProvider() in the call to TimingFunctionUtils.timingFunctionMatch(). The conversion to default parameters should get done on TimingFunctionUtils.timingFunctionMatch().

Also, I think values for ease-in-out should only be used when there are no parameters supplied to cubic-bezier(). For values out-of-range, they should be moved to closest value in range. For example, the range of valid values for x values (first and third parameters) is 0-1, so values less than 0 should be set to 0 and values greater than 1 should be set to 1.

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. The .valid flag is set if _validateCubicBezierParams() passes. Should I change the name of the flag?
  2. That's a good idea, but we still need to insert some values in case there are non-numeric values (e.g. "invalid").

@marcelgerber
Copy link
Contributor Author

@redmunds Just added another commit with the requested changes included, could you please take a look, too?
I had to add match[1], hope this isn't breaking anything.

@redmunds
Copy link
Contributor

redmunds commented Feb 5, 2014

No, I don't think the match array should change. All of your changes should be in TimingFunctionUtils. I forgot 1 piece to the puzzle. I hope this explains how this should work.

Starting with these regexes:

    var BEZIER_CURVE_STRICT_REGEX       = /cubic-bezier\(\s*(\S+)\s*,\s*(\S+)\s*,\s*(\S+)\s*,\s*(\S+)\s*\)/,
        BEZIER_CURVE_LAX_REGEX          = /cubic-bezier\((.*)\)/;

First make the original call:

    var match = str.match(BEZIER_CURVE_STRICT_REGEX);

If that fails, then try with BEZIER_CURVE_LAX_REGEX. If that matches, then call getValidBezierParams() to create and/or fix params. This function needs to build a new bezier-curve() function call string -- this is the piece I forgot.

Then run the new string that was created against the strict regex pattern:

    var match = newStr.match(BEZIER_CURVE_STRICT_REGEX);

Then getValidBezierParams() returns either this match or null.

This keeps all processing in TimingFunctionUtils and also keeps the same format for match. Does that make sense?

Marcel Gerber added 2 commits February 5, 2014 22:14
s# Please enter the commit message for your changes. Lines starting
@marcelgerber
Copy link
Contributor Author

@redmunds Just pushed changes again. I hope this was what you meant, BezierCurveEditor.js isn't changed at all.

*/
function _getValidBezierParams(match, def) {
var param,
old_index = match.index, // we need to store the old match.index to re-set the index afterwards
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better way to store and re-set this later?

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this is necessary since the string will be re-matched against BEZIER_CURVE_STRICT_REGEX at the end.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But the index is set to another value because there is no more context (first few chars in row), and the index is used to set the cm selection later on.
Without re-setting, the selection will be on the wrong place (this is the reason why this code is there).

Copy link
Contributor

Choose a reason for hiding this comment

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

Gotcha. If we update the index, then I think the replacement string will be wrong when we make first update to doc after an edit.

Is that correct? If so, then I see 2 options:

  1. Immediately update doc with updated params. I was hoping to avoid this, but I guess it's not so bad since user can undo.
  2. Keep match object that same but add on a new property which is an array of validatedParams.

Other suggestions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you mean? An update caused by the Inline Editor, or an update while the Inline Editor is open?
The first one seemed to work (with the match.index set like this), even if I did not test that much. Haven't tested the latter yet.

Copy link
Contributor

Choose a reason for hiding this comment

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

My concern is with the first edit after the params have been inserted/corrected. If that works, then I think it's OK.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Works - just tested.

@marcelgerber
Copy link
Contributor Author

@redmunds I hope this is finally the right one.
Will add other functions (from the todo list above) later or tomorrow.

match = str.match(BEZIER_CURVE_VALID_REGEX);
if (match && _validateCubicBezierParams(match)) { // cubic-bezier() with valid params
return _tagMatch(match, BEZIER);
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't need this else statement since the if clause returns.

@redmunds
Copy link
Contributor

redmunds commented Feb 6, 2014

@SAplayer This works pretty great! I made a couple minor comments about the code.

I am able to see the "length" error I was concerned about. Start with cubic-bezier(-0.001, Infinity, 1.001, NaN). The parameters are converted to a valid values. But, only part of the function call is selected in the doc, so when you make an edit, it's not updated correctly and css becomes corrupted.

Works great for initial value of cubic-bezier()!

@marcelgerber
Copy link
Contributor Author

Thanks for the repro steps. I may know where this comes from.
Can we also set a .realLength = oldLength; to use in main.js#L109?

Btw, this may also happen with multiple css values in a line:
transition: cubic-bezier(); margin: 0 auto;
Haven't tried yet.

@redmunds
Copy link
Contributor

redmunds commented Feb 7, 2014

Can we also set a .realLength = oldLength; to use in main.js#L109?

Adding a new property is good, but the term "real" can be confusing. Maybe .originalLength ?

@marcelgerber
Copy link
Contributor Author

Hey @redmunds, just pushed a few commits to finally fix the nasty selection bugs and implement this feature for steps(), too.
Could you do another review (only the unit tests are missing)?

@redmunds
Copy link
Contributor

One way to fix work around it would be to put a delay on the start of the transition.

@marcelgerber
Copy link
Contributor Author

Ah thanks, that works (but is a bit laggy when doing further changes).

@@ -491,6 +491,7 @@ define({
"INLINE_TIMING_EDITOR_PROGRESSION" : "Progression",
"BEZIER_EDITOR_INFO" : "<kbd>↑</kbd><kbd>↓</kbd><kbd>←</kbd><kbd>→</kbd> Move selected point<br><kbd class='text'>Shift</kbd> Move by ten units<br><kbd class='text'>Tab</kbd> Switch points",
"STEPS_EDITOR_INFO" : "<kbd>↑</kbd><kbd>↓</kbd> Increase or decrease steps<br><kbd>←</kbd><kbd>→</kbd> 'Start' or 'End'",
"INLINE_TIMING_EDITOR_INVALID" : "The old value <code>{0}</code> is not valid, so the displayed one was changed to <code>{1}</code>. The document will be updated with the first edit.",
Copy link
Contributor

Choose a reason for hiding this comment

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

One more wording tweak: change "the displayed one" to be a little more descriptive such as "the displayed function".

@@ -530,6 +530,9 @@ define(function (require, exports, module) {
// current cubic-bezier() function params
this._cubicBezierCoords = this._getCubicBezierCoords(bezierCurve);

this.hint = $(".hint", this.$element);
TimingFunctionUtils.showHideHint(this, bezierCurve.originalLength, bezierCurve.originalString, "cubic-bezier(" + this._cubicBezierCoords.join(", ") + ")");
Copy link
Contributor

Choose a reason for hiding this comment

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

The usage of originalLength is a bit too magical here. It needs to be easier for the next dev to figure out this code. I'm thinking of something like:

        // If function was auto-corrected, then originalString and originalLength hold the original function,
        // and an informational message need to be shown
        var show = (bezierCurve.originalLength ? true : false);
        if (show) {
            TimingFunctionUtils.showHideHint(this, true, bezierCurve.originalString, "cubic-bezier(" + this._cubicBezierCoords.join(", ") + ")");
        } else {
            TimingFunctionUtils.showHideHint(this, false);
        }

This also has the benefit of only building and passing the display strings in the correction case (which is not the usual case).

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, if you are storing originalString then there's no need to store originalLength, is there? Can't you just use originalString.length?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do. Can I do show = !!bezierCurve.originalString.length as well (is this accepted within the coding standard?)
I thought about removing originalLength as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not necessary to create the show var, and originalString may not exist so maybe:

        // If function was auto-corrected, then originalString holds the original function,
        // and an informational message need to be shown
        if (bezierCurve.originalString) {
            TimingFunctionUtils.showHideHint(this, true, bezierCurve.originalString, "cubic-bezier(" + this._cubicBezierCoords.join(", ") + ")");
        } else {
            TimingFunctionUtils.showHideHint(this, false);
        }

@redmunds
Copy link
Contributor

Done with first final review :)

@marcelgerber
Copy link
Contributor Author

Pushed changes, tests still passing.

@marcelgerber
Copy link
Contributor Author

Fixed the animation to not use setTimeout and last 0.6s (plus 0.1s delay, so 0.7s overall) instead of 1.2s.

Maybe @couzteau for the german translation?

@@ -107,12 +234,44 @@ define(function (require, exports, module) {
function _validateStepsParams(match) {
var count = _convertToNumber(match[1]);

if (!count.isNumber || count.value <= 0) {
if (!count.isNumber || count.value < 1 || Math.floor(count.value) !== count.value) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Is there a better method to check for an integer?

Copy link
Contributor

Choose a reason for hiding this comment

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

You could probably use Lo-Dash http://lodash.com/docs#isFinite?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Have you tried this? I think it will accept values like 2.0 as they are still finite...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just tested it - it's not testing for an integer, but for a (finite) number. So 2.0 or even 2.1 are still valid.

Copy link
Contributor

Choose a reason for hiding this comment

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

No, I haven't tested it... I just read the documentation I thought it might work. I am not sure why it does that, doesn't seem that useful, but there isn't an isInteger function.

So I guess your method is fine. Here are other methods if you want to check http://stackoverflow.com/questions/3885817/how-to-check-if-a-number-is-float-or-integer

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No problem.
It's just checking that the input is not ±Infinity, "", true, false, null or undefined.
But I can't believe it's that difficult in JavaScript to check for an integer - it's a built-in function in most other languages.
I will still use this solution as it should still work for things like 1.0 (some on StackOverflow don't).

Copy link
Contributor

Choose a reason for hiding this comment

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

I know... But is comming: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger

Too bad is not supported in Brackets yet :(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes, they are using the same check with floor as I do ;) (we don't need the other checks).
It would be cool if we had NumberUtils in Brackets to do such checks.

@redmunds
Copy link
Contributor

@ingorichter or @pthiess - could review the German strings?

@redmunds
Copy link
Contributor

This looks good. After the German strings are reviewed, then just need to rebase.

@marcelgerber
Copy link
Contributor Author

@redmunds Here's a rebased version: #6922

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

Successfully merging this pull request may close these issues.

3 participants