Source: AceEditor.es.js

/**
 * Copyright (c) 2000-present Liferay, Inc. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 *
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 */

import Component from 'metal-component';
import Soy from 'metal-soy';
import {Config} from 'metal-state';

import templates from './AceEditor.soy';

/**
 * @param  {...any} args
 */
const FragmentAutocompleteProcessor = function(...args) {
	FragmentAutocompleteProcessor.superclass.constructor.apply(this, args);
};

/**
 * Creates an Ace Editor component to use for code editing.
 */
class AceEditor extends Component {
	/**
	 * @inheritDoc
	 */
	attached() {
		this._editorDocument = null;
		this._editorSession = null;

		this._getAutocompleteSuggestion = this._getAutocompleteSuggestion.bind(
			this
		);

		this._handleDocumentChanged = this._handleDocumentChanged.bind(this);

		AUI().use(
			'aui-ace-editor',
			'aui-ace-autocomplete-plugin',
			'aui-ace-autocomplete-templateprocessor',

			A => {
				this._editor = new A.AceEditor({
					boundingBox: this.refs.wrapper,
					highlightActiveLine: false,
					mode: this.syntax,
					tabSize: 2
				});

				this._editor.set('readOnly', this.readOnly);

				this._editorDocument = this._editor.getSession().getDocument();
				this._editorSession = this._editor.getSession();

				this._overrideSetAnnotations(this._editorSession);

				this.refs.wrapper.style.height = '';
				this.refs.wrapper.style.width = '';

				this._editorDocument.on('change', this._handleDocumentChanged);

				this._editorSession.on(
					'changeAnnotation',
					this._handleDocumentChanged
				);

				if (this.initialContent) {
					this._editorDocument.setValue(this.initialContent);
				}

				this._initAutocomplete(A, this._editor);
			}
		);
	}

	/**
	 * @inheritDoc
	 */
	disposed() {
		if (this._editor) {
			this._editor.destroy();
		}
	}

	/**
	 * @inheritDoc
	 */
	shouldUpdate() {
		return false;
	}

	/**
	 * @param {object} A
	 * @param {object} editor
	 * @private
	 * @review
	 */
	_initAutocomplete(A, editor) {
		if (this.syntax !== 'html') {
			return;
		}

		A.extend(
			FragmentAutocompleteProcessor,
			A.AceEditor.TemplateProcessor,

			{
				getMatch: this._getAutocompleteMatch,
				getSuggestion: this._getAutocompleteSuggestion
			}
		);

		const autocompleteProcessor = new FragmentAutocompleteProcessor({
			directives: this.autocompleteTags.map(tag => tag.name)
		});

		editor.plug(A.Plugin.AceAutoComplete, {
			processor: autocompleteProcessor,
			render: true,
			visible: false,
			zIndex: 10000
		});
	}

	/**
	 * Returns a match object (if any) for <code>lfr-</code> tags inside the
	 * given content.
	 *
	 * @param {string} content The given content.
	 * @private
	 * @return {object} The matching result.
	 */
	_getAutocompleteMatch(content) {
		let match = null;
		let matchContent = content;
		let matchIndex = null;

		if (matchContent.lastIndexOf('<') >= 0) {
			matchIndex = matchContent.lastIndexOf('<');

			matchContent = matchContent.substring(matchIndex);

			if (/<lfr[\w]*[^<lfr]*$/.test(matchContent)) {
				match = {
					content: matchContent.substring(1),
					start: matchIndex,
					type: 0
				};
			}
		}

		return match;
	}

	/**
	 * Returns a tag completion suggestion for the given match and selected
	 * suggestion.
	 *
	 * @param {object} match The match.
	 * @param {string} selectedSuggestion The selected suggestion.
	 * @private
	 * @return {string} The suggested tag autocompletion.
	 */
	_getAutocompleteSuggestion(match, selectedSuggestion) {
		const tag = this.autocompleteTags.find(
			_tag => _tag.name === selectedSuggestion
		);

		return tag ? tag.content.substring(1) : '';
	}

	/**
	 * Callback executed when the internal Ace Editor is modified; this
	 * propagates the <code>contentChanged</code> event.
	 *
	 * @private
	 */
	_handleDocumentChanged() {
		const valid = this._editorSession
			.getAnnotations()
			.reduce((acc, annotation) => {
				return !acc || annotation.type === 'error' ? false : acc;
			}, true);

		this.emit('contentChanged', {
			content: this._editorDocument.getValue(),
			valid
		});
	}

	/**
	 * Overrides Ace Editor's session <code>setAnnotations</code> method to avoid
	 * showing misleading messages.
	 *
	 * @param {Object} session AceEditor session
	 * @private
	 */
	_overrideSetAnnotations(session) {
		const setAnnotations = session.setAnnotations.bind(session);

		session.setAnnotations = () => {
			setAnnotations([]);
		};
	}
}

/**
 * Available Ace Editor syntax.
 *
 * @static
 * @type {Object}
 */
AceEditor.SYNTAX = {
	css: 'css',
	html: 'html',
	javascript: 'javascript',
	json: 'json'
};

/**
 * State definition.
 *
 * @static
 * @type {!Object}
 */
AceEditor.STATE = {
	/**
	 * Ace editor plugin instance
	 * @default null
	 * @instance
	 * @memberof AceEditor
	 * @type object
	 */
	_editor: Config.object()
		.internal()
		.value(null),

	/**
	 * Ace editor plugin document instance
	 * @default null
	 * @instance
	 * @memberof AceEditor
	 * @type object
	 */
	_editorDocument: Config.object()
		.internal()
		.value(null),

	/**
	 * Ace editor plugin session instance
	 * @default null
	 * @instance
	 * @memberof AceEditor
	 * @type object
	 */
	_editorSession: Config.object()
		.internal()
		.value(null),

	/**
	 * List of tags for custom autocompletion in the HTML editor.
	 *
	 * @default []
	 * @instance
	 * @memberOf AceEditor
	 * @type Array
	 */
	autocompleteTags: Config.arrayOf(
		Config.shapeOf({
			attributes: Config.arrayOf(Config.string()),
			name: Config.string()
		})
	),

	/**
	 * Initial content sent to the editor.
	 *
	 * @default ''
	 * @instance
	 * @memberOf AceEditor
	 * @type {string}
	 */
	initialContent: Config.string().value(''),

	/**
	 * Sets the editor in readOnly mode preventing any input from the user.
	 *
	 * @default undefined
	 * @instance
	 * @memberOf AceEditor
	 * @type {boolean}
	 */
	readOnly: Config.bool().required(),

	/**
	 * Syntax used for the Ace Editor that is rendered on the interface.
	 *
	 * @default undefined
	 * @instance
	 * @memberOf AceEditor
	 * @see {@link AceEditor.SYNTAX|SYNTAX}
	 * @type {!string}
	 */
	syntax: Config.oneOf(Object.values(AceEditor.SYNTAX)).required()
};

Soy.register(AceEditor, templates);

export {AceEditor};
export default AceEditor;