/** * Copyright (c) 2011 Oscar Godson http://oscargodson.com * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function(window, undefined){ /** * showdown.js -- A javascript port of Markdown. * Copyright (c) 2007 John Fraser. * Original Markdown Copyright (c) 2004-2005 John Gruber * Redistributable under a BSD-style open source license. */ var Showdown={};"object"===typeof exports&&(Showdown=exports,Showdown.parse=function(h,i){return(new Showdown.converter).makeHtml(h,i)});var GitHub; Showdown.converter=function(){var h,i,o,p=0;this.makeHtml=function(a,c){"undefined"!==typeof c&&("string"===typeof c&&(c={nameWithOwner:c}),GitHub=c);h=[];i=[];o=[];a=a.replace(/~/g,"~T");a=a.replace(/\$/g,"~D");a=a.replace(/\r\n/g,"\n");a=a.replace(/\r/g,"\n");a="\n\n"+a+"\n\n";a=v(a);a=a.replace(/^[ \t]+$/mg,"");a=w(a);a=C(a);a=q(a);a=x(a);a=a.replace(/~D/g,"$$");a=a.replace(/~T/g,"~");a=a.replace(/https?\:\/\/[^"\s\<\>]*[^.,;'">\:\s\<\>\)\]\!]/g,function(b,c){var d=a.slice(0,c),f=a.slice(c);return d.match(/<[^>]+$/)&& f.match(/^[^>]*>/)?b:""+b+""});a=a.replace(/[a-z0-9_\-+=.]+@[a-z0-9\-]+(\.[a-z0-9-]+)+/ig,function(a){return""+a+""});a=a.replace(/[a-f0-9]{40}/ig,function(b,c){if("undefined"==typeof GitHub||"undefined"==typeof GitHub.nameWithOwner)return b;var d=a.slice(0,c),f=a.slice(c);return d.match(/@$/)||d.match(/<[^>]+$/)&&f.match(/^[^>]*>/)?b:""+b.substring(0,7)+""}); a=a.replace(/([a-z0-9_\-+=.]+)@([a-f0-9]{40})/ig,function(b,c,d,f){if("undefined"==typeof GitHub||"undefined"==typeof GitHub.nameWithOwner)return b;GitHub.repoName=GitHub.repoName||GitHub.nameWithOwner.match(/^.+\/(.+)$/)[1];var j=a.slice(0,f),f=a.slice(f);return j.match(/\/$/)||j.match(/<[^>]+$/)&&f.match(/^[^>]*>/)?b:""+c+"@"+d.substring(0,7)+""});a=a.replace(/([a-z0-9_\-+=.]+\/[a-z0-9_\-+=.]+)@([a-f0-9]{40})/ig, function(a,c,d){return""+c+"@"+d.substring(0,7)+""});a=a.replace(/#([0-9]+)/ig,function(b,c,d){if("undefined"==typeof GitHub||"undefined"==typeof GitHub.nameWithOwner)return b;var f=a.slice(0,d),d=a.slice(d);return""==f||f.match(/[a-z0-9_\-+=.]$/)||f.match(/<[^>]+$/)&&d.match(/^[^>]*>/)?b:""+b+""});a=a.replace(/([a-z0-9_\-+=.]+)#([0-9]+)/ig, function(b,c,d,f){if("undefined"==typeof GitHub||"undefined"==typeof GitHub.nameWithOwner)return b;GitHub.repoName=GitHub.repoName||GitHub.nameWithOwner.match(/^.+\/(.+)$/)[1];var j=a.slice(0,f),f=a.slice(f);return j.match(/\/$/)||j.match(/<[^>]+$/)&&f.match(/^[^>]*>/)?b:""+b+""});return a=a.replace(/([a-z0-9_\-+=.]+\/[a-z0-9_\-+=.]+)#([0-9]+)/ig,function(a,c,d){return""+a+""})};var C=function(a){return a=a.replace(/^[ ]{0,3}\[(.+)\]:[ \t]*\n?[ \t]*(\S+?)>?[ \t]*\n?[ \t]*(?:(\n*)["(](.+?)[")][ \t]*)?(?:\n+|\Z)/gm,function(a,b,e,d,f){b=b.toLowerCase();h[b]=y(e);if(d)return d+f;f&&(i[b]=f.replace(/"/g,"""));return""})},w=function(a){a=a.replace(/\n/g,"\n\n");a=a.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del)\b[^\r]*?\n<\/\2>[ \t]*(?=\n+))/gm,m);a=a.replace(/^(<(p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math)\b[^\r]*?.*<\/\2>[ \t]*(?=\n+)\n)/gm, m);a=a.replace(/(\n[ ]{0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,m);a=a.replace(/(\n\n[ ]{0,3}[ \t]*(?=\n{2,}))/g,m);a=a.replace(/(?:\n\n)([ ]{0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,m);return a=a.replace(/\n\n/g,"\n")},m=function(a,c){var b;b=c.replace(/\n\n/g,"\n");b=b.replace(/^\n/,"");b=b.replace(/\n+$/g,"");return b="\n\n~K"+(o.push(b)-1)+"K\n\n"},q=function(a){for(var a=D(a),c=k("
"),d+="
",a.push(d))}c=a.length;for(e=0;e"+a+"\n
")+e});return a=a.replace(/~0/,"")},E=function(a){return a=a.replace(/`{3}(?:(.*$)\n)?([\s\S]*?)`{3}/gm,function(a,
b,e){return''+e+"
"+a+"
"})},B=function(a){a=a.replace(/&/g,"&");a=a.replace(//g,">");return a=l(a,"*_{}[]\\",!1)},G=function(a){return a=a.replace(/((^[ \t]*>[ \t]?.+\n(.+\n)*\n*)+)/gm,
function(a,b){var e;e=b.replace(/^[ \t]*>[ \t]?/gm,"~0");e=e.replace(/~0/g,"");e=e.replace(/^[ \t]+$/gm,"");e=q(e);e=e.replace(/(^|\n)/g,"$1 ");e=e.replace(/(\s*[^\r]+?<\/pre>)/gm,function(a,b){var c;c=b.replace(/^ /mg,"~0");return c=c.replace(/~0/g,"")});return k("\n"+e+"\n")})},y=function(a){a=a.replace(/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/g,"&");return a=a.replace(/<(?![a-z\/?\$!])/gi,"<")},J=function(a){a=a.replace(/<((https?|ftp|dict):[^'">\s]+)>/gi,"$1"); return a=a.replace(/<(?:mailto:)?([-.\w]+\@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi,function(a,b){return K(x(b))})},K=function(a){var c=[function(a){return""+a.charCodeAt(0)+";"},function(a){a=a.charCodeAt(0);return""+("0123456789ABCDEF".charAt(a>>4)+"0123456789ABCDEF".charAt(a&15))+";"},function(a){return a}],a=("mailto:"+a).replace(/./g,function(a){if("@"==a)a=c[Math.floor(2*Math.random())](a);else if(":"!=a)var e=Math.random(),a=0.9'+a+"").replace(/">.+:/g,'">')},x=function(a){return a=a.replace(/~E(\d+)E/g,function(a,b){var e=parseInt(b);return String.fromCharCode(e)})},u=function(a){a=a.replace(/^(\t|[ ]{1,4})/gm,"~0");return a=a.replace(/~0/g,"")},v=function(a){a=a.replace(/\t(?=\t)/g," ");a=a.replace(/\t/g,"~A~B");a=a.replace(/~B(.+?)~A/g,function(a,b){for(var e=b,d=4-e.length%4,f=0;f tag specifically for CSS * @param {string} path The path to the CSS file * @param {object} context In what context you want to apply this to (document, iframe, etc) * @param {string} id An id for you to reference later for changing properties of the * @returns {undefined} */ function _insertCSSLink(path,context,id){ id = id || ''; var headID = context.getElementsByTagName("head")[0]; var cssNode = context.createElement('link'); _applyAttrs(cssNode,{ type:'text/css' , id:id , rel:'stylesheet' , href: path+'?'+new Date().getTime() , name: path , media: 'screen' }); cssNode.media = 'screen'; headID.appendChild(cssNode); } /** * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1 * @param {boolean} [deepMerge=false] If true, will deep merge meaning it will merge sub-objects like {obj:obj2{foo:'bar'}} * @param {object} first object * @param {object} second object * @returnss {object} a new object based on obj1 and obj2 */ function _mergeObjs(){ // copy reference to target object var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options; function isFunction(functionToCheck) { var getType = {}; return functionToCheck && getType.toString.call(functionToCheck) == '[object Function]'; } // Handle a deep copy situation if (typeof target === "boolean"){ deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } // Handle case when target is a string or something (possible in deep copy) if (typeof target !== "object" && !isFunction(target)){ target = {}; } // extend jQuery itself if only one argument is passed if (length == i){ target = this; --i; } for ( ; i < length; i++ ){ // Only deal with non-null/undefined values if ( (options = arguments[ i ]) != null ){ // Extend the base object for ( var name in options ) { var src = target[ name ], copy = options[ name ]; // Prevent never-ending loop if ( target === copy ){ continue; } // Recurse if we're merging object values if ( deep && copy && typeof copy === "object" && !copy.nodeType ){ target[ name ] = _mergeObjs( deep, // Never move original objects, clone them src || ( copy.length != null ? [ ] : { } ) , copy ); } // Don't bring in undefined values else if ( copy !== undefined ){ target[ name ] = copy; } } } } // Return the modified object return target; }; /** * Initiates the EpicEditor object and sets up offline storage as well * @class Represents an EpicEditor instance * @param {object} e A DOM object for the editor to be placed in * @returns {object} EpicEditor will be returned */ function EpicEditor(e){ var uId = 'epiceditor-'+Math.round(Math.random()*100000) , fileName = e.id; //TODO: Check for data-filename as well if(!fileName){ //If there is no id on the element to use, just use "default" fileName = 'default'; } //Default settings (will be overwritten if .options() is called with parameters) this.settings = { basePath:'epiceditor' , themes: { preview:'/themes/preview/preview-dark.css' , editor:'/themes/editor/epic-dark.css' } , file: { name:fileName //Use the DOM element's ID for an unique persistent file name , defaultContent:'' } //Because there might be multiple editors, we create a random id , id:uId , focusOnLoad:false }; //Setup local storage of files if(localStorage){ if(!localStorage['epiceditor']){ //TODO: Needs a dynamic file name! var defaultStorage = {files:{}}; defaultStorage.files[this.settings.file.name] = this.settings.file.defaultContent; defaultStorage = JSON.stringify(defaultStorage); localStorage['epiceditor'] = defaultStorage; } else if(!JSON.parse(localStorage['epiceditor']).files[this.settings.file.name]){ JSON.parse(localStorage['epiceditor']).files[this.settings.file.name] = this.settings.file.defaultContent; } else{ this.content = this.settings.file.defaultContent; } } //Now that it exists, allow binding of events if it doesn't exist yet if(!this.events){ this.events = {}; } this.element = e; return this; } /** * Changes default options such as theme, id, etc for the EpicEditor instance * @param {object} options A key/value pair of options you want to change from the default * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.options = function(options){ var self = this; self.settings = _mergeObjs(true, this.settings, options); return self; } /** * Inserts the EpicEditor into the DOM via an iframe and gets it ready for editing and previewing * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.load = function(callback){ var self = this; callback = callback || function(){}; //The editor HTML //TODO: edit-mode class should be dynamicly added! var _HtmlTemplate = ' '+ ''; //Write an iframe and then select it for the editor this.element.innerHTML = ''; var iframeElement = document.getElementById(self.settings.id); //Grab the innards of the iframe (returns the document.body) self.iframe = iframeElement.contentDocument || iframeElement.contentWindow.document; self.iframe.open(); self.iframe.write(_HtmlTemplate); //Set the default styles for the iframe var widthDiff = _outerWidth(this.element) - this.element.offsetWidth; var heightDiff = _outerHeight(this.element) - this.element.offsetHeight; iframeElement.style.width = this.element.offsetWidth - widthDiff +'px'; iframeElement.style.height = this.element.offsetHeight - heightDiff +'px'; //Remove the default browser CSS body styles, then add base CSS styles for the editor var iframeBody = self.iframe.body; iframeBody.style.padding = '0'; iframeBody.style.margin = '0'; _insertCSSLink(self.settings.basePath+self.settings.themes.editor,self.iframe); //Add a relative style to the overall wrapper to keep CSS relative to the editor self.iframe.getElementsByClassName('epiceditor-wrapper')[0].style.position = 'relative'; //Now grab the editor and previewer for later use this.editor = self.iframe.getElementsByClassName('epiceditor-textarea')[0]; this.previewer = self.iframe.getElementsByClassName('epiceditor-preview')[0]; //Firefox's gets all fucked up so, to be sure, we need to hardcode it self.iframe.body.style.height = this.element.offsetHeight+'px'; //Generate the width this.editor.style.width = '100%'; this.editor.style.height = this.element.offsetHeight+'px'; //Should actually check what mode it's in! this.previewer.style.display = 'none'; this.previewer.style.overflow = 'auto'; //Fit the preview window into the container //FIXME Should be in theme! this.previewer.style['padding'] = '10px'; this.previewer.style.width = this.element.offsetWidth - _outerWidth(this.previewer) - widthDiff +'px'; this.previewer.style.height = this.element.offsetHeight - _outerHeight(this.previewer) - heightDiff +'px'; //If there is a file to be opened with that filename and it has content... this.open(self.settings.file.name); if(this.settings.focusOnLoad){ this.editor.focus(); } //Sets up the onclick event on the previewer/editor toggle button self.iframe.getElementsByClassName('epiceditor-toggle-btn')[0].addEventListener('click',function(){ var editorWrapper = self.iframe.getElementsByClassName('epiceditor-wrapper')[0]; //Simply replaces a class (o), to a new class (n) on an element provided (e) function replaceClass(e, o, n){ e.className = editorWrapper.className.replace(o, n); } //If it was in edit mode... if(editorWrapper.className.indexOf('epiceditor-edit-mode') > -1){ replaceClass(editorWrapper,'epiceditor-edit-mode','epiceditor-preview-mode'); self.preview(); } //If it was in preview mode... else{ replaceClass(editorWrapper,'epiceditor-preview-mode','epiceditor-edit-mode'); self.edit(); } }); //Sets up the fullscreen editor/previewer //TODO: Deal with the fact Firefox doesn't really support fullscreen and don't browser sniff if (fullScreenApi.supportsFullScreen && document.body.webkitRequestFullScreen) { var fsElement = document.getElementById(self.settings.id) , fsBtns = self.iframe.getElementsByClassName('epiceditor-utilbar')[0]; //A simple helper to save the state of the styles on an element to make reverting easier var currentStyleState = []; var styleState = function(e,t){ t = t || 'load'; if(t === 'save'){ currentStyleState[e] = { width:_getStyle(e,'width') , height:_getStyle(e,'height') , float:_getStyle(e,'float') , display:_getStyle(e,'display') } } else{ for(x in currentStyleState[e]){ if(currentStyleState[e].hasOwnProperty(x)){ e.style[x] = currentStyleState[e][x]; } } } } styleState(self.editor,'save'); styleState(self.previewer,'save'); var revertBackTo = self.editor; self.iframe.getElementsByClassName('epiceditor-fullscreen-btn')[0].addEventListener('click',function(){ if(_getStyle(self.previewer,'display') === 'block'){ revertBackTo = self.previewer; } fullScreenApi.requestFullScreen(fsElement); }); fsElement.addEventListener(fullScreenApi.fullScreenEventName,function(){ if (fullScreenApi.isFullScreen()) { fsBtns.style.visibility = 'hidden'; //Editor styles self.editor.style.height = window.outerHeight+'px'; self.editor.style.width = window.outerWidth/2+'px'; //Half of the screen self.editor.style.float = 'left'; self.editor.style.display = 'block'; //Previewer styles self.previewer.style.height = window.outerHeight+'px' self.previewer.style.width = (window.outerWidth-_outerWidth(self.editor))+'px'; //Fill in the remaining space self.previewer.style.float = 'right'; self.previewer.style.display = 'block'; self.preview(true); var fullscreenLivePreview = self.editor.addEventListener('keyup',function(){ self.preview(true); }); } else{ fsBtns.style.visibility = 'visible'; styleState(self.editor); styleState(self.previewer); if(revertBackTo === self.editor){ self.edit(); } else{ self.preview(); } } }, true); } else{ //TODO: homebrew support by position:fixed and width/height 100% of document size self.iframe.getElementsByClassName('epiceditor-fullscreen-btn')[0].style.display = 'none'; } var utilBar = self.iframe.getElementsByClassName('epiceditor-utilbar')[0]; //Hide it at first until they move their mouse utilBar.style.display = 'none'; //Hide and show the util bar based on mouse movements var utilBarTimer , mousePos = { y:-1, x:-1 }; this.iframe.addEventListener('mousemove',function(e){ //Here we check if the mouse has moves more than 5px in any direction before triggering the mousemove code //we do this for 2 reasons: //1. On Mac OS X lion when you scroll and it does the iOS like "jump" when it hits the top/bottom of the page itll fire off // a mousemove of a few pixels depending on how hard you scroll //2. We give a slight buffer to the user in case he barely touches his touchpad or mouse and not trigger the UI if(Math.abs(mousePos.y-e.pageY) >= 5 || Math.abs(mousePos.x-e.pageX) >= 5){ utilBar.style.display = 'block'; // if we have a timer already running, kill it out if(utilBarTimer){ clearTimeout(utilBarTimer); } // begin a new timer that hides our object after 1000 ms utilBarTimer = window.setTimeout(function() { utilBar.style.display = 'none'; }, 1000); } mousePos = { y:e.pageY, x:e.pageX }; }); //Make sure, on window resize, if the containing element changes size keep it fitting inside window.addEventListener('resize',function(){ var widthDiff = _outerWidth(self.element) - self.element.offsetWidth; iframeElement.style.width = self.element.offsetWidth - widthDiff +'px'; }); //TODO: This should have a timer to save on performance //TODO: The save file shoudl be dynamic, not just default //On keyup, save the content to the proper file for offline use this.editor.addEventListener('keyup',function(){ self.content = this.value; self.save(self.settings.file.name,this.value); }); self.iframe.close(); //The callback and call are the same thing, but different ways to access them callback.call(this); this.emit('load'); return this; } /** * Will remove the editor, but not offline files * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.unload = function(callback){ var self = this; var editor = window.parent.document.getElementById(self.settings.id); editor.parentNode.removeChild(editor); callback.call(this); self.emit('unload'); return self; } /** * Will take the markdown and generate a preview view based on the theme * @param {string} theme The path to the theme you want to preview in * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.preview = function(theme,live){ var self = this , themePath = self.settings.basePath+self.settings.themes.preview; if(typeof theme === 'boolean'){ live = theme; theme = themePath } else{ theme = theme || themePath } //Check if no CSS theme link exists if(!self.iframe.getElementById('theme')){ _insertCSSLink(theme, self.iframe, 'theme'); } else if(self.iframe.getElementById('theme').name !== theme){ self.iframe.getElementById('theme').href = theme; } //Add the generated HTML into the previewer this.previewer.innerHTML = this.exportHTML(); //Hide the editor and display the previewer if(!live){ this.editor.style.display = 'none'; this.previewer.style.display = 'block'; } self.emit('preview'); return this; } /** * Hides the preview and shows the editor again * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.edit = function(){ var self = this; this.editor.style.display = 'block'; this.previewer.style.display = 'none'; self.emit('edit'); return this; } /** * Grabs a specificed HTML node. Use it as a shortcut to getting the iframe contents * @param {String} name The name of the node (can be document, body, editor, or previewer) * @returns {Object|Null} */ EpicEditor.prototype.get = function(name){ var available = { document: this.iframe , body: this.iframe.body , editor: this.editor , previewer: this.previewer } if(!available[name]){ return null; } else{ return available[name]; } } /** * Opens a file * @param {string} name The name of the file you want to open * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.open = function(name){ var self = this; name = name || self.settings.file.name; if(localStorage && localStorage['epiceditor']){ var fileObj = JSON.parse(localStorage['epiceditor']).files; if(fileObj[name]){ self.editor.value = fileObj[name]; } else{ self.editor.value = this.settings.file.defaultContent; } self.settings.file.name = name; this.previewer.innerHTML = this.exportHTML(); this.emit('open'); } return this; } /** * Saves content for offline use * @param {string} file A filename for the content to be saved to * @param {string} content The content you want saved * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.save = function(file,content){ var self = this; file = file || self.settings.file.name; content = content || this.editor.value; var s = JSON.parse(localStorage['epiceditor']); s.files[file] = content; localStorage['epiceditor'] = JSON.stringify(s); this.emit('save'); return this; } /** * Removes a page * @param {string} name The name of the file you want to remove from localStorage * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.remove = function(name){ var self = this; name = name || self.settings.file.name; var s = JSON.parse(localStorage['epiceditor']); delete s.files[name]; localStorage['epiceditor'] = JSON.stringify(s); this.emit('remove'); return this; }; /** * Imports a MD file instead of having to manual inject content via * .get(editor).value = 'the content' * @param {string} name The name of the file * @param {stirng} content The MD to import * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.import = function(name,content){ var self = this; content = content || ''; self.open(name).get('editor').value = content; //we reopen the file after saving so that it will preview correctly if in the previewer self.save().open(name); return this; }; /** * Renames a file * @param {string} oldName The old file name * @param {string} newName The new file name * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.rename = function(oldName,newName){ var self = this; var s = JSON.parse(localStorage['epiceditor']); s.files[newName] = s.files[oldName]; delete s.files[oldName]; localStorage['epiceditor'] = JSON.stringify(s); self.open(newName); return this; }; /** * Converts content into HTML from markdown * @returns {string} Returns the HTML that was converted from the markdown */ EpicEditor.prototype.exportHTML = function(){ var c = new Showdown.converter(); return c.makeHtml(this.editor.value); } //EVENTS //TODO: Support for namespacing events like "preview.foo" /** * Sets up an event handler for a specified event * @param {string} ev The event name * @param {function} handler The callback to run when the event fires * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.on = function(ev, handler){ var self = this; if (!this.events[ev]){ this.events[ev] = []; } this.events[ev].push(handler); return self; }; /** * This will emit or "trigger" an event specified * @param {string} ev The event name * @param {any} data Any data you want to pass into the callback * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.emit = function(ev, data){ var self = this; if (!this.events[ev]){ return; } //TODO: Cross browser support! this.events[ev].forEach(invokeHandler); function invokeHandler(handler) { handler.call(self.iframe,data); } return self; }; /** * Will remove any listeners added from EpicEditor.on() * @param {string} ev The event name * @param {function} handler Handler to remove * @returns {object} EpicEditor will be returned */ EpicEditor.prototype.removeListener = function(ev, handler){ var self = this; if(!handler){ this.events[ev] = []; return self; } if(!this.events[ev]){ return self; } //Otherwise a handler and event exist, so take care of it this._events[ev].splice(this._events[ev].indexOf(handler), 1); return self; } window.EpicEditor = EpicEditor; })(window); '+ ''+ ''+ ''+ ''+ '