Răsfoiți Sursa

Add MentionUI stub.

Maciej Gołaszewski 6 ani în urmă
părinte
comite
9f4e212819

+ 2 - 1
packages/ckeditor5-mention/src/mention.js

@@ -10,6 +10,7 @@
 import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
 
 import MentionEditing from './mentionediting';
+import MentionUI from './mentionui';
 
 /**
  * The mention plugin.
@@ -30,6 +31,6 @@ export default class Mention extends Plugin {
 	 * @inheritDoc
 	 */
 	static get requires() {
-		return [ MentionEditing ];
+		return [ MentionEditing, MentionUI ];
 	}
 }

+ 285 - 0
packages/ckeditor5-mention/src/mentionui.js

@@ -0,0 +1,285 @@
+/**
+ * @license Copyright (c) 2003-2019, CKSource - Frederico Knabben. All rights reserved.
+ * For licensing, see LICENSE.md.
+ */
+
+/**
+ * @module mention/mentionui
+ */
+
+import Plugin from '@ckeditor/ckeditor5-core/src/plugin';
+import mix from '@ckeditor/ckeditor5-utils/src/mix';
+import EmitterMixin from '@ckeditor/ckeditor5-utils/src/emittermixin';
+import View from '@ckeditor/ckeditor5-ui/src/view';
+import ContextualBalloon from '@ckeditor/ckeditor5-ui/src/panel/balloon/contextualballoon';
+import ListView from '@ckeditor/ckeditor5-ui/src/list/listview';
+import ListItemView from '@ckeditor/ckeditor5-ui/src/list/listitemview';
+import ButtonView from '@ckeditor/ckeditor5-ui/src/button/buttonview';
+import Collection from '@ckeditor/ckeditor5-utils/src/collection';
+import BalloonPanelView from '@ckeditor/ckeditor5-ui/src/panel/balloon/balloonpanelview';
+
+/**
+ * The mention ui feature.
+ *
+ * @extends module:core/plugin~Plugin
+ */
+export default class MentionUI extends Plugin {
+	/**
+	 * @inheritDoc
+	 */
+	static get pluginName() {
+		return 'MentionUI';
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	static get requries() {
+		return [ ContextualBalloon ];
+	}
+
+	/**
+	 * @inheritDoc
+	 */
+	init() {
+		const editor = this.editor;
+
+		const locale = editor.locale;
+
+		// this._panel = editor.plugins.get( 'ContextualBalloon' );
+
+		this._panel = new BalloonPanelView( locale );
+		this._panel.withArrow = false;
+		this._panel.render();
+
+		// document.body.appendChild( this._panel.element );
+		this.editor.ui.view.body.add( this._panel );
+
+		this._mentions = new MentionsView( locale );
+		// this._mentions.render();
+
+		const items = new Collection();
+
+		this._panel.content.add( this._mentions );
+
+		this._mentions.listView.items.bindTo( items ).using( item => {
+			const { label } = item;
+			const listItemView = new ListItemView( locale );
+			const buttonView = new ButtonView( locale );
+
+			buttonView.label = label;
+			buttonView.withText = true;
+			buttonView.item = item;
+
+			listItemView.children.add( buttonView );
+
+			buttonView.delegate( 'execute' ).to( this._mentions );
+
+			return listItemView;
+		} );
+
+		const regExp = / (@)([\w]*?)$/;
+
+		const watcher = new TextWatcher( editor, testCallback );
+
+		this._mentions.on( 'execute', evt => {
+			const label = evt.source.label;
+
+			const text = watcher.last;
+
+			if ( !text ) {
+				return;
+			}
+
+			const matched = getMatchedText( text );
+
+			editor.model.change( writer => {
+				const end = writer.createPositionAt( editor.model.document.selection.focus );
+				const start = end.getShiftedBy( -( 1 + matched.feedText.length ) );
+
+				const range = writer.createRange( start, end );
+
+				writer.setAttribute( 'mention', label, range );
+				writer.remove( range );
+
+				writer.insertText( `@${ label }`, { mention: 'label' }, start );
+				writer.insertText( ' ', editor.model.document.selection.focus );
+			} );
+		} );
+
+		function testCallback( text ) {
+			return regExp.test( text );
+		}
+
+		function getMatchedText( text ) {
+			const match = text.match( regExp );
+
+			const marker = match[ 1 ];
+			const feedText = match[ 2 ];
+
+			return { marker, feedText };
+		}
+
+		watcher.on( 'matched', ( evt, data ) => {
+			const text = data.text;
+
+			const matched = getMatchedText( text );
+
+			items.clear();
+
+			const feed = [ 'Jodator', 'Foo', 'Bar' ];
+
+			const strings = feed.filter( item => {
+				return item.toLowerCase().startsWith( matched.feedText.toLowerCase() );
+			} );
+
+			for ( const item of strings ) {
+				items.add( { label: item } );
+			}
+
+			this._showForm();
+		} );
+
+		watcher.on( 'unmatched', () => {
+			this._hideForm();
+		} );
+	}
+
+	_showForm() {
+		if ( this._isVisible ) {
+			// return;
+		}
+
+		const editor = this.editor;
+
+		// Pin the panel to an element with the "target" id DOM.
+		this._panel.pin( getBalloonPositionData( editor ) );
+
+		this._panel.show();
+	}
+
+	_hideForm() {
+		this._panel.hide();
+	}
+}
+
+// Returns whole text from parent element by adding all data from text nodes together.
+//
+// @private
+// @param {module:engine/model/element~Element} element
+// @returns {String}
+function getText( element ) {
+	return Array.from( element.getChildren() ).reduce( ( a, b ) => a + b.data, '' );
+}
+
+class TextWatcher {
+	constructor( editor, callbackOrRegex ) {
+		this.editor = editor;
+		this.testCallback = callbackOrRegex;
+
+		this.hasMatch = false;
+
+		this._startListening();
+	}
+
+	get last() {
+		return this._getText();
+	}
+
+	_startListening() {
+		const editor = this.editor;
+
+		editor.model.document.on( 'change', ( evt, batch ) => {
+			if ( batch.type == 'transparent' ) {
+				return;
+			}
+
+			const changes = Array.from( editor.model.document.differ.getChanges() );
+			const entry = changes[ 0 ];
+
+			// Typing is represented by only a single change.
+			if ( changes.length != 1 || entry.type !== 'insert' || entry.name != '$text' || entry.length != 1 ) {
+				return undefined;
+			}
+
+			const text = this._getText();
+
+			const textHasMatch = this.testCallback( text );
+
+			if ( !textHasMatch && this.hasMatch ) {
+				this.fire( 'unmatched' );
+			}
+
+			this.hasMatch = textHasMatch;
+
+			if ( textHasMatch ) {
+				this.fire( 'matched', { text } );
+			}
+		} );
+
+		this._panel = this.editor.plugins.get( 'ContextualBalloon' );
+	}
+
+	_getText() {
+		const editor = this.editor;
+		const selection = editor.model.document.selection;
+
+		// Do nothing if selection is not collapsed.
+		if ( !selection.isCollapsed ) {
+			return undefined;
+		}
+
+		const block = selection.focus.parent;
+
+		return getText( block ).slice( 0, selection.focus.offset );
+	}
+}
+
+mix( TextWatcher, EmitterMixin );
+
+class MentionsView extends View {
+	constructor( locale ) {
+		super( locale );
+
+		this.listView = new ListView( locale );
+
+		this.setTemplate( {
+			tag: 'div',
+
+			attributes: {
+				class: [
+					'ck',
+					'ck-mention'
+				],
+
+				tabindex: '-1'
+			},
+
+			children: [
+				this.listView
+			]
+		} );
+	}
+}
+
+function getBalloonPositionData( editor ) {
+	const editingView = editor.editing.view;
+	const defaultPositions = BalloonPanelView.defaultPositions;
+
+	return {
+		target: () => {
+			// const text = editingView.domConverter.findCorrespondingDomText( editingView.document.selection.focus.parent );
+			const textParent = editingView.domConverter.viewToDom( editingView.document.selection.focus.parent.parent );
+
+			return textParent;
+		},
+		positions: [
+			defaultPositions.northArrowSouth,
+			defaultPositions.northArrowSouthWest,
+			defaultPositions.northArrowSouthEast,
+			defaultPositions.southArrowNorth,
+			defaultPositions.southArrowNorthWest,
+			defaultPositions.southArrowNorthEast
+		]
+	};
+}