ckeditor/ckeditor5-widget

View on GitHub
src/highlightstack.js

Summary

Maintainability
A
1 hr
Test Coverage
/**
 * @license Copyright (c) 2003-2020, CKSource - Frederico Knabben. All rights reserved.
 * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
 */

/**
 * @module widget/highlightstack
 */

import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
import mix from '@ckeditor/ckeditor5-utils/src/mix';

/**
 * Class used to handle correct order of highlights on elements.
 *
 * When different highlights are applied to same element correct order should be preserved:
 *
 * * highlight with highest priority should be applied,
 * * if two highlights have same priority - sort by CSS class provided in
 * {@link module:engine/conversion/downcasthelpers~HighlightDescriptor}.
 *
 * This way, highlight will be applied with the same rules it is applied on texts.
 */
export default class HighlightStack {
    /**
     * Creates class instance.
     */
    constructor() {
        this._stack = [];
    }

    /**
     * Adds highlight descriptor to the stack.
     *
     * @fires change:top
     * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
     * @param {module:engine/view/downcastwriter~DowncastWriter} writer
     */
    add( descriptor, writer ) {
        const stack = this._stack;

        // Save top descriptor and insert new one. If top is changed - fire event.
        const oldTop = stack[ 0 ];
        this._insertDescriptor( descriptor );
        const newTop = stack[ 0 ];

        // When new object is at the top and stores different information.
        if ( oldTop !== newTop && !compareDescriptors( oldTop, newTop ) ) {
            this.fire( 'change:top', {
                oldDescriptor: oldTop,
                newDescriptor: newTop,
                writer
            } );
        }
    }

    /**
     * Removes highlight descriptor from the stack.
     *
     * @fires change:top
     * @param {String} id Id of the descriptor to remove.
     * @param {module:engine/view/downcastwriter~DowncastWriter} writer
     */
    remove( id, writer ) {
        const stack = this._stack;

        const oldTop = stack[ 0 ];
        this._removeDescriptor( id );
        const newTop = stack[ 0 ];

        // When new object is at the top and stores different information.
        if ( oldTop !== newTop && !compareDescriptors( oldTop, newTop ) ) {
            this.fire( 'change:top', {
                oldDescriptor: oldTop,
                newDescriptor: newTop,
                writer
            } );
        }
    }

    /**
     * Inserts given descriptor in correct place in the stack. It also takes care about updating information when
     * descriptor with same id is already present.
     *
     * @private
     * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} descriptor
     */
    _insertDescriptor( descriptor ) {
        const stack = this._stack;
        const index = stack.findIndex( item => item.id === descriptor.id );

        // Inserting exact same descriptor - do nothing.
        if ( compareDescriptors( descriptor, stack[ index ] ) ) {
            return;
        }

        // If descriptor with same id but with different information is on the stack - remove it.
        if ( index > -1 ) {
            stack.splice( index, 1 );
        }

        // Find correct place to insert descriptor in the stack.
        // It have different information (for example priority) so it must be re-inserted in correct place.
        let i = 0;

        while ( stack[ i ] && shouldABeBeforeB( stack[ i ], descriptor ) ) {
            i++;
        }

        stack.splice( i, 0, descriptor );
    }

    /**
     * Removes descriptor with given id from the stack.
     *
     * @private
     * @param {String} id Descriptor's id.
     */
    _removeDescriptor( id ) {
        const stack = this._stack;
        const index = stack.findIndex( item => item.id === id );

        // If descriptor with same id is on the list - remove it.
        if ( index > -1 ) {
            stack.splice( index, 1 );
        }
    }
}

mix( HighlightStack, EmitterMixin );

// Compares two descriptors by checking their priority and class list.
//
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} a
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} b
// @returns {Boolean} Returns true if both descriptors are defined and have same priority and classes.
function compareDescriptors( a, b ) {
    return a && b && a.priority == b.priority && classesToString( a.classes ) == classesToString( b.classes );
}

// Checks whenever first descriptor should be placed in the stack before second one.
//
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} a
// @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} b
// @returns {Boolean}
function shouldABeBeforeB( a, b ) {
    if ( a.priority > b.priority ) {
        return true;
    } else if ( a.priority < b.priority ) {
        return false;
    }

    // When priorities are equal and names are different - use classes to compare.
    return classesToString( a.classes ) > classesToString( b.classes );
}

// Converts CSS classes passed with {@link module:engine/conversion/downcasthelpers~HighlightDescriptor} to
// sorted string.
//
// @param {String|Array<String>} descriptor
// @returns {String}
function classesToString( classes ) {
    return Array.isArray( classes ) ? classes.sort().join( ',' ) : classes;
}

/**
 * Fired when top element on {@link module:widget/highlightstack~HighlightStack} has been changed
 *
 * @event change:top
 * @param {Object} data Additional information about the change.
 * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} [data.newDescriptor] New highlight
 * descriptor. It will be `undefined` when last descriptor is removed from the stack.
 * @param {module:engine/conversion/downcasthelpers~HighlightDescriptor} [data.oldDescriptor] Old highlight
 * descriptor. It will be `undefined` when first descriptor is added to the stack.
 * @param {module:engine/view/downcastwriter~DowncastWriter} writer View writer that can be used to modify element.
 */