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

Commit

Permalink
Merge pull request #81 from ckeditor/t/77
Browse files Browse the repository at this point in the history
Fix: Image captions in the view are hidden instead of being removed. Closes #77.
  • Loading branch information
scofalik authored Mar 30, 2017
2 parents ecbe435 + 93d60f1 commit aae2957
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 153 deletions.
185 changes: 76 additions & 109 deletions src/imagecaption/imagecaptionengine.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import ModelTreeWalker from '@ckeditor/ckeditor5-engine/src/model/treewalker';
import ModelElement from '@ckeditor/ckeditor5-engine/src/model/element';
import ViewContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement';
import ViewElement from '@ckeditor/ckeditor5-engine/src/view/element';
import ViewRange from '@ckeditor/ckeditor5-engine/src/view/range';
import viewWriter from '@ckeditor/ckeditor5-engine/src/view/writer';
import ModelPosition from '@ckeditor/ckeditor5-engine/src/model/position';
import ViewPosition from '@ckeditor/ckeditor5-engine/src/view/position';
Expand Down Expand Up @@ -43,16 +42,16 @@ export default class ImageCaptionEngine extends Plugin {
const schema = document.schema;
const data = editor.data;
const editing = editor.editing;
const mapper = editing.mapper;

/**
* Last selected caption editable.
* It is used for hiding editable when is empty and image widget is no longer selected.
*
* @private
* @member {module:engine/view/editableelement~EditableElement} #_lastSelectedEditable
* @member {module:engine/view/editableelement~EditableElement} #_lastSelectedCaption
*/

this._viewCaptionsToUpdate = [];

/**
* Function used to create editable caption element in the editing view.
*
Expand All @@ -77,102 +76,77 @@ export default class ImageCaptionEngine extends Plugin {
.toElement( 'caption' );

// Model to view converter for the data pipeline.
data.modelToView.on( 'insert:caption', captionModelToView( new ViewContainerElement( 'figcaption' ) ) );
data.modelToView.on( 'insert:caption', captionModelToView( new ViewContainerElement( 'figcaption' ), false ) );

// Model to view converter for the editing pipeline.
editing.modelToView.on( 'insert:caption', captionModelToView( this._createCaption ) );

// When inserting something to caption in the model - make sure that caption in the view is also present.
// See https://github.com/ckeditor/ckeditor5-image/issues/58.
editing.modelToView.on( 'insert', insertMissingViewCaptionElement( this._createCaption, mapper ), { priority: 'high' } );
// Always show caption in view when something is inserted in model.
editing.modelToView.on( 'insert', ( evt, data ) => this._fixCaptionVisibility( data.item ), { priority: 'high' } );

// Hide caption when everything is removed from it.
editing.modelToView.on( 'remove', ( evt, data ) => this._fixCaptionVisibility( data.sourcePosition.parent ), { priority: 'high' } );

// Update view before each rendering.
this.listenTo( viewDocument, 'render', () => this._updateView(), { priority: 'high' } );
this.listenTo( viewDocument, 'render', () => this._updateCaptionVisibility(), { priority: 'high' } );
}

/**
* Checks if there is an empty caption element to remove from the view.
* Updates view before each rendering, making sure that empty captions (so unnecessary ones) are hidden
* and then visible when the image is selected.
*
* @private
*/
_removeEmptyCaption() {
_updateCaptionVisibility() {
const mapper = this.editor.editing.mapper;
const viewSelection = this.editor.editing.view.selection;
const viewCaptionElement = this._lastSelectedEditable;

// No caption to hide.
if ( !viewCaptionElement ) {
return;
}
const selectedElement = viewSelection.getSelectedElement();
let viewCaption;

// If selection is placed inside caption - do not remove it.
if ( viewSelection.editableElement === viewCaptionElement ) {
return;
// Hide last selected caption if have no child elements.
if ( this._lastSelectedCaption && !this._lastSelectedCaption.childCount ) {
this._lastSelectedCaption.addClass( 'ck-hidden' );
}

// Do not remove caption if selection is placed on image that contains that caption.
const selectedElement = viewSelection.getSelectedElement();

// If whole image widget is selected.
if ( selectedElement && isImageWidget( selectedElement ) ) {
const viewImage = viewCaptionElement.findAncestor( element => element == selectedElement );

if ( viewImage ) {
return;
}
const modelImage = mapper.toModelElement( selectedElement );
const modelCaption = getCaptionFromImage( modelImage );
viewCaption = mapper.toViewElement( modelCaption );
}

// Remove image caption if its empty.
if ( viewCaptionElement.childCount === 0 ) {
const mapper = this.editor.editing.mapper;
viewWriter.remove( ViewRange.createOn( viewCaptionElement ) );
mapper.unbindViewElement( viewCaptionElement );
// If selection is placed inside caption.
if ( isCaption( viewSelection.editableElement ) ) {
viewCaption = viewSelection.editableElement;
}
}

/**
* Checks if selected image needs a new caption element inside.
*
* @private
*/
_addCaptionWhenSelected() {
const editing = this.editor.editing;
const selection = editing.view.selection;
const viewImage = selection.getSelectedElement();
const mapper = editing.mapper;

if ( viewImage && isImageWidget( viewImage ) ) {
const modelImage = mapper.toModelElement( viewImage );
const modelCaption = getCaptionFromImage( modelImage );
let viewCaption = mapper.toViewElement( modelCaption );

if ( !viewCaption ) {
viewCaption = this._createCaption();
insertViewCaptionAndBind( viewCaption, modelCaption, viewImage, mapper );
}

this._lastSelectedEditable = viewCaption;
if ( viewCaption ) {
viewCaption.removeClass( 'ck-hidden' );
this._lastSelectedCaption = viewCaption;
}
}

/**
* Updates view before each rendering, making sure that empty captions (so unnecessary ones) are removed
* and then re-added when the image is selected.
* Fixes caption visibility during model to view conversion.
* Checks if changed node is placed inside caption element and fixes it's visibility in the view.
*
* @private
* @param {module:engine/model/node~Node} node
*/
_updateView() {
const selection = this.editor.editing.view.selection;

// Check if there is an empty caption view element to remove.
this._removeEmptyCaption();

// Check if image widget is selected and caption view element needs to be added.
this._addCaptionWhenSelected();

// If selection is currently inside caption editable - store it to hide when empty.
const editableElement = selection.editableElement;

if ( editableElement && isCaption( selection.editableElement ) ) {
this._lastSelectedEditable = selection.editableElement;
_fixCaptionVisibility( node ) {
const modelCaption = getParentCaption( node );
const mapper = this.editor.editing.mapper;

if ( modelCaption ) {
const viewCaption = mapper.toViewElement( modelCaption );

if ( viewCaption ) {
if ( modelCaption.childCount ) {
viewCaption.removeClass( 'ck-hidden' );
} else {
viewCaption.addClass( 'ck-hidden' );
}
}
}
}
}
Expand Down Expand Up @@ -201,42 +175,22 @@ function insertMissingModelCaptionElement( evt, changeType, data, batch ) {
}
}
}

// Returns function that should be executed when model to view conversion is made. It checks if insertion is placed
// inside model caption and makes sure that corresponding view element exists.
//
// @private
// @param {function} creator Function that returns view caption element.
// @param {module:engine/conversion/mapper~Mapper} mapper
// @return {function}
function insertMissingViewCaptionElement( creator, mapper ) {
return ( evt, data ) => {
if ( isInsideCaption( data.item ) ) {
const modelCaption = data.item.parent;
const modelImage = modelCaption.parent;

const viewImage = mapper.toViewElement( modelImage );
let viewCaption = mapper.toViewElement( modelCaption );

// Image should be already converted to the view.
if ( viewImage && !viewCaption ) {
viewCaption = creator();
insertViewCaptionAndBind( viewCaption, modelCaption, viewImage, mapper );
}
}
};
}

// Creates a converter that converts image caption model element to view element.
//
// @private
// @param {Function|module:engine/view/element~Element} elementCreator
// @param {Boolean} [hide=true] When set to `false` view element will not be inserted when it's empty.
// @return {Function}
function captionModelToView( elementCreator ) {
function captionModelToView( elementCreator, hide = true ) {
return ( evt, data, consumable, conversionApi ) => {
const captionElement = data.item;

if ( isImage( captionElement.parent ) && ( captionElement.childCount > 0 ) ) {
// Return if element shouldn't be present when empty.
if ( !captionElement.childCount && !hide ) {
return;
}

if ( isImage( captionElement.parent ) ) {
if ( !consumable.consume( data.item, 'insert' ) ) {
return;
}
Expand All @@ -246,20 +200,16 @@ function captionModelToView( elementCreator ) {
elementCreator.clone( true ) :
elementCreator();

// Hide if empty.
if ( !captionElement.childCount ) {
viewCaption.addClass( 'ck-hidden' );
}

insertViewCaptionAndBind( viewCaption, data.item, viewImage, conversionApi.mapper );
}
};
}

// Returns `true` if provided `node` is placed inside image's caption.
//
// @private
// @param {module:engine/model/node~Node} node
// @return {Boolean}
function isInsideCaption( node ) {
return !!( node.parent && node.parent.name == 'caption' && node.parent.parent && node.parent.parent.name == 'image' );
}

// Inserts `viewCaption` at the end of `viewImage` and binds it to `modelCaption`.
//
// @private
Expand All @@ -273,3 +223,20 @@ function insertViewCaptionAndBind( viewCaption, modelCaption, viewImage, mapper
viewWriter.insert( viewPosition, viewCaption );
mapper.bindElements( modelCaption, viewCaption );
}

/**
* Checks if provided node or one of its ancestors is caption element and returns it.
*
* @param {module:engine/model/node~Node} node
* @returns {module:engine/model/element~Element|null}
*/
function getParentCaption( node ) {
const ancestors = node.getAncestors( { includeNode: true } );
const caption = ancestors.find( ancestor => ancestor.name == 'caption' );

if ( caption && caption.parent && caption.parent.name == 'image' ) {
return caption;
}

return null;
}
Loading

0 comments on commit aae2957

Please sign in to comment.