Skip to content
This repository has been archived by the owner on Nov 16, 2017. It is now read-only.

t/131: Contextual toolbar sample #135

Merged
merged 17 commits into from
Dec 16, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 54 additions & 12 deletions src/balloonpanel/balloonpanelview.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ import { getOptimalPosition } from '../../utils/dom/position.js';
import toUnit from '../../utils/dom/tounit.js';

const toPx = toUnit( 'px' );
const arrowHOffset = 30;
const arrowVOffset = 15;

/**
* The balloon panel view class.
Expand Down Expand Up @@ -51,8 +49,12 @@ export default class BalloonPanelView extends View {
this.set( 'left', 0 );

/**
* Balloon panel's current position. Must correspond with
* {@link module:ui/balloonpanel/balloonpanelview~BalloonPanelView.defaultPositions}.
* Balloon panel's current position. The position name is reflected in the CSS class set
* to the balloon, i.e. `.ck-balloon-panel_arrow_se` for "se" position. The class
* controls the minor aspects of the balloon's visual appearance like placement
* of the "arrow". To support a new position, an additional CSS must be created.
*
* Default position names correspond with {@link #defaultPositions}.
*
* @observable
* @default 'se'
Expand Down Expand Up @@ -158,6 +160,43 @@ export default class BalloonPanelView extends View {
}
}

/**
* A horizontal offset of the arrow tip from the edge of the balloon. Controlled by CSS.
*
* +-----|---------...
* | |
* | |
* | |
* | |
* +--+ | +------...
* \ | /
* \|/
* >|-----|<---------------- horizontal offset
*
* @default 30
* @member {Number} module:ui/balloonpanel/balloonpanelview~BalloonPanelView.arrowHorizontalOffset
*/
BalloonPanelView.arrowHorizontalOffset = 30;

/**
* A vertical offset of the arrow from the edge of the balloon. Controlled by CSS.
*
* +-------------...
* |
* |
* | /-- vertical offset
* | V
* +--+ +-----... ---------
* \ / |
* \/ |
* -------------------------------
* ^
*
* @default 15
* @member {Number} module:ui/balloonpanel/balloonpanelview~BalloonPanelView.arrowVerticalOffset
*/
BalloonPanelView.arrowVerticalOffset = 15;

/**
* A default set of positioning functions used by the balloon panel view
* when attaching using {@link #attachTo} method.
Expand Down Expand Up @@ -203,30 +242,33 @@ export default class BalloonPanelView extends View {
*
* Positioning functions must be compatible with {@link module:utils/dom/position~Position}.
*
* The name that position function returns will be reflected in balloon panel's class that
* controls the placement of the "arrow". See {@link #position} to learn more.
*
* @member {Object} module:ui/balloonpanel/balloonpanelview~BalloonPanelView.defaultPositions
*/
BalloonPanelView.defaultPositions = {
se: ( targetRect ) => ( {
top: targetRect.bottom + arrowVOffset,
left: targetRect.left + targetRect.width / 2 - arrowHOffset,
top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
name: 'se'
} ),

sw: ( targetRect, balloonRect ) => ( {
top: targetRect.bottom + arrowVOffset,
left: targetRect.left + targetRect.width / 2 - balloonRect.width + arrowHOffset,
top: targetRect.bottom + BalloonPanelView.arrowVerticalOffset,
left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
name: 'sw'
} ),

ne: ( targetRect, balloonRect ) => ( {
top: targetRect.top - balloonRect.height - arrowVOffset,
left: targetRect.left + targetRect.width / 2 - arrowHOffset,
top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
left: targetRect.left + targetRect.width / 2 - BalloonPanelView.arrowHorizontalOffset,
name: 'ne'
} ),

nw: ( targetRect, balloonRect ) => ( {
top: targetRect.top - balloonRect.height - arrowVOffset,
left: targetRect.left + targetRect.width / 2 - balloonRect.width + arrowHOffset,
top: targetRect.top - balloonRect.height - BalloonPanelView.arrowVerticalOffset,
left: targetRect.left + targetRect.width / 2 - balloonRect.width + BalloonPanelView.arrowHorizontalOffset,
name: 'nw'
} )
};
16 changes: 16 additions & 0 deletions tests/manual/contextualtoolbar/contextualtoolbar.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<head>
<link rel="stylesheet" href="/theme/ckeditor.css">
</head>

<div id="editor">
<p><em>This</em> is a <strong>first line</strong> of text.</p>
<p><em>This</em> is a <strong>second line</strong> of text.</p>
<p><em>This</em> is the <strong>end</strong> of text.</p>
</div>

<style>
.ck-editor {
margin: 5em auto;
max-width: 70%;
}
</style>
131 changes: 131 additions & 0 deletions tests/manual/contextualtoolbar/contextualtoolbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* @license Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
* For licensing, see LICENSE.md.
*/

/* globals window, document, console:false */

import ClassicEditor from 'ckeditor5/editor-classic/classic.js';
import DomEventObserver from 'ckeditor5/engine/view/observer/domeventobserver.js';
import Enter from 'ckeditor5/enter/enter.js';
import Typing from 'ckeditor5/typing/typing.js';
import Paragraph from 'ckeditor5/paragraph/paragraph.js';
import Undo from 'ckeditor5/undo/undo.js';
import Bold from 'ckeditor5/basic-styles/bold.js';
import Italic from 'ckeditor5/basic-styles/italic.js';

import Template from 'ckeditor5/ui/template.js';
import ToolbarView from 'ckeditor5/ui/toolbar/toolbarview.js';
import BalloonPanelView from 'ckeditor5/ui/balloonpanel/balloonpanelview.js';

const arrowVOffset = BalloonPanelView.arrowVerticalOffset;
const positions = {
// [text range]
// ^
// +-----------------+
// | Balloon |
// +-----------------+
forwardSelection: ( targetRect, balloonRect ) => ( {
top: targetRect.bottom + arrowVOffset,
left: targetRect.right - balloonRect.width / 2,
name: 's'
Copy link
Member

Choose a reason for hiding this comment

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

I'm thinking now... how does this value affect anything? It certainly isn't used anywhere in this code. So perhaps it doesn't need to be defined at all? Am I right that only a feature which wants to understand which position the getOptimalPosition function had returned needs to define it?

Copy link
Member

Choose a reason for hiding this comment

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

OK, I see that these values are also used in themes. So you should also update BallonPanelView#position values. Or even better – extract it to some typedef and document that it's related to the theme's classes.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not quite sure what you expect me to change. Positions are documented https://github.com/ckeditor/ckeditor5-utils/blob/master/src/dom/position.js#L263-L272. A position must have a name for any code using the getOptimalPosition utility to know, which of the positions have been chosen, not only what coordinates represent it. We've discussed it already.

Copy link
Member

Choose a reason for hiding this comment

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

What I'm expecting is that the set of supported names is defined somewhere. Initially I thought that this set is totally open, but it's not – the names must much the positions supported by the theme. So we should indicate (to help the developer understanding what it all is) that this is a set of 6 values and that you can add more if you style them.

} ),

// +-----------------+
// | Balloon |
// +-----------------+
// V
// [text range]
backwardSelection: ( targetRect, balloonRect ) => ( {
top: targetRect.top - balloonRect.height - arrowVOffset,
left: targetRect.left - balloonRect.width / 2,
name: 'n'
} )
};

ClassicEditor.create( document.querySelector( '#editor' ), {
plugins: [ Enter, Typing, Paragraph, Undo, Bold, Italic ],
toolbar: [ 'bold', 'italic', 'undo', 'redo' ]
} )
.then( editor => {
createContextualToolbar( editor );
window.editor = editor;
} )
.catch( err => {
console.error( err.stack );
} );

function createContextualToolbar( editor ) {
// Create a plain toolbar instance.
const toolbar = new ToolbarView();

// Create a BalloonPanelView instance.
const panel = new BalloonPanelView( editor.locale );

Template.extend( panel.template, {
attributes: {
class: [
'ck-toolbar__container',
]
}
} );

// Putting the toolbar inside of the balloon panel.
panel.content.add( toolbar );

editor.ui.view.body.add( panel ).then( () => {
const editingView = editor.editing.view;

// Fill the toolbar with some buttons. Simply copy default editor toolbar.
for ( let name of editor.config.get( 'toolbar' ) ) {
toolbar.items.add( editor.ui.componentFactory.create( name ) );
}

// Let the focusTracker know about new focusable UI element.
editor.ui.focusTracker.add( panel.element );

// Hide the panel when editor loses focus but no the other way around.
panel.listenTo( editor.ui.focusTracker, 'change:isFocused', ( evt, name, is, was ) => {
if ( was && !is ) {
panel.hide();
}
} );

// Add "mouseup" event observer. It's enought to use ClickObserver in Chrome
// but Firefox requires "mouseup" to work properly.
editingView.addObserver( class extends DomEventObserver {
get domEventType() {
return [ 'mouseup' ];
}

onDomEvent( domEvent ) {
this.fire( domEvent.type, domEvent );
}
} );

// Position the panel each time the user clicked in editable.
editor.listenTo( editingView, 'mouseup', () => {
// This implementation assumes that only non–collapsed selections gets the contextual toolbar.
if ( !editingView.selection.isCollapsed ) {
const isBackward = editingView.selection.isBackward;

// getBoundingClientRect() makes no sense when the selection spans across number
// of lines of text. Using getClientRects() allows us to browse micro–ranges
// that would normally make up the bounding client rect.
const rangeRects = editingView.domConverter.viewRangeToDom( editingView.selection.getFirstRange() ).getClientRects();

// Select the proper range rect depending on the direction of the selection.
const rangeRect = isBackward ? rangeRects.item( 0 ) : rangeRects.item( rangeRects.length - 1 );

panel.attachTo( {
target: rangeRect,
positions: [
positions[ isBackward ? 'backwardSelection' : 'forwardSelection' ]
]
} );
} else {
panel.hide();
}
} );
} );
}
5 changes: 5 additions & 0 deletions tests/manual/contextualtoolbar/contextualtoolbar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## Contextual toolbar demo

1. Create a non–collapsed selection.
2. Create another non–collapsed selection but in another direction.
3. For each selection, a contextual toolbar should appear and the beginning/end of the selection, duplicating main editor toolbar.
30 changes: 30 additions & 0 deletions theme/components/contextualtoolbar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) 2003-2016, CKSource - Frederico Knabben. All rights reserved.
// For licensing, see LICENSE.md or http://ckeditor.com/license

.ck-toolbar__container {
background: ck-color( 'foreground' );

.ck-toolbar {
@include ck-editor-toolbar;

border: 0;
}

&.ck-balloon-panel {
&_arrow_s,
&_arrow_se,
&_arrow_sw {
&:after {
border-bottom-color: ck-color( 'foreground' );
}
}

&_arrow_n,
&_arrow_ne,
&_arrow_nw {
&:after {
border-top-color: ck-color( 'foreground' );
}
}
}
}
1 change: 1 addition & 0 deletions theme/theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
// For licensing, see LICENSE.md or http://ckeditor.com/license

@import 'components/stickytoolbar';
@import 'components/contextualtoolbar';