/*** * @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;