| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- /**
- * @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 link/linkcommand
- */
- import Command from '@ckeditor/ckeditor5-core/src/command';
- import findLinkRange from './findlinkrange';
- import toMap from '@ckeditor/ckeditor5-utils/src/tomap';
- import Collection from '@ckeditor/ckeditor5-utils/src/collection';
- import first from '@ckeditor/ckeditor5-utils/src/first';
- /**
- * The link command. It is used by the {@link module:link/link~Link link feature}.
- *
- * @extends module:core/command~Command
- */
- export default class LinkCommand extends Command {
- /**
- * The value of the `'linkHref'` attribute if the start of the selection is located in a node with this attribute.
- *
- * @observable
- * @readonly
- * @member {Object|undefined} #value
- */
- constructor( editor ) {
- super( editor );
- /**
- * A collection of {@link module:link/utils~ManualDecorator manual decorators}
- * corresponding to the {@link module:link/link~LinkConfig#decorators decorator configuration}.
- *
- * You can consider it a model with states of manual decorators added to the currently selected link.
- *
- * @readonly
- * @type {module:utils/collection~Collection}
- */
- this.manualDecorators = new Collection();
- }
- /**
- * Synchronizes the state of {@link #manualDecorators} with the currently present elements in the model.
- */
- restoreManualDecoratorStates() {
- for ( const manualDecorator of this.manualDecorators ) {
- manualDecorator.value = this._getDecoratorStateFromModel( manualDecorator.id );
- }
- }
- /**
- * @inheritDoc
- */
- refresh() {
- const model = this.editor.model;
- const doc = model.document;
- const selectedElement = first( doc.selection.getSelectedBlocks() );
- // A check for the `LinkImage` plugin. If the selection contains an element, get values from the element.
- // Currently the selection reads attributes from text nodes only. See #7429 and #7465.
- if ( selectedElement && selectedElement.is( 'image' ) && model.schema.checkAttribute( 'image', 'linkHref' ) ) {
- this.value = selectedElement.getAttribute( 'linkHref' );
- this.isEnabled = model.schema.checkAttribute( selectedElement, 'linkHref' );
- } else {
- this.value = doc.selection.getAttribute( 'linkHref' );
- this.isEnabled = model.schema.checkAttributeInSelection( doc.selection, 'linkHref' );
- }
- for ( const manualDecorator of this.manualDecorators ) {
- manualDecorator.value = this._getDecoratorStateFromModel( manualDecorator.id );
- }
- }
- /**
- * Executes the command.
- *
- * When the selection is non-collapsed, the `linkHref` attribute will be applied to nodes inside the selection, but only to
- * those nodes where the `linkHref` attribute is allowed (disallowed nodes will be omitted).
- *
- * When the selection is collapsed and is not inside the text with the `linkHref` attribute, a
- * new {@link module:engine/model/text~Text text node} with the `linkHref` attribute will be inserted in place of the caret, but
- * only if such element is allowed in this place. The `_data` of the inserted text will equal the `href` parameter.
- * The selection will be updated to wrap the just inserted text node.
- *
- * When the selection is collapsed and inside the text with the `linkHref` attribute, the attribute value will be updated.
- *
- * # Decorators and model attribute management
- *
- * There is an optional argument to this command that applies or removes model
- * {@glink framework/guides/architecture/editing-engine#text-attributes text attributes} brought by
- * {@link module:link/utils~ManualDecorator manual link decorators}.
- *
- * Text attribute names in the model correspond to the entries in the {@link module:link/link~LinkConfig#decorators configuration}.
- * For every decorator configured, a model text attribute exists with the "link" prefix. For example, a `'linkMyDecorator'` attribute
- * corresponds to `'myDecorator'` in the configuration.
- *
- * To learn more about link decorators, check out the {@link module:link/link~LinkConfig#decorators `config.link.decorators`}
- * documentation.
- *
- * Here is how to manage decorator attributes with the link command:
- *
- * const linkCommand = editor.commands.get( 'link' );
- *
- * // Adding a new decorator attribute.
- * linkCommand.execute( 'http://example.com', {
- * linkIsExternal: true
- * } );
- *
- * // Removing a decorator attribute from the selection.
- * linkCommand.execute( 'http://example.com', {
- * linkIsExternal: false
- * } );
- *
- * // Adding multiple decorator attributes at the same time.
- * linkCommand.execute( 'http://example.com', {
- * linkIsExternal: true,
- * linkIsDownloadable: true,
- * } );
- *
- * // Removing and adding decorator attributes at the same time.
- * linkCommand.execute( 'http://example.com', {
- * linkIsExternal: false,
- * linkFoo: true,
- * linkIsDownloadable: false,
- * } );
- *
- * **Note**: If the decorator attribute name is not specified, its state remains untouched.
- *
- * **Note**: {@link module:link/unlinkcommand~UnlinkCommand#execute `UnlinkCommand#execute()`} removes all
- * decorator attributes.
- *
- * @fires execute
- * @param {String} href Link destination.
- * @param {Object} [manualDecoratorIds={}] The information about manual decorator attributes to be applied or removed upon execution.
- */
- execute( href, manualDecoratorIds = {} ) {
- const model = this.editor.model;
- const selection = model.document.selection;
- // Stores information about manual decorators to turn them on/off when command is applied.
- const truthyManualDecorators = [];
- const falsyManualDecorators = [];
- for ( const name in manualDecoratorIds ) {
- if ( manualDecoratorIds[ name ] ) {
- truthyManualDecorators.push( name );
- } else {
- falsyManualDecorators.push( name );
- }
- }
- model.change( writer => {
- // If selection is collapsed then update selected link or insert new one at the place of caret.
- if ( selection.isCollapsed ) {
- const position = selection.getFirstPosition();
- // When selection is inside text with `linkHref` attribute.
- if ( selection.hasAttribute( 'linkHref' ) ) {
- // Then update `linkHref` value.
- const linkRange = findLinkRange( position, selection.getAttribute( 'linkHref' ), model );
- writer.setAttribute( 'linkHref', href, linkRange );
- truthyManualDecorators.forEach( item => {
- writer.setAttribute( item, true, linkRange );
- } );
- falsyManualDecorators.forEach( item => {
- writer.removeAttribute( item, linkRange );
- } );
- // Put the selection at the end of the updated link.
- writer.setSelection( writer.createPositionAfter( linkRange.end.nodeBefore ) );
- }
- // If not then insert text node with `linkHref` attribute in place of caret.
- // However, since selection in collapsed, attribute value will be used as data for text node.
- // So, if `href` is empty, do not create text node.
- else if ( href !== '' ) {
- const attributes = toMap( selection.getAttributes() );
- attributes.set( 'linkHref', href );
- truthyManualDecorators.forEach( item => {
- attributes.set( item, true );
- } );
- const node = writer.createText( href, attributes );
- model.insertContent( node, position );
- // Put the selection at the end of the inserted link.
- writer.setSelection( writer.createPositionAfter( node ) );
- }
- // Remove the `linkHref` attribute and all link decorators from the selection.
- // It stops adding a new content into the link element.
- [ 'linkHref', ...truthyManualDecorators, ...falsyManualDecorators ].forEach( item => {
- writer.removeSelectionAttribute( item );
- } );
- } else {
- // If selection has non-collapsed ranges, we change attribute on nodes inside those ranges
- // omitting nodes where the `linkHref` attribute is disallowed.
- const ranges = model.schema.getValidRanges( selection.getRanges(), 'linkHref' );
- // But for the first, check whether the `linkHref` attribute is allowed on selected blocks (e.g. the "image" element).
- const allowedRanges = [];
- for ( const element of selection.getSelectedBlocks() ) {
- if ( model.schema.checkAttribute( element, 'linkHref' ) ) {
- allowedRanges.push( writer.createRangeOn( element ) );
- }
- }
- // Ranges that accept the `linkHref` attribute. Since we will iterate over `allowedRanges`, let's clone it.
- const rangesToUpdate = allowedRanges.slice();
- // For all selection ranges we want to check whether given range is inside an element that accepts the `linkHref` attribute.
- // If so, we don't want to propagate applying the attribute to its children.
- for ( const range of ranges ) {
- if ( this._isRangeToUpdate( range, allowedRanges ) ) {
- rangesToUpdate.push( range );
- }
- }
- for ( const range of rangesToUpdate ) {
- writer.setAttribute( 'linkHref', href, range );
- truthyManualDecorators.forEach( item => {
- writer.setAttribute( item, true, range );
- } );
- falsyManualDecorators.forEach( item => {
- writer.removeAttribute( item, range );
- } );
- }
- }
- } );
- }
- /**
- * Provides information whether a decorator with a given name is present in the currently processed selection.
- *
- * @private
- * @param {String} decoratorName The name of the manual decorator used in the model
- * @returns {Boolean} The information whether a given decorator is currently present in the selection.
- */
- _getDecoratorStateFromModel( decoratorName ) {
- const doc = this.editor.model.document;
- return doc.selection.getAttribute( decoratorName );
- }
- /**
- * Checks whether specified `range` is inside an element that accepts the `linkHref` attribute.
- *
- * @private
- * @param {module:engine/view/range~Range} range A range to check.
- * @param {Array.<module:engine/view/range~Range>} allowedRanges An array of ranges created on elements where the attribute is accepted.
- * @returns {Boolean}
- */
- _isRangeToUpdate( range, allowedRanges ) {
- for ( const allowedRange of allowedRanges ) {
- // A range is inside an element that will have the `linkHref` attribute. Do not modify its nodes.
- if ( allowedRange.containsRange( range ) ) {
- return false;
- }
- }
- return true;
- }
- }
|