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

Inline toolbar context feature #1195

Merged
merged 85 commits into from
Nov 23, 2017
Merged

Inline toolbar context feature #1195

merged 85 commits into from
Nov 23, 2017

Conversation

mlewand
Copy link
Contributor

@mlewand mlewand commented Nov 21, 2017

What is the purpose of this pull request?

New feature

This PR contains

  • Unit tests
  • Manual tests

What changes did you make?

Added context classes for inline toolbar. It makes stupid easy to maintain multiple inline toolbars in a single editor.

See CKEDITOR.editor.inlineToolbar (local docs link) for more details.

Copy link
Contributor

@f1ames f1ames left a comment

Choose a reason for hiding this comment

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

Overall nice and solid piece of code 👍 Just some minor stuff here and there.

Also one test failing on Safari:
screen shot 2017-11-22 at 12 38 01

@@ -37,6 +39,10 @@
/**
* Class representing instance of inline toolbar.
*
* The easiest way to create an inline toolbar is by using {@link CKEDITOR.editor.inlineToolbar#create} function.
*
* However it's possible to maintain it by manually, like below:
Copy link
Contributor

Choose a reason for hiding this comment

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

to maintain it by manually...

},

/**
* @param {CKEDITOR.dom.element} [pointedElement] Element that should be pointed by the inline toolbar.
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing method description.

this.toolbar.show();
},

hide: function() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here, missing method description.


Context.prototype = {
/**
* Destroy inline toolbar context
Copy link
Contributor

Choose a reason for hiding this comment

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

I have a doubt about this description, what this method does is basically destroying the toolbar which is bound to this context (same with show and hide), so maybe it can be rephrased to emphasize this fact?

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 agree, corrected the description so that it clearly says that it hides toolbar controlled by the context.

* Checks if any of `options.widgets` widgets is currently focused.
*
* @private
* @returns {Boolean/CKEDITOR.dom.element} `true` if currently selected widget matches any tracked by this
Copy link
Contributor

Choose a reason for hiding this comment

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

From what I see, this function may return false, undefined or CKEDITOR.dom.element. So true is never returned. Maybe the return type should be {CKEDITOR.dom.element/null} (it is more natural when the function returns an instance it may also return null which means there is no instance). Mixing Boolean with CKEDITOR.dom.element looks a little strange (same for _matchElement below).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, in that particular case describing true does not make sense. As you pointed out it's not returned. So it make sense to rewrite it.

However in general _matchWidget has return API compatible with other matching functions.

Meaning that if it returns:

  • true - manager will determine what element should be pointed by the toolbar
  • false - element is simply not matched
  • CKEDITOR.dom.element - entity is positively matched, and function explicitly says what element should be pointed by the toolbar

Having that said I'd stick to current return API.

Copy link
Contributor

@f1ames f1ames Nov 22, 2017

Choose a reason for hiding this comment

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

It makes sense to keep things consistent this way.

However, one thing I see is that _matchWidget/_matchElement and _matchRefresh are a little different. All three are internal functions. But the first two reflects/works on simple properties (string or array) passed in contextDefiniton object, while the _matchRefresh reflects refresh function from contextDefinition thus also reflecting its public API.
And as I said before mixing Boolean and CKEDITOR.dom.element as return values seems a little unintuitive to me.
One more thing is that this API is compatible only by definition (well, maybe that's what compatibility means:D), because in real use cases _matchRefresh usually will return true when element matches and the other two will always return CKEDITOR.dom.element.

Anyway, I'm not convinced that one solution is far better than the other so we may leave it as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok this discussion was moved to #1195 (comment) and #1195 (comment)

return el.getName() === 'strong';
} );
},

Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we test a case when more than one matching option is defined to check how it behaves (and to prevent situation that after some changes/refactoring it will start behaving differently)?
Or it is just treated as misuse and we simply don't handle it?

Copy link
Contributor Author

@mlewand mlewand Nov 22, 2017

Choose a reason for hiding this comment

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

Not sure if I got it right, but if you're asking about multiple combinations of options.widget/cssSelector/refresh, then they are already tested in priorities test suite.

On top of that integration tests adds up some extra protection against that, so I'd say it's pretty well covered.

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant the case when you have more than one match option defined for the same context like:

{
	buttons: 'Bold,Italic',
	refresh: sinon.stub().returns( true ),
	widgets: [ 'foo', 'bar' ]
}

</style>

<textarea id="editor1" cols="10" rows="10">
<p>Sample paragraph with <em>emphasize</em>, <strong>strong</strong>, <u>underline</u> and <s>strike thourgh</s>.</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo: strike through

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔


</style>

<textarea id="editor1" cols="10" rows="10">
Copy link
Contributor

Choose a reason for hiding this comment

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

Have you consider addink links/images to initial content so test can be performed easier/faster (without a need to add those elements)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Make sense, I have added a link to the set.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔

} );
},

// This case is yet to be decided.
Copy link
Contributor

Choose a reason for hiding this comment

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

AFAIR options.widgets has priority over options.cssSelector so this test could be added (uncommented) 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.

Actually I had to remove it as this test didn't have sense anymore.

It assumed that options.widget will take effect if the selection is within nested editable, say a strong (and this case is covered with unit test in widget.js).

However after research we have implemented it in a way that options.widget takes effect only when the widget itself is focused.

Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔

* manager, to recheck all the contexts.
* @returns {CKEDITOR.plugins.inlinetoolbar.context}
*/
_getContextStub: function( refreshCallback, autoRefresh ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is also duplicated in cssselector.js test file.

Copy link
Contributor Author

@mlewand mlewand Nov 22, 2017

Choose a reason for hiding this comment

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

To be honest, each function like that is slightly different, and after making a common interface it would not be as usable for each suite.

Thus I don't see too much value in doing that. WDYT?

Assert was repeated the same way everywhere, and it made a perfect sense, however I find it a slightly different case.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, you would have to make generic function which can take refresh, widgets and cssSelector and create specific context based on that. As it will overcomplicate thinks, lets leave it as is 👍

@mlewand
Copy link
Contributor Author

mlewand commented Nov 22, 2017

Fixed failing test on Safari. Turned out there was a typo in test case, causing wrong selection in that browser.

@mlewand mlewand requested a review from f1ames November 22, 2017 14:43
Copy link
Contributor

@f1ames f1ames left a comment

Choose a reason for hiding this comment

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

It seems you didn't push all changes you have made (assuming based on comments)?

Please also check #1195 (comment) and #1195 (comment) and #1195 (comment).

this.editorBot.setHtmlWithSelection( '<p><strong data-foo-bar="1">foo^ bar</strong></p>' );

this._assertToolbarVisible( false, context );
},
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not seeing those test, maybe you just didn't push them?

},

/*
* Returns a Context instance with toolbar show/hide methods stubbed.
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔


/*
* @param {Boolean} expected What's the expected visibility? If `true` toolbar must be visible.
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔


this.editorBot.setHtmlWithSelection( '<p>Test^ <strong>Foo</strong><em>bar</em></p>' );

emElem = this.editor.editable().findOne( 'em' );
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔

// However later on a cssSelector listener, will hint element B.
// Make sure B is pointed by the toolbar.

this.editorBot.setHtmlWithSelection( '<p><em>foo<strong>b^ar</strong>baz</em</p>' );
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔

/*
* @param {Boolean} expected What's the expected visibility? If `true` toolbar must be visible.
*/
_assertToolbarVisible: function( expected, context, msg ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔


</style>

<textarea id="editor1" cols="10" rows="10">
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔

</style>

<textarea id="editor1" cols="10" rows="10">
<p>Sample paragraph with <em>emphasize</em>, <strong>strong</strong>, <u>underline</u> and <s>strike thourgh</s>.</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔

} );
},

// This case is yet to be decided.
Copy link
Contributor

Choose a reason for hiding this comment

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

Looks like it was not updated 🤔

@f1ames f1ames self-requested a review November 23, 2017 10:21
Copy link
Contributor

@f1ames f1ames left a comment

Choose a reason for hiding this comment

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

Looks good 👍 I just found some places where code could be further simplified after _matchRefresh refactoring.

I also don't see links added to manual/complex test as stated in this comment - https://github.com/ckeditor/ckeditor-dev/pull/1195/files#r152571754.

And I'm not sure if you have seen this comment #1195 (comment)?

return this.editor.widgets.focused.element;
} else {
return null;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Could be simplified to:

var ret = null;

if ( this.options.widgets ) {
	var widgetNames = this.options.widgets,
		curWidgetName = this.editor.widgets && this.editor.widgets.focused && this.editor.widgets.focused.name;

	if ( typeof widgetNames === 'string' ) {
		widgetNames = widgetNames.split( ',' );
	}

	if ( CKEDITOR.tools.array.indexOf( widgetNames, curWidgetName ) !== -1 ) {
		ret = this.editor.widgets.focused.element;
	}
}

return ret;

well, the number of lines is the same. I find it a little more readable, but I suppose it is the matter of preferences. If you feel the same you may change it.

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 agree that it might add a bit to the readability. Also I have extracted widgetNames before the first if as it make sense to use widgetNames instead of this.options.widgets there already.

var ret = null;

if ( this.options.refresh ) {
ret = this.options.refresh( this.editor, path, selection );
Copy link
Contributor

Choose a reason for hiding this comment

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

If options.refresh returns false the whole function will also return false instead of null. I added and pushed one unit test checking return type of this function.

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 have adjusted this test case a bit in c1dc8cd, just by using more precise assertions for type checking in .


contextMatched = curContext;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Now as _refresh* functions API got simpler and returns null or element, couldn't this be simplified to:

if ( !contextMatched || contextMatched.options.priority > curContext.options.priority ) {
	highlightElement = matchingFunction( curContext, matchingArg1 );

	if ( !!highlightElement ) {
		contextMatched = curContext;
	}
}

?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can't simplify it that much as it would break test case when subsequent (higher priority) context returns null again. In that case it would cause highlightElement to be reset back to null.

Covered this with a test case in 8e89c0330836e0519bf6ff2f26e5185b13b5beb0.

Nonetheless we could simplify it now a bit, which I did in e382734.

}

contextMatched.show( highlightElement );
}
Copy link
Contributor

Choose a reason for hiding this comment

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

If matchEachContext above can be simplified, also this part can as contextMatched is set only when highlightElement is present so you can just go with:

if ( contextMatched ) {
    contextMatched.show( highlightElement );
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now due to this part: https://github.com/ckeditor/ckeditor-dev/blob/e382734/plugins/inlinetoolbar/plugin.js#L425-L428

It's guaranteed that highlightElement is an element, so that gives me the confidence to remove this defensive bit 🙂

@f1ames f1ames self-requested a review November 23, 2017 14:40
Copy link
Contributor

@f1ames f1ames left a comment

Choose a reason for hiding this comment

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

Everything looks good 👍

However I found one issue related to inline editor and drag/drop. When link is selected and toolbar appears and you drag it and then unlink - error is thrown:

nov-23-2017 15-39-10

Wasn't able to reproduce it on t/933, there panel follows the dropped content 🤔

nov-23-2017 15-53-50

Tried with managing toolbar like:

editor.on( 'selectionChange', function( evt ) {
	var lastElement = evt.data.path.lastElement;

	if ( lastElement && lastElement.getName() == 'a' ) {
		panel.attach( lastElement );
	} else {
		panel.hide();
	}
} );

Still not sure if it's related to Context API changes.

@mlewand
Copy link
Contributor Author

mlewand commented Nov 23, 2017

@f1ames above is actually an upstream issue, looks to me it's due to invalid selection restoration for inline editors.

See #1219 to repro the case.

@f1ames f1ames self-requested a review November 23, 2017 16:10
Copy link
Contributor

@f1ames f1ames left a comment

Choose a reason for hiding this comment

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

LGTM 👍

wojtekw92 and others added 17 commits November 23, 2017 17:13
… elements path in the context.refresh function.
…possible to disable listeners in some unit tests.

It was causing a lot of extra toolbar.show / hide calls, which in turn required me to call toolbar.show.reset() in every TC. That was not an elegant solution, so instead I made it easier to disable.

What's more, I'm thinking about removing it completely from the constructor, and attach only on first show() method. That would be even more performance-friendly solution. And we'll be able to remove these _attachListeners stubs from init functions.
mlewand and others added 25 commits November 23, 2017 17:13
Notes: Actually I had to remove it as this test didn't have sense anymore.

It assumed that options.widget will take effect if the selection is within nested editable, say a strong (and this case is covered with unit test in widget.js).

However after research we have implemented it in a way that options.widget takes effect only when the widget itself is focused.
…ger a bool value. Added an extra TC that was not covered before.
…ill not reset last previously returned (correct) pointed element.
…ossible for it not to be an element.

Since it's guaranteed by https://github.com/ckeditor/ckeditor-dev/blob/e382734/plugins/inlinetoolbar/plugin.js#L425-L428 (it's the only place where contextMached is set).
@f1ames f1ames merged commit 143c2d6 into t/933 Nov 23, 2017
@f1ames f1ames deleted the t/933-2996 branch November 23, 2017 16:32
mlewand pushed a commit that referenced this pull request Nov 29, 2017
Inline toolbar context feature
@mlewand mlewand mentioned this pull request Dec 4, 2017
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.

3 participants