UNPKG

@martindilling/ckeditor5-image-extended

Version:

Extended image features for CKEditor 5.

544 lines (474 loc) 16.6 kB
/** * @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 ) ); // }