diff --git a/src/link.js b/src/link.js
index 6852add..227a130 100644
--- a/src/link.js
+++ b/src/link.js
@@ -11,12 +11,11 @@ import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
import LinkEngine from './linkengine';
import LinkElement from './linkelement';
+import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/contextualballoon';
import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
-
import LinkFormView from './ui/linkformview';
import linkIcon from '../theme/icons/link.svg';
@@ -25,9 +24,10 @@ import unlinkIcon from '../theme/icons/unlink.svg';
import '../theme/theme.scss';
/**
- * The link feature. It introduces the Link and Unlink buttons and the Ctrl+K keystroke.
+ * The link plugin. It introduces the Link and Unlink buttons and the Ctrl+K keystroke.
*
- * It uses the {@link module:link/linkengine~LinkEngine link engine feature}.
+ * It uses the {@link module:link/linkengine~LinkEngine link engine plugin} and the
+ * {@link module:ui/contextualballoon~ContextualBalloon contextual balloon plugin}.
*
* @extends module:core/plugin~Plugin
*/
@@ -36,7 +36,7 @@ export default class Link extends Plugin {
* @inheritDoc
*/
static get requires() {
- return [ LinkEngine ];
+ return [ LinkEngine, ContextualBalloon ];
}
/**
@@ -53,27 +53,67 @@ export default class Link extends Plugin {
this.editor.editing.view.addObserver( ClickObserver );
/**
- * Balloon panel view to display the main UI.
+ * The form view displayed inside of the balloon.
*
- * @member {module:link/ui/balloonpanel~BalloonPanelView}
+ * @member {module:link/ui/linkformview~LinkFormView}
*/
- this.balloonPanelView = this._createBalloonPanel();
+ this.formView = this._createForm();
/**
- * The form view inside {@link #balloonPanelView}.
+ * The contextual balloon plugin instance.
*
- * @member {module:link/ui/linkformview~LinkFormView}
+ * @private
+ * @member {module:ui/contextualballoon~ContextualBalloon}
*/
- this.formView = this._createForm();
+ this._balloon = this.editor.plugins.get( ContextualBalloon );
// Create toolbar buttons.
this._createToolbarLinkButton();
this._createToolbarUnlinkButton();
+
+ // Attach lifecycle actions to the the balloon.
+ this._attachActions();
+ }
+
+ /**
+ * Creates the {@link module:link/ui/linkformview~LinkFormView} instance.
+ *
+ * @private
+ * @returns {module:link/ui/linkformview~LinkFormView} Link form instance.
+ */
+ _createForm() {
+ const editor = this.editor;
+ const formView = new LinkFormView( editor.locale );
+
+ formView.urlInputView.bind( 'value' ).to( editor.commands.get( 'link' ), 'value' );
+
+ // Execute link command after clicking on formView `Save` button.
+ this.listenTo( formView, 'submit', () => {
+ editor.execute( 'link', formView.urlInputView.inputView.element.value );
+ this._hidePanel( true );
+ } );
+
+ // Execute unlink command after clicking on formView `Unlink` button.
+ this.listenTo( formView, 'unlink', () => {
+ editor.execute( 'unlink' );
+ this._hidePanel( true );
+ } );
+
+ // Hide the panel after clicking on formView `Cancel` button.
+ this.listenTo( formView, 'cancel', () => this._hidePanel( true ) );
+
+ // Close the panel on esc key press when the form has focus.
+ formView.keystrokes.set( 'Esc', ( data, cancel ) => {
+ this._hidePanel( true );
+ cancel();
+ } );
+
+ return formView;
}
/**
* Creates a toolbar link button. Clicking this button will show
- * {@link #balloonPanelView} attached to the selection.
+ * {@link #_balloon} attached to the selection.
*
* @private
*/
@@ -82,8 +122,8 @@ export default class Link extends Plugin {
const linkCommand = editor.commands.get( 'link' );
const t = editor.t;
- // Handle `Ctrl+K` keystroke and show panel.
- editor.keystrokes.set( 'CTRL+K', () => this._showPanel() );
+ // Handle `Ctrl+K` keystroke and show the panel.
+ editor.keystrokes.set( 'CTRL+K', () => this._showPanel( true ) );
editor.ui.componentFactory.add( 'link', ( locale ) => {
const button = new ButtonView( locale );
@@ -98,7 +138,7 @@ export default class Link extends Plugin {
button.bind( 'isEnabled' ).to( linkCommand, 'isEnabled' );
// Show the panel on button click.
- this.listenTo( button, 'execute', () => this._showPanel() );
+ this.listenTo( button, 'execute', () => this._showPanel( true ) );
return button;
} );
@@ -134,22 +174,13 @@ export default class Link extends Plugin {
}
/**
- * Creates the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance.
+ * Attaches actions which control whether the balloon panel containing the
+ * {@link #formView} is visible or not.
*
* @private
- * @returns {module:ui/panel/balloon/balloonpanelview~BalloonPanelView} Link balloon panel instance.
*/
- _createBalloonPanel() {
- const editor = this.editor;
- const viewDocument = editor.editing.view;
-
- // Create the balloon panel instance.
- const balloonPanelView = new BalloonPanelView( editor.locale );
- balloonPanelView.maxWidth = 300;
-
- // Add balloonPanel.view#element to FocusTracker.
- // @TODO: Do it automatically ckeditor5-core#23
- editor.ui.focusTracker.add( balloonPanelView.element );
+ _attachActions() {
+ const viewDocument = this.editor.editing.view;
// Handle click on view document and show panel when selection is placed inside the link element.
// Keep panel open until selection will be inside the same link element.
@@ -157,123 +188,87 @@ export default class Link extends Plugin {
const viewSelection = viewDocument.selection;
const parentLink = getPositionParentLink( viewSelection.getFirstPosition() );
+ // When collapsed selection is inside link element (link element is clicked).
if ( viewSelection.isCollapsed && parentLink ) {
- this._attachPanelToElement();
+ // Then show panel but keep focus inside editor editable.
+ this._showPanel();
+ // Avoid duplication of the same listener.
+ this.stopListening( viewDocument, 'render' );
+
+ // Start listen to view document changes and close the panel when selection will be moved
+ // out of the actual link element.
this.listenTo( viewDocument, 'render', () => {
const currentParentLink = getPositionParentLink( viewSelection.getFirstPosition() );
if ( !viewSelection.isCollapsed || parentLink !== currentParentLink ) {
this._hidePanel();
} else {
- this._attachPanelToElement( parentLink );
+ this._balloon.updatePosition();
}
} );
-
- this.listenTo( balloonPanelView, 'change:isVisible', () => this.stopListening( viewDocument, 'render' ) );
}
} );
- // Focus the form if balloon panel is open and tab key has been pressed.
- editor.keystrokes.set( 'Tab', ( data, cancel ) => {
- if ( balloonPanelView.isVisible && !this.formView.focusTracker.isFocused ) {
+ // Focus the form if the balloon is visible and the Tab key has been pressed.
+ this.editor.keystrokes.set( 'Tab', ( data, cancel ) => {
+ if ( this._balloon.visibleView === this.formView && !this.formView.focusTracker.isFocused ) {
this.formView.focus();
cancel();
}
} );
- // Close the panel on esc key press when editable has focus.
- editor.keystrokes.set( 'Esc', ( data, cancel ) => {
- if ( balloonPanelView.isVisible ) {
- this._hidePanel( true );
+ // Close the panel on the Esc key press when the editable has focus and the balloon is visible.
+ this.editor.keystrokes.set( 'Esc', ( data, cancel ) => {
+ if ( this._balloon.visibleView === this.formView ) {
+ this._hidePanel();
cancel();
}
} );
// Close on click outside of balloon panel element.
clickOutsideHandler( {
- emitter: balloonPanelView,
- activator: () => balloonPanelView.isVisible,
- contextElement: balloonPanelView.element,
+ emitter: this.formView,
+ activator: () => this._balloon.hasView( this.formView ),
+ contextElement: this._balloon.view.element,
callback: () => this._hidePanel()
} );
-
- editor.ui.view.body.add( balloonPanelView );
-
- return balloonPanelView;
}
/**
- * Creates the {@link module:link/ui/linkformview~LinkFormView} instance.
+ * Adds the {@link #formView} to the {@link #_balloon}.
*
* @private
- * @returns {module:link/ui/linkformview~LinkFormView} Link form instance.
+ * @param {Boolean} [focusInput=false] When `true`, link form will be focused on panel show.
*/
- _createForm() {
- const editor = this.editor;
- const formView = new LinkFormView( editor.locale );
-
- formView.urlInputView.bind( 'value' ).to( editor.commands.get( 'link' ), 'value' );
-
- // Execute link command after clicking on formView `Save` button.
- this.listenTo( formView, 'submit', () => {
- editor.execute( 'link', formView.urlInputView.inputView.element.value );
- this._hidePanel( true );
- } );
-
- // Execute unlink command after clicking on formView `Unlink` button.
- this.listenTo( formView, 'unlink', () => {
- editor.execute( 'unlink' );
- this._hidePanel( true );
- } );
+ _showPanel( focusInput ) {
+ if ( this._balloon.hasView( this.formView ) ) {
+ return;
+ }
- // Close the panel on esc key press when the form has focus.
- formView.keystrokes.set( 'Esc', ( data, cancel ) => {
- this._hidePanel( true );
- cancel();
+ this._balloon.add( {
+ view: this.formView,
+ position: this._getBalloonPositionData()
} );
- // Hide balloon panel after clicking on formView `Cancel` button.
- this.listenTo( formView, 'cancel', () => this._hidePanel( true ) );
-
- this.balloonPanelView.content.add( formView );
-
- return formView;
- }
-
- /**
- * Shows {@link #balloonPanelView link balloon panel} and attach to target element.
- * If selection is collapsed and is placed inside link element, then panel will be attached
- * to whole link element, otherwise will be attached to the selection.
- *
- * @private
- * @param {module:link/linkelement~LinkElement} [parentLink] Target element.
- */
- _attachPanelToElement( parentLink ) {
- const viewDocument = this.editor.editing.view;
- const targetLink = parentLink || getPositionParentLink( viewDocument.selection.getFirstPosition() );
-
- const target = targetLink ?
- // When selection is inside link element, then attach panel to this element.
- viewDocument.domConverter.getCorrespondingDomElement( targetLink )
- :
- // Otherwise attach panel to the selection.
- viewDocument.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() );
-
- this.balloonPanelView.attachTo( {
- target,
- limiter: viewDocument.domConverter.getCorrespondingDomElement( viewDocument.selection.editableElement )
- } );
+ if ( focusInput ) {
+ this.formView.urlInputView.select();
+ }
}
/**
- * Hides {@link #balloonPanelView balloon panel view}.
+ * Removes the {@link #formView} from the {@link #_balloon}.
*
* @private
- * @param {Boolean} [focusEditable=false] When `true` then editable focus will be restored on panel hide.
+ * @param {Boolean} [focusEditable=false] When `true`, editable focus will be restored on panel hide.
*/
_hidePanel( focusEditable ) {
- this.balloonPanelView.hide();
+ if ( !this._balloon.hasView( this.formView ) ) {
+ return;
+ }
+
+ this._balloon.remove( this.formView );
+ this.stopListening( this.editor.editing.view, 'render' );
if ( focusEditable ) {
this.editor.editing.view.focus();
@@ -281,13 +276,30 @@ export default class Link extends Plugin {
}
/**
- * Shows {@link #balloonPanelView balloon panel view}.
+ * Returns positioning options for the {@link #_balloon}. They control the way balloon is attached
+ * to the target element or selection.
+ *
+ * If the selection is collapsed and inside a link element, then the panel will be attached to the
+ * entire link element. Otherwise, it will be attached to the selection.
*
* @private
+ * @returns {module:utils/dom/position~Options}
*/
- _showPanel() {
- this._attachPanelToElement();
- this.formView.urlInputView.select();
+ _getBalloonPositionData() {
+ const viewDocument = this.editor.editing.view;
+ const targetLink = getPositionParentLink( viewDocument.selection.getFirstPosition() );
+
+ const target = targetLink ?
+ // When selection is inside link element, then attach panel to this element.
+ viewDocument.domConverter.getCorrespondingDomElement( targetLink )
+ :
+ // Otherwise attach panel to the selection.
+ viewDocument.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() );
+
+ return {
+ target,
+ limiter: viewDocument.domConverter.getCorrespondingDomElement( viewDocument.selection.editableElement )
+ };
}
}
diff --git a/tests/link.js b/tests/link.js
index cdb2eb0..c3d4e78 100644
--- a/tests/link.js
+++ b/tests/link.js
@@ -12,8 +12,8 @@ import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-util
import Link from '../src/link';
import LinkEngine from '../src/linkengine';
+import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/contextualballoon';
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
-import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
import Range from '@ckeditor/ckeditor5-engine/src/view/range';
import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
@@ -21,7 +21,7 @@ import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobs
testUtils.createSinonSandbox();
describe( 'Link', () => {
- let editor, linkFeature, linkButton, unlinkButton, balloonPanelView, formView, editorElement;
+ let editor, linkFeature, linkButton, unlinkButton, balloon, formView, editorElement;
beforeEach( () => {
editorElement = document.createElement( 'div' );
@@ -38,8 +38,11 @@ describe( 'Link', () => {
linkFeature = editor.plugins.get( Link );
linkButton = editor.ui.componentFactory.create( 'link' );
unlinkButton = editor.ui.componentFactory.create( 'unlink' );
- balloonPanelView = linkFeature.balloonPanelView;
+ balloon = editor.plugins.get( ContextualBalloon );
formView = linkFeature.formView;
+
+ // There is no point to execute `BalloonPanelView#attachTo` so override it.
+ testUtils.sinon.stub( balloon.view, 'attachTo', () => {} );
} );
} );
@@ -55,6 +58,10 @@ describe( 'Link', () => {
expect( editor.plugins.get( LinkEngine ) ).to.instanceOf( LinkEngine );
} );
+ it( 'should load ContextualBalloon', () => {
+ expect( editor.plugins.get( ContextualBalloon ) ).to.instanceOf( ContextualBalloon );
+ } );
+
it( 'should register click observer', () => {
expect( editor.editing.view.getObserver( ClickObserver ) ).to.instanceOf( ClickObserver );
} );
@@ -74,15 +81,15 @@ describe( 'Link', () => {
expect( linkButton.isEnabled ).to.be.false;
} );
- it( 'should open panel on linkButtonView execute event', () => {
+ it( 'should add link form to the ContextualBalloon on execute event', () => {
linkButton.fire( 'execute' );
- expect( linkFeature.balloonPanelView.isVisible ).to.true;
+ expect( balloon.visibleView ).to.equal( formView );
} );
- it( 'should open panel attached to the link element, when collapsed selection is inside link element', () => {
- const attachToSpy = testUtils.sinon.spy( balloonPanelView, 'attachTo' );
-
+ it( 'should add link form to the ContextualBalloon and attach balloon to the link element ' +
+ 'when collapsed selection is inside link element',
+ () => {
editor.document.schema.allow( { name: '$text', inside: '$root' } );
setModelData( editor.document, '<$text linkHref="url">some[] url$text>' );
editor.editing.view.isFocused = true;
@@ -91,15 +98,13 @@ describe( 'Link', () => {
const linkElement = editorElement.querySelector( 'a' );
- sinon.assert.calledWithExactly( attachToSpy, sinon.match( {
+ sinon.assert.calledWithExactly( balloon.view.attachTo, sinon.match( {
target: linkElement,
limiter: editorElement
} ) );
} );
- it( 'should open panel attached to the selection, when there is non-collapsed selection', () => {
- const attachToSpy = testUtils.sinon.spy( balloonPanelView, 'attachTo' );
-
+ it( 'should add link form to the ContextualBalloon and attach balloon to the selection, when selection is non-collapsed', () => {
editor.document.schema.allow( { name: '$text', inside: '$root' } );
setModelData( editor.document, 'so[me ur]l' );
editor.editing.view.isFocused = true;
@@ -108,13 +113,13 @@ describe( 'Link', () => {
const selectedRange = editorElement.ownerDocument.getSelection().getRangeAt( 0 );
- sinon.assert.calledWithExactly( attachToSpy, sinon.match( {
+ sinon.assert.calledWithExactly( balloon.view.attachTo, sinon.match( {
target: selectedRange,
limiter: editorElement
} ) );
} );
- it( 'should select panel input value when panel is opened', () => {
+ it( 'should select link input value when link balloon is opened', () => {
const selectUrlInputSpy = testUtils.sinon.spy( linkFeature.formView.urlInputView, 'select' );
editor.editing.view.isFocused = true;
@@ -150,37 +155,34 @@ describe( 'Link', () => {
} );
} );
- describe( 'link balloon panel', () => {
- let hidePanelSpy, focusEditableSpy;
+ describe( 'ContextualBalloon', () => {
+ let focusEditableSpy;
beforeEach( () => {
- hidePanelSpy = testUtils.sinon.spy( balloonPanelView, 'hide' );
focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
} );
- it( 'should be created', () => {
- expect( balloonPanelView ).to.instanceOf( BalloonPanelView );
+ it( 'should not be added to ContextualBalloon at default', () => {
+ expect( balloon.visibleView ).to.null;
} );
- it( 'should be appended to the document body', () => {
- expect( document.body.contains( balloonPanelView.element ) );
- } );
-
- it( 'should open with selected url input on `CTRL+K` keystroke', () => {
- const selectUrlInputSpy = testUtils.sinon.spy( linkFeature.formView.urlInputView, 'select' );
+ it( 'should be added to ContextualBalloon and form should be selected on `CTRL+K` keystroke', () => {
+ const selectUrlInputSpy = testUtils.sinon.spy( formView.urlInputView, 'select' );
editor.keystrokes.press( { keyCode: keyCodes.k, ctrlKey: true } );
- expect( balloonPanelView.isVisible ).to.true;
+ expect( balloon.visibleView ).to.equal( formView );
expect( selectUrlInputSpy.calledOnce ).to.true;
} );
- it( 'should add balloon panel element to focus tracker', () => {
- editor.ui.focusTracker.isFocused = false;
-
- balloonPanelView.element.dispatchEvent( new Event( 'focus' ) );
+ it( 'should not add panel to ContextualBalloon more than once', () => {
+ // Add panel to balloon by pressing toolbar button.
+ linkButton.fire( 'execute' );
- expect( editor.ui.focusTracker.isFocused ).to.true;
+ // Press button once again.
+ expect( () => {
+ linkButton.fire( 'execute' );
+ } ).to.not.throw();
} );
it( 'should focus the link form on Tab key press', () => {
@@ -190,8 +192,7 @@ describe( 'Link', () => {
stopPropagation: sinon.spy()
};
- // Mock balloon invisible, form not focused.
- balloonPanelView.isVisible = false;
+ // Balloon is invisible, form not focused.
formView.focusTracker.isFocused = false;
const spy = sinon.spy( formView, 'focus' );
@@ -201,8 +202,8 @@ describe( 'Link', () => {
sinon.assert.notCalled( keyEvtData.stopPropagation );
sinon.assert.notCalled( spy );
- // Mock balloon visible, form focused.
- balloonPanelView.isVisible = true;
+ // Balloon is visible, form focused.
+ balloon.add( { view: formView } );
formView.focusTracker.isFocused = true;
editor.keystrokes.press( keyEvtData );
@@ -210,8 +211,7 @@ describe( 'Link', () => {
sinon.assert.notCalled( keyEvtData.stopPropagation );
sinon.assert.notCalled( spy );
- // Mock balloon visible, form not focused.
- balloonPanelView.isVisible = true;
+ // Balloon is still visible, form not focused.
formView.focusTracker.isFocused = false;
editor.keystrokes.press( keyEvtData );
@@ -222,63 +222,80 @@ describe( 'Link', () => {
describe( 'close listeners', () => {
describe( 'keyboard', () => {
- it( 'should close after Esc key press (from editor)', () => {
+ it( 'should close after Esc key press (from editor) and not focus editable', () => {
const keyEvtData = {
keyCode: keyCodes.esc,
preventDefault: sinon.spy(),
stopPropagation: sinon.spy()
};
- balloonPanelView.isVisible = false;
+ // Balloon is visible.
+ balloon.add( { view: formView } );
editor.keystrokes.press( keyEvtData );
- sinon.assert.notCalled( hidePanelSpy );
+ expect( balloon.visibleView ).to.null;
sinon.assert.notCalled( focusEditableSpy );
+ } );
- balloonPanelView.isVisible = true;
+ it( 'should not close after Esc key press (from editor) when panel is in stack but not visible', () => {
+ const keyEvtData = {
+ keyCode: keyCodes.esc,
+ preventDefault: () => {},
+ stopPropagation: () => {}
+ };
+
+ const viewMock = {
+ destroy: () => {}
+ };
+
+ balloon.add( { view: formView } );
+ balloon.add( { view: viewMock } );
editor.keystrokes.press( keyEvtData );
- sinon.assert.calledOnce( hidePanelSpy );
- sinon.assert.calledOnce( focusEditableSpy );
+ expect( balloon.visibleView ).to.equal( viewMock );
+ expect( balloon.hasView( formView ) ).to.true;
+ sinon.assert.notCalled( focusEditableSpy );
} );
- it( 'should close after Esc key press (from the form)', () => {
+ it( 'should close after Esc key press (from the form) and focus editable', () => {
const keyEvtData = {
keyCode: keyCodes.esc,
preventDefault: sinon.spy(),
stopPropagation: sinon.spy()
};
+ balloon.add( { view: formView } );
+
formView.keystrokes.press( keyEvtData );
- sinon.assert.calledOnce( hidePanelSpy );
+ expect( balloon.visibleView ).to.null;
sinon.assert.calledOnce( focusEditableSpy );
} );
} );
describe( 'mouse', () => {
it( 'should close and not focus editable on click outside the panel', () => {
- balloonPanelView.isVisible = true;
+ balloon.add( { view: formView } );
document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
- expect( hidePanelSpy.calledOnce ).to.true;
+ expect( balloon.visibleView ).to.null;
expect( focusEditableSpy.notCalled ).to.true;
} );
it( 'should not close on click inside the panel', () => {
- balloonPanelView.isVisible = true;
- balloonPanelView.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
+ balloon.add( { view: formView } );
+ balloon.view.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) );
- expect( hidePanelSpy.notCalled ).to.true;
+ expect( balloon.visibleView ).to.equal( formView );
} );
} );
} );
describe( 'click on editable', () => {
it( 'should open with not selected url input when collapsed selection is inside link element', () => {
- const selectUrlInputSpy = testUtils.sinon.spy( linkFeature.formView.urlInputView, 'select' );
+ const selectUrlInputSpy = testUtils.sinon.spy( formView.urlInputView, 'select' );
const observer = editor.editing.view.getObserver( ClickObserver );
editor.document.schema.allow( { name: '$text', inside: '$root' } );
@@ -286,7 +303,7 @@ describe( 'Link', () => {
observer.fire( 'click', { target: document.body } );
- expect( balloonPanelView.isVisible ).to.true;
+ expect( balloon.visibleView ).to.equal( formView );
expect( selectUrlInputSpy.notCalled ).to.true;
} );
@@ -301,15 +318,43 @@ describe( 'Link', () => {
observer.fire( 'click', { target: document.body } );
- expect( balloonPanelView.isVisible ).to.true;
+ expect( balloon.visibleView ).to.equal( formView );
+
+ // Reset attachTo call counter.
+ balloon.view.attachTo.reset();
+
+ // Move selection.
+ editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 1 ) ], true );
+ editor.editing.view.render();
+
+ // Check if balloon is still open and position was updated.
+ expect( balloon.visibleView ).to.equal( formView );
+ expect( balloon.view.attachTo.calledOnce ).to.true;
+ } );
+
+ it( 'should not duplicate `render` listener on `ViewDocument`', () => {
+ const observer = editor.editing.view.getObserver( ClickObserver );
+ const updatePositionSpy = testUtils.sinon.spy( balloon, 'updatePosition' );
+
+ editor.document.schema.allow( { name: '$text', inside: '$root' } );
+ setModelData( editor.document, '<$text linkHref="url">b[]ar$text>' );
+
+ // Click at the same link more than once.
+ observer.fire( 'click', { target: document.body } );
+ observer.fire( 'click', { target: document.body } );
+ observer.fire( 'click', { target: document.body } );
+
+ sinon.assert.notCalled( updatePositionSpy );
- const attachToSpy = testUtils.sinon.spy( balloonPanelView, 'attachTo' );
+ const root = editor.editing.view.getRoot();
+ const text = root.getChild( 0 ).getChild( 0 );
+ // Move selection.
editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 1 ) ], true );
editor.editing.view.render();
- expect( balloonPanelView.isVisible ).to.true;
- expect( attachToSpy.calledOnce ).to.true;
+ // Position should be updated only once.
+ sinon.assert.calledOnce( updatePositionSpy );
} );
it( 'should close when selection goes outside the link element', () => {
@@ -323,12 +368,12 @@ describe( 'Link', () => {
observer.fire( 'click', { target: document.body } );
- expect( balloonPanelView.isVisible ).to.true;
+ expect( balloon.visibleView ).to.equal( formView );
editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 3, text, 3 ) ], true );
editor.editing.view.render();
- expect( balloonPanelView.isVisible ).to.false;
+ expect( balloon.visibleView ).to.null;
} );
it( 'should close when selection goes to the other link element with the same href', () => {
@@ -342,12 +387,12 @@ describe( 'Link', () => {
observer.fire( 'click', { target: document.body } );
- expect( balloonPanelView.isVisible ).to.true;
+ expect( balloon.visibleView ).to.equal( formView );
editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 1 ) ], true );
editor.editing.view.render();
- expect( balloonPanelView.isVisible ).to.false;
+ expect( balloon.visibleView ).to.null;
} );
it( 'should close when selection becomes non-collapsed', () => {
@@ -361,12 +406,12 @@ describe( 'Link', () => {
observer.fire( 'click', { target: {} } );
- expect( balloonPanelView.isVisible ).to.true;
+ expect( balloon.visibleView ).to.equal( formView );
editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 2 ) ] );
editor.editing.view.render();
- expect( balloonPanelView.isVisible ).to.false;
+ expect( balloon.visibleView ).to.null;
} );
it( 'should stop updating position after close', () => {
@@ -380,16 +425,19 @@ describe( 'Link', () => {
observer.fire( 'click', { target: {} } );
- expect( balloonPanelView.isVisible ).to.true;
+ expect( balloon.visibleView ).to.equal( formView );
- balloonPanelView.isVisible = false;
+ // Close balloon by dispatching `cancel` event on formView.
+ formView.fire( 'cancel' );
- const attachToSpy = testUtils.sinon.spy( balloonPanelView, 'attachTo' );
+ // Reset attachTo call counter.
+ balloon.view.attachTo.reset();
+ // Move selection inside link element.
editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 2, text, 2 ) ], true );
editor.editing.view.render();
- expect( attachToSpy.notCalled ).to.true;
+ expect( balloon.view.attachTo.notCalled ).to.true;
} );
it( 'should not open when selection is not inside link element', () => {
@@ -399,7 +447,7 @@ describe( 'Link', () => {
observer.fire( 'click', { target: {} } );
- expect( balloonPanelView.isVisible ).to.false;
+ expect( balloon.visibleView ).to.null;
} );
it( 'should not open when selection is non-collapsed', () => {
@@ -410,16 +458,15 @@ describe( 'Link', () => {
observer.fire( 'click', { target: document.body } );
- expect( balloonPanelView.isVisible ).to.false;
+ expect( balloon.visibleView ).to.null;
} );
} );
} );
describe( 'link form', () => {
- let hidePanelSpy, focusEditableSpy;
+ let focusEditableSpy;
beforeEach( () => {
- hidePanelSpy = testUtils.sinon.spy( balloonPanelView, 'hide' );
focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' );
} );
@@ -447,9 +494,11 @@ describe( 'Link', () => {
} );
it( 'should hide and focus editable on formView#submit event', () => {
+ balloon.add( { view: formView } );
+
formView.fire( 'submit' );
- expect( hidePanelSpy.calledOnce ).to.true;
+ expect( balloon.visibleView ).to.null;
expect( focusEditableSpy.calledOnce ).to.true;
} );
@@ -463,16 +512,20 @@ describe( 'Link', () => {
} );
it( 'should hide and focus editable on formView#unlink event', () => {
+ balloon.add( { view: formView } );
+
formView.fire( 'unlink' );
- expect( hidePanelSpy.calledOnce ).to.true;
+ expect( balloon.visibleView ).to.null;
expect( focusEditableSpy.calledOnce ).to.true;
} );
it( 'should hide and focus editable on formView#cancel event', () => {
+ balloon.add( { view: formView } );
+
formView.fire( 'cancel' );
- expect( hidePanelSpy.calledOnce ).to.true;
+ expect( balloon.visibleView ).to.null;
expect( focusEditableSpy.calledOnce ).to.true;
} );
} );