| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- /**
- * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
- * For licensing, see LICENSE.md.
- */
- /**
- * @module mention/mentionediting
- */
- import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
- import uid from '@ckeditor/ckeditor5-utils/src/uid';
- import MentionCommand from './mentioncommand';
- /**
- * The mention editing feature.
- *
- * It introduces the {@link module:mention/mentioncommand~MentionCommand command} and the `mention`
- * attribute in the {@link module:engine/model/model~Model model} which renders in the {@link module:engine/view/view view}
- * as a `<span class="mention" data-mention="name">`.
- *
- * @extends module:core/plugin~Plugin
- */
- export default class MentionEditing extends Plugin {
- /**
- * @inheritDoc
- */
- static get pluginName() {
- return 'MentionEditing';
- }
- /**
- * @inheritDoc
- */
- init() {
- const editor = this.editor;
- const model = editor.model;
- const doc = model.document;
- // Allow mention attribute on all text nodes.
- model.schema.extend( '$text', { allowAttributes: 'mention' } );
- editor.conversion.for( 'upcast' ).elementToAttribute( {
- view: {
- name: 'span',
- key: 'data-mention',
- classes: 'mention'
- },
- model: {
- key: 'mention',
- value: parseMentionViewItemAttributes
- }
- } );
- editor.conversion.for( 'downcast' ).attributeToElement( {
- model: 'mention',
- view: createViewMentionElement
- } );
- doc.registerPostFixer( writer => removePartialMentionPostFixer( writer, doc ) );
- doc.registerPostFixer( writer => extendAttributeOnMentionPostFixer( writer, doc ) );
- doc.registerPostFixer( writer => selectionMentionAttributePostFixer( writer, doc ) );
- editor.commands.add( 'mention', new MentionCommand( editor ) );
- }
- }
- // Parses matched view element to mention attribute value.
- //
- // @param {module:engine/view/element} viewElement
- // @returns {Object} Mention attribute value
- function parseMentionViewItemAttributes( viewElement ) {
- const dataMention = viewElement.getAttribute( 'data-mention' );
- const textNode = viewElement.getChild( 0 );
- // Do not parse empty mentions.
- if ( !textNode || !textNode.is( 'text' ) ) {
- return;
- }
- const mentionString = textNode.data;
- // Assume that mention is set as marker + mention name.
- const marker = mentionString.slice( 0, 1 );
- const name = mentionString.slice( 1 );
- // Do not upcast partial mentions - might come from copy-paste of partially selected mention.
- if ( name != dataMention ) {
- return;
- }
- // Set UID for mention to not merge mentions in the same block that are next to each other.
- return { name: dataMention, _marker: marker, _id: uid() };
- }
- // Creates mention element from mention data.
- //
- // @param {Object} mention
- // @param {module:engine/view/downcastwriter~DowncastWriter} viewWriter
- // @returns {module:engine/view/attributeelement~AttributeElement}
- function createViewMentionElement( mention, viewWriter ) {
- if ( !mention ) {
- return;
- }
- const attributes = {
- class: 'mention',
- 'data-mention': mention.name
- };
- const options = {
- id: mention._id
- };
- return viewWriter.createAttributeElement( 'span', attributes, options );
- }
- // Model post-fixer that disallows typing with selection when selection is placed after the text node with mention attribute.
- //
- // @param {module:engine/model/writer~Writer} writer
- // @param {module:engine/model/document~Document} doc
- // @returns {Boolean} Returns true if selection was fixed.
- function selectionMentionAttributePostFixer( writer, doc ) {
- const selection = doc.selection;
- const focus = selection.focus;
- if ( selection.isCollapsed && selection.hasAttribute( 'mention' ) && isNodeBeforeAText( focus ) ) {
- writer.removeSelectionAttribute( 'mention' );
- return true;
- }
- function isNodeBeforeAText( position ) {
- return position.nodeBefore && position.nodeBefore.is( 'text' );
- }
- }
- // Model post-fixer that removes mention attribute from modified text node.
- //
- // @param {module:engine/model/writer~Writer} writer
- // @param {module:engine/model/document~Document} doc
- // @returns {Boolean} Returns true if selection was fixed.
- function removePartialMentionPostFixer( writer, doc ) {
- const changes = doc.differ.getChanges();
- let wasChanged = false;
- for ( const change of changes ) {
- // Check text node on current position;
- if ( change.name == '$text' ) {
- // Check textNode where the change occurred.
- if ( change.type == 'insert' || change.type == 'remove' ) {
- const textNode = change.position.textNode;
- if ( !checkMentionAttributeOnNode( textNode ) ) {
- writer.removeAttribute( 'mention', textNode );
- wasChanged = true;
- }
- }
- // Additional check: when removing text on mention boundaries.
- if ( change.type == 'remove' ) {
- const nodeBefore = change.position.nodeBefore;
- if ( !checkMentionAttributeOnNode( nodeBefore ) ) {
- writer.removeAttribute( 'mention', nodeBefore );
- wasChanged = true;
- }
- const nodeAfter = change.position.nodeAfter;
- if ( !checkMentionAttributeOnNode( nodeAfter ) ) {
- writer.removeAttribute( 'mention', nodeAfter );
- wasChanged = true;
- }
- }
- }
- }
- return wasChanged;
- }
- // This post-fixer will extend attribute applied on part of a mention so a whole text node of a mention will have added attribute.
- //
- // @param {module:engine/model/writer~Writer} writer
- // @param {module:engine/model/document~Document} doc
- // @returns {Boolean} Returns true if selection was fixed.
- function extendAttributeOnMentionPostFixer( writer, doc ) {
- const changes = doc.differ.getChanges();
- let wasChanged = false;
- for ( const change of changes ) {
- if ( change.type === 'attribute' ) {
- for ( const node of getBrokenMentionsFromRange( change.range ) ) {
- for ( const partialMention of getFullMentionNode( node ) ) {
- if ( partialMention.getAttribute( change.attributeKey ) !== change.attributeNewValue ) {
- writer.setAttribute( change.attributeKey, change.attributeNewValue, partialMention );
- wasChanged = true;
- }
- }
- }
- }
- }
- return wasChanged;
- }
- // Checks if node has correct mention attribute if present.
- // Returns false if node is text and has mention attribute and its text does not match expected mention text.
- //
- // @param {module:engine/model/node~Node} node a node to check
- // @returns {Boolean}
- function checkMentionAttributeOnNode( node ) {
- if ( !node || !( node.is( 'text' ) || node.is( 'textProxy' ) ) || !node.hasAttribute( 'mention' ) ) {
- return true;
- }
- const text = node.data;
- const mention = node.getAttribute( 'mention' );
- const expectedText = mention._marker + mention.name;
- return text == expectedText;
- }
- function* getBrokenMentionsFromRange( range ) {
- for ( const item of range.getItems() ) {
- if ( !checkMentionAttributeOnNode( item ) ) {
- yield item;
- }
- }
- }
- function getFullMentionNode( node ) {
- const textNode = node.textNode;
- const previousSibling = textNode.previousSibling;
- const nextSibling = textNode.nextSibling;
- const mention = textNode.getAttribute( 'mention' );
- const nodes = [];
- const isPreviousPartial = !checkMentionAttributeOnNode( previousSibling );
- if ( isPreviousPartial ) {
- const previousSiblingMention = previousSibling.getAttribute( 'mention' );
- const sameMention = previousSiblingMention._id == mention._id;
- if ( sameMention ) {
- nodes.push( previousSibling );
- }
- }
- nodes.push( textNode );
- const isNextPartial = !checkMentionAttributeOnNode( nextSibling );
- if ( isNextPartial ) {
- const nextSiblingMention = nextSibling.getAttribute( 'mention' );
- const sameMention = nextSiblingMention._id == mention._id;
- if ( sameMention ) {
- nodes.push( nextSibling );
- }
- }
- return nodes;
- }
|