Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Feature: Introduced BlockToolbar plugin. #392

Merged
merged 57 commits into from May 30, 2018
Merged
Show file tree
Hide file tree
Changes from 44 commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8e074be
Introduced BlockToolbar plugin.
oskarwrobel May 18, 2018
85d490f
Added missing dev dependencies.
oskarwrobel May 18, 2018
660f62b
Moved blocktoolbar styles to correct directory.
May 18, 2018
970e357
Tests: Added very basic block toolbar manual test.
oskarwrobel May 18, 2018
6bb0a80
Changed `ltrs-toolbar-block-button` to `ck-toolbar-block-button`.
May 18, 2018
315c7a7
Added importing stylesheet to blocktoolbar view.
May 18, 2018
159dee0
Removed obsolete CSS properties.
May 18, 2018
113984e
Added extra class to blockToolbar.panelView.
May 18, 2018
136e116
Fix: Fixed NaN value in a case of missing line-height property.
oskarwrobel May 18, 2018
7fb9097
Tests: Removed unused code from MT.
oskarwrobel May 18, 2018
e45d69d
Tests: Changed sinon.reset() to sinon.resetHistory().
oskarwrobel May 18, 2018
808db85
Changed naming of block toolbar.
May 18, 2018
90186d3
Change: Changed block toolbar panel limiter.
oskarwrobel May 18, 2018
d7dd0b5
Tests: Improved block toolbar manual test.
oskarwrobel May 18, 2018
a9db7dd
Changed class name.
oskarwrobel May 18, 2018
bab9252
Typo.
May 18, 2018
2fce455
Removed duplicated icon.
oskarwrobel May 18, 2018
6f12abc
Docs: Improved API docs.
oskarwrobel May 18, 2018
92e3851
Tests: Added more toolbar items.
oskarwrobel May 18, 2018
567bc88
Added decorable method for checking if block toolbar is allowed + imp…
oskarwrobel May 18, 2018
fe875a5
Docs: Added missind event docs.
oskarwrobel May 18, 2018
421f1ca
Tests: Improved CC.
oskarwrobel May 18, 2018
33fd205
Docs: Fixed invalid link.
oskarwrobel May 18, 2018
904fcd9
Tests: Fixed test that not check anything.
oskarwrobel May 18, 2018
57beddc
Tests: Improved manual test.
oskarwrobel May 18, 2018
d3c75e0
Tests: Added test cases to manual test.
oskarwrobel May 18, 2018
bf0939c
Added missing dependencies.
oskarwrobel May 18, 2018
6a0f1ec
Tests: Made tests more bulletproof.
oskarwrobel May 18, 2018
2cdcee4
Tests: Used getComputedStyle mock instead of setting styles.
oskarwrobel May 19, 2018
eb6a3c4
Docs: Added EditorConfig docs.
oskarwrobel May 19, 2018
ff2c991
Focused toolbar on panel open.
oskarwrobel May 21, 2018
aed85eb
Tests: Added test case with external changes.
oskarwrobel May 21, 2018
d79ecdb
Allowed BlockToolbar to be displayed next to $block elements.
oskarwrobel May 21, 2018
6b0b6ce
Improved BlockToolbar vertical aligning.
oskarwrobel May 21, 2018
2b9b439
Changed BlockToolbar horizontal position.
oskarwrobel May 21, 2018
b7176d7
Improved checking if BlockToolbar is allowed to be displayed.
oskarwrobel May 22, 2018
48e8cce
Docs: Improved BlockToolbar general docs.
oskarwrobel May 22, 2018
ca02b84
Tests: Improved test names.
oskarwrobel May 22, 2018
0153af0
Improved BlockToolbar manual test.
oskarwrobel May 22, 2018
13cd9eb
Removed decorable method for disabling BlockToolbar.
oskarwrobel May 22, 2018
0a6bc44
Tests: Adjusted manual test to last changes in BlockToolbar.
oskarwrobel May 22, 2018
8052d29
Refactored handling read-only by BlockToolbar mode.
oskarwrobel May 22, 2018
685364c
Tests: Minor improvements in manual test.
oskarwrobel May 22, 2018
3e2cb20
Added missing dev dependency.
oskarwrobel May 22, 2018
1a6e145
Docs: Added missing view docs.
oskarwrobel May 22, 2018
b2aad6d
Added additional class to the ToolbarView in BlockToolbar.
oskarwrobel May 22, 2018
c05c836
Changed additional BalloonPanelView class.
oskarwrobel May 22, 2018
c241cc9
Hide BlockButton on init.
oskarwrobel May 23, 2018
bd8c2b3
Docs: Minor change.
oskarwrobel May 24, 2018
2b807fc
Tests: Changed test case of manual test.
oskarwrobel May 28, 2018
d0c8480
Docs: Removed obsolete event docs.
oskarwrobel May 28, 2018
ec3caaa
Docs: Improved docs in the BlockToolbar plugin.
oleq May 28, 2018
49b3538
Docs, Tests: Improved comments and manual test descriptions.
oleq May 28, 2018
b5ddbfd
Tests: Improved assertions in the BlockToolbar tests.
oleq May 28, 2018
a8e7cf4
Tests: Improved ButtonToolbar unit test descriptions.
oleq May 28, 2018
54ef26f
Moved BlockButtonView from the view directory.
oskarwrobel May 29, 2018
0e87365
Moved the pilcrow icon to the ckeditor5-core.
oleq May 30, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions package.json
Expand Up @@ -15,14 +15,18 @@
},
"devDependencies": {
"@ckeditor/ckeditor5-basic-styles": "^10.0.0",
"@ckeditor/ckeditor5-block-quote": "^10.0.0",
"@ckeditor/ckeditor5-cloud-services": "^10.0.0",
"@ckeditor/ckeditor5-editor-balloon": "^10.0.0",
"@ckeditor/ckeditor5-editor-classic": "^10.0.0",
"@ckeditor/ckeditor5-engine": "^10.0.0",
"@ckeditor/ckeditor5-enter": "^10.0.0",
"@ckeditor/ckeditor5-easy-image": "^10.0.0",
"@ckeditor/ckeditor5-essentials": "^10.0.0",
"@ckeditor/ckeditor5-heading": "^10.0.0",
"@ckeditor/ckeditor5-image": "^10.0.0",
"@ckeditor/ckeditor5-link": "^10.0.0",
"@ckeditor/ckeditor5-list": "^10.0.0",
"@ckeditor/ckeditor5-paragraph": "^10.0.0",
"@ckeditor/ckeditor5-typing": "^10.0.0",
"@ckeditor/ckeditor5-undo": "^10.0.0",
Expand Down
342 changes: 342 additions & 0 deletions src/toolbar/block/blocktoolbar.js
@@ -0,0 +1,342 @@
/**
* Copyright (c) 2016 - 2017, CKSource - Frederico Knabben. All rights reserved.
*/

/**
* @module ui/toolbar/block/blocktoolbar
*/

/* global window */

import Plugin from '@ckeditor/ckeditor5-core/src/plugin';

import BlockButtonView from './view/blockbuttonview';
import BalloonPanelView from '../../panel/balloon/balloonpanelview';
import ToolbarView from '../toolbarview';

import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
import clickOutsideHandler from '../../bindings/clickoutsidehandler';

import { getOptimalPosition } from '@ckeditor/ckeditor5-utils/src/dom/position';
import Rect from '@ckeditor/ckeditor5-utils/src/dom/rect';

import iconPilcrow from '../../../theme/icons/pilcrow.svg';

/**
* The block toolbar plugin.
*
* This plugin provides button attached to the block of content where the selection is currently placed.
* After clicking on the button, dropdown with editor features defined through
* {@link module:core/editor/editorconfig~EditorConfig#blockToolbar} appears.
*
* By default button is allowed to be displayed next to all elements marked in
* {@link module:engine/model/schema~Schema} as `$block` elements for which there is at least
* one available option in toolbar. E.g. Toolbar with {@link module:paragraph/paragraph~Paragraph} and
* {@link module:heading/heading~Heading} won't be displayed next to {@link module:image/image~Image} because
* {@link module:engine/model/schema~Schema} disallows to change format of {@link module:image/image~Image}.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true anymore.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still true. BlockToolbar will be not displayed next to element for which schema disallows to modify it using toolbar options. Still schema decides.

*
* By default button right bound will be attached to the left bound of the
* {@link module:engine/view/editableelement~EditableElement}:
*
* __ |
* | || This is a block of content that
* ¯¯ | button is attached to. This is a
* | block of content that button is
* | attached to.
*
* The position of the button can be adjusted using css transform:
*
* .ck-block-toolbar-button {
* transform: translateX( -10px );
* }
*
* __ |
* | | | This is a block of content that
* ¯¯ | button is attached to. This is a
* | block of content that button is
* | attached to.
*
* @extends module:core/plugin~Plugin
*/
export default class BlockToolbar extends Plugin {
/**
* @inheritDoc
*/
static get pluginName() {
return 'BlockToolbar';
}

/**
* @inheritDoc
*/
init() {
const editor = this.editor;

editor.editing.view.addObserver( ClickObserver );

/**
* Toolbar view.
*
* @type {module:ui/toolbar/toolbarview~ToolbarView}
*/
this.toolbarView = new ToolbarView( editor.locale );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely missing

this.toolbar.extendTemplate( {
	attributes: {
		class: [
			// https://github.com/ckeditor/ckeditor5-editor-inline/issues/11
			'ck-toolbar_floating'
		]
	}
} );

just like in the InlineEditorUIView and BalloonToolbar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


/**
* Panel view.
*
* @type {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
*/
this.panelView = this._createPanelView();

/**
* Button view.
*
* @type {module:ui/toolbar/block/view/blockbuttonview~BlockButtonView}
*/
this.buttonView = this._createButtonView();

// Close #panelView on click out of the plugin UI.
clickOutsideHandler( {
emitter: this.panelView,
contextElements: [ this.panelView.element, this.buttonView.element ],
activator: () => this.panelView.isVisible,
callback: () => this._hidePanel()
} );

// Try to hide button when editor switch to read-only.
// Do not hide when panel was visible to avoid confusing situation when
// UI unexpectedly disappears.
this.listenTo( editor, 'change:isReadOnly', () => {
if ( !this.panelView.isVisible ) {
this.buttonView.isVisible = false;
}
} );
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it mean that if the panel was visible the button won't disappears when the panel will be hidden? For me, it is fine if the panel will disappear when you switch to the read-only mode.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When editor switches to read-ony while you are using BlockButton (panel is opened) then panel won't disappear as long as you don't close it. When you close it button will disappear as well. This is to avoid situation when you want to click something on panel but pannel together with button disappears unexpectedly because of editor switches to read-only.


// Enable as default.
this._initListeners();
}

/**
* Creates toolbar components based on given configuration.
* This needs to be done when all plugins are ready.
*
* @inheritDoc
*/
afterInit() {
const factory = this.editor.ui.componentFactory;
const config = this.editor.config.get( 'blockToolbar' );

this.toolbarView.fillFromConfig( config, factory );

// Hide panel before executing each button in the panel.
for ( const item of this.toolbarView.items ) {
item.on( 'execute', () => this._hidePanel( true ), { priority: 'high' } );
}
}

/**
* Creates panel view.
*
* @private
* @returns {module:ui/panel/balloon/balloonpanelview~BalloonPanelView}
*/
_createPanelView() {
const editor = this.editor;
const panelView = new BalloonPanelView( editor.locale );

panelView.content.add( this.toolbarView );
panelView.className = 'ck-balloon-panel-block-toolbar';
editor.ui.view.body.add( panelView );
editor.ui.focusTracker.add( panelView.element );

// Close #panelView on `Esc` press.
this.toolbarView.keystrokes.set( 'Esc', ( evt, cancel ) => {
this._hidePanel( true );
cancel();
} );

return panelView;
}

/**
* Creates button view.
*
* @private
* @returns {module:ui/toolbar/block/view/blockbuttonview~BlockButtonView}
*/
_createButtonView() {
const editor = this.editor;
const buttonView = new BlockButtonView( editor.locale );

buttonView.label = editor.t( 'Edit block' );
buttonView.icon = iconPilcrow;
buttonView.withText = false;

// Bind panelView to buttonView.
buttonView.bind( 'isOn' ).to( this.panelView, 'isVisible' );
buttonView.bind( 'tooltip' ).to( this.panelView, 'isVisible', isVisible => !isVisible );

// Toggle panelView on buttonView#execute.
this.listenTo( buttonView, 'execute', () => {
if ( !this.panelView.isVisible ) {
this._showPanel();
} else {
this._hidePanel( true );
}
} );

editor.ui.view.body.add( buttonView );
editor.ui.focusTracker.add( buttonView.element );

return buttonView;
}

/**
* Starts displaying button next to allowed elements.
*
* @private
*/
_initListeners() {
const editor = this.editor;
const model = editor.model;
const view = editor.editing.view;
let modelTarget, domTarget;

// Hides panel on a direct selection change.
this.listenTo( editor.model.document.selection, 'change:range', ( evt, data ) => {
if ( data.directChange ) {
this._hidePanel();
}
} );

this.listenTo( view, 'render', () => {
// Get first selected block, button will be attached to this element.
modelTarget = Array.from( model.document.selection.getSelectedBlocks() )[ 0 ];

// Do not attach block button when there is no enabled item in toolbar for current block element.
if ( !modelTarget || Array.from( this.toolbarView.items ).every( item => !item.isEnabled ) ) {
this.buttonView.isVisible = false;

return;
}

// Get DOM target element.
domTarget = view.domConverter.mapViewToDom( editor.editing.mapper.toViewElement( modelTarget ) );

// Show block button.
this.buttonView.isVisible = true;

// Attach block button to target DOM element.
this._attachButtonToElement( domTarget );

// When panel is opened then refresh it position to be properly aligned with block button.
if ( this.panelView.isVisible ) {
this._showPanel();
}
}, { priority: 'low' } );

this.listenTo( this.buttonView, 'change:isVisible', ( evt, name, isVisible ) => {
if ( isVisible ) {
// Keep correct position of button and panel on window#resize.
this.buttonView.listenTo( window, 'resize', () => this._attachButtonToElement( domTarget ) );
} else {
// Stop repositioning button when is hidden.
this.buttonView.stopListening( window, 'resize' );

// Hide the panel when the button disappears.
this._hidePanel();
}
} );
}

/**
* Attaches #buttonView to the target block of content.
*
* @protected
* @param {HTMLElement} targetElement Target element.
*/
_attachButtonToElement( targetElement ) {
const contentStyles = window.getComputedStyle( targetElement );

const editableRect = new Rect( this.editor.ui.view.editableElement );
const contentPaddingTop = parseInt( contentStyles.paddingTop, 10 );
// When line height is not an integer then thread it as "normal".
// MDN says that 'normal' == ~1.2 on desktop browsers.
const contentLineHeight = parseInt( contentStyles.lineHeight, 10 ) || parseInt( contentStyles.fontSize, 10 ) * 1.2;

const position = getOptimalPosition( {
element: this.buttonView.element,
target: targetElement,
positions: [
( contentRect, buttonRect ) => {
return {
top: contentRect.top + contentPaddingTop + ( ( contentLineHeight - buttonRect.height ) / 2 ),
left: editableRect.left - buttonRect.width
};
}
]
} );

this.buttonView.top = position.top;
this.buttonView.left = position.left;
}

/**
* Shows toolbar attached to the block button.
* When toolbar is already opened then just repositions it.
*
* @private
*/
_showPanel() {
const wasVisible = this.panelView.isVisible;

this.panelView.pin( {
target: this.buttonView.element,
limiter: this.editor.ui.view.editableElement
} );

if ( !wasVisible ) {
this.toolbarView.items.get( 0 ).focus();
}
}

/**
* Hides toolbar.
*
* @private
* @param {Boolean} [focusEditable=false] When `true` then editable will be focused after hiding panel.
*/
_hidePanel( focusEditable ) {
this.panelView.isVisible = false;

if ( focusEditable ) {
this.editor.editing.view.focus();
}
}

/**
* This event is fired when {@link #checkAllowed} method is executed. It makes it possible to override
* default method behavior and provides a custom rules.
*
* @event checkAllowed
*/
}

/**
* Block toolbar configuration. Used by the {@link module:ui/toolbar/block/blocktoolbar~BlockToolbar}
* feature.
*
* const config = {
* blockToolbar: [ 'paragraph', 'heading1', 'heading2', 'bulletedList', 'numberedList' ]
* };
*
* You can also use `'|'` to create a separator between groups of items:
*
* const config = {
* blockToolbar: [ 'paragraph', 'heading1', 'heading2', '|', 'bulletedList', 'numberedList' ]
* };
*
* Read also about configuring the main editor toolbar in {@link module:core/editor/editorconfig~EditorConfig#toolbar}.
*
* @member {Array.<String>|Object} module:core/editor/editorconfig~EditorConfig#blockToolbar
*/