@martindilling/ckeditor5-image-extended
Version:
Extended image features for CKEditor 5.
544 lines (474 loc) • 16.6 kB
JavaScript
/**
* @module image-extended/imageinsertui
*/
import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
import ClickObserver from '@ckeditor/ckeditor5-engine/src/view/observer/clickobserver';
// import Range from '@ckeditor/ckeditor5-engine/src/view/range';
// import { isLinkElement } from './utils';
import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
import clickOutsideHandler from '@ckeditor/ckeditor5-ui/src/bindings/clickoutsidehandler';
import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
import ImageInsertView from './ui/imageinsertview';
// import LinkFormView from './ui/linkformview';
// import LinkActionsView from './ui/linkactionsview';
import imageIcon from '@ckeditor/ckeditor5-core/theme/icons/image.svg';
// const linkKeystroke = 'Ctrl+K';
/**
* The link UI plugin. It introduces the Link and Unlink buttons and the <kbd>Ctrl+K</kbd> keystroke.
*
* It uses the
* {@link module:ui/panel/balloon/contextualballoon~ContextualBalloon contextual balloon plugin}.
*
* @extends module:core/plugin~Plugin
*/
export default class ImageInsertUI extends Plugin {
/**
* @inheritDoc
*/
static get requires() {
return [ ContextualBalloon ];
}
/**
* @inheritDoc
*/
init() {
const editor = this.editor;
editor.editing.view.addObserver( ClickObserver );
// /**
// * The actions view displayed inside of the balloon.
// *
// * @member {module:image-extended/ui/linkactionsview~LinkActionsView}
// */
// this.editView = this._createEditView();
/**
* The form view displayed inside the balloon.
*
* @member {module:image-extended/ui/imageinsertview~ImageInsertView}
*/
this.insertView = this._createInsertView();
/**
* The contextual balloon plugin instance.
*
* @private
* @member {module:ui/panel/balloon/contextualballoon~ContextualBalloon}
*/
this._balloon = editor.plugins.get( ContextualBalloon );
// Create toolbar buttons.
this._createToolbarButton();
// Attach lifecycle actions to the the balloon.
this._enableUserBalloonInteractions();
}
// /**
// * Creates the {@link module:image-extended/ui/linkactionsview~LinkActionsView} instance.
// *
// * @private
// * @returns {module:image-extended/ui/linkactionsview~LinkActionsView} The link actions view instance.
// */
// _createEditView() {
// const editor = this.editor;
// const actionsView = new LinkActionsView( editor.locale );
// const linkCommand = editor.commands.get( 'link' );
// const unlinkCommand = editor.commands.get( 'unlink' );
//
// actionsView.bind( 'href' ).to( linkCommand, 'value' );
// actionsView.editButtonView.bind( 'isEnabled' ).to( linkCommand );
// actionsView.unlinkButtonView.bind( 'isEnabled' ).to( unlinkCommand );
//
// // Execute unlink command after clicking on the "Edit" button.
// this.listenTo( actionsView, 'edit', () => {
// this._addInsertView();
// } );
//
// // Execute unlink command after clicking on the "Unlink" button.
// this.listenTo( actionsView, 'unlink', () => {
// editor.execute( 'unlink' );
// this._hideUI();
// } );
//
// // Close the panel on esc key press when the **actions have focus**.
// actionsView.keystrokes.set( 'Esc', ( data, cancel ) => {
// this._hideUI();
// cancel();
// } );
//
// // Open the form view on Ctrl+K when the **actions have focus**..
// actionsView.keystrokes.set( linkKeystroke, ( data, cancel ) => {
// this._addInsertView();
// cancel();
// } );
//
// return actionsView;
// }
/**
* Creates the {@link module:image-extended/ui/imageinsertview~ImageInsertView} instance.
*
* @private
* @returns {module:image-extended/ui/imageinsertview~ImageInsertView} The link form instance.
*/
_createInsertView() {
const editor = this.editor;
const insertView = new ImageInsertView( editor.locale );
const imageInsertCommand = editor.commands.get( 'imageInsert' );
// insertView.urlInputView.bind( 'value' ).to( imageInsertCommand, 'value' );
// Form elements should be read-only when corresponding commands are disabled.
// insertView.urlInputView.bind( 'isReadOnly' ).to( imageInsertCommand, 'isEnabled', value => !value );
// insertView.saveButtonView.bind( 'isEnabled' ).to( imageInsertCommand );
// Execute link command after clicking the "Save" button.
this.listenTo( insertView, 'submit', () => {
editor.execute( 'imageInsert', insertView.urlInputView.inputView.element.value );
this._removeInsertView();
} );
// Hide the panel after clicking the "Cancel" button.
this.listenTo( insertView, 'cancel', () => {
this._removeInsertView();
} );
// Close the panel on esc key press when the **form has focus**.
insertView.keystrokes.set( 'Esc', ( data, cancel ) => {
this._removeInsertView();
cancel();
} );
return insertView;
}
/**
* Creates a toolbar Link button. Clicking this button will show
* a {@link #_balloon} attached to the selection.
*
* @private
*/
_createToolbarButton() {
const editor = this.editor;
const insertImageCommand = editor.commands.get( 'insertImage' );
const t = editor.t;
// // Handle the `Ctrl+K` keystroke and show the panel.
// editor.keystrokes.set( linkKeystroke, ( keyEvtData, cancel ) => {
// // Prevent focusing the search bar in FF and opening new tab in Edge. #153, #154.
// cancel();
//
// if ( linkCommand.isEnabled ) {
// this._showUI();
// }
// } );
editor.ui.componentFactory.add( 'imageInsert', locale => {
const button = new ButtonView( locale );
button.isEnabled = true;
button.label = t( 'Insert image' );
button.icon = imageIcon;
// button.keystroke = linkKeystroke;
button.tooltip = true;
// Bind button to the command.
button.bind( 'isEnabled' ).to( insertImageCommand, 'isEnabled' );
// Show the panel on button click.
this.listenTo( button, 'execute', () => this._showUI() );
return button;
} );
}
/**
* Attaches actions that control whether the balloon panel containing the
* {@link #insertView} is visible or not.
*
* @private
*/
_enableUserBalloonInteractions() {
const viewDocument = this.editor.editing.view.document;
// Handle click on view document and show panel when selection is placed inside the link element.
// Keep panel open until selection will be inside the same link element.
this.listenTo( viewDocument, 'click', () => {
// const parentLink = this._getSelectedLinkElement();
//
// if ( parentLink ) {
// Then show panel but keep focus inside editor editable.
this._showUI();
// }
} );
// // Focus the form if the balloon is visible and the Tab key has been pressed.
// this.editor.keystrokes.set( 'Tab', ( data, cancel ) => {
// if ( this._isEditVisible && !this.editView.focusTracker.isFocused ) {
// this.editView.focus();
// cancel();
// }
// }, {
// // Use the high priority because the link UI navigation is more important
// // than other feature's actions, e.g. list indentation.
// // https://github.com/ckeditor/ckeditor5-link/issues/146
// priority: 'high'
// } );
// 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._isUIVisible ) {
this._hideUI();
cancel();
}
} );
// Close on click outside of balloon panel element.
clickOutsideHandler( {
emitter: this.insertView,
activator: () => this._isUIVisible,
contextElements: [ this._balloon.view.element ],
callback: () => this._hideUI()
} );
}
// /**
// * Adds the {@link #editView} to the {@link #_balloon}.
// *
// * @protected
// */
// _addEditView() {
// if ( this._isEditInPanel ) {
// return;
// }
//
// this._balloon.add( {
// view: this.editView,
// position: this._getBalloonPositionData()
// } );
// }
/**
* Adds the {@link #insertView} to the {@link #_balloon}.
*
* @protected
*/
_addInsertView() {
if ( this._isInsertInPanel ) {
return;
}
const editor = this.editor;
const insertImageCommand = editor.commands.get( 'insertImage' );
this._balloon.add( {
view: this.insertView,
position: this._getBalloonPositionData()
} );
this.insertView.urlInputView.select();
// Make sure that each time the panel shows up, the URL field remains in sync with the value of
// the command. If the user typed in the input, then canceled the balloon (`urlInputView#value` stays
// unaltered) and re-opened it without changing the value of the link command (e.g. because they
// clicked the same link), they would see the old value instead of the actual value of the command.
// https://github.com/ckeditor/ckeditor5-link/issues/78
// https://github.com/ckeditor/ckeditor5-link/issues/123
this.insertView.urlInputView.inputView.element.value = insertImageCommand.value || '';
}
/**
* Removes the {@link #insertView} from the {@link #_balloon}.
*
* @protected
*/
_removeInsertView() {
if ( this._isInsertInPanel ) {
this._balloon.remove( this.insertView );
// Because the form has an input which has focus, the focus must be brought back
// to the editor. Otherwise, it would be lost.
this.editor.editing.view.focus();
}
}
/**
* Shows the right kind of the UI for current state of the command. It's either
* {@link #insertView} or {@link #editView}.
*
* @private
*/
_showUI() {
const editor = this.editor;
const insertImageCommand = editor.commands.get( 'insertImage' );
if ( !insertImageCommand.isEnabled ) {
return;
}
this._addInsertView();
// // When there's no link under the selection, go straight to the editing UI.
// if ( !this._getSelectedLinkElement() ) {
// this._addEditView();
// this._addInsertView();
// }
// // If theres a link under the selection...
// else {
// // Go to the editing UI if actions are already visible.
// if ( this._isEditVisible ) {
// this._addInsertView();
// }
// // Otherwise display just the actions UI.
// else {
// this._addEditView();
// }
// }
// Begin responding to view#render once the UI is added.
this._startUpdatingUIOnViewRender();
}
/**
* Removes the {@link #insertView} from the {@link #_balloon}.
*
* See {@link #_addInsertView}, {@link #_addEditView}.
*
* @protected
*/
_hideUI() {
if ( !this._isUIInPanel ) {
return;
}
const editingView = this.editor.editing.view;
this.stopListening( editingView, 'render' );
// Remove form first because it's on top of the stack.
this._removeInsertView();
// // Then remove the actions view because it's beneath the form.
// this._balloon.remove( this.editView );
// Make sure the focus always gets back to the editable.
editingView.focus();
}
/**
* Makes the UI react to the {@link module:engine/view/view~View#event:render} in the view to
* reposition itself as the document changes.
*
* See: {@link #_hideUI} to learn when the UI stops reacting to the `render` event.
*
* @protected
*/
_startUpdatingUIOnViewRender() {
const editor = this.editor;
const editing = editor.editing;
const editingView = editing.view;
// let prevSelectedLink = this._getSelectedLinkElement();
let prevSelectionParent = getSelectionParent();
this.listenTo( editingView, 'render', () => {
// const selectedLink = this._getSelectedLinkElement();
const selectionParent = getSelectionParent();
// Hide the panel if:
//
// * the selection went out of the EXISTING link element. E.g. user moved the caret out
// of the link,
// * the selection went to a different parent when creating a NEW link. E.g. someone
// else modified the document.
// * the selection has expanded (e.g. displaying link actions then pressing SHIFT+Right arrow).
//
// Note: #_getSelectedLinkElement will return a link for a non-collapsed selection only
// when fully selected.
if ( selectionParent !== prevSelectionParent ) {
this._hideUI();
}
// 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 there was no link (e.g. inserting one), the balloon must be moved
// to the new position in the editing view (a new native DOM range).
this._balloon.updatePosition( this._getBalloonPositionData() );
}
// prevSelectedLink = selectedLink;
prevSelectionParent = selectionParent;
} );
function getSelectionParent() {
return editingView.document.selection.focus.getAncestors()
.reverse()
.find( node => node.is( 'element' ) );
}
}
/**
* Returns true when {@link #insertView} is in the {@link #_balloon}.
*
* @readonly
* @protected
* @type {Boolean}
*/
get _isInsertInPanel() {
return this._balloon.hasView( this.insertView );
}
// /**
// * Returns true when {@link #editView} is in the {@link #_balloon}.
// *
// * @readonly
// * @protected
// * @type {Boolean}
// */
// get _isEditInPanel() {
// return this._balloon.hasView( this.editView );
// }
// /**
// * Returns true when {@link #editView} is in the {@link #_balloon} and it is
// * currently visible.
// *
// * @readonly
// * @protected
// * @type {Boolean}
// */
// get _isEditVisible() {
// return this._balloon.visibleView === this.editView;
// }
/**
* Returns true when {@link #editView} or {@link #insertView} is in the {@link #_balloon}.
*
* @readonly
* @protected
* @type {Boolean}
*/
get _isUIInPanel() {
return this._isInsertInPanel/* || this._isEditInPanel*/;
}
/**
* Returns true when {@link #editView} or {@link #insertView} is in the {@link #_balloon} and it is
* currently visible.
*
* @readonly
* @protected
* @type {Boolean}
*/
get _isUIVisible() {
const visibleView = this._balloon.visibleView;
return visibleView == this.insertView/* || this._isEditVisible*/;
}
/**
* Returns positioning options for the {@link #_balloon}. They control the way the balloon is attached
* to the target element or selection.
*
* If the selection is collapsed and inside a link element, the panel will be attached to the
* entire link element. Otherwise, it will be attached to the selection.
*
* @private
* @returns {module:utils/dom/position~Options}
*/
_getBalloonPositionData() {
const view = this.editor.editing.view;
const viewDocument = view.document;
const target = view.domConverter.viewRangeToDom( viewDocument.selection.getFirstRange() );
return { target };
}
// /**
// * Returns the link {@link module:engine/view/attributeelement~AttributeElement} under
// * the {@link module:engine/view/document~Document editing view's} selection or `null`
// * if there is none.
// *
// * **Note**: For a non–collapsed selection the link element is only returned when **fully**
// * selected and the **only** element within the selection boundaries.
// *
// * @private
// * @returns {module:engine/view/attributeelement~AttributeElement|null}
// */
// _getSelectedLinkElement() {
// const selection = this.editor.editing.view.document.selection;
//
// if ( selection.isCollapsed ) {
// return findLinkElementAncestor( selection.getFirstPosition() );
// } else {
// // The range for fully selected link is usually anchored in adjacent text nodes.
// // Trim it to get closer to the actual link element.
// const range = selection.getFirstRange().getTrimmed();
// const startLink = findLinkElementAncestor( range.start );
// const endLink = findLinkElementAncestor( range.end );
//
// if ( !startLink || startLink != endLink ) {
// return null;
// }
//
// // Check if the link element is fully selected.
// if ( Range.createIn( startLink ).getTrimmed().isEqual( range ) ) {
// return startLink;
// } else {
// return null;
// }
// }
// }
}
// // Returns a link element if there's one among the ancestors of the provided `Position`.
// //
// // @private
// // @param {module:engine/view/position~Position} View position to analyze.
// // @returns {module:engine/view/attributeelement~AttributeElement|null} Link element at the position or null.
// function findLinkElementAncestor( position ) {
// return position.getAncestors().find( ancestor => isLinkElement( ancestor ) );
// }