@ckeditor/ckeditor5-link
Advanced tools
Comparing version 0.6.0 to 0.7.0
Changelog | ||
========= | ||
## [0.7.0](https://github.com/ckeditor/ckeditor5-link/compare/v0.6.0...v0.6.1) (2017-05-07) | ||
### Bug fixes | ||
* `Esc` key should close the link panel even if none of the `LinkFormView` fields is focused. Closes [#90](https://github.com/ckeditor/ckeditor5-link/issues/90). ([866fa49](https://github.com/ckeditor/ckeditor5-link/commit/866fa49)) | ||
* The link balloon should hide the "Unlink" button when creating a link. Closes [#53](https://github.com/ckeditor/ckeditor5-link/issues/53). ([686e625](https://github.com/ckeditor/ckeditor5-link/commit/686e625)) | ||
* The link balloon should update its position upon external document changes. Closes [#113](https://github.com/ckeditor/ckeditor5-link/issues/113). ([18a5b90](https://github.com/ckeditor/ckeditor5-link/commit/18a5b90)) | ||
* The link plugin should manage focus when the balloon is open. Made Link plugins `_showPanel()` and `_hidePanel()` methods protected. Closes [#95](https://github.com/ckeditor/ckeditor5-link/issues/95). Closes [#94](https://github.com/ckeditor/ckeditor5-link/issues/94). ([5a83b70](https://github.com/ckeditor/ckeditor5-link/commit/5a83b70)) | ||
* Link should not be allowed directly in the root element. Closes [#97](https://github.com/ckeditor/ckeditor5-link/issues/97). ([81d4ba5](https://github.com/ckeditor/ckeditor5-link/commit/81d4ba5)) | ||
### Other changes | ||
* Integrated the link plugin with the `ContextualBalloon` plugin. Closes [#91](https://github.com/ckeditor/ckeditor5-link/issues/91). ([26f148e](https://github.com/ckeditor/ckeditor5-link/commit/26f148e)) | ||
* Updated translations. ([7a35617](https://github.com/ckeditor/ckeditor5-link/commit/7a35617)) | ||
## [0.6.0](https://github.com/ckeditor/ckeditor5-link/compare/v0.5.1...v0.6.0) (2017-04-05) | ||
@@ -5,0 +21,0 @@ |
{ | ||
"name": "@ckeditor/ckeditor5-link", | ||
"version": "0.6.0", | ||
"version": "0.7.0", | ||
"description": "Link feature for CKEditor 5.", | ||
"keywords": [], | ||
"dependencies": { | ||
"@ckeditor/ckeditor5-core": "^0.8.0", | ||
"@ckeditor/ckeditor5-engine": "^0.9.0", | ||
"@ckeditor/ckeditor5-theme-lark": "^0.7.0", | ||
"@ckeditor/ckeditor5-ui": "^0.8.0" | ||
"@ckeditor/ckeditor5-core": "^0.8.1", | ||
"@ckeditor/ckeditor5-engine": "^0.10.0", | ||
"@ckeditor/ckeditor5-theme-lark": "^0.8.0", | ||
"@ckeditor/ckeditor5-ui": "^0.9.0" | ||
}, | ||
"devDependencies": { | ||
"@ckeditor/ckeditor5-dev-lint": "^2.0.2", | ||
"@ckeditor/ckeditor5-editor-classic": "^0.7.2", | ||
"@ckeditor/ckeditor5-enter": "^0.9.0", | ||
"@ckeditor/ckeditor5-heading": "^0.9.0", | ||
"@ckeditor/ckeditor5-paragraph": "^0.7.0", | ||
"@ckeditor/ckeditor5-typing": "^0.9.0", | ||
"@ckeditor/ckeditor5-undo": "^0.8.0", | ||
"@ckeditor/ckeditor5-utils": "^0.9.0", | ||
"@ckeditor/ckeditor5-editor-classic": "^0.7.3", | ||
"@ckeditor/ckeditor5-enter": "^0.9.1", | ||
"@ckeditor/ckeditor5-heading": "^0.9.1", | ||
"@ckeditor/ckeditor5-paragraph": "^0.8.0", | ||
"@ckeditor/ckeditor5-typing": "^0.9.1", | ||
"@ckeditor/ckeditor5-undo": "^0.8.1", | ||
"@ckeditor/ckeditor5-utils": "^0.9.1", | ||
"gulp": "^3.9.0", | ||
@@ -22,0 +22,0 @@ "guppy-pre-commit": "^0.4.0" |
295
src/link.js
@@ -14,2 +14,3 @@ /** | ||
import LinkElement from './linkelement'; | ||
import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon'; | ||
@@ -19,4 +20,2 @@ 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'; | ||
@@ -30,5 +29,6 @@ | ||
/** | ||
* The link feature. It introduces the Link and Unlink buttons and the <kbd>Ctrl+K</kbd> keystroke. | ||
* The link plugin. It introduces the Link and Unlink buttons and the <kbd>Ctrl+K</kbd> 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/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}. | ||
* | ||
@@ -42,3 +42,3 @@ * @extends module:core/plugin~Plugin | ||
static get requires() { | ||
return [ LinkEngine ]; | ||
return [ LinkEngine, ContextualBalloon ]; | ||
} | ||
@@ -60,14 +60,15 @@ | ||
/** | ||
* 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/panel/balloon/contextualballoon~ContextualBalloon} | ||
*/ | ||
this.formView = this._createForm(); | ||
this._balloon = this.editor.plugins.get( ContextualBalloon ); | ||
@@ -77,7 +78,46 @@ // Create toolbar buttons. | ||
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. | ||
* | ||
@@ -91,4 +131,4 @@ * @private | ||
// 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 ) ); | ||
@@ -108,3 +148,3 @@ editor.ui.componentFactory.add( 'link', ( locale ) => { | ||
// Show the panel on button click. | ||
this.listenTo( button, 'execute', () => this._showPanel() ); | ||
this.listenTo( button, 'execute', () => this._showPanel( true ) ); | ||
@@ -145,19 +185,10 @@ return button; | ||
/** | ||
* 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; | ||
_attachActions() { | ||
const viewDocument = this.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 ); | ||
// Handle click on view document and show panel when selection is placed inside the link element. | ||
@@ -167,24 +198,14 @@ // Keep panel open until selection will be inside the same link element. | ||
const viewSelection = viewDocument.selection; | ||
const parentLink = getPositionParentLink( viewSelection.getFirstPosition() ); | ||
const parentLink = this._getSelectedLinkElement(); | ||
// When collapsed selection is inside link element (link element is clicked). | ||
if ( viewSelection.isCollapsed && parentLink ) { | ||
this._attachPanelToElement(); | ||
this.listenTo( viewDocument, 'render', () => { | ||
const currentParentLink = getPositionParentLink( viewSelection.getFirstPosition() ); | ||
if ( !viewSelection.isCollapsed || parentLink !== currentParentLink ) { | ||
this._hidePanel(); | ||
} else { | ||
this._attachPanelToElement( parentLink ); | ||
} | ||
} ); | ||
this.listenTo( balloonPanelView, 'change:isVisible', () => this.stopListening( viewDocument, 'render' ) ); | ||
// Then show panel but keep focus inside editor editable. | ||
this._showPanel(); | ||
} | ||
} ); | ||
// 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(); | ||
@@ -195,6 +216,6 @@ 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(); | ||
@@ -206,109 +227,141 @@ } | ||
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}. | ||
* When view is already added then try to focus it `focusInput` parameter is set as true. | ||
* | ||
* @private | ||
* @returns {module:link/ui/linkformview~LinkFormView} Link form instance. | ||
* @protected | ||
* @param {Boolean} [focusInput=false] When `true`, link form will be focused on panel show. | ||
* @return {Promise} A promise resolved when the {@link #formView} {@link module:ui/view~View#init} is done. | ||
*/ | ||
_createForm() { | ||
const editor = this.editor; | ||
const formView = new LinkFormView( editor.locale ); | ||
_showPanel( focusInput ) { | ||
const editing = this.editor.editing; | ||
const showViewDocument = editing.view; | ||
const showIsCollapsed = showViewDocument.selection.isCollapsed; | ||
const showSelectedLink = this._getSelectedLinkElement(); | ||
formView.urlInputView.bind( 'value' ).to( editor.commands.get( 'link' ), 'value' ); | ||
// https://github.com/ckeditor/ckeditor5-link/issues/53 | ||
this.formView.unlinkButtonView.isVisible = !!showSelectedLink; | ||
// Execute link command after clicking on formView `Save` button. | ||
this.listenTo( formView, 'submit', () => { | ||
editor.execute( 'link', formView.urlInputView.inputView.element.value ); | ||
this._hidePanel( true ); | ||
} ); | ||
this.listenTo( showViewDocument, 'render', () => { | ||
const renderSelectedLink = this._getSelectedLinkElement(); | ||
const renderIsCollapsed = showViewDocument.selection.isCollapsed; | ||
const hasSellectionExpanded = showIsCollapsed && !renderIsCollapsed; | ||
// Execute unlink command after clicking on formView `Unlink` button. | ||
this.listenTo( formView, 'unlink', () => { | ||
editor.execute( 'unlink' ); | ||
this._hidePanel( true ); | ||
// Hide the panel if: | ||
// * the selection went out of the original link element | ||
// (e.g. paragraph containing the link was removed), | ||
// * the selection has expanded | ||
// upon the #render event. | ||
if ( hasSellectionExpanded || showSelectedLink !== renderSelectedLink ) { | ||
this._hidePanel( true ); | ||
} | ||
// Update the position of the panel when: | ||
// * the selection remains in the original link element, | ||
// * there was no link element in the first place, i.e. creating a new link | ||
else { | ||
// If still in a link element, simply update the position of the balloon. | ||
if ( renderSelectedLink ) { | ||
this._balloon.updatePosition(); | ||
} | ||
// If there was no link, upon #render, the balloon must be moved | ||
// to the new position in the editing view (a new native DOM range). | ||
else { | ||
this._balloon.updatePosition( this._getBalloonPositionData() ); | ||
} | ||
} | ||
} ); | ||
// Close the panel on esc key press when the form has focus. | ||
formView.keystrokes.set( 'Esc', ( data, cancel ) => { | ||
this._hidePanel( true ); | ||
cancel(); | ||
} ); | ||
if ( this._balloon.hasView( this.formView ) ) { | ||
// Check if formView should be focused and focus it if is visible. | ||
if ( focusInput && this._balloon.visibleView === this.formView ) { | ||
this.formView.urlInputView.select(); | ||
} | ||
// Hide balloon panel after clicking on formView `Cancel` button. | ||
this.listenTo( formView, 'cancel', () => this._hidePanel( true ) ); | ||
return Promise.resolve(); | ||
} else { | ||
return this._balloon.add( { | ||
view: this.formView, | ||
position: this._getBalloonPositionData() | ||
} ).then( () => { | ||
if ( focusInput ) { | ||
this.formView.urlInputView.select(); | ||
} | ||
} ); | ||
} | ||
} | ||
this.balloonPanelView.content.add( formView ); | ||
/** | ||
* Removes the {@link #formView} from the {@link #_balloon}. | ||
* | ||
* See {@link #_showPanel}. | ||
* | ||
* @protected | ||
* @param {Boolean} [focusEditable=false] When `true`, editable focus will be restored on panel hide. | ||
*/ | ||
_hidePanel( focusEditable ) { | ||
this.stopListening( this.editor.editing.view, 'render' ); | ||
return formView; | ||
if ( !this._balloon.hasView( this.formView ) ) { | ||
return; | ||
} | ||
if ( focusEditable ) { | ||
this.editor.editing.view.focus(); | ||
} | ||
this.stopListening( this.editor.editing.view, 'render' ); | ||
this._balloon.remove( this.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. | ||
* 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 | ||
* @param {module:link/linkelement~LinkElement} [parentLink] Target element. | ||
* @returns {module:utils/dom/position~Options} | ||
*/ | ||
_attachPanelToElement( parentLink ) { | ||
_getBalloonPositionData() { | ||
const viewDocument = this.editor.editing.view; | ||
const targetLink = parentLink || getPositionParentLink( viewDocument.selection.getFirstPosition() ); | ||
const targetLink = this._getSelectedLinkElement(); | ||
const target = targetLink ? | ||
// When selection is inside link element, then attach panel to this element. | ||
viewDocument.domConverter.getCorrespondingDomElement( 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() ); | ||
// Otherwise attach panel to the selection. | ||
viewDocument.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() ); | ||
this.balloonPanelView.attachTo( { | ||
return { | ||
target, | ||
limiter: viewDocument.domConverter.getCorrespondingDomElement( viewDocument.selection.editableElement ) | ||
} ); | ||
}; | ||
} | ||
/** | ||
* Hides {@link #balloonPanelView balloon panel view}. | ||
* Returns the {@link module:link/linkelement~LinkElement} at the first | ||
* {@link module:engine/model/position~Position} of | ||
* {@link module:engine/view/document~Document editing view's} selection or `null` | ||
* if there's none. | ||
* | ||
* @private | ||
* @param {Boolean} [focusEditable=false] When `true` then editable focus will be restored on panel hide. | ||
* @returns {module:link/linkelement~LinkElement|null} | ||
*/ | ||
_hidePanel( focusEditable ) { | ||
this.balloonPanelView.hide(); | ||
if ( focusEditable ) { | ||
this.editor.editing.view.focus(); | ||
} | ||
_getSelectedLinkElement() { | ||
return this.editor.editing.view | ||
.selection | ||
.getFirstPosition() | ||
.parent | ||
.getAncestors() | ||
.find( ancestor => ancestor instanceof LinkElement ); | ||
} | ||
/** | ||
* Shows {@link #balloonPanelView balloon panel view}. | ||
* | ||
* @private | ||
*/ | ||
_showPanel() { | ||
this._attachPanelToElement(); | ||
this.formView.urlInputView.select(); | ||
} | ||
} | ||
// Try to find if one of the position parent ancestors is a LinkElement, | ||
// if yes return this element. | ||
// | ||
// @private | ||
// @param {engine.view.Position} position | ||
// @returns {module:link/linkelement~LinkElement|null} | ||
function getPositionParentLink( position ) { | ||
return position.parent.getAncestors().find( ( ancestor ) => ancestor instanceof LinkElement ); | ||
} |
@@ -34,3 +34,3 @@ /** | ||
// Allow link attribute on all inline nodes. | ||
editor.document.schema.allow( { name: '$inline', attributes: 'linkHref' } ); | ||
editor.document.schema.allow( { name: '$inline', attributes: 'linkHref', inside: '$block' } ); | ||
@@ -37,0 +37,0 @@ // Build converter from model to view for data and editing pipelines. |
@@ -127,3 +127,6 @@ /** | ||
'ck-link-form', | ||
] | ||
], | ||
// https://github.com/ckeditor/ckeditor5-link/issues/90 | ||
tabindex: '-1' | ||
}, | ||
@@ -130,0 +133,0 @@ |
@@ -13,6 +13,7 @@ /** | ||
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; | ||
import Link from '../src/link'; | ||
import LinkEngine from '../src/linkengine'; | ||
import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon'; | ||
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; | ||
import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview'; | ||
@@ -25,3 +26,3 @@ import Range from '@ckeditor/ckeditor5-engine/src/view/range'; | ||
describe( 'Link', () => { | ||
let editor, linkFeature, linkButton, unlinkButton, balloonPanelView, formView, editorElement; | ||
let editor, linkFeature, linkButton, unlinkButton, balloon, formView, editorElement; | ||
@@ -33,3 +34,3 @@ beforeEach( () => { | ||
return ClassicTestEditor.create( editorElement, { | ||
plugins: [ Link ] | ||
plugins: [ Link, Paragraph ] | ||
} ) | ||
@@ -44,4 +45,10 @@ .then( newEditor => { | ||
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 and pin methods so lets override it. | ||
testUtils.sinon.stub( balloon.view, 'attachTo', () => {} ); | ||
testUtils.sinon.stub( balloon.view, 'pin', () => {} ); | ||
return formView.init(); | ||
} ); | ||
@@ -51,2 +58,4 @@ } ); | ||
afterEach( () => { | ||
editorElement.remove(); | ||
return editor.destroy(); | ||
@@ -63,2 +72,6 @@ } ); | ||
it( 'should load ContextualBalloon', () => { | ||
expect( editor.plugins.get( ContextualBalloon ) ).to.instanceOf( ContextualBalloon ); | ||
} ); | ||
it( 'should register click observer', () => { | ||
@@ -68,337 +81,532 @@ expect( editor.editing.view.getObserver( ClickObserver ) ).to.instanceOf( ClickObserver ); | ||
describe( 'link toolbar button', () => { | ||
it( 'should register link button', () => { | ||
expect( linkButton ).to.instanceOf( ButtonView ); | ||
describe( '_showPanel()', () => { | ||
let balloonAddSpy; | ||
beforeEach( () => { | ||
balloonAddSpy = testUtils.sinon.spy( balloon, 'add' ); | ||
editor.editing.view.isFocused = true; | ||
} ); | ||
it( 'should bind linkButtonView to link command', () => { | ||
const command = editor.commands.get( 'link' ); | ||
it( 'should return promise', () => { | ||
const returned = linkFeature._showPanel(); | ||
command.isEnabled = true; | ||
expect( linkButton.isEnabled ).to.be.true; | ||
expect( returned ).to.instanceof( Promise ); | ||
command.isEnabled = false; | ||
expect( linkButton.isEnabled ).to.be.false; | ||
return returned; | ||
} ); | ||
it( 'should open panel on linkButtonView execute event', () => { | ||
linkButton.fire( 'execute' ); | ||
it( 'should add #formView to the #_balloon and attach the #_balloon to the selection when text fragment is selected', () => { | ||
setModelData( editor.document, '<paragraph>f[o]o</paragraph>' ); | ||
const selectedRange = editorElement.ownerDocument.getSelection().getRangeAt( 0 ); | ||
expect( linkFeature.balloonPanelView.isVisible ).to.true; | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
expect( balloon.visibleView ).to.equal( formView ); | ||
sinon.assert.calledWithExactly( balloonAddSpy, { | ||
view: formView, | ||
position: { | ||
target: selectedRange, | ||
limiter: editorElement | ||
} | ||
} ); | ||
} ); | ||
} ); | ||
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 #formView to the #_balloon and attach the #_balloon to the selection when selection is collapsed', () => { | ||
setModelData( editor.document, '<paragraph>f[]oo</paragraph>' ); | ||
const selectedRange = editorElement.ownerDocument.getSelection().getRangeAt( 0 ); | ||
editor.document.schema.allow( { name: '$text', inside: '$root' } ); | ||
setModelData( editor.document, '<$text linkHref="url">some[] url</$text>' ); | ||
editor.editing.view.isFocused = true; | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
expect( balloon.visibleView ).to.equal( formView ); | ||
linkButton.fire( 'execute' ); | ||
sinon.assert.calledWithExactly( balloonAddSpy, { | ||
view: formView, | ||
position: { | ||
target: selectedRange, | ||
limiter: editorElement | ||
} | ||
} ); | ||
} ); | ||
} ); | ||
it( 'should add #formView to the #_balloon and attach the #_balloon to the link element when collapsed selection is inside ' + | ||
'that link', | ||
() => { | ||
setModelData( editor.document, '<paragraph><$text linkHref="url">f[]oo</$text></paragraph>' ); | ||
const linkElement = editorElement.querySelector( 'a' ); | ||
sinon.assert.calledWithExactly( attachToSpy, sinon.match( { | ||
target: linkElement, | ||
limiter: editorElement | ||
} ) ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
expect( balloon.visibleView ).to.equal( formView ); | ||
sinon.assert.calledWithExactly( balloonAddSpy, { | ||
view: formView, | ||
position: { | ||
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 not focus the #formView at default', () => { | ||
const spy = testUtils.sinon.spy( formView.urlInputView, 'select' ); | ||
editor.document.schema.allow( { name: '$text', inside: '$root' } ); | ||
setModelData( editor.document, 'so[me ur]l' ); | ||
editor.editing.view.isFocused = true; | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
sinon.assert.notCalled( spy ); | ||
} ); | ||
} ); | ||
linkButton.fire( 'execute' ); | ||
it( 'should not focus the #formView when called with a `false` parameter', () => { | ||
const spy = testUtils.sinon.spy( formView.urlInputView, 'select' ); | ||
const selectedRange = editorElement.ownerDocument.getSelection().getRangeAt( 0 ); | ||
sinon.assert.calledWithExactly( attachToSpy, sinon.match( { | ||
target: selectedRange, | ||
limiter: editorElement | ||
} ) ); | ||
return linkFeature._showPanel( false ) | ||
.then( () => { | ||
sinon.assert.notCalled( spy ); | ||
} ); | ||
} ); | ||
it( 'should select panel input value when panel is opened', () => { | ||
const selectUrlInputSpy = testUtils.sinon.spy( linkFeature.formView.urlInputView, 'select' ); | ||
it( 'should not focus the #formView when called with a `true` parameter while the balloon is opened but link form is not visible', () => { | ||
const spy = testUtils.sinon.spy( formView.urlInputView, 'select' ); | ||
const viewMock = { | ||
ready: true, | ||
init: () => {}, | ||
destroy: () => {} | ||
}; | ||
editor.editing.view.isFocused = true; | ||
return linkFeature._showPanel( false ) | ||
.then( () => balloon.add( { view: viewMock } ) ) | ||
.then( () => linkFeature._showPanel( true ) ) | ||
.then( () => { | ||
sinon.assert.notCalled( spy ); | ||
} ); | ||
} ); | ||
linkButton.fire( 'execute' ); | ||
it( 'should focus the #formView when called with a `true` parameter', () => { | ||
const spy = testUtils.sinon.spy( formView.urlInputView, 'select' ); | ||
expect( selectUrlInputSpy.calledOnce ).to.true; | ||
return linkFeature._showPanel( true ) | ||
.then( () => { | ||
sinon.assert.calledOnce( spy ); | ||
} ); | ||
} ); | ||
} ); | ||
describe( 'unlink toolbar button', () => { | ||
it( 'should register unlink button', () => { | ||
expect( unlinkButton ).to.instanceOf( ButtonView ); | ||
it( 'should focus the #formView when called with a `true` parameter while the balloon is open and the #formView is visible', () => { | ||
const spy = testUtils.sinon.spy( formView.urlInputView, 'select' ); | ||
return linkFeature._showPanel( false ) | ||
.then( () => linkFeature._showPanel( true ) ) | ||
.then( () => { | ||
sinon.assert.calledOnce( spy ); | ||
} ); | ||
} ); | ||
it( 'should bind unlinkButtonView to unlink command', () => { | ||
const command = editor.commands.get( 'unlink' ); | ||
// https://github.com/ckeditor/ckeditor5-link/issues/53 | ||
it( 'should set formView.unlinkButtonView#isVisible depending on the selection in a link or not', () => { | ||
setModelData( editor.document, '<paragraph>f[]oo</paragraph>' ); | ||
command.isEnabled = true; | ||
expect( unlinkButton.isEnabled ).to.be.true; | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
expect( formView.unlinkButtonView.isVisible ).to.be.false; | ||
command.isEnabled = false; | ||
expect( unlinkButton.isEnabled ).to.be.false; | ||
setModelData( editor.document, '<paragraph><$text linkHref="url">f[]oo</$text></paragraph>' ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
expect( formView.unlinkButtonView.isVisible ).to.be.true; | ||
} ); | ||
} ); | ||
} ); | ||
it( 'should execute unlink command on unlinkButtonView execute event', () => { | ||
const executeSpy = testUtils.sinon.spy( editor, 'execute' ); | ||
describe( 'when the document is rendering', () => { | ||
it( 'should not duplicate #render listeners', () => { | ||
const viewDocument = editor.editing.view; | ||
unlinkButton.fire( 'execute' ); | ||
setModelData( editor.document, '<paragraph>f[]oo</paragraph>' ); | ||
expect( executeSpy.calledOnce ).to.true; | ||
expect( executeSpy.calledWithExactly( 'unlink' ) ).to.true; | ||
} ); | ||
} ); | ||
const spy = testUtils.sinon.stub( balloon, 'updatePosition', () => {} ); | ||
describe( 'link balloon panel', () => { | ||
let hidePanelSpy, focusEditableSpy; | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
viewDocument.render(); | ||
linkFeature._hidePanel(); | ||
beforeEach( () => { | ||
hidePanelSpy = testUtils.sinon.spy( balloonPanelView, 'hide' ); | ||
focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' ); | ||
} ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
viewDocument.render(); | ||
sinon.assert.calledTwice( spy ); | ||
} ); | ||
} ); | ||
} ); | ||
it( 'should be created', () => { | ||
expect( balloonPanelView ).to.instanceOf( BalloonPanelView ); | ||
} ); | ||
//https://github.com/ckeditor/ckeditor5-link/issues/113 | ||
it( 'updates the position of the panel – editing a link, then the selection remains in the link upon #render', () => { | ||
const viewDocument = editor.editing.view; | ||
it( 'should be appended to the document body', () => { | ||
expect( document.body.contains( balloonPanelView.element ) ); | ||
} ); | ||
setModelData( editor.document, '<paragraph><$text linkHref="url">f[]oo</$text></paragraph>' ); | ||
it( 'should open with selected url input on `CTRL+K` keystroke', () => { | ||
const selectUrlInputSpy = testUtils.sinon.spy( linkFeature.formView.urlInputView, 'select' ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
const spy = testUtils.sinon.stub( balloon, 'updatePosition', () => {} ); | ||
editor.keystrokes.press( { keyCode: keyCodes.k, ctrlKey: true } ); | ||
const root = viewDocument.getRoot(); | ||
const text = root.getChild( 0 ).getChild( 0 ).getChild( 0 ); | ||
expect( balloonPanelView.isVisible ).to.true; | ||
expect( selectUrlInputSpy.calledOnce ).to.true; | ||
} ); | ||
// Move selection to foo[]. | ||
viewDocument.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 3, text, 3 ) ], true ); | ||
viewDocument.render(); | ||
it( 'should add balloon panel element to focus tracker', () => { | ||
editor.ui.focusTracker.isFocused = false; | ||
sinon.assert.calledOnce( spy ); | ||
sinon.assert.calledWithExactly( spy ); | ||
} ); | ||
} ); | ||
balloonPanelView.element.dispatchEvent( new Event( 'focus' ) ); | ||
//https://github.com/ckeditor/ckeditor5-link/issues/113 | ||
it( 'updates the position of the panel – creating a new link, then the selection moved upon #render', () => { | ||
const viewDocument = editor.editing.view; | ||
expect( editor.ui.focusTracker.isFocused ).to.true; | ||
setModelData( editor.document, '<paragraph>f[]oo</paragraph>' ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
const spy = testUtils.sinon.stub( balloon, 'updatePosition', () => {} ); | ||
// Fires #render. | ||
const root = viewDocument.getRoot(); | ||
const text = root.getChild( 0 ).getChild( 0 ); | ||
viewDocument.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 3, text, 3 ) ], true ); | ||
viewDocument.render(); | ||
sinon.assert.calledOnce( spy ); | ||
sinon.assert.calledWithExactly( spy, { | ||
target: editorElement.ownerDocument.getSelection().getRangeAt( 0 ), | ||
limiter: editorElement | ||
} ); | ||
} ); | ||
} ); | ||
//https://github.com/ckeditor/ckeditor5-link/issues/113 | ||
it( 'hides of the panel – editing a link, then the selection moved out of the link upon #render', () => { | ||
const viewDocument = editor.editing.view; | ||
setModelData( editor.document, '<paragraph><$text linkHref="url">f[]oo</$text>bar</paragraph>' ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
const spyUpdate = testUtils.sinon.stub( balloon, 'updatePosition', () => {} ); | ||
const spyHide = testUtils.sinon.spy( linkFeature, '_hidePanel' ); | ||
const root = viewDocument.getRoot(); | ||
const text = root.getChild( 0 ).getChild( 1 ); | ||
// Move selection to b[]ar. | ||
viewDocument.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 1 ) ], true ); | ||
viewDocument.render(); | ||
sinon.assert.calledOnce( spyHide ); | ||
sinon.assert.notCalled( spyUpdate ); | ||
} ); | ||
} ); | ||
//https://github.com/ckeditor/ckeditor5-link/issues/113 | ||
it( 'hides of the panel – editing a link, then the selection moved to another link upon #render', () => { | ||
const viewDocument = editor.editing.view; | ||
setModelData( editor.document, '<paragraph><$text linkHref="url">f[]oo</$text>bar<$text linkHref="url">b[]az</$text></paragraph>' ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
const spyUpdate = testUtils.sinon.stub( balloon, 'updatePosition', () => {} ); | ||
const spyHide = testUtils.sinon.spy( linkFeature, '_hidePanel' ); | ||
const root = viewDocument.getRoot(); | ||
const text = root.getChild( 0 ).getChild( 2 ).getChild( 0 ); | ||
// Move selection to b[]az. | ||
viewDocument.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 1 ) ], true ); | ||
viewDocument.render(); | ||
sinon.assert.calledOnce( spyHide ); | ||
sinon.assert.notCalled( spyUpdate ); | ||
} ); | ||
} ); | ||
//https://github.com/ckeditor/ckeditor5-link/issues/113 | ||
it( 'hides the panel – editing a link, then the selection expands upon #render', () => { | ||
const viewDocument = editor.editing.view; | ||
setModelData( editor.document, '<paragraph><$text linkHref="url">f[]oo</$text></paragraph>' ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
const spyUpdate = testUtils.sinon.stub( balloon, 'updatePosition', () => {} ); | ||
const spyHide = testUtils.sinon.spy( linkFeature, '_hidePanel' ); | ||
const root = viewDocument.getRoot(); | ||
const text = root.getChild( 0 ).getChild( 0 ).getChild( 0 ); | ||
// Move selection to f[o]o. | ||
viewDocument.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 2 ) ], true ); | ||
viewDocument.render(); | ||
sinon.assert.calledOnce( spyHide ); | ||
sinon.assert.notCalled( spyUpdate ); | ||
} ); | ||
} ); | ||
} ); | ||
} ); | ||
it( 'should focus the link form on Tab key press', () => { | ||
const keyEvtData = { | ||
keyCode: keyCodes.tab, | ||
preventDefault: sinon.spy(), | ||
stopPropagation: sinon.spy() | ||
}; | ||
describe( '_hidePanel()', () => { | ||
beforeEach( () => { | ||
return balloon.add( { view: formView } ); | ||
} ); | ||
// Mock balloon invisible, form not focused. | ||
balloonPanelView.isVisible = false; | ||
formView.focusTracker.isFocused = false; | ||
it( 'should remove #formView from the #_balloon', () => { | ||
linkFeature._hidePanel(); | ||
expect( balloon.hasView( formView ) ).to.false; | ||
} ); | ||
const spy = sinon.spy( formView, 'focus' ); | ||
it( 'should not focus the `editable` by default', () => { | ||
const spy = testUtils.sinon.spy( editor.editing.view, 'focus' ); | ||
editor.keystrokes.press( keyEvtData ); | ||
sinon.assert.notCalled( keyEvtData.preventDefault ); | ||
sinon.assert.notCalled( keyEvtData.stopPropagation ); | ||
linkFeature._hidePanel(); | ||
sinon.assert.notCalled( spy ); | ||
} ); | ||
// Mock balloon visible, form focused. | ||
balloonPanelView.isVisible = true; | ||
formView.focusTracker.isFocused = true; | ||
it( 'should not focus the `editable` when called with a `false` parameter', () => { | ||
const spy = testUtils.sinon.spy( editor.editing.view, 'focus' ); | ||
editor.keystrokes.press( keyEvtData ); | ||
sinon.assert.notCalled( keyEvtData.preventDefault ); | ||
sinon.assert.notCalled( keyEvtData.stopPropagation ); | ||
linkFeature._hidePanel( false ); | ||
sinon.assert.notCalled( spy ); | ||
} ); | ||
// Mock balloon visible, form not focused. | ||
balloonPanelView.isVisible = true; | ||
formView.focusTracker.isFocused = false; | ||
it( 'should focus the `editable` when called with a `true` parameter', () => { | ||
const spy = testUtils.sinon.spy( editor.editing.view, 'focus' ); | ||
editor.keystrokes.press( keyEvtData ); | ||
sinon.assert.calledOnce( keyEvtData.preventDefault ); | ||
sinon.assert.calledOnce( keyEvtData.stopPropagation ); | ||
linkFeature._hidePanel( true ); | ||
sinon.assert.calledOnce( spy ); | ||
} ); | ||
describe( 'close listeners', () => { | ||
describe( 'keyboard', () => { | ||
it( 'should close after Esc key press (from editor)', () => { | ||
const keyEvtData = { | ||
keyCode: keyCodes.esc, | ||
preventDefault: sinon.spy(), | ||
stopPropagation: sinon.spy() | ||
}; | ||
it( 'should not throw an error when #formView is not added to the `balloon`', () => { | ||
linkFeature._hidePanel( true ); | ||
balloonPanelView.isVisible = false; | ||
expect( () => { | ||
linkFeature._hidePanel( true ); | ||
} ).to.not.throw(); | ||
} ); | ||
editor.keystrokes.press( keyEvtData ); | ||
it( 'should clear `render` listener from ViewDocument', () => { | ||
const spy = sinon.spy(); | ||
sinon.assert.notCalled( hidePanelSpy ); | ||
sinon.assert.notCalled( focusEditableSpy ); | ||
linkFeature.listenTo( editor.editing.view, 'render', spy ); | ||
balloonPanelView.isVisible = true; | ||
linkFeature._hidePanel(); | ||
editor.keystrokes.press( keyEvtData ); | ||
editor.editing.view.render(); | ||
sinon.assert.calledOnce( hidePanelSpy ); | ||
sinon.assert.calledOnce( focusEditableSpy ); | ||
} ); | ||
sinon.assert.notCalled( spy ); | ||
} ); | ||
} ); | ||
it( 'should close after Esc key press (from the form)', () => { | ||
const keyEvtData = { | ||
keyCode: keyCodes.esc, | ||
preventDefault: sinon.spy(), | ||
stopPropagation: sinon.spy() | ||
}; | ||
describe( 'link toolbar button', () => { | ||
it( 'should register link button', () => { | ||
expect( linkButton ).to.instanceOf( ButtonView ); | ||
} ); | ||
formView.keystrokes.press( keyEvtData ); | ||
it( 'should bind linkButtonView to link command', () => { | ||
const command = editor.commands.get( 'link' ); | ||
sinon.assert.calledOnce( hidePanelSpy ); | ||
sinon.assert.calledOnce( focusEditableSpy ); | ||
} ); | ||
} ); | ||
command.isEnabled = true; | ||
expect( linkButton.isEnabled ).to.be.true; | ||
describe( 'mouse', () => { | ||
it( 'should close and not focus editable on click outside the panel', () => { | ||
balloonPanelView.isVisible = true; | ||
document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) ); | ||
command.isEnabled = false; | ||
expect( linkButton.isEnabled ).to.be.false; | ||
} ); | ||
expect( hidePanelSpy.calledOnce ).to.true; | ||
expect( focusEditableSpy.notCalled ).to.true; | ||
} ); | ||
it( 'should show the #_balloon on execute event with the selected #formView', () => { | ||
// Method is stubbed because it returns internal promise which can't be returned in test. | ||
const spy = testUtils.sinon.stub( linkFeature, '_showPanel', () => {} ); | ||
it( 'should not close on click inside the panel', () => { | ||
balloonPanelView.isVisible = true; | ||
balloonPanelView.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) ); | ||
linkButton.fire( 'execute' ); | ||
expect( hidePanelSpy.notCalled ).to.true; | ||
} ); | ||
} ); | ||
sinon.assert.calledWithExactly( spy, true ); | ||
} ); | ||
} ); | ||
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 observer = editor.editing.view.getObserver( ClickObserver ); | ||
describe( 'unlink toolbar button', () => { | ||
it( 'should register unlink button', () => { | ||
expect( unlinkButton ).to.instanceOf( ButtonView ); | ||
} ); | ||
editor.document.schema.allow( { name: '$text', inside: '$root' } ); | ||
setModelData( editor.document, '<$text linkHref="url">fo[]o</$text>' ); | ||
it( 'should bind unlinkButtonView to unlink command', () => { | ||
const command = editor.commands.get( 'unlink' ); | ||
observer.fire( 'click', { target: document.body } ); | ||
command.isEnabled = true; | ||
expect( unlinkButton.isEnabled ).to.be.true; | ||
expect( balloonPanelView.isVisible ).to.true; | ||
expect( selectUrlInputSpy.notCalled ).to.true; | ||
} ); | ||
command.isEnabled = false; | ||
expect( unlinkButton.isEnabled ).to.be.false; | ||
} ); | ||
it( 'should keep open and update position until collapsed selection stay inside the same link element', () => { | ||
const observer = editor.editing.view.getObserver( ClickObserver ); | ||
it( 'should execute unlink command on unlinkButtonView execute event', () => { | ||
const executeSpy = testUtils.sinon.spy( editor, 'execute' ); | ||
editor.document.schema.allow( { name: '$text', inside: '$root' } ); | ||
setModelData( editor.document, '<$text linkHref="url">b[]ar</$text>' ); | ||
unlinkButton.fire( 'execute' ); | ||
const root = editor.editing.view.getRoot(); | ||
const text = root.getChild( 0 ).getChild( 0 ); | ||
expect( executeSpy.calledOnce ).to.true; | ||
expect( executeSpy.calledWithExactly( 'unlink' ) ).to.true; | ||
} ); | ||
} ); | ||
observer.fire( 'click', { target: document.body } ); | ||
describe( 'keyboard support', () => { | ||
it( 'should show the #_balloon with selected #formView on `CTRL+K` keystroke', () => { | ||
// Method is stubbed because it returns internal promise which can't be returned in test. | ||
const spy = testUtils.sinon.stub( linkFeature, '_showPanel', () => {} ); | ||
expect( balloonPanelView.isVisible ).to.true; | ||
editor.keystrokes.press( { keyCode: keyCodes.k, ctrlKey: true } ); | ||
const attachToSpy = testUtils.sinon.spy( balloonPanelView, 'attachTo' ); | ||
sinon.assert.calledWithExactly( spy, true ); | ||
} ); | ||
editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 1 ) ], true ); | ||
editor.editing.view.render(); | ||
it( 'should focus the the #formView on `Tab` key press when the #_balloon is open', () => { | ||
const keyEvtData = { | ||
keyCode: keyCodes.tab, | ||
preventDefault: sinon.spy(), | ||
stopPropagation: sinon.spy() | ||
}; | ||
expect( balloonPanelView.isVisible ).to.true; | ||
expect( attachToSpy.calledOnce ).to.true; | ||
} ); | ||
// Balloon is invisible, form not focused. | ||
formView.focusTracker.isFocused = false; | ||
it( 'should close when selection goes outside the link element', () => { | ||
const observer = editor.editing.view.getObserver( ClickObserver ); | ||
const spy = sinon.spy( formView, 'focus' ); | ||
editor.document.schema.allow( { name: '$text', inside: '$root' } ); | ||
setModelData( editor.document, 'foo <$text linkHref="url">b[]ar</$text>' ); | ||
editor.keystrokes.press( keyEvtData ); | ||
sinon.assert.notCalled( keyEvtData.preventDefault ); | ||
sinon.assert.notCalled( keyEvtData.stopPropagation ); | ||
sinon.assert.notCalled( spy ); | ||
const root = editor.editing.view.getRoot(); | ||
const text = root.getChild( 0 ); | ||
// Balloon is visible, form focused. | ||
return linkFeature._showPanel( true ) | ||
.then( () => { | ||
formView.focusTracker.isFocused = true; | ||
observer.fire( 'click', { target: document.body } ); | ||
editor.keystrokes.press( keyEvtData ); | ||
sinon.assert.notCalled( keyEvtData.preventDefault ); | ||
sinon.assert.notCalled( keyEvtData.stopPropagation ); | ||
sinon.assert.notCalled( spy ); | ||
expect( balloonPanelView.isVisible ).to.true; | ||
// Balloon is still visible, form not focused. | ||
formView.focusTracker.isFocused = false; | ||
editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 3, text, 3 ) ], true ); | ||
editor.editing.view.render(); | ||
editor.keystrokes.press( keyEvtData ); | ||
sinon.assert.calledOnce( keyEvtData.preventDefault ); | ||
sinon.assert.calledOnce( keyEvtData.stopPropagation ); | ||
sinon.assert.calledOnce( spy ); | ||
} ); | ||
} ); | ||
expect( balloonPanelView.isVisible ).to.false; | ||
it( 'should hide the #_balloon after Esc key press (from editor) and not focus the editable', () => { | ||
const spy = testUtils.sinon.spy( linkFeature, '_hidePanel' ); | ||
const keyEvtData = { | ||
keyCode: keyCodes.esc, | ||
preventDefault: sinon.spy(), | ||
stopPropagation: sinon.spy() | ||
}; | ||
// Balloon is visible. | ||
return linkFeature._showPanel( false ).then( () => { | ||
editor.keystrokes.press( keyEvtData ); | ||
sinon.assert.calledWithExactly( spy ); | ||
} ); | ||
} ); | ||
it( 'should close when selection goes to the other link element with the same href', () => { | ||
const observer = editor.editing.view.getObserver( ClickObserver ); | ||
it( 'should not hide #_balloon after Esc key press (from editor) when #_balloon is open but is not visible', () => { | ||
const spy = testUtils.sinon.spy( linkFeature, '_hidePanel' ); | ||
const keyEvtData = { | ||
keyCode: keyCodes.esc, | ||
preventDefault: () => {}, | ||
stopPropagation: () => {} | ||
}; | ||
editor.document.schema.allow( { name: '$text', inside: '$root' } ); | ||
setModelData( editor.document, '<$text linkHref="url">f[]oo</$text> bar <$text linkHref="url">biz</$text>' ); | ||
const viewMock = { | ||
ready: true, | ||
init: () => {}, | ||
destroy: () => {} | ||
}; | ||
const root = editor.editing.view.getRoot(); | ||
const text = root.getChild( 2 ).getChild( 0 ); | ||
return linkFeature._showPanel( false ) | ||
.then( () => balloon.add( { view: viewMock } ) ) | ||
.then( () => { | ||
editor.keystrokes.press( keyEvtData ); | ||
observer.fire( 'click', { target: document.body } ); | ||
sinon.assert.notCalled( spy ); | ||
} ); | ||
} ); | ||
expect( balloonPanelView.isVisible ).to.true; | ||
it( 'should hide the #_balloon after Esc key press (from the form) and focus the editable', () => { | ||
const spy = testUtils.sinon.spy( linkFeature, '_hidePanel' ); | ||
const keyEvtData = { | ||
keyCode: keyCodes.esc, | ||
preventDefault: sinon.spy(), | ||
stopPropagation: sinon.spy() | ||
}; | ||
editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 1 ) ], true ); | ||
editor.editing.view.render(); | ||
return linkFeature._showPanel( true ) | ||
.then( () => { | ||
formView.keystrokes.press( keyEvtData ); | ||
expect( balloonPanelView.isVisible ).to.false; | ||
} ); | ||
sinon.assert.calledWithExactly( spy, true ); | ||
} ); | ||
} ); | ||
} ); | ||
it( 'should close when selection becomes non-collapsed', () => { | ||
const observer = editor.editing.view.getObserver( ClickObserver ); | ||
describe( 'mouse support', () => { | ||
it( 'should hide #_balloon and not focus editable on click outside the #_balloon', () => { | ||
const spy = testUtils.sinon.spy( linkFeature, '_hidePanel' ); | ||
editor.document.schema.allow( { name: '$text', inside: '$root' } ); | ||
setModelData( editor.document, '<$text linkHref="url">f[]oo</$text>' ); | ||
return linkFeature._showPanel( true ) | ||
.then( () => { | ||
document.body.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) ); | ||
const root = editor.editing.view.getRoot(); | ||
const text = root.getChild( 0 ).getChild( 0 ); | ||
sinon.assert.calledWithExactly( spy ); | ||
} ); | ||
} ); | ||
observer.fire( 'click', { target: {} } ); | ||
it( 'should not hide #_balloon on click inside the #_balloon', () => { | ||
const spy = testUtils.sinon.spy( linkFeature, '_hidePanel' ); | ||
expect( balloonPanelView.isVisible ).to.true; | ||
return linkFeature._showPanel( true ) | ||
.then( () => { | ||
balloon.view.element.dispatchEvent( new Event( 'mouseup', { bubbles: true } ) ); | ||
editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 1, text, 2 ) ] ); | ||
editor.editing.view.render(); | ||
sinon.assert.notCalled( spy ); | ||
} ); | ||
} ); | ||
expect( balloonPanelView.isVisible ).to.false; | ||
describe( 'clicking on editable', () => { | ||
let observer; | ||
beforeEach( () => { | ||
observer = editor.editing.view.getObserver( ClickObserver ); | ||
} ); | ||
it( 'should stop updating position after close', () => { | ||
const observer = editor.editing.view.getObserver( ClickObserver ); | ||
it( 'should open with not selected formView when collapsed selection is inside link element', () => { | ||
// Method is stubbed because it returns internal promise which can't be returned in test. | ||
const spy = testUtils.sinon.stub( linkFeature, '_showPanel', () => {} ); | ||
editor.document.schema.allow( { name: '$text', inside: '$root' } ); | ||
setModelData( editor.document, '<$text linkHref="url">b[]ar</$text>' ); | ||
setModelData( editor.document, '<$text linkHref="url">fo[]o</$text>' ); | ||
const root = editor.editing.view.getRoot(); | ||
const text = root.getChild( 0 ).getChild( 0 ); | ||
observer.fire( 'click', { target: document.body } ); | ||
observer.fire( 'click', { target: {} } ); | ||
expect( balloonPanelView.isVisible ).to.true; | ||
balloonPanelView.isVisible = false; | ||
const attachToSpy = testUtils.sinon.spy( balloonPanelView, 'attachTo' ); | ||
editor.editing.view.selection.setRanges( [ Range.createFromParentsAndOffsets( text, 2, text, 2 ) ], true ); | ||
editor.editing.view.render(); | ||
expect( attachToSpy.notCalled ).to.true; | ||
sinon.assert.calledWithExactly( spy ); | ||
} ); | ||
it( 'should not open when selection is not inside link element', () => { | ||
const observer = editor.editing.view.getObserver( ClickObserver ); | ||
const showSpy = testUtils.sinon.stub( linkFeature, '_showPanel' ); | ||
@@ -409,7 +617,7 @@ setModelData( editor.document, '[]' ); | ||
expect( balloonPanelView.isVisible ).to.false; | ||
sinon.assert.notCalled( showSpy ); | ||
} ); | ||
it( 'should not open when selection is non-collapsed', () => { | ||
const observer = editor.editing.view.getObserver( ClickObserver ); | ||
const showSpy = testUtils.sinon.stub( linkFeature, '_showPanel' ); | ||
@@ -419,5 +627,5 @@ editor.document.schema.allow( { name: '$text', inside: '$root' } ); | ||
observer.fire( 'click', { target: document.body } ); | ||
observer.fire( 'click', { target: {} } ); | ||
expect( balloonPanelView.isVisible ).to.false; | ||
sinon.assert.notCalled( showSpy ); | ||
} ); | ||
@@ -428,9 +636,19 @@ } ); | ||
describe( 'link form', () => { | ||
let hidePanelSpy, focusEditableSpy; | ||
let focusEditableSpy; | ||
beforeEach( () => { | ||
hidePanelSpy = testUtils.sinon.spy( balloonPanelView, 'hide' ); | ||
focusEditableSpy = testUtils.sinon.spy( editor.editing.view, 'focus' ); | ||
} ); | ||
it( 'should mark the editor ui as focused when the #formView is focused', () => { | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
editor.ui.focusTracker.isFocused = false; | ||
formView.element.dispatchEvent( new Event( 'focus' ) ); | ||
expect( editor.ui.focusTracker.isFocused ).to.true; | ||
} ); | ||
} ); | ||
describe( 'binding', () => { | ||
@@ -460,6 +678,9 @@ it( 'should bind formView.urlInputView#value to link command value', () => { | ||
it( 'should hide and focus editable on formView#submit event', () => { | ||
formView.fire( 'submit' ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
formView.fire( 'submit' ); | ||
expect( hidePanelSpy.calledOnce ).to.true; | ||
expect( focusEditableSpy.calledOnce ).to.true; | ||
expect( balloon.visibleView ).to.null; | ||
expect( focusEditableSpy.calledOnce ).to.true; | ||
} ); | ||
} ); | ||
@@ -477,13 +698,19 @@ | ||
it( 'should hide and focus editable on formView#unlink event', () => { | ||
formView.fire( 'unlink' ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
formView.fire( 'unlink' ); | ||
expect( hidePanelSpy.calledOnce ).to.true; | ||
expect( focusEditableSpy.calledOnce ).to.true; | ||
expect( balloon.visibleView ).to.null; | ||
expect( focusEditableSpy.calledOnce ).to.true; | ||
} ); | ||
} ); | ||
it( 'should hide and focus editable on formView#cancel event', () => { | ||
formView.fire( 'cancel' ); | ||
return linkFeature._showPanel() | ||
.then( () => { | ||
formView.fire( 'cancel' ); | ||
expect( hidePanelSpy.calledOnce ).to.true; | ||
expect( focusEditableSpy.calledOnce ).to.true; | ||
expect( balloon.visibleView ).to.null; | ||
expect( focusEditableSpy.calledOnce ).to.true; | ||
} ); | ||
} ); | ||
@@ -490,0 +717,0 @@ } ); |
@@ -10,3 +10,5 @@ /** | ||
import UnlinkCommand from '../src/unlinkcommand'; | ||
import VirtualTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/virtualtesteditor'; | ||
import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; | ||
import { getData as getModelData, setData as setModelData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; | ||
@@ -20,3 +22,3 @@ import { getData as getViewData } from '@ckeditor/ckeditor5-engine/src/dev-utils/view'; | ||
return VirtualTestEditor.create( { | ||
plugins: [ LinkEngine ] | ||
plugins: [ Paragraph, LinkEngine ] | ||
} ) | ||
@@ -27,4 +29,2 @@ .then( newEditor => { | ||
doc = editor.document; | ||
doc.schema.allow( { name: '$text', inside: '$root' } ); | ||
} ); | ||
@@ -38,3 +38,4 @@ } ); | ||
it( 'should set proper schema rules', () => { | ||
expect( doc.schema.check( { name: '$inline', attributes: [ 'linkHref' ] } ) ).to.be.true; | ||
expect( doc.schema.check( { name: '$inline', attributes: [ 'linkHref' ], inside: '$root' } ) ).to.be.false; | ||
expect( doc.schema.check( { name: '$inline', attributes: [ 'linkHref' ], inside: '$block' } ) ).to.be.true; | ||
} ); | ||
@@ -62,6 +63,19 @@ | ||
it( 'should convert `<a href="url">` to `linkHref="url"` attribute', () => { | ||
editor.setData( '<p><a href="url">foo</a>bar</p>' ); | ||
expect( getModelData( doc, { withoutSelection: true } ) ) | ||
.to.equal( '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' ); | ||
expect( editor.getData() ).to.equal( '<p><a href="url">foo</a>bar</p>' ); | ||
} ); | ||
it( 'should be integrated with autoparagraphing', () => { | ||
// Incorrect results because autoparagraphing works incorrectly (issue in paragraph). | ||
// https://github.com/ckeditor/ckeditor5-paragraph/issues/10 | ||
editor.setData( '<a href="url">foo</a>bar' ); | ||
expect( getModelData( doc, { withoutSelection: true } ) ).to.equal( '<$text linkHref="url">foo</$text>bar' ); | ||
expect( editor.getData() ).to.equal( '<a href="url">foo</a>bar' ); | ||
expect( getModelData( doc, { withoutSelection: true } ) ).to.equal( '<paragraph>foobar</paragraph>' ); | ||
expect( editor.getData() ).to.equal( '<p>foobar</p>' ); | ||
} ); | ||
@@ -72,13 +86,13 @@ } ); | ||
it( 'should convert attribute', () => { | ||
setModelData( doc, '<$text linkHref="url">foo</$text>bar' ); | ||
setModelData( doc, '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' ); | ||
expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( '<a href="url">foo</a>bar' ); | ||
expect( getViewData( editor.editing.view, { withoutSelection: true } ) ).to.equal( '<p><a href="url">foo</a>bar</p>' ); | ||
} ); | ||
it( 'should convert to `LinkElement` instance', () => { | ||
setModelData( doc, '<$text linkHref="url">foo</$text>bar' ); | ||
setModelData( doc, '<paragraph><$text linkHref="url">foo</$text>bar</paragraph>' ); | ||
expect( editor.editing.view.getRoot().getChild( 0 ) ).to.be.instanceof( LinkElement ); | ||
expect( editor.editing.view.getRoot().getChild( 0 ).getChild( 0 ) ).to.be.instanceof( LinkElement ); | ||
} ); | ||
} ); | ||
} ); |
@@ -29,2 +29,3 @@ /** | ||
expect( view.element.classList.contains( 'ck-link-form' ) ).to.true; | ||
expect( view.element.getAttribute( 'tabindex' ) ).to.equal( '-1' ); | ||
} ); | ||
@@ -31,0 +32,0 @@ |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
115648
60
2103
+ Added@ckeditor/ckeditor5-theme-lark@0.8.0(transitive)
+ Added@ckeditor/ckeditor5-ui@0.9.0(transitive)
- Removed@ckeditor/ckeditor5-engine@0.9.0(transitive)
- Removed@ckeditor/ckeditor5-theme-lark@0.7.0(transitive)
- Removed@ckeditor/ckeditor5-ui@0.8.0(transitive)