-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
plugins/inlinetoolbar/plugin.js
Outdated
@@ -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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to maintain it
bymanually...
plugins/inlinetoolbar/plugin.js
Outdated
}, | ||
|
||
/** | ||
* @param {CKEDITOR.dom.element} [pointedElement] Element that should be pointed by the inline toolbar. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing method description.
plugins/inlinetoolbar/plugin.js
Outdated
this.toolbar.show(); | ||
}, | ||
|
||
hide: function() { |
There was a problem hiding this comment.
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.
plugins/inlinetoolbar/plugin.js
Outdated
|
||
Context.prototype = { | ||
/** | ||
* Destroy inline toolbar context |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
plugins/inlinetoolbar/plugin.js
Outdated
* 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 |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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 toolbarfalse
- element is simply not matchedCKEDITOR.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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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'; | ||
} ); | ||
}, | ||
|
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: strike through
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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)?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 👍
Fixed failing test on Safari. Turned out there was a typo in test case, causing wrong selection in that browser. |
There was a problem hiding this 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 ); | ||
}, |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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. | ||
*/ |
There was a problem hiding this comment.
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' ); |
There was a problem hiding this comment.
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>' ); |
There was a problem hiding this comment.
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 ) { |
There was a problem hiding this comment.
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"> |
There was a problem hiding this comment.
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> |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 🤔
There was a problem hiding this 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)?
plugins/inlinetoolbar/plugin.js
Outdated
return this.editor.widgets.focused.element; | ||
} else { | ||
return null; | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
plugins/inlinetoolbar/plugin.js
Outdated
var ret = null; | ||
|
||
if ( this.options.refresh ) { | ||
ret = this.options.refresh( this.editor, path, selection ); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 .
plugins/inlinetoolbar/plugin.js
Outdated
|
||
contextMatched = curContext; | ||
} | ||
} |
There was a problem hiding this comment.
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;
}
}
?
There was a problem hiding this comment.
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.
plugins/inlinetoolbar/plugin.js
Outdated
} | ||
|
||
contextMatched.show( highlightElement ); | ||
} |
There was a problem hiding this comment.
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 );
}
There was a problem hiding this comment.
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 🙂
There was a problem hiding this 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:
Wasn't able to reproduce it on t/933
, there panel follows the dropped content 🤔
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 👍
…ve features, major fixes.
Misc fixes to correct the API.
…ements path is used.
… 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.
…oes not explode when loaded in IE8.
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.
…ed helper methods.
…ment or null. No booleans anymore.
…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).
Inline toolbar context feature
What is the purpose of this pull request?
New feature
This PR contains
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.