From dca89762fb6298f3cdef4e8b29a329e31a7ca0d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 29 Mar 2017 16:20:14 +0200 Subject: [PATCH 01/10] Integrated link UI with editor ContextualBalloon. --- src/link.js | 225 +++++++++++++++++++++++++++----------------------- tests/link.js | 174 ++++++++++++++++++++++---------------- 2 files changed, 225 insertions(+), 174 deletions(-) diff --git a/src/link.js b/src/link.js index 6852add..01dfac0 100644 --- a/src/link.js +++ b/src/link.js @@ -15,8 +15,6 @@ import LinkElement from './linkelement'; 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'; @@ -26,6 +24,7 @@ import '../theme/theme.scss'; /** * The link feature. It introduces the Link and Unlink buttons and the Ctrl+K keystroke. + * Link UI is displayed using {@link module:core/editor/editorui~EditorUI#balloon}. * * It uses the {@link module:link/linkengine~LinkEngine link engine feature}. * @@ -50,15 +49,9 @@ export default class Link extends Plugin { * @inheritDoc */ init() { + // Register click observer to handle `click` event on the view document. this.editor.editing.view.addObserver( ClickObserver ); - /** - * Balloon panel view to display the main UI. - * - * @member {module:link/ui/balloonpanel~BalloonPanelView} - */ - this.balloonPanelView = this._createBalloonPanel(); - /** * The form view inside {@link #balloonPanelView}. * @@ -69,6 +62,67 @@ export default class Link extends Plugin { // Create toolbar buttons. this._createToolbarLinkButton(); this._createToolbarUnlinkButton(); + + // Attach lifecycle actions to the link balloon. + this._attachActions(); + } + + /** + * Returns `true` when link panel is added to the {@link module:core/editor/editorui~EditorUI#balloon} stack. + * + * @private + * @returns {Boolean} + */ + get _isInStack() { + return this.editor.ui.balloon.isPanelInStack( this.formView ); + } + + /** + * Returns `true` when link panel is currently visible in {@link module:core/editor/editorui~EditorUI#balloon}. + * + * @private + * @returns {Boolean} + */ + get _isVisible() { + const balloon = this.editor.ui.balloon; + + return balloon.visible && balloon.visible.view === this.formView; + } + + /** + * 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 balloon 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; } /** @@ -83,7 +137,7 @@ export default class Link extends Plugin { const t = editor.t; // Handle `Ctrl+K` keystroke and show panel. - editor.keystrokes.set( 'CTRL+K', () => this._showPanel() ); + editor.keystrokes.set( 'CTRL+K', () => this._showPanel( true ) ); editor.ui.componentFactory.add( 'link', ( locale ) => { const button = new ButtonView( locale ); @@ -98,7 +152,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 +188,13 @@ export default class Link extends Plugin { } /** - * Creates the {@link module:ui/panel/balloon/balloonpanelview~BalloonPanelView} instance. + * Attaches actions which defines when panel should be open or close. * * @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; + const balloon = this.editor.ui.balloon; // 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 +202,84 @@ 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(); + // 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 ); + 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 link panel is visible and tab key has been pressed. + this.editor.keystrokes.set( 'Tab', ( data, cancel ) => { + if ( this._isVisible && !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 on `Esc` press when the editor is focused and link balloon is currently visible. + this.editor.keystrokes.set( 'Esc', ( data, cancel ) => { + if ( this._isVisible ) { + 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._isInStack, + contextElement: balloon.view.element, callback: () => this._hidePanel() } ); - - editor.ui.view.body.add( balloonPanelView ); - - return balloonPanelView; } /** - * Creates the {@link module:link/ui/linkformview~LinkFormView} instance. + * Adds panel to {@link: core/editor/editorui~EditorUI#balloon}. * * @private - * @returns {module:link/ui/linkformview~LinkFormView} Link form instance. + * @param {Boolean} [focusInput=false] When `true` then 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._isInStack ) { + return; + } - // Close the panel on esc key press when the form has focus. - formView.keystrokes.set( 'Esc', ( data, cancel ) => { - this._hidePanel( true ); - cancel(); + this.editor.ui.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 panel from {@link: core/editor/editorui~EditorUI#balloon}. * * @private * @param {Boolean} [focusEditable=false] When `true` then editable focus will be restored on panel hide. */ _hidePanel( focusEditable ) { - this.balloonPanelView.hide(); + if ( !this._isInStack ) { + return; + } + + this.editor.ui.balloon.remove( this.formView ); + this.stopListening( this.editor.editing.view, 'render' ); if ( focusEditable ) { this.editor.editing.view.focus(); @@ -281,13 +287,28 @@ export default class Link extends Plugin { } /** - * Shows {@link #balloonPanelView balloon panel view}. + * Returns position configuration for the balloon panel. According to this data balloon will be attached + * to the 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 + * @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..65fc372 100644 --- a/tests/link.js +++ b/tests/link.js @@ -13,7 +13,6 @@ import { setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-util import Link from '../src/link'; import LinkEngine from '../src/linkengine'; 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 +20,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 +37,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.ui.balloon; formView = linkFeature.formView; + + // There is no point to execute `BalloonPanelView#attachTo` so override it. + testUtils.sinon.stub( balloon.view, 'attachTo', () => {} ); } ); } ); @@ -74,15 +76,13 @@ describe( 'Link', () => { expect( linkButton.isEnabled ).to.be.false; } ); - it( 'should open panel on linkButtonView execute event', () => { + it( 'should add panel to the `ui#balloon` on execute event', () => { linkButton.fire( 'execute' ); - expect( linkFeature.balloonPanelView.isVisible ).to.true; + expect( balloon.visible.view ).to.deep.equal( linkFeature.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 panel to `ui#balloon` attached 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' ); editor.editing.view.isFocused = true; @@ -91,15 +91,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 panel to `ui#balloon` attached to the selection, when there is non-collapsed selection', () => { editor.document.schema.allow( { name: '$text', inside: '$root' } ); setModelData( editor.document, 'so[me ur]l' ); editor.editing.view.isFocused = true; @@ -108,7 +106,7 @@ 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 } ) ); @@ -150,37 +148,34 @@ describe( 'Link', () => { } ); } ); - describe( 'link balloon panel', () => { - let hidePanelSpy, focusEditableSpy; + describe( 'balloon panel', () => { + 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 `balloon#ui` at default', () => { + expect( editor.ui.balloon.visible ).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 `balloon#ui` and focus the link form 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( editor.ui.balloon.visible.view ).to.deep.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 do nothing when panel is being added to `balloon#ui` 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 +185,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 +195,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 +204,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 +215,89 @@ 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.visible ).to.null; sinon.assert.notCalled( focusEditableSpy ); + } ); - balloonPanelView.isVisible = true; + it( 'should not close after Esc key press (from editor) when panel is not visible', () => { + const keyEvtData = { + keyCode: keyCodes.esc, + preventDefault: sinon.spy(), + stopPropagation: sinon.spy() + }; + + const viewMock = { + destroy: () => {} + }; + + // Balloon is visible. + balloon.add( { view: formView } ); + + // Balloon is not visible because other panel is added to the balloon stack. + balloon.add( { view: viewMock } ); editor.keystrokes.press( keyEvtData ); - sinon.assert.calledOnce( hidePanelSpy ); - sinon.assert.calledOnce( focusEditableSpy ); + // Balloon is visible. + expect( balloon.visible.view ).to.equal( viewMock ); + + // Link panel is in balloon stack. + expect( balloon.isPanelInStack( formView ) ).to.true; + + // Editable was not focused. + 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 is panel. + balloon.add( { view: formView } ); + formView.keystrokes.press( keyEvtData ); - sinon.assert.calledOnce( hidePanelSpy ); + expect( balloon.visible ).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.visible ).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.visible.view ).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 +305,7 @@ describe( 'Link', () => { observer.fire( 'click', { target: document.body } ); - expect( balloonPanelView.isVisible ).to.true; + expect( balloon.visible.view === formView ).to.true; expect( selectUrlInputSpy.notCalled ).to.true; } ); @@ -301,15 +320,18 @@ describe( 'Link', () => { observer.fire( 'click', { target: document.body } ); - expect( balloonPanelView.isVisible ).to.true; + expect( balloon.visible.view === formView ).to.true; - const attachToSpy = testUtils.sinon.spy( balloonPanelView, 'attachTo' ); + // 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(); - expect( balloonPanelView.isVisible ).to.true; - expect( attachToSpy.calledOnce ).to.true; + // Check if balloon is still open and position was updated. + expect( balloon.visible.view === formView ).to.true; + expect( balloon.view.attachTo.calledOnce ).to.true; } ); it( 'should close when selection goes outside the link element', () => { @@ -323,12 +345,12 @@ describe( 'Link', () => { observer.fire( 'click', { target: document.body } ); - expect( balloonPanelView.isVisible ).to.true; + expect( balloon.visible.view === formView ).to.true; editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 3, text, 3 ) ], true ); editor.editing.view.render(); - expect( balloonPanelView.isVisible ).to.false; + expect( balloon.visible ).to.null; } ); it( 'should close when selection goes to the other link element with the same href', () => { @@ -342,12 +364,12 @@ describe( 'Link', () => { observer.fire( 'click', { target: document.body } ); - expect( balloonPanelView.isVisible ).to.true; + expect( balloon.visible.view === formView ).to.true; editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 1 ) ], true ); editor.editing.view.render(); - expect( balloonPanelView.isVisible ).to.false; + expect( balloon.visible ).to.null; } ); it( 'should close when selection becomes non-collapsed', () => { @@ -361,12 +383,12 @@ describe( 'Link', () => { observer.fire( 'click', { target: {} } ); - expect( balloonPanelView.isVisible ).to.true; + expect( balloon.visible.view === formView ).to.true; editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 2 ) ] ); editor.editing.view.render(); - expect( balloonPanelView.isVisible ).to.false; + expect( balloon.visible ).to.null; } ); it( 'should stop updating position after close', () => { @@ -380,16 +402,19 @@ describe( 'Link', () => { observer.fire( 'click', { target: {} } ); - expect( balloonPanelView.isVisible ).to.true; + expect( balloon.visible.view === formView ).to.true; - 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 +424,7 @@ describe( 'Link', () => { observer.fire( 'click', { target: {} } ); - expect( balloonPanelView.isVisible ).to.false; + expect( balloon.visible ).to.null; } ); it( 'should not open when selection is non-collapsed', () => { @@ -410,16 +435,15 @@ describe( 'Link', () => { observer.fire( 'click', { target: document.body } ); - expect( balloonPanelView.isVisible ).to.false; + expect( balloon.visible ).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 +471,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.visible ).to.null; expect( focusEditableSpy.calledOnce ).to.true; } ); @@ -463,16 +489,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.visible ).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.visible ).to.null; expect( focusEditableSpy.calledOnce ).to.true; } ); } ); From 39e713dda86c100e35779c94da8d6972b4a1b2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 30 Mar 2017 09:34:31 +0200 Subject: [PATCH 02/10] Docs. --- src/link.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/link.js b/src/link.js index 01dfac0..8b18b12 100644 --- a/src/link.js +++ b/src/link.js @@ -49,7 +49,6 @@ export default class Link extends Plugin { * @inheritDoc */ init() { - // Register click observer to handle `click` event on the view document. this.editor.editing.view.addObserver( ClickObserver ); /** @@ -229,7 +228,7 @@ export default class Link extends Plugin { } } ); - // Close on `Esc` press when the editor is focused and link balloon is currently visible. + // Close the panel on esc key press when editable has focus and link balloon is currently visible. this.editor.keystrokes.set( 'Esc', ( data, cancel ) => { if ( this._isVisible ) { this._hidePanel(); From 8bbae8c06b59ef1357c88ff302a8f057974ead24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Thu, 30 Mar 2017 09:42:51 +0200 Subject: [PATCH 03/10] Improved tests. --- tests/link.js | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/tests/link.js b/tests/link.js index 65fc372..93019e9 100644 --- a/tests/link.js +++ b/tests/link.js @@ -79,7 +79,7 @@ describe( 'Link', () => { it( 'should add panel to the `ui#balloon` on execute event', () => { linkButton.fire( 'execute' ); - expect( balloon.visible.view ).to.deep.equal( linkFeature.formView ); + expect( balloon.visible.view ).to.equal( linkFeature.formView ); } ); it( 'should add panel to `ui#balloon` attached to the link element, when collapsed selection is inside link element', () => { @@ -159,16 +159,16 @@ describe( 'Link', () => { expect( editor.ui.balloon.visible ).to.null; } ); - it( 'should be added to `balloon#ui` and focus the link form on `CTRL+K` keystroke', () => { + it( 'should be added to `balloon#ui` 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( editor.ui.balloon.visible.view ).to.deep.equal( formView ); + expect( editor.ui.balloon.visible.view ).to.equal( formView ); expect( selectUrlInputSpy.calledOnce ).to.true; } ); - it( 'should do nothing when panel is being added to `balloon#ui` more than once', () => { + it( 'should do not add panel to `balloon#ui` more than once', () => { // Add panel to balloon by pressing toolbar button. linkButton.fire( 'execute' ); @@ -231,32 +231,24 @@ describe( 'Link', () => { sinon.assert.notCalled( focusEditableSpy ); } ); - it( 'should not close after Esc key press (from editor) when panel is not visible', () => { + it( 'should not close after Esc key press (from editor) when panel is in stack but not visible', () => { const keyEvtData = { keyCode: keyCodes.esc, - preventDefault: sinon.spy(), - stopPropagation: sinon.spy() + preventDefault: () => {}, + stopPropagation: () => {} }; const viewMock = { destroy: () => {} }; - // Balloon is visible. balloon.add( { view: formView } ); - - // Balloon is not visible because other panel is added to the balloon stack. balloon.add( { view: viewMock } ); editor.keystrokes.press( keyEvtData ); - // Balloon is visible. expect( balloon.visible.view ).to.equal( viewMock ); - - // Link panel is in balloon stack. expect( balloon.isPanelInStack( formView ) ).to.true; - - // Editable was not focused. sinon.assert.notCalled( focusEditableSpy ); } ); @@ -267,7 +259,6 @@ describe( 'Link', () => { stopPropagation: sinon.spy() }; - // Balloon is panel. balloon.add( { view: formView } ); formView.keystrokes.press( keyEvtData ); From 9597968167de0772ae6da584f7a26389393e385f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Mon, 3 Apr 2017 08:40:00 +0200 Subject: [PATCH 04/10] Reduced attached render events. --- src/link.js | 3 +++ tests/link.js | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/link.js b/src/link.js index 8b18b12..de8e652 100644 --- a/src/link.js +++ b/src/link.js @@ -206,6 +206,9 @@ export default class Link extends Plugin { // 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', () => { diff --git a/tests/link.js b/tests/link.js index 93019e9..b92db44 100644 --- a/tests/link.js +++ b/tests/link.js @@ -325,6 +325,31 @@ describe( 'Link', () => { 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' ); + + // 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 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(); + + // Position should be updated only once. + sinon.assert.calledOnce( updatePositionSpy ); + } ); + it( 'should close when selection goes outside the link element', () => { const observer = editor.editing.view.getObserver( ClickObserver ); From dad85bb4857d3682fbab7c9199a0f43b3f162eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 5 Apr 2017 13:29:11 +0200 Subject: [PATCH 05/10] Aligned code to the latest ContextualBalloon API. --- src/link.js | 44 +++++++++++++++++++++++++------------------- tests/link.js | 31 +++++++++++++++++++------------ 2 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/link.js b/src/link.js index de8e652..3b2758e 100644 --- a/src/link.js +++ b/src/link.js @@ -11,6 +11,7 @@ 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'; @@ -23,10 +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. - * Link UI is displayed using {@link module:core/editor/editorui~EditorUI#balloon}. + * 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 + * {@link module:ui/contextualballoon~ContextualBalloon contextual balloon plugin}. * * @extends module:core/plugin~Plugin */ @@ -35,7 +36,7 @@ export default class Link extends Plugin { * @inheritDoc */ static get requires() { - return [ LinkEngine ]; + return [ LinkEngine, ContextualBalloon ]; } /** @@ -58,6 +59,14 @@ export default class Link extends Plugin { */ this.formView = this._createForm(); + /** + * Contextual balloon plugin instance. + * + * @private + * @member {module:ui/contextualballoon~ContextualBalloon} + */ + this._balloon = this.editor.plugins.get( ContextualBalloon ); + // Create toolbar buttons. this._createToolbarLinkButton(); this._createToolbarUnlinkButton(); @@ -67,25 +76,23 @@ export default class Link extends Plugin { } /** - * Returns `true` when link panel is added to the {@link module:core/editor/editorui~EditorUI#balloon} stack. + * Returns `true` when link view is added to the {@link module:ui/contextualballoon~ContextualBalloon}. * * @private * @returns {Boolean} */ get _isInStack() { - return this.editor.ui.balloon.isPanelInStack( this.formView ); + return this._balloon.isViewInStack( this.formView ); } /** - * Returns `true` when link panel is currently visible in {@link module:core/editor/editorui~EditorUI#balloon}. + * Returns `true` when link view is currently visible in {@link module:ui/contextualballoon~ContextualBalloon}. * * @private * @returns {Boolean} */ get _isVisible() { - const balloon = this.editor.ui.balloon; - - return balloon.visible && balloon.visible.view === this.formView; + return this._balloon.visible && this._balloon.visible.view === this.formView; } /** @@ -112,7 +119,7 @@ export default class Link extends Plugin { this._hidePanel( true ); } ); - // Hide balloon panel after clicking on formView `Cancel` button. + // 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. @@ -135,7 +142,7 @@ export default class Link extends Plugin { const linkCommand = editor.commands.get( 'link' ); const t = editor.t; - // Handle `Ctrl+K` keystroke and show panel. + // Handle `Ctrl+K` keystroke and show the panel. editor.keystrokes.set( 'CTRL+K', () => this._showPanel( true ) ); editor.ui.componentFactory.add( 'link', ( locale ) => { @@ -193,7 +200,6 @@ export default class Link extends Plugin { */ _attachActions() { const viewDocument = this.editor.editing.view; - const balloon = this.editor.ui.balloon; // 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. @@ -217,7 +223,7 @@ export default class Link extends Plugin { if ( !viewSelection.isCollapsed || parentLink !== currentParentLink ) { this._hidePanel(); } else { - balloon.updatePosition(); + this._balloon.updatePosition(); } } ); } @@ -243,13 +249,13 @@ export default class Link extends Plugin { clickOutsideHandler( { emitter: this.formView, activator: () => this._isInStack, - contextElement: balloon.view.element, + contextElement: this._balloon.view.element, callback: () => this._hidePanel() } ); } /** - * Adds panel to {@link: core/editor/editorui~EditorUI#balloon}. + * Adds link view to {_@link module:ui/contextualballoon~ContextualBalloon}. * * @private * @param {Boolean} [focusInput=false] When `true` then link form will be focused on panel show. @@ -259,7 +265,7 @@ export default class Link extends Plugin { return; } - this.editor.ui.balloon.add( { + this._balloon.add( { view: this.formView, position: this._getBalloonPositionData() } ); @@ -270,7 +276,7 @@ export default class Link extends Plugin { } /** - * Removes panel from {@link: core/editor/editorui~EditorUI#balloon}. + * Removes link view from {_@link module:ui/contextualballoon~ContextualBalloon}. * * @private * @param {Boolean} [focusEditable=false] When `true` then editable focus will be restored on panel hide. @@ -280,7 +286,7 @@ export default class Link extends Plugin { return; } - this.editor.ui.balloon.remove( this.formView ); + this._balloon.remove( this.formView ); this.stopListening( this.editor.editing.view, 'render' ); if ( focusEditable ) { diff --git a/tests/link.js b/tests/link.js index b92db44..e78a618 100644 --- a/tests/link.js +++ b/tests/link.js @@ -12,6 +12,7 @@ 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 Range from '@ckeditor/ckeditor5-engine/src/view/range'; @@ -37,7 +38,7 @@ describe( 'Link', () => { linkFeature = editor.plugins.get( Link ); linkButton = editor.ui.componentFactory.create( 'link' ); unlinkButton = editor.ui.componentFactory.create( 'unlink' ); - balloon = editor.ui.balloon; + balloon = editor.plugins.get( ContextualBalloon ); formView = linkFeature.formView; // There is no point to execute `BalloonPanelView#attachTo` so override it. @@ -57,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 ); } ); @@ -76,13 +81,15 @@ describe( 'Link', () => { expect( linkButton.isEnabled ).to.be.false; } ); - it( 'should add panel to the `ui#balloon` on execute event', () => { + it( 'should add link form to the ContextualBalloon on execute event', () => { linkButton.fire( 'execute' ); expect( balloon.visible.view ).to.equal( linkFeature.formView ); } ); - it( 'should add panel to `ui#balloon` attached to the link element, when collapsed selection is inside link element', () => { + 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' ); editor.editing.view.isFocused = true; @@ -97,7 +104,7 @@ describe( 'Link', () => { } ) ); } ); - it( 'should add panel to `ui#balloon` attached to the selection, when there is non-collapsed selection', () => { + 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; @@ -112,7 +119,7 @@ describe( 'Link', () => { } ) ); } ); - 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; @@ -148,27 +155,27 @@ describe( 'Link', () => { } ); } ); - describe( 'balloon panel', () => { + describe( 'ContextualBalloon', () => { let focusEditableSpy; beforeEach( () => { focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' ); } ); - it( 'should not be added to `balloon#ui` at default', () => { - expect( editor.ui.balloon.visible ).to.null; + it( 'should not be added to ContextualBalloon at default', () => { + expect( balloon.visible ).to.null; } ); - it( 'should be added to `balloon#ui` and form should be selected on `CTRL+K` keystroke', () => { + 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( editor.ui.balloon.visible.view ).to.equal( formView ); + expect( balloon.visible.view ).to.equal( formView ); expect( selectUrlInputSpy.calledOnce ).to.true; } ); - it( 'should do not add panel to `balloon#ui` more than once', () => { + it( 'should not add panel to ContextualBalloon more than once', () => { // Add panel to balloon by pressing toolbar button. linkButton.fire( 'execute' ); @@ -248,7 +255,7 @@ describe( 'Link', () => { editor.keystrokes.press( keyEvtData ); expect( balloon.visible.view ).to.equal( viewMock ); - expect( balloon.isPanelInStack( formView ) ).to.true; + expect( balloon.isViewInStack( formView ) ).to.true; sinon.assert.notCalled( focusEditableSpy ); } ); From 21947602f150edb0c3d147457925854b527abba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 5 Apr 2017 14:36:51 +0200 Subject: [PATCH 06/10] Aligned code to the lastest ContextualBalloon API. --- src/link.js | 2 +- tests/link.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/link.js b/src/link.js index 3b2758e..c88fb9f 100644 --- a/src/link.js +++ b/src/link.js @@ -82,7 +82,7 @@ export default class Link extends Plugin { * @returns {Boolean} */ get _isInStack() { - return this._balloon.isViewInStack( this.formView ); + return this._balloon.hasView( this.formView ); } /** diff --git a/tests/link.js b/tests/link.js index e78a618..7ff494c 100644 --- a/tests/link.js +++ b/tests/link.js @@ -255,7 +255,7 @@ describe( 'Link', () => { editor.keystrokes.press( keyEvtData ); expect( balloon.visible.view ).to.equal( viewMock ); - expect( balloon.isViewInStack( formView ) ).to.true; + expect( balloon.hasView( formView ) ).to.true; sinon.assert.notCalled( focusEditableSpy ); } ); From 06e5c0e05633a5a9d5ba1d30edcdfadd6f2e2811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 5 Apr 2017 14:49:35 +0200 Subject: [PATCH 07/10] Improved getter name. --- src/link.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/link.js b/src/link.js index c88fb9f..698d788 100644 --- a/src/link.js +++ b/src/link.js @@ -81,7 +81,7 @@ export default class Link extends Plugin { * @private * @returns {Boolean} */ - get _isInStack() { + get _isInBalloon() { return this._balloon.hasView( this.formView ); } @@ -248,7 +248,7 @@ export default class Link extends Plugin { // Close on click outside of balloon panel element. clickOutsideHandler( { emitter: this.formView, - activator: () => this._isInStack, + activator: () => this._isInBalloon, contextElement: this._balloon.view.element, callback: () => this._hidePanel() } ); @@ -261,7 +261,7 @@ export default class Link extends Plugin { * @param {Boolean} [focusInput=false] When `true` then link form will be focused on panel show. */ _showPanel( focusInput ) { - if ( this._isInStack ) { + if ( this._isInBalloon ) { return; } @@ -282,7 +282,7 @@ export default class Link extends Plugin { * @param {Boolean} [focusEditable=false] When `true` then editable focus will be restored on panel hide. */ _hidePanel( focusEditable ) { - if ( !this._isInStack ) { + if ( !this._isInBalloon ) { return; } From 9d349b3621e3c838388bd22d9a78f29a595f41b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 5 Apr 2017 15:23:09 +0200 Subject: [PATCH 08/10] Aligned to the latest ContextualBalloon changes. --- src/link.js | 16 +++------------- tests/link.js | 46 +++++++++++++++++++++++----------------------- 2 files changed, 26 insertions(+), 36 deletions(-) diff --git a/src/link.js b/src/link.js index 698d788..ee10e02 100644 --- a/src/link.js +++ b/src/link.js @@ -85,16 +85,6 @@ export default class Link extends Plugin { return this._balloon.hasView( this.formView ); } - /** - * Returns `true` when link view is currently visible in {@link module:ui/contextualballoon~ContextualBalloon}. - * - * @private - * @returns {Boolean} - */ - get _isVisible() { - return this._balloon.visible && this._balloon.visible.view === this.formView; - } - /** * Creates the {@link module:link/ui/linkformview~LinkFormView} instance. * @@ -229,9 +219,9 @@ export default class Link extends Plugin { } } ); - // Focus the form if link panel is visible and tab key has been pressed. + // Focus the form if link balloon is currently visible and tab key has been pressed. this.editor.keystrokes.set( 'Tab', ( data, cancel ) => { - if ( this._isVisible && !this.formView.focusTracker.isFocused ) { + if ( this._balloon.visibleView === this.formView && !this.formView.focusTracker.isFocused ) { this.formView.focus(); cancel(); } @@ -239,7 +229,7 @@ export default class Link extends Plugin { // Close the panel on esc key press when editable has focus and link balloon is currently visible. this.editor.keystrokes.set( 'Esc', ( data, cancel ) => { - if ( this._isVisible ) { + if ( this._balloon.visibleView === this.formView ) { this._hidePanel(); cancel(); } diff --git a/tests/link.js b/tests/link.js index 7ff494c..c3d4e78 100644 --- a/tests/link.js +++ b/tests/link.js @@ -84,7 +84,7 @@ describe( 'Link', () => { it( 'should add link form to the ContextualBalloon on execute event', () => { linkButton.fire( 'execute' ); - expect( balloon.visible.view ).to.equal( linkFeature.formView ); + expect( balloon.visibleView ).to.equal( formView ); } ); it( 'should add link form to the ContextualBalloon and attach balloon to the link element ' + @@ -163,7 +163,7 @@ describe( 'Link', () => { } ); it( 'should not be added to ContextualBalloon at default', () => { - expect( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; } ); it( 'should be added to ContextualBalloon and form should be selected on `CTRL+K` keystroke', () => { @@ -171,7 +171,7 @@ describe( 'Link', () => { editor.keystrokes.press( { keyCode: keyCodes.k, ctrlKey: true } ); - expect( balloon.visible.view ).to.equal( formView ); + expect( balloon.visibleView ).to.equal( formView ); expect( selectUrlInputSpy.calledOnce ).to.true; } ); @@ -234,7 +234,7 @@ describe( 'Link', () => { editor.keystrokes.press( keyEvtData ); - expect( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; sinon.assert.notCalled( focusEditableSpy ); } ); @@ -254,7 +254,7 @@ describe( 'Link', () => { editor.keystrokes.press( keyEvtData ); - expect( balloon.visible.view ).to.equal( viewMock ); + expect( balloon.visibleView ).to.equal( viewMock ); expect( balloon.hasView( formView ) ).to.true; sinon.assert.notCalled( focusEditableSpy ); } ); @@ -270,7 +270,7 @@ describe( 'Link', () => { formView.keystrokes.press( keyEvtData ); - expect( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; sinon.assert.calledOnce( focusEditableSpy ); } ); } ); @@ -280,7 +280,7 @@ describe( 'Link', () => { balloon.add( { view: formView } ); document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) ); - expect( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; expect( focusEditableSpy.notCalled ).to.true; } ); @@ -288,7 +288,7 @@ describe( 'Link', () => { balloon.add( { view: formView } ); balloon.view.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) ); - expect( balloon.visible.view ).to.equal( formView ); + expect( balloon.visibleView ).to.equal( formView ); } ); } ); } ); @@ -303,7 +303,7 @@ describe( 'Link', () => { observer.fire( 'click', { target: document.body } ); - expect( balloon.visible.view === formView ).to.true; + expect( balloon.visibleView ).to.equal( formView ); expect( selectUrlInputSpy.notCalled ).to.true; } ); @@ -318,7 +318,7 @@ describe( 'Link', () => { observer.fire( 'click', { target: document.body } ); - expect( balloon.visible.view === formView ).to.true; + expect( balloon.visibleView ).to.equal( formView ); // Reset attachTo call counter. balloon.view.attachTo.reset(); @@ -328,7 +328,7 @@ describe( 'Link', () => { editor.editing.view.render(); // Check if balloon is still open and position was updated. - expect( balloon.visible.view === formView ).to.true; + expect( balloon.visibleView ).to.equal( formView ); expect( balloon.view.attachTo.calledOnce ).to.true; } ); @@ -368,12 +368,12 @@ describe( 'Link', () => { observer.fire( 'click', { target: document.body } ); - expect( balloon.visible.view === formView ).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( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; } ); it( 'should close when selection goes to the other link element with the same href', () => { @@ -387,12 +387,12 @@ describe( 'Link', () => { observer.fire( 'click', { target: document.body } ); - expect( balloon.visible.view === formView ).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( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; } ); it( 'should close when selection becomes non-collapsed', () => { @@ -406,12 +406,12 @@ describe( 'Link', () => { observer.fire( 'click', { target: {} } ); - expect( balloon.visible.view === formView ).to.true; + expect( balloon.visibleView ).to.equal( formView ); editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 2 ) ] ); editor.editing.view.render(); - expect( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; } ); it( 'should stop updating position after close', () => { @@ -425,7 +425,7 @@ describe( 'Link', () => { observer.fire( 'click', { target: {} } ); - expect( balloon.visible.view === formView ).to.true; + expect( balloon.visibleView ).to.equal( formView ); // Close balloon by dispatching `cancel` event on formView. formView.fire( 'cancel' ); @@ -447,7 +447,7 @@ describe( 'Link', () => { observer.fire( 'click', { target: {} } ); - expect( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; } ); it( 'should not open when selection is non-collapsed', () => { @@ -458,7 +458,7 @@ describe( 'Link', () => { observer.fire( 'click', { target: document.body } ); - expect( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; } ); } ); } ); @@ -498,7 +498,7 @@ describe( 'Link', () => { formView.fire( 'submit' ); - expect( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; expect( focusEditableSpy.calledOnce ).to.true; } ); @@ -516,7 +516,7 @@ describe( 'Link', () => { formView.fire( 'unlink' ); - expect( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; expect( focusEditableSpy.calledOnce ).to.true; } ); @@ -525,7 +525,7 @@ describe( 'Link', () => { formView.fire( 'cancel' ); - expect( balloon.visible ).to.null; + expect( balloon.visibleView ).to.null; expect( focusEditableSpy.calledOnce ).to.true; } ); } ); From bea3fc60abb805a155a1171b6f74e36ccef244ee Mon Sep 17 00:00:00 2001 From: Aleksander Nowodzinski Date: Wed, 5 Apr 2017 16:35:33 +0200 Subject: [PATCH 09/10] Docs: Improved docs in the Link plugin. --- src/link.js | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/link.js b/src/link.js index ee10e02..f9da0d0 100644 --- a/src/link.js +++ b/src/link.js @@ -26,7 +26,7 @@ import '../theme/theme.scss'; /** * 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 plugin} and + * 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 @@ -53,14 +53,14 @@ export default class Link extends Plugin { this.editor.editing.view.addObserver( ClickObserver ); /** - * The form view inside {@link #balloonPanelView}. + * The form view displayed inside of the balloon. * * @member {module:link/ui/linkformview~LinkFormView} */ this.formView = this._createForm(); /** - * Contextual balloon plugin instance. + * The contextual balloon plugin instance. * * @private * @member {module:ui/contextualballoon~ContextualBalloon} @@ -71,7 +71,7 @@ export default class Link extends Plugin { this._createToolbarLinkButton(); this._createToolbarUnlinkButton(); - // Attach lifecycle actions to the link balloon. + // Attach lifecycle actions to the the balloon. this._attachActions(); } @@ -123,7 +123,7 @@ export default class Link extends Plugin { /** * Creates a toolbar link button. Clicking this button will show - * {@link #balloonPanelView} attached to the selection. + * {@link #_balloon} attached to the selection. * * @private */ @@ -184,7 +184,8 @@ export default class Link extends Plugin { } /** - * Attaches actions which defines when panel should be open or close. + * Attaches actions which control whether the balloon panel containing the + * {@link #formView} is visible or not. * * @private */ @@ -219,7 +220,7 @@ export default class Link extends Plugin { } } ); - // Focus the form if link balloon is currently visible and tab key has been pressed. + // 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(); @@ -227,7 +228,7 @@ export default class Link extends Plugin { } } ); - // Close the panel on esc key press when editable has focus and link balloon is currently visible. + // 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(); @@ -245,10 +246,10 @@ export default class Link extends Plugin { } /** - * Adds link view to {_@link module:ui/contextualballoon~ContextualBalloon}. + * Adds the {@link #formView} to the {@link #_balloon}. * * @private - * @param {Boolean} [focusInput=false] When `true` then link form will be focused on panel show. + * @param {Boolean} [focusInput=false] When `true`, link form will be focused on panel show. */ _showPanel( focusInput ) { if ( this._isInBalloon ) { @@ -266,10 +267,10 @@ export default class Link extends Plugin { } /** - * Removes link view from {_@link module:ui/contextualballoon~ContextualBalloon}. + * 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 ) { if ( !this._isInBalloon ) { @@ -285,9 +286,11 @@ export default class Link extends Plugin { } /** - * Returns position configuration for the balloon panel. According to this data balloon will be attached - * to the 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. + * 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} From 4163fb7c6ce9247c5b612ea827529829a114f039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oskar=20Wr=C3=B3bel?= Date: Wed, 5 Apr 2017 16:56:25 +0200 Subject: [PATCH 10/10] Dropped _isInBalloon getter. --- src/link.js | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/link.js b/src/link.js index f9da0d0..227a130 100644 --- a/src/link.js +++ b/src/link.js @@ -75,16 +75,6 @@ export default class Link extends Plugin { this._attachActions(); } - /** - * Returns `true` when link view is added to the {@link module:ui/contextualballoon~ContextualBalloon}. - * - * @private - * @returns {Boolean} - */ - get _isInBalloon() { - return this._balloon.hasView( this.formView ); - } - /** * Creates the {@link module:link/ui/linkformview~LinkFormView} instance. * @@ -239,7 +229,7 @@ export default class Link extends Plugin { // Close on click outside of balloon panel element. clickOutsideHandler( { emitter: this.formView, - activator: () => this._isInBalloon, + activator: () => this._balloon.hasView( this.formView ), contextElement: this._balloon.view.element, callback: () => this._hidePanel() } ); @@ -252,7 +242,7 @@ export default class Link extends Plugin { * @param {Boolean} [focusInput=false] When `true`, link form will be focused on panel show. */ _showPanel( focusInput ) { - if ( this._isInBalloon ) { + if ( this._balloon.hasView( this.formView ) ) { return; } @@ -273,7 +263,7 @@ export default class Link extends Plugin { * @param {Boolean} [focusEditable=false] When `true`, editable focus will be restored on panel hide. */ _hidePanel( focusEditable ) { - if ( !this._isInBalloon ) { + if ( !this._balloon.hasView( this.formView ) ) { return; }