Show: Inherited Protected Private Deprecated

# File `src/ui/react/src/components/main.jsx`

                                (function () {
                                    'use strict';

                                    /**
                                     * The main editor UI class manages a hierarchy of widgets (toolbars and buttons).
                                     *
                                     * @uses WidgetExclusive
                                     * @uses WidgetFocusManager
                                     *
                                     * @class UI
                                     */
                                    var UI = React.createClass({
                                        mixins: [AlloyEditor.WidgetExclusive, AlloyEditor.WidgetFocusManager],

                                        // Allows validating props being passed to the component.
                                        propTypes: {
                                            /**
                                             * Localized messages for live aria updates. Should include the following messages:
                                             * - noToolbar: Notification for no available toolbar in the editor.
                                             * - oneToolbar: Notification for just one available toolbar in the editor.
                                             * - manyToolbars: Notification for more than one available toolbar in the editor.
                                             *
                                             * @property {Object} ariaUpdates
                                             */
                                            ariaUpdates: React.PropTypes.object,

                                            /**
                                             * The editor instance where the component is being used.
                                             *
                                             * @property {Object} editor
                                             */
                                            editor: React.PropTypes.object.isRequired,

                                            /**
                                             * The delay (ms), after which key or mouse events will be processed.
                                             *
                                             * @property {Number} eventsDelay
                                             */
                                            eventsDelay: React.PropTypes.number,

                                            /**
                                             * The toolbars configuration for this editor instance
                                             *
                                             * @property {Object} toolbars
                                             */
                                            toolbars: React.PropTypes.object.isRequired
                                        },

                                        /**
                                         * Lifecycle. Invoked once before the component is mounted.
                                         *
                                         * @method getInitialState
                                         */
                                        getInitialState: function() {
                                            return {
                                                hidden: false
                                            };
                                        },

                                        /**
                                         * Lifecycle. Returns the default values of the properties used in the widget.
                                         *
                                         * @method getDefaultProps
                                         * @return {Object} The default properties.
                                         */
                                        getDefaultProps: function() {
                                            return {
                                                circular: true,
                                                descendants: '[class^=ae-toolbar-]',
                                                eventsDelay: 0,
                                                keys: {
                                                    next: 9
                                                }
                                            };
                                        },

                                        /**
                                         * Lifecycle. Invoked once, only on the client, immediately after the initial rendering occurs.
                                         *
                                         * @method componentDidMount
                                         */
                                        componentDidMount: function () {
                                            var editor = this.props.editor.get('nativeEditor');

                                            editor.on('editorInteraction', this._onEditorInteraction, this);
                                            editor.on('actionPerformed', this._onActionPerformed, this);
                                            editor.on('key', this._onEditorKey, this);

                                            // Set up events for hiding the UI when user stops interacting with the editor.
                                            // This may happen when he just clicks outside of the editor. However,
                                            // this does not include a situation when he clicks on some button, part of
                                            // editor's UI.

                                            // It is not easy to debounce _setUIHidden on mousedown, because if we
                                            // debounce it, when the handler is being invoked, the target might be no more part
                                            // of the editor's UI - onActionPerformed causes re-render.
                                            this._mousedownListener = function (event) {
                                                this._setUIHidden(event.target);
                                            }.bind(this);

                                            this._keyDownListener = CKEDITOR.tools.debounce(function(event) {
                                                this._setUIHidden(document.activeElement);
                                            }, this.props.eventsDelay, this);

                                            document.addEventListener('mousedown', this._mousedownListener);
                                            document.addEventListener('keydown', this._keyDownListener);
                                        },

                                        /**
                                         * Lifecycle. Invoked immediately after the component's updates are flushed to the DOM.
                                         * Fires `ariaUpdate` event passing ARIA related messages.
                                         * Fires `editorUpdate` event passing the previous and current properties and state.
                                         *
                                         * @method componentDidUpdate
                                         */
                                        componentDidUpdate: function (prevProps, prevState) {
                                            var domNode = ReactDOM.findDOMNode(this);

                                            var editor = this.props.editor.get('nativeEditor');

                                            if (domNode) {
                                                editor.fire('ariaUpdate', {
                                                    message: this._getAvailableToolbarsMessage(domNode)
                                                });
                                            }

                                            editor.fire('editorUpdate', {
                                                prevProps: prevProps,
                                                prevState: prevState,
                                                props: this.props,
                                                state: this.state
                                            });
                                        },

                                        _getAriaUpdateTemplate: function(ariaUpdate) {
                                            if (!this._ariaUpdateTemplates) {
                                                this._ariaUpdateTemplates = {};
                                            }

                                            if (!this._ariaUpdateTemplates[ariaUpdate]) {
                                                this._ariaUpdateTemplates[ariaUpdate] = new CKEDITOR.template(this._getAriaUpdates()[ariaUpdate]);
                                            }

                                            return this._ariaUpdateTemplates[ariaUpdate];
                                        },

                                        /**
                                         * Returns the templates for ARIA messages.
                                         *
                                         * @protected
                                         * @method _getAriaUpdates
                                         * @return {Object} ARIA relates messages. Default:
                                         * {
                                         *      noToolbar: AlloyEditor.Strings.ariaUpdateNoToolbar,
                                         *      oneToolbar: AlloyEditor.Strings.ariaUpdateOneToolbar,
                                         *      manyToolbars: AlloyEditor.Strings.ariaUpdateManyToolbars
                                         *  }
                                         */
                                        _getAriaUpdates: function() {
                                            return this.props.ariaUpdates || {
                                                noToolbar: AlloyEditor.Strings.ariaUpdateNoToolbar,
                                                oneToolbar: AlloyEditor.Strings.ariaUpdateOneToolbar,
                                                manyToolbars: AlloyEditor.Strings.ariaUpdateManyToolbars
                                            };
                                        },

                                        /**
                                         * Returns an ARIA message which represents the number of currently available toolbars.
                                         *
                                         * @method _getAvailableToolbarsMessage
                                         * @protected
                                         * @param {CKEDITOR.dom.element} domNode The DOM node from which the available toolbars will be retrieved.
                                         * @return {String} The ARIA message for the number of available toolbars
                                         */
                                        _getAvailableToolbarsMessage: function(domNode) {
                                            var toolbarsNodeList = domNode.querySelectorAll('[role="toolbar"]');

                                            if (!toolbarsNodeList.length) {
                                                return this._getAriaUpdates().noToolbar;
                                            } else {
                                                var toolbarNames = Array.prototype.slice.call(toolbarsNodeList).map(function(toolbar) {
                                                    return toolbar.getAttribute('aria-label');
                                                });

                                                var ariaUpdate = toolbarNames.length === 1 ? 'oneToolbar' : 'manyToolbars';

                                                return this._getAriaUpdateTemplate(ariaUpdate).output({
                                                    toolbars: toolbarNames.join(',').replace(/,([^,]*)$/, ' and ' + '$1')
                                                });
                                            }
                                        },

                                        /**
                                         * Lifecycle. Invoked immediately before a component is unmounted from the DOM.
                                         *
                                         * @method componentWillUnmount
                                         */
                                        componentWillUnmount: function() {
                                            if (this._mousedownListener) {
                                                document.removeEventListener('mousedown', this._mousedownListener);
                                            }

                                            if (this._keyDownListener) {
                                                this._keyDownListener.detach();
                                                document.removeEventListener('keydown', this._keyDownListener);
                                            }
                                        },

                                        /**
                                         * Lifecycle. Renders the UI of the editor. This may include several toolbars and buttons.
                                         * The editor's UI also takes care of rendering the items in exclusive mode.
                                         *
                                         * @method render
                                         * @return {Object} The content which should be rendered.
                                         */
                                        render: function() {
                                            if (this.state.hidden) {
                                                return null;
                                            }

                                            var toolbars = Object.keys(this.props.toolbars).map(function(toolbar) {
                                                return AlloyEditor.Toolbars[toolbar] || window[toolbar];
                                            });

                                            toolbars = this.filterExclusive(toolbars).map(function(toolbar) {
                                                var props = this.mergeExclusiveProps({
                                                    config: this.props.toolbars[toolbar.key],
                                                    editor: this.props.editor,
                                                    editorEvent: this.state.editorEvent,
                                                    key: toolbar.key,
                                                    onDismiss: this._onDismissToolbarFocus,
                                                    selectionData: this.state.selectionData
                                                }, toolbar.key);

                                                return React.createElement(toolbar, props);
                                            }.bind(this));

                                            return (
                                                <div className="ae-toolbars" onKeyDown={this.handleKey}>
                                                    {toolbars}
                                                </div>
                                            );
                                        },

                                        /**
                                         * Listener to the editor's `actionPerformed` event. Sets state and redraws the UI of the editor.
                                         *
                                         * @protected
                                         * @method _onActionPerformed
                                         * @param {SynteticEvent} event The provided event
                                         */
                                        _onActionPerformed: function(event) {
                                            var editor = this.props.editor.get('nativeEditor');

                                            editor.focus();

                                            this.setState({
                                                itemExclusive: null,
                                                selectionData: editor.getSelectionData()
                                            });
                                        },

                                        /**
                                         * Executed when a dismiss key is pressed over a toolbar to return the focus to the editor.
                                         *
                                         * @protected
                                         * @method _onDismissToolbarFocus
                                         */
                                        _onDismissToolbarFocus: function() {
                                            var editor = this.props.editor.get('nativeEditor');

                                            editor.focus();
                                        },

                                        /**
                                         * Listener to the editor's `userInteraction` event. Retrieves the data about the user selection and
                                         * provides it via component's state property.
                                         *
                                         * @protected
                                         * @method _onEditorInteraction
                                         * @param {SynteticEvent} event The provided event
                                         */
                                        _onEditorInteraction: function(event) {
                                            this.setState({
                                                editorEvent: event,
                                                hidden: false,
                                                itemExclusive: null,
                                                selectionData: event.data.selectionData
                                            });
                                        },

                                        /**
                                         * Focuses on the active toolbar when the combination ALT+F10 is pressed inside the editor.
                                         *
                                         * @protected
                                         * @method _onEditorKey
                                         */
                                        _onEditorKey: function(event) {
                                            var nativeEvent = event.data.domEvent.$;

                                            if (nativeEvent.altKey && nativeEvent.keyCode === 121) {
                                                this.focus();
                                            }
                                        },

                                        /**
                                         * Checks if the target with which the user interacted is part of editor's UI or it is
                                         * the editable area. If none of these, sets the state of editor's UI to be hidden.
                                         *
                                         * @protected
                                         * @method _setUIHidden
                                         * @param {DOMElement} target The DOM element with which user interacted lastly.
                                         */
                                        _setUIHidden: function(target) {
                                            var domNode = ReactDOM.findDOMNode(this);

                                            if (domNode) {
                                                var editable = this.props.editor.get('nativeEditor').editable();
                                                var targetNode = new CKEDITOR.dom.node(target);

                                                var res = (editable.$ === target) || editable.contains(targetNode) ||
                                                    (new CKEDITOR.dom.element(domNode)).contains(targetNode);

                                                if (!res) {
                                                    this.setState({
                                                        hidden: true
                                                    });
                                                }
                                            }
                                        }
                                    });

                                    /**
                                     * Fired when component updates and when it is rendered in the DOM.
                                     * The payload consists from a `message` property containing the ARIA message.
                                     *
                                     * @event ariaUpdate
                                     */

                                    /**
                                     * Fired when component updates. The payload consists from an object with the following
                                     * properties:
                                     * - prevProps - The previous properties of the component
                                     * - prevState - The previous state of the component
                                     * - props - The current properties of the component
                                     * - state - The current state of the component
                                     *
                                     * @event ariaUpdate
                                     */

                                    AlloyEditor.UI = UI;
                                }());