Source: controls/Editor.js

/***
 * @guicreatable
 * @requirefiles {helpers.js}
 */

/**
 * Creates an instance of a new Editor control based on the contenteditable property.
 * @mixin Editor
 * @desc The Editor mixin provides an editable block of text.
 * @param {JQuery} element The element to which the editor should be applied.
 * @param settings
 * @param {number[]} [settings.BlockedKeys=[]] Key codes which should be blocked from input.
 * @param {Control|Object} [settings.data=null] The data object passed along with events.
 * @param {EditorGlobal#Font} [settings.DefaultFont=EditorGlobal.DefaultFont] Override of default font settings.
 * @param {number} [settings.Height=0] Height of editor.
 * @param {JQuery} [settings.Height=null] Element to sync height to.
 * @param {boolean} [settings.NumericOnly=false] Blocks non-numeric values from being entered.
 * @param {boolean} [settings.onChange=null] Event called when editor value is changed.
 * @param {boolean} [settings.onKeyBlock=null] Event called when editor blocks a key from entry.
 * @param {boolean} [settings.onResize=null] Event called when editor is resized.
 * @param {boolean} [settings.ReadOnly=false] Sets whether or not the editor is read-only.
 * @param {Validator} [settings.Validator=Validator] Validator to use in conjunction with the editor mixin.
 * @param {boolean|undefined} [autoNew=true] Creates a new editor object when missing.
 * @constructor
 */
function Editor(element, settings, autoNew) {
    if (element.length > 1) {
        for (var i = 0; i < element.length; i++) {
            new Editor($(element[i]), settings);
        }
        return;
    } else if(arguments.length < 3) {
        new Editor($(element), settings, true);
        return;
    }
    /**
     * Flag signifying whether or not the editor is ready to use.
     * @name Editor#Ready
     * @type {boolean}
     */
    this.Ready = false;
    /**
     * Element which has been transformed into an editor.
     * @name Editor#Parent
     * @type {JQuery}
     */
    this.Parent = element;
    /**
     * The data object passed along with events.
     * @name Editor#data
     * @type {Control|Object}
     */
    this.data = null;
    /**
     * Event called when editor value is changed.
     * @event Editor#onChange
     */
    this.onChange = null;
    /**
     * Event called when editor blocks a key from entry.
     * @event Editor#onKeyBlock
     * @param {IJQueryEventObject} event The keydown event object.
     */
    this.onKeyBlock = null;
    /**
     * Event called when editor is resized.
     * @event Editor#onResize
     */
    this.onResize = null;
    /**
     * Setting signifying whether or not the editor should block non-numeric values from being entered.
     * @name Editor#NumericOnly
     * @type {boolean}
     */
    this.NumericOnly = false;
    this._readOnly = (settings.hasOwnProperty('ReadOnly') ? settings.ReadOnly : false);
    /**
     * Key codes which should be blocked from input.
     * @name Editor#BlockedKeys
     * @type number[]
     */
    this.BlockedKeys = new Array();
    /**
     * Validator to use in conjunction with the editor.
     * @name Editor#Validator
     * @type {Validator}
     */
    this.Validator = Validator;
    if (settings.hasOwnProperty('Validator')) { this.Validator = settings.Validator; }
    /**
     * Most recently validated value within the editor.
     * @name Editor#LastValidValue
     * @type {string}
     */
    if (this.Validator.AllowFormatting === true) {
        this.LastValidValue = this.Validator.Force(this.Parent.html());
    } else {
        this.LastValidValue = this.Validator.Force(this.Parent.text());
    }
    this.Parent.html(this.LastValidValue);
    if (settings.hasOwnProperty('BlockedKeys')) { this.BlockedKeys = settings.BlockedKeys; }
    if (settings.hasOwnProperty('NumericOnly')) { this.NumericOnly = settings.NumericOnly; }
    if (settings.hasOwnProperty('onChange')) { this.onChange = settings.onChange; }
    if (settings.hasOwnProperty('onKeyBlock')) { this.onKeyBlock = settings.onKeyBlock; }
    if (settings.hasOwnProperty('onResize')) { this.onResize = settings.onResize; }
    if (settings.hasOwnProperty('data')) { this.data = settings.data; }
    /**
     * Whether or not the editor is read-only.
     * @name Editor#ReadOnly
     * @type boolean
     * @default false
     */
    Object.defineProperty(this, 'ReadOnly', {
        get: function () { return (this._readOnly); },
        set: function (value) {
            this._readOnly = value;
            this.Parent.attr('contenteditable', (this._readOnly ? 'false' : 'true'));
            if (this._readOnly == true) {
                this.Parent.css('cursor', 'default');
                this.Parent.css('-webkit-touch-callout', 'none');
                this.Parent.css('-webkit-user-select', 'none');
                this.Parent.css('-khtml-user-select', 'none');
                this.Parent.css('-moz-user-select', 'none');
                this.Parent.css('-ms-user-select', 'none');
                this.Parent.css('user-select', 'none');
            } else {
                this.Parent.css('cursor', 'text');
                this.Parent.css('-webkit-touch-callout', '');
                this.Parent.css('-webkit-user-select', '');
                this.Parent.css('-khtml-user-select', '');
                this.Parent.css('-moz-user-select', '');
                this.Parent.css('-ms-user-select', '');
                this.Parent.css('user-select', '');
            }
        }
    });
    this.ReadOnly = this._readOnly;
    this.Parent.bind('focus', this, function (event) { EditorGlobal.ActiveEditor = event.data; });
    //this.Parent.bind('blur', this, function (event) { if (EditorGlobal.ActiveEditor == event.data) { EditorGlobal.ActiveEditor = null; } });
    this.Parent.bind('drop', this, function (event) { event.data.Changed(); });
    this.Parent.bind('keydown', this, function (event) { if (!event.ctrlKey && !event.metaKey) { event.data.Changed(); } });
    this.Parent.bind("select", this, function (event) { event.data.PossibleSelection(); });
    this.Parent.bind("selectstart", this, function (event) { event.data.PossibleSelection(); });
    this.Parent.bind("keydown", this, function (event) {
        if (event.data.BlockedKeys.indexOf(event.which) >= 0) {
            if (window.ControlGlobal.Controls[window.ControlGlobal.Controls.length - 1] == event.data.data) {
                if (event.data.onKeyBlock != null) {
                    event.data.onKeyBlock(event);
                }
                return (false);
            }
        }
        event.data.Changed();
        event.data.PossibleSelection();
    });
    this.Parent.bind("keypress", this, function (event) { event.data.PossibleSelection(); });
    this.Parent.bind("keyup", this, function (event) { event.data.PossibleSelection(); });
    this.Parent.bind("mousedown", this, function (event) { if (event.data.data != null) { window.ControlGlobal.Controls.Activate(event.data.data); } event.data.PossibleSelection(); event.stopPropagation(); });
    this.Parent.bind("mousemove", this, function (event) { event.data.PossibleSelection(); });
    this.Parent.bind("mouseup", this, function (event) { event.data.PossibleSelection(); });
    this.Parent.bind("cut", this, function (event) { event.data.Changed(); });
    this.Parent.bind("paste", this, function (event) { event.data.Changed(); });
    this.Parent.bind("undo", this, function (event) { event.data.Changed(); });
    this.Parent.bind("redo", this, function (event) { event.data.Changed(); });
    this.Parent.bind("input", this, function(event) { event.data.Changed(); });
    if(!settings.hasOwnProperty('DefaultFont')) {
        settings.DefaultFont = { Font: EditorGlobal.DefaultFont.Font, Family: EditorGlobal.DefaultFont.Family, Size: EditorGlobal.DefaultFont.Size, Styles: EditorGlobal.DefaultFont.Styles };
    }
    if (!settings.hasOwnProperty('Height')) { settings.Height = 0; }
    if (!settings.hasOwnProperty('HeightElement')) { settings.HeightElement = null; }
    /**
     * @typedef {Object} Editor#SelectionObject
     * @property {string} [Selection.Raw=""]
     * @property {string[]} [Selection.Fonts=[EditorGlobal.DefaultFont.Font]]
     * @property {string[]} [Selection.Families=[EditorGlobal.DefaultFont.Family]]
     * @property {string[]} [Selection.Sizes=[EditorGlobal.DefaultFont.Size]]
     */
    /**
     * Object storing selection information for the editor.
     * @name Editor#Selection
     * @type {Editor#SelectionObject}
     */
    this.Selection = { Raw: '', Fonts: new Array(EditorGlobal.DefaultFont.Font), Families: new Array(EditorGlobal.DefaultFont.Family), Sizes: new Array(EditorGlobal.DefaultFont.Size) };
    /*
    if(settings.hasOwnProperty('Events')) {
        for(var ev in settings.Events) {
            if(settings.Events.hasOwnProperty(ev)) {
                if (!settings.Events[ev].hasOwnProperty('Name')) { settings.Events[ev].Name = ev; }
                if(settings.Events[ev].hasOwnProperty('Callback')) {
                    this.Parent.on(settings.Events[ev].Name, { editor: this, data: settings.Events[ev] }, settings.Events[ev].Callback);
                }
            }
        }
    }*/
    /**
     * @typedef {Object} Editor#SettingsObject
     * @property {number[]} SettingsObject.BlockedKeys Key codes which should be blocked from input.
     * @property {Control|Object} SettingsObject.data The data object passed along with events.
     * @property {EditorGlobal#Font} SettingsObject.DefaultFont Override of default font settings.
     * @property {number} SettingsObject.Height Height of editor.
     * @property {JQuery} SettingsObject.Height Element to sync height to.
     * @property {boolean} SettingsObject.NumericOnly Blocks non-numeric values from being entered.
     * @property {boolean} SettingsObject.onChange Event called when editor value is changed.
     * @property {boolean} SettingsObject.onKeyBlock Event called when editor blocks a key from entry.
     * @property {boolean} SettingsObject.onResize Event called when editor is resized.
     * @property {boolean} SettingsObject.ReadOnly Sets whether or not the editor is read-only.
     * @property {Validator} SettingsObject.Validator Validator to use in conjunction with the editor mixin.
     */
    /**
     * Object storing settings for the editor.
     * @name Editor#Settings
     * @type {Editor#SettingsObject}
     */
    this.Settings = settings;
    EditorGlobal.Editors.push(this);
    this.Ready = true;
    //this.Exec('styleWithCSS', true);
    this.Changed();
}

/**
 * Handles the change logic for the editor and calls the onChange event if appropriate.
 * @function
 * @name Editor#Changed
 */
function Editor_Changed() {
    var value;
    if (this.Validator.AllowFormatting === true) {
        value = this.Parent.html();
    } else {
        value = this.Parent.getRawText();
    }
    if (this.Validator.Validate(value)) {
        this.LastValidValue = value;
    } else {
        this.Parent.html(this.LastValidValue);
    }
    this.PossibleSelection();
    this.Resize();
    if (this.onChange != null) { this.onChange(this.LastValidValue); }
}

/**
 * Executes a document command on the active editor.<br />
 * <span class="inline-note">See <a href="https://developer.mozilla.org/en-US/docs/Web/API/document/execCommand" target="_blank">https://developer.mozilla.org/en-US/docs/Web/API/document/execCommand</a> for more information.</span>
 *
 * @function
 * @name Editor#Exec
 * @param {string} commandName The command to execute on the editor region.
 * @param {string} value Value parameter to send with command being executed.
 */
function Editor_Exec(commandName, value) {
    if (typeof value == "undefined") {
        value = null;
    }
    if (typeof window.getSelection != "undefined") {
        document.execCommand(commandName, false, value);
        return;
        var sel = window.getSelection();
        var savedRanges = [];
        for (var i = 0, len = sel.rangeCount; i < len; ++i) {
            savedRanges[i] = sel.getRangeAt(i).cloneRange();
        }
        document.designMode = "on";
        sel = window.getSelection();
        var range = document.createRange();
        range.selectNodeContents(this.Parent[0]);
        sel.removeAllRanges();
        sel.addRange(range);
        document.execCommand(commandName, false, value);
        document.designMode = "off";
        sel = window.getSelection();
        sel.removeAllRanges();
        for (var i = 0, len = savedRanges.length; i < len; ++i) {
            sel.addRange(savedRanges[i]);
        }
    } else if (typeof document.body.createTextRange != "undefined") {
        document.body.execCommand(commandName, false, value);
        var textRange = document.body.createTextRange();
        textRange.moveToElementText(this.Parent[0]);
        textRange.execCommand(commandName, false, value);
    }
}

/**
 * @typedef {Object} Editor#RawSelectionObject
 * @property {string} Selection.Raw
 * @property {string[]} Selection.Fonts
 * @property {string[]} Selection.Families
 * @property {string[]} Selection.Sizes
 * @property {string} Selection.Bold
 * @property {string} Selection.Italic
 * @property {string} Selection.Underline
 * @property {string} Selection.Strikethrough
 * @property {string} Selection.Subscript
 * @property {string} Selection.Superscript
 * @property {string} Selection.Styles
 */
/**
 * Retrieves raw selection data.
 * @function
 * @name Editor#GetRawSelection
 * @returns {Editor#RawSelectionObject}
 */
function Editor_GetRawSelection() {/*
    var html = "";
    if (typeof window.getSelection != "undefined") {
        var sel = window.getSelection();
        if (sel.rangeCount) {
            var container = document.createElement("div");
            for (var i = 0, len = sel.rangeCount; i < len; ++i) {
                container.appendChild(sel.getRangeAt(i).cloneContents());
            }
            html = container.innerHTML;
        }
    } else if (typeof document.selection != "undefined") {
        if (document.selection.type == "Text") {
            html = document.selection.createRange().htmlText;
        }
    }
    var sel = document.getSelection();
    return ({ Raw: html, Font: { Name: document.queryCommandValue('fontName').split(',')[0], Size: document.queryCommandValue('fontSize') } });*/
    var ret = {
        Raw: '',
        Fonts: new Array(),
        Families: new Array(),
        Sizes: new Array(),
        Bold: document.queryCommandState('bold'),
        Italic: document.queryCommandState('italic'),
        Underline: document.queryCommandState('underline'),
        Strikethrough: document.queryCommandState('strikeThrough'),
        Subscript: document.queryCommandState('subscript'),
        Superscript: document.queryCommandState('superscript'),
        Styles: new Array()
    };
    var DefaultFont = { Font: this.Settings.DefaultFont.Font, Family: this.Settings.DefaultFont.Family, Size: this.Settings.DefaultFont.Size, Styles: this.Settings.DefaultFont.Styles };
    if (typeof document.getSelection != "undefined") {
        var sel = document.getSelection();
        var faceFound = false;
        var sizeFound = false;
        var stylesFound = false;
        var styles;
        var node = sel.focusNode;
        var onnode = 0;
        while (onnode < 2) {
            while (node != null) {
                if ((node.nodeName + '').toLowerCase() == 'font') {
                    if ((faceFound == false) && ($(node).css('font-family').length > 0)) {
                        DefaultFont.Family = $(node).css('font-family');
                        faceFound = true;
                    }
                    if ((sizeFound == false) && ($(node).css('font-size').length > 0)) {
                        DefaultFont.Size = $(node).css('font-size');
                        sizeFound = true;
                    }
                    styles = $(node).attr('class');
                    if ((styles != undefined) && (styles.length > 0)) {
                        styles = styles.split( /\s+/ );
                        if ((styles.length > 0) && (styles[0].length > 0)) {
                            if (stylesFound == false) {
                                DefaultFont.Styles = styles;
                            } else {
                                for (var i = 0; i < styles.length; i++) { if (styles[i].length > 0) { DefaultFont.Styles.push(styles[i]); } }
                            }
                            stylesFound = true;
                        }
                    }
                }
                node = node.parentNode;
            }
            node = sel.anchorNode;
            onnode++;
        }
        DefaultFont.Font = DefaultFont.Family.replace(/ ,/g, ',').replace(/, /g, ',').split(',')[0].replace(/\'/g, '').replace(/\"/g, '');
        if (sel.rangeCount) {
            var container = document.createElement("div");
            for (var i = 0, len = sel.rangeCount; i < len; ++i) {
                container.appendChild(sel.getRangeAt(i).cloneContents());
            }

            $('font', $(container)).each(function () {
                try { if ($(this).css('font-family').length > 0) { ret.Families.push($(this).css('font-family')); } } catch (ex) { }
                try { if ($(this).css('font-size').length > 0) { ret.Sizes.push($(this).css('font-size')); } } catch (ex) { }
                try {
                    styles = $(this).attr('class').split(/\s+/);
                    if ((styles.length > 0) && (styles[0].length > 0)) {
                        for (var i = 0; i < styles.length; i++) { if (styles[i].length > 0) { ret.Styles.push(styles[i]); } }
                    }
                } catch (ex) { }
            });

            ret.Families = ret.Families.sort().sort(function (a, b) { return a - b; }).filter(function (el, i, a) { if (i == a.indexOf(el) & el.length > 0) return 1; return 0; });
            ret.Sizes = ret.Sizes.sort().sort(function (a, b) { return a - b; }).filter(function (el, i, a) { if (i == a.indexOf(el) & el.length > 0) return 1; return 0; });
            ret.Styles = ret.Styles.sort().sort(function (a, b) { return a - b; }).filter(function (el, i, a) { if (i == a.indexOf(el) & el.length > 0) return 1; return 0; });

            for(var i = 0; i < ret.Families.length; i++) {
                if(ret.Families[i].indexOf(',') >= 0) {
                    ret.Fonts.push(ret.Families[i].replace(/ ,/g, ',').replace(/, /g, ',').split(',')[0].replace(/\'/g, '').replace(/\"/g, ''));
                } else {
                    ret.Fonts.push(ret.Families[i]);
                }
            }

            ret.Raw = container.innerHTML;
            delete container;
        }
    } else if (typeof document.selection != "undefined") {
        if (document.selection.type == "Text") {
            ret.Raw = document.selection.createRange().htmlText;
        }
    }
    if (ret.Fonts.length < 1) { ret.Fonts.push(DefaultFont.Font); }
    if (ret.Families.length < 1) { ret.Families.push(DefaultFont.Family); }
    if (ret.Sizes.length < 1) { ret.Sizes.push(DefaultFont.Size); }
    if (ret.Styles.length < 1) { for (var i = 0; i < DefaultFont.Styles.length; i++) { if (DefaultFont.Styles[i].length > 0) { ret.Styles.push(DefaultFont.Styles[i]); } } }
    // noinspection JSValidateTypes
    return (ret);
}

/**
 * Triggers a check to determine if the selection has changed and fire off the appropriate event if so.
 * @function
 * @name Editor#PossibleSelection
 */
function Editor_PossibleSelection() {
    var obj = this.Parent[0];
    if(obj != undefined) {
        var sel = this.GetRawSelection();
        if ((this.Selection.Raw != sel.Raw) || (!arraysEqual(this.Selection.Fonts, sel.Fonts)) || (!arraysEqual(this.Selection.Families, sel.Families)) || (!arraysEqual(this.Selection.Sizes, sel.Sizes))) {
            delete this.Selection;
            this.Selection = sel;
            this.SelectionChanged();
        }
    }
}

/**
 * Resizes the editor based on the size constraints applied to it.
 * @function
 * @name Editor#Resize
 */
function Editor_Resize() {
    var completed = false;
    if (this.Settings.HeightElement != null) {
        try {
            this.Parent.css('height', '');
            var height = Math.max(this.Settings.HeightElement.height(), this.Parent.height());
            this.Parent.css('height', height + 'px');
            completed = true;
        } catch(ex) {
            try {
                this.Parent.css('height', this.Settings.HeightElement.height() + 'px');
                completed = true;
            } catch(ex) {

            }
        }
    }
    if (completed == false) {
        if (this.Settings.Height > 0) {
            try {
                this.Parent.css('height', this.Settings.Height + 'px');
                completed = true;
            } catch(ex) {

            }
        } else {
            completed = true;
        }
    }
    if (completed == false) {
        this.Parent.css('height', this.Parent.outerHeight() + 'px');
    }
    if (this.onResize != null) { var self = this; setTimeout(function () { self.onResize(); }, 1); }
}

/**
 * Calls the SelectionChanged event for the editor.
 * @function
 * @name Editor#SelectionChanged
 */
function Editor_SelectionChanged() {
    if (this.Settings.Events.SelectionChanged != null) { this.Settings.Events.SelectionChanged(this); }
}

/**
 * Sets a CSS class to the editor.
 * @function
 * @name Editor#SetClass
 * @param {string} className The CSS class to set.
 */
function Editor_SetClass(className) {
    this.Exec("fontSize", 3);
    this.Parent.find('font[size="3"]').each(function () {
        $(this).find('font').removeClass();
        $(this).removeAttr('size');
        $(this).removeClass();
        $(this).addClass(className);
    });
    this.Changed();
}

/**
 * Sets a fixed height to the editor.
 * @function
 * @name Editor#SetHeight
 * @param {string} height The height to set.
 */
function Editor_SetHeight(height) {
    this.Settings.Height = height;
    this.Settings.HeightElement = null;
    this.Resize();
}

/**
 * Sets an object height binding to the editor.
 * @function
 * @name Editor#SetHeightElement
 * @param {string} element The element to set the editor height off of.
 */
function Editor_SetHeightElement(element) {
    this.Settings.HeightElement = element;
    this.Settings.Height = 0;
    this.Resize();
}

/**
 * Applies a CSS style property to the editor.
 * @function
 * @name Editor#SetStyle
 * @param {string} name The name of the CSS property to set.
 * @param {string} value Value of CSS property to be set.
 */
function Editor_SetStyle(name, value) {
    this.Exec("fontSize", 3);
    this.Parent.find('font[size="3"]').removeAttr('size').css(name, value);
    this.Changed();
}
Editor.prototype.constructor = Editor;
Editor.prototype.Changed = Editor_Changed;
Editor.prototype.Exec = Editor_Exec;
Editor.prototype.GetRawSelection = Editor_GetRawSelection;
Editor.prototype.PossibleSelection = Editor_PossibleSelection;
Editor.prototype.Resize = Editor_Resize;
Editor.prototype.SelectionChanged = Editor_SelectionChanged;
Editor.prototype.SetClass = Editor_SetClass;
Editor.prototype.SetHeight = Editor_SetHeight;
Editor.prototype.SetHeightElement = Editor_SetHeightElement;
Editor.prototype.SetStyle = Editor_SetStyle;