/** * @license Copyright (c) 2003-2018, CKSource - Frederico Knabben. All rights reserved. * For licensing, see LICENSE.md. */ /* global document */ import ClassicTestEditor from '@ckeditor/ckeditor5-core/tests/_utils/classictesteditor'; import BalloonEditor from '@ckeditor/ckeditor5-editor-balloon/src/ballooneditor'; import Plugin from '@ckeditor/ckeditor5-core/src/plugin'; import Paragraph from '@ckeditor/ckeditor5-paragraph/src/paragraph'; import Bold from '@ckeditor/ckeditor5-basic-styles/src/bold'; import Widget from '../src/widget'; import WidgetToolbarRepository from '../src/widgettoolbarrepository'; import { isWidget, toWidget } from '../src/utils'; import { downcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/downcast-converters'; import { upcastElementToElement } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview'; import View from '@ckeditor/ckeditor5-ui/src/view'; import { setData } from '@ckeditor/ckeditor5-engine/src/dev-utils/model'; import testUtils from '@ckeditor/ckeditor5-core/tests/_utils/utils'; import CKEditorError from '@ckeditor/ckeditor5-utils/src/ckeditorerror'; describe( 'WidgetToolbarRepository', () => { let editor, model, balloon, widgetToolbarRepository, editorElement; testUtils.createSinonSandbox(); beforeEach( () => { editorElement = document.createElement( 'div' ); document.body.appendChild( editorElement ); return ClassicTestEditor .create( editorElement, { plugins: [ Paragraph, FakeButton, WidgetToolbarRepository, FakeWidget ], fake: { toolbar: [ 'fake_button' ] } } ) .then( newEditor => { editor = newEditor; model = newEditor.model; widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository ); balloon = editor.plugins.get( 'ContextualBalloon' ); } ); } ); afterEach( () => { editorElement.remove(); return editor.destroy(); } ); it( 'should be loaded', () => { expect( editor.plugins.get( WidgetToolbarRepository ) ).to.be.instanceOf( WidgetToolbarRepository ); } ); describe( 'register()', () => { it( 'should create a widget toolbar and add it to the collection', () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: () => false, } ); expect( widgetToolbarRepository._toolbars.size ).to.equal( 1 ); expect( widgetToolbarRepository._toolbars.get( 'fake' ) ).to.be.an( 'object' ); } ); it( 'should throw when adding two times widget with the same id', () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: () => false } ); expect( () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: () => false } ); } ).to.throw( CKEditorError, /^widget-toolbar-duplicated/ ); } ); } ); describe( 'integration tests', () => { beforeEach( () => { editor.ui.focusTracker.isFocused = true; } ); it( 'toolbar should be visible when the `visibleWhen` callback returns true', () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: isFakeWidgetSelected } ); setData( model, 'foo[]' ); const fakeWidgetToolbarView = widgetToolbarRepository._toolbars.get( 'fake' ).view; expect( balloon.visibleView ).to.equal( fakeWidgetToolbarView ); } ); it( 'toolbar should be hidden when the `visibleWhen` callback returns false', () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: isFakeWidgetSelected } ); setData( model, '[foo]' ); expect( balloon.visibleView ).to.equal( null ); } ); it( 'toolbar should be hidden when the `visibleWhen` callback returns false #2', () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: isFakeWidgetSelected } ); setData( model, 'foo[]' ); model.change( writer => { // Select the foo. writer.setSelection( model.document.getRoot().getChild( 0 ), 'in' ); } ); expect( balloon.visibleView ).to.equal( null ); } ); it( 'toolbar should update its position when other widget is selected', () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: isFakeWidgetSelected } ); setData( model, '[]' ); model.change( writer => { // Select the second widget. writer.setSelection( model.document.getRoot().getChild( 1 ), 'on' ); } ); const fakeWidgetToolbarView = widgetToolbarRepository._toolbars.get( 'fake' ).view; expect( balloon.visibleView ).to.equal( fakeWidgetToolbarView ); } ); it( 'it should be possible to create a widget toolbar for content inside the widget', () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: isFakeWidgetContentSelected } ); setData( model, '[foo]' ); const fakeWidgetToolbarView = widgetToolbarRepository._toolbars.get( 'fake' ).view; expect( balloon.visibleView ).to.equal( fakeWidgetToolbarView ); } ); it( 'toolbar should not engage when is in the balloon yet invisible', () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: isFakeWidgetSelected } ); const fakeWidgetToolbarView = widgetToolbarRepository._toolbars.get( 'fake' ).view; setData( model, '[]' ); expect( balloon.visibleView ).to.equal( fakeWidgetToolbarView ); const lastView = new View(); lastView.element = document.createElement( 'div' ); balloon.add( { view: lastView, position: { target: document.body } } ); expect( balloon.visibleView ).to.equal( lastView ); editor.ui.fire( 'update' ); expect( balloon.visibleView ).to.equal( lastView ); } ); } ); } ); describe( 'WidgetToolbarRepository - integration with the BalloonToolbar', () => { let clock, editor, model, balloon, balloonToolbar, widgetToolbarRepository, editorElement; testUtils.createSinonSandbox(); beforeEach( () => { editorElement = document.createElement( 'div' ); document.body.appendChild( editorElement ); clock = testUtils.sinon.useFakeTimers(); return BalloonEditor .create( editorElement, { plugins: [ Paragraph, FakeButton, WidgetToolbarRepository, FakeWidget, Bold ], balloonToolbar: [ 'bold' ], fake: { toolbar: [ 'fake_button' ] } } ) .then( newEditor => { editor = newEditor; model = newEditor.model; widgetToolbarRepository = editor.plugins.get( WidgetToolbarRepository ); balloon = editor.plugins.get( 'ContextualBalloon' ); balloonToolbar = editor.plugins.get( 'BalloonToolbar' ); editor.editing.view.document.isFocused = true; } ); } ); afterEach( () => { editorElement.remove(); return editor.destroy(); } ); it( 'balloon toolbar should be hidden when the widget is selected', () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: isFakeWidgetSelected, } ); const fakeWidgetToolbarView = widgetToolbarRepository._toolbars.get( 'fake' ).view; setData( model, '[]foo' ); editor.ui.focusTracker.isFocused = true; clock.tick( 200 ); expect( balloon.visibleView ).to.equal( fakeWidgetToolbarView ); } ); it( 'balloon toolbar should be visible when the widget is not selected', () => { widgetToolbarRepository.register( 'fake', { items: editor.config.get( 'fake.toolbar' ), visibleWhen: isFakeWidgetSelected } ); setData( model, '[foo]' ); editor.ui.focusTracker.isFocused = true; clock.tick( 200 ); expect( balloon.visibleView ).to.equal( balloonToolbar.toolbarView ); } ); } ); const fakeWidgetSymbol = Symbol( 'fakeWidget' ); function isFakeWidgetSelected( selection ) { const viewElement = selection.getSelectedElement(); return !!viewElement && isWidget( viewElement ) && !!viewElement.getCustomProperty( fakeWidgetSymbol ); } function isFakeWidgetContentSelected( selection ) { const pos = selection.getFirstPosition(); let node = pos.parent; while ( node ) { if ( node.is( 'element' ) && isWidget( node ) && node.getCustomProperty( fakeWidgetSymbol ) ) { return true; } node = node.parent; } return false; } // Plugin that adds fake_button to editor's component factory. class FakeButton extends Plugin { init() { this.editor.ui.componentFactory.add( 'fake_button', locale => { const view = new ButtonView( locale ); view.set( { label: 'fake button' } ); return view; } ); } } // Simple widget plugin // It registers `` block in model and represents `div` in the view. // It allows having text inside self. class FakeWidget extends Plugin { static get requires() { return [ Widget ]; } init() { const editor = this.editor; const schema = editor.model.schema; schema.register( 'fake-widget', { isObject: true, isBlock: true, allowWhere: '$block', } ); schema.extend( '$text', { allowIn: 'fake-widget' } ); const conversion = editor.conversion; conversion.for( 'dataDowncast' ).add( downcastElementToElement( { model: 'fake-widget', view: ( modelElement, viewWriter ) => { return viewWriter.createContainerElement( 'div' ); } } ) ); conversion.for( 'editingDowncast' ).add( downcastElementToElement( { model: 'fake-widget', view: ( modelElement, viewWriter ) => { const fakeWidget = viewWriter.createContainerElement( 'div' ); viewWriter.setCustomProperty( fakeWidgetSymbol, true, fakeWidget ); return toWidget( fakeWidget, viewWriter, { label: 'fake-widget' } ); } } ) ); conversion.for( 'upcast' ).add( upcastElementToElement( { view: { name: 'div' }, model: ( view, modelWriter ) => { return modelWriter.createElement( 'fake-widget' ); } } ) ); } }