/** * 编辑器主类,包含编辑器提供的大部分公用接口 * @file * @module UE * @class Editor * @since 1.2.6.1 */ /** * UEditor公用空间,UEditor所有的功能都挂载在该空间下 * @unfile * @module UE */ /** * UEditor的核心类,为用户提供与编辑器交互的接口。 * @unfile * @module UE * @class Editor */ (function() { var uid = 0, _selectionChangeTimer; /** * 获取编辑器的html内容,赋值到编辑器所在表单的textarea文本域里面 * @private * @method setValue * @param { UE.Editor } editor 编辑器事例 */ function setValue(form, editor) { var textarea; if (editor.options.textarea) { if (utils.isString(editor.options.textarea)) { for ( var i = 0, ti, tis = domUtils.getElementsByTagName(form, "textarea"); (ti = tis[i++]); ) { if (ti.id == "ueditor_textarea_" + editor.options.textarea) { textarea = ti; break; } } } else { textarea = editor.textarea; } } if (!textarea) { form.appendChild( (textarea = domUtils.createElement(document, "textarea", { name: editor.options.textarea, id: "ueditor_textarea_" + editor.options.textarea, style: "display:none" })) ); //不要产生多个textarea editor.textarea = textarea; } !textarea.getAttribute("name") && textarea.setAttribute("name", editor.options.textarea); textarea.value = editor.hasContents() ? editor.options.allHtmlEnabled ? editor.getAllHtml() : editor.getContent(null, null, true) : ""; } function loadPlugins(me) { //初始化插件 for (var pi in UE.plugins) { UE.plugins[pi].call(me); } } function checkCurLang(I18N) { for (var lang in I18N) { return lang; } } function langReadied(me) { me.langIsReady = true; me.fireEvent("langReady"); } /** * 编辑器准备就绪后会触发该事件 * @module UE * @class Editor * @event ready * @remind render方法执行完成之后,会触发该事件 * @remind * @example * ```javascript * editor.addListener( 'ready', function( editor ) { * editor.execCommand( 'focus' ); //编辑器家在完成后,让编辑器拿到焦点 * } ); * ``` */ /** * 执行destroy方法,会触发该事件 * @module UE * @class Editor * @event destroy * @see UE.Editor:destroy() */ /** * 执行reset方法,会触发该事件 * @module UE * @class Editor * @event reset * @see UE.Editor:reset() */ /** * 执行focus方法,会触发该事件 * @module UE * @class Editor * @event focus * @see UE.Editor:focus(Boolean) */ /** * 语言加载完成会触发该事件 * @module UE * @class Editor * @event langReady */ /** * 运行命令之后会触发该命令 * @module UE * @class Editor * @event beforeExecCommand */ /** * 运行命令之后会触发该命令 * @module UE * @class Editor * @event afterExecCommand */ /** * 运行命令之前会触发该命令 * @module UE * @class Editor * @event firstBeforeExecCommand */ /** * 在getContent方法执行之前会触发该事件 * @module UE * @class Editor * @event beforeGetContent * @see UE.Editor:getContent() */ /** * 在getContent方法执行之后会触发该事件 * @module UE * @class Editor * @event afterGetContent * @see UE.Editor:getContent() */ /** * 在getAllHtml方法执行时会触发该事件 * @module UE * @class Editor * @event getAllHtml * @see UE.Editor:getAllHtml() */ /** * 在setContent方法执行之前会触发该事件 * @module UE * @class Editor * @event beforeSetContent * @see UE.Editor:setContent(String) */ /** * 在setContent方法执行之后会触发该事件 * @module UE * @class Editor * @event afterSetContent * @see UE.Editor:setContent(String) */ /** * 每当编辑器内部选区发生改变时,将触发该事件 * @event selectionchange * @warning 该事件的触发非常频繁,不建议在该事件的处理过程中做重量级的处理 * @example * ```javascript * editor.addListener( 'selectionchange', function( editor ) { * console.log('选区发生改变'); * } */ /** * 在所有selectionchange的监听函数执行之前,会触发该事件 * @module UE * @class Editor * @event beforeSelectionChange * @see UE.Editor:selectionchange */ /** * 在所有selectionchange的监听函数执行完之后,会触发该事件 * @module UE * @class Editor * @event afterSelectionChange * @see UE.Editor:selectionchange */ /** * 编辑器内容发生改变时会触发该事件 * @module UE * @class Editor * @event contentChange */ /** * 以默认参数构建一个编辑器实例 * @constructor * @remind 通过 改构造方法实例化的编辑器,不带ui层.需要render到一个容器,编辑器实例才能正常渲染到页面 * @example * ```javascript * var editor = new UE.Editor(); * editor.execCommand('blod'); * ``` * @see UE.Config */ /** * 以给定的参数集合创建一个编辑器实例,对于未指定的参数,将应用默认参数。 * @constructor * @remind 通过 改构造方法实例化的编辑器,不带ui层.需要render到一个容器,编辑器实例才能正常渲染到页面 * @param { Object } setting 创建编辑器的参数 * @example * ```javascript * var editor = new UE.Editor(); * editor.execCommand('blod'); * ``` * @see UE.Config */ var Editor = (UE.Editor = function(options) { var me = this; me.uid = uid++; EventBase.call(me); me.commands = {}; me.options = utils.extend(utils.clone(options || {}), UEDITOR_CONFIG, true); me.shortcutkeys = {}; me.inputRules = []; me.outputRules = []; //设置默认的常用属性 me.setOpt(Editor.defaultOptions(me)); /* 尝试异步加载后台配置 */ me.loadServerConfig(); if (!utils.isEmptyObject(UE.I18N)) { //修改默认的语言类型 me.options.lang = checkCurLang(UE.I18N); UE.plugin.load(me); langReadied(me); } else { utils.loadFile( document, { src: me.options.langPath + me.options.lang + "/" + me.options.lang + ".js", tag: "script", type: "text/javascript", defer: "defer" }, function() { UE.plugin.load(me); langReadied(me); } ); } UE.instants["ueditorInstant" + me.uid] = me; }); Editor.prototype = { registerCommand: function(name, obj) { this.commands[name] = obj; }, /** * 编辑器对外提供的监听ready事件的接口, 通过调用该方法,达到的效果与监听ready事件是一致的 * @method ready * @param { Function } fn 编辑器ready之后所执行的回调, 如果在注册事件之前编辑器已经ready,将会 * 立即触发该回调。 * @remind 需要等待编辑器加载完成后才能执行的代码,可以使用该方法传入 * @example * ```javascript * editor.ready( function( editor ) { * editor.setContent('初始化完毕'); * } ); * ``` * @see UE.Editor.event:ready */ ready: function(fn) { var me = this; if (fn) { me.isReady ? fn.apply(me) : me.addListener("ready", fn); } }, /** * 该方法是提供给插件里面使用,设置配置项默认值 * @method setOpt * @warning 三处设置配置项的优先级: 实例化时传入参数 > setOpt()设置 > config文件里设置 * @warning 该方法仅供编辑器插件内部和编辑器初始化时调用,其他地方不能调用。 * @param { String } key 编辑器的可接受的选项名称 * @param { * } val 该选项可接受的值 * @example * ```javascript * editor.setOpt( 'initContent', '欢迎使用编辑器' ); * ``` */ /** * 该方法是提供给插件里面使用,以{key:value}集合的方式设置插件内用到的配置项默认值 * @method setOpt * @warning 三处设置配置项的优先级: 实例化时传入参数 > setOpt()设置 > config文件里设置 * @warning 该方法仅供编辑器插件内部和编辑器初始化时调用,其他地方不能调用。 * @param { Object } options 将要设置的选项的键值对对象 * @example * ```javascript * editor.setOpt( { * 'initContent': '欢迎使用编辑器' * } ); * ``` */ setOpt: function(key, val) { var obj = {}; if (utils.isString(key)) { obj[key] = val; } else { obj = key; } utils.extend(this.options, obj, true); }, getOpt: function(key) { return this.options[key]; }, /** * 销毁编辑器实例,使用textarea代替 * @method destroy * @example * ```javascript * editor.destroy(); * ``` */ destroy: function() { var me = this; me.fireEvent("destroy"); var container = me.container.parentNode; var textarea = me.textarea; if (!textarea) { textarea = document.createElement("textarea"); container.parentNode.insertBefore(textarea, container); } else { textarea.style.display = ""; } textarea.style.width = me.iframe.offsetWidth + "px"; textarea.style.height = me.iframe.offsetHeight + "px"; textarea.value = me.getContent(); textarea.id = me.key; container.innerHTML = ""; domUtils.remove(container); var key = me.key; //trace:2004 for (var p in me) { if (me.hasOwnProperty(p)) { delete this[p]; } } UE.delEditor(key); }, /** * 渲染编辑器的DOM到指定容器 * @method render * @param { String } containerId 指定一个容器ID * @remind 执行该方法,会触发ready事件 * @warning 必须且只能调用一次 */ /** * 渲染编辑器的DOM到指定容器 * @method render * @param { Element } containerDom 直接指定容器对象 * @remind 执行该方法,会触发ready事件 * @warning 必须且只能调用一次 */ render: function(container) { var me = this, options = me.options, getStyleValue = function(attr) { return parseInt(domUtils.getComputedStyle(container, attr)); }; if (utils.isString(container)) { container = document.getElementById(container); } if (container) { if (options.initialFrameWidth) { options.minFrameWidth = options.initialFrameWidth; } else { options.minFrameWidth = options.initialFrameWidth = container.offsetWidth; } if (options.initialFrameHeight) { options.minFrameHeight = options.initialFrameHeight; } else { options.initialFrameHeight = options.minFrameHeight = container.offsetHeight; } container.style.width = /%$/.test(options.initialFrameWidth) ? "100%" : options.initialFrameWidth - getStyleValue("padding-left") - getStyleValue("padding-right") + "px"; container.style.height = /%$/.test(options.initialFrameHeight) ? "100%" : options.initialFrameHeight - getStyleValue("padding-top") - getStyleValue("padding-bottom") + "px"; container.style.zIndex = options.zIndex; var html = (ie && browser.version < 9 ? "" : "") + "" + "
" + "" + (options.iframeCssUrl ? "" : "") + (options.initialStyle ? "" : "") + "" + "" + "" + (options.iframeJsUrl ? "" : "") + ""; container.appendChild( domUtils.createElement(document, "iframe", { id: "ueditor_" + me.uid, width: "100%", height: "100%", frameborder: "0", //先注释掉了,加的原因忘记了,但开启会直接导致全屏模式下内容多时不会出现滚动条 // scrolling :'no', src: "javascript:void(function(){document.open();" + (options.customDomain && document.domain != location.hostname ? 'document.domain="' + document.domain + '";' : "") + 'document.write("' + html + '");document.close();}())' }) ); container.style.overflow = "hidden"; //解决如果是给定的百分比,会导致高度算不对的问题 setTimeout(function() { if (/%$/.test(options.initialFrameWidth)) { options.minFrameWidth = options.initialFrameWidth = container.offsetWidth; //如果这里给定宽度,会导致ie在拖动窗口大小时,编辑区域不随着变化 // container.style.width = options.initialFrameWidth + 'px'; } if (/%$/.test(options.initialFrameHeight)) { options.minFrameHeight = options.initialFrameHeight = container.offsetHeight; container.style.height = options.initialFrameHeight + "px"; } }); } }, /** * 编辑器初始化 * @method _setup * @private * @param { Element } doc 编辑器Iframe中的文档对象 */ _setup: function(doc) { var me = this, options = me.options; if (ie) { doc.body.disabled = true; doc.body.contentEditable = true; doc.body.disabled = false; } else { doc.body.contentEditable = true; } doc.body.spellcheck = false; me.document = doc; me.window = doc.defaultView || doc.parentWindow; me.iframe = me.window.frameElement; me.body = doc.body; me.selection = new dom.Selection(doc); //gecko初始化就能得到range,无法判断isFocus了 var geckoSel; if (browser.gecko && (geckoSel = this.selection.getNative())) { geckoSel.removeAllRanges(); } this._initEvents(); //为form提交提供一个隐藏的textarea for ( var form = this.iframe.parentNode; !domUtils.isBody(form); form = form.parentNode ) { if (form.tagName == "FORM") { me.form = form; if (me.options.autoSyncData) { domUtils.on(me.window, "blur", function() { setValue(form, me); }); } else { domUtils.on(form, "submit", function() { setValue(this, me); }); } break; } } if (options.initialContent) { if (options.autoClearinitialContent) { var oldExecCommand = me.execCommand; me.execCommand = function() { me.fireEvent("firstBeforeExecCommand"); return oldExecCommand.apply(me, arguments); }; this._setDefaultContent(options.initialContent); } else this.setContent(options.initialContent, false, true); } //编辑器不能为空内容 if (domUtils.isEmptyNode(me.body)) { me.body.innerHTML = "" + (browser.ie ? "" : "
") + "
123456
* var content = editor.getContent(); //返回值:123456
* ``` */ /** * 获取编辑器的内容。 可以通过参数定义编辑器内置的判空规则 * @method getContent * @param { Function } fn 自定的判空规则, 要求该方法返回一个boolean类型的值, * 代表当前编辑器的内容是否空, * 如果返回true, 则该方法将直接返回空字符串;如果返回false,则编辑器将返回 * 经过内置过滤规则处理后的内容。 * @remind 该方法在处理包含有初始化内容的时候能起到很好的作用。 * @warning 该方法获取到的是经过编辑器内置的过滤规则进行过滤后得到的内容 * @return { String } 编辑器的内容字符串 * @example * ```javascript * // editor 是一个编辑器的实例 * var content = editor.getContent( function ( editor ) { * return editor.body.innerHTML === '欢迎使用UEditor'; //返回空字符串 * } ); * ``` */ getContent: function(cmd, fn, notSetCursor, ignoreBlank, formatter) { var me = this; if (cmd && utils.isFunction(cmd)) { fn = cmd; cmd = ""; } if (fn ? !fn() : !this.hasContents()) { return ""; } me.fireEvent("beforegetcontent"); var root = UE.htmlparser(me.body.innerHTML, ignoreBlank); me.filterOutputRule(root); me.fireEvent("aftergetcontent", cmd, root); return root.toHtml(formatter); }, /** * 取得完整的html代码,可以直接显示成完整的html文档 * @method getAllHtml * @return { String } 编辑器的内容html文档字符串 * @eaxmple * ```javascript * editor.getAllHtml(); //返回格式大致是: ...... * ``` */ getAllHtml: function() { var me = this, headHtml = [], html = ""; me.fireEvent("getAllHtml", headHtml); if (browser.ie && browser.version > 8) { var headHtmlForIE9 = ""; utils.each(me.document.styleSheets, function(si) { headHtmlForIE9 += si.href ? '' : ""; }); utils.each(me.document.getElementsByTagName("script"), function(si) { headHtmlForIE9 += si.outerHTML; }); } return ( "" + (me.options.charset ? '' : "") + (headHtmlForIE9 || me.document.getElementsByTagName("head")[0].innerHTML) + headHtml.join("\n") + "" + "" + me.getContent(null, null, true) + "" ); }, /** * 得到编辑器的纯文本内容,但会保留段落格式 * @method getPlainTxt * @return { String } 编辑器带段落格式的纯文本内容字符串 * @example * ```javascript * //编辑器html内容:1
2
* console.log(editor.getPlainTxt()); //输出:"1\n2\n * ``` */ getPlainTxt: function() { var reg = new RegExp(domUtils.fillChar, "g"), html = this.body.innerHTML.replace(/[\n\r]/g, ""); //ie要先去了\n在处理 html = html .replace(/<(p|div)[^>]*>(1
2
* console.log(editor.getPlainTxt()); //输出:"12 * ``` */ getContentTxt: function() { var reg = new RegExp(domUtils.fillChar, "g"); //取出来的空格会有c2a0会变成乱码,处理这种情况\u00a0 return this.body[browser.ie ? "innerText" : "textContent"] .replace(reg, "") .replace(/\u00a0/g, " "); }, /** * 设置编辑器的内容,可修改编辑器当前的html内容 * @method setContent * @warning 通过该方法插入的内容,是经过编辑器内置的过滤规则进行过滤后得到的内容 * @warning 该方法会触发selectionchange事件 * @param { String } html 要插入的html内容 * @example * ```javascript * editor.getContent('test
'); * ``` */ /** * 设置编辑器的内容,可修改编辑器当前的html内容 * @method setContent * @warning 通过该方法插入的内容,是经过编辑器内置的过滤规则进行过滤后得到的内容 * @warning 该方法会触发selectionchange事件 * @param { String } html 要插入的html内容 * @param { Boolean } isAppendTo 若传入true,不清空原来的内容,在最后插入内容,否则,清空内容再插入 * @example * ```javascript * //假设设置前的编辑器内容是old text
* editor.setContent('new text
', true); //插入的结果是old text
new text
* ``` */ setContent: function(html, isAppendTo, notFireSelectionchange) { var me = this; me.fireEvent("beforesetcontent", html); var root = UE.htmlparser(html); me.filterInputRule(root); html = root.toHtml(); me.body.innerHTML = (isAppendTo ? me.body.innerHTML : "") + html; function isCdataDiv(node) { return node.tagName == "DIV" && node.getAttribute("cdata_tag"); } //给文本或者inline节点套p标签 if (me.options.enterTag == "p") { var child = this.body.firstChild, tmpNode; if ( !child || (child.nodeType == 1 && (dtd.$cdata[child.tagName] || isCdataDiv(child) || domUtils.isCustomeNode(child)) && child === this.body.lastChild) ) { this.body.innerHTML = "" +
(browser.ie ? " " : "
") +
"
" + (ie ? "" : "
") + "
' + cont + "
"; me.addListener("firstBeforeExecCommand focus", clear); }; })(), /** * 显示编辑器 * @method setShow * @example * ```javascript * editor.setShow() * ``` */ setShow: function() { var me = this, range = me.selection.getRange(); if (me.container.style.display == "none") { //有可能内容丢失了 try { range.moveToBookmark(me.lastBk); delete me.lastBk; } catch (e) { range.setStartAtFirst(me.body).collapse(true); } //ie下focus实效,所以做了个延迟 setTimeout(function() { range.select(true); }, 100); me.container.style.display = ""; } }, show: function() { return this.setShow(); }, /** * 隐藏编辑器 * @method setHide * @example * ```javascript * editor.setHide() * ``` */ setHide: function() { var me = this; if (!me.lastBk) { me.lastBk = me.selection.getRange().createBookmark(true); } me.container.style.display = "none"; }, hide: function() { return this.setHide(); }, /** * 根据指定的路径,获取对应的语言资源 * @method getLang * @param { String } path 路径根据的是lang目录下的语言文件的路径结构 * @return { Object | String } 根据路径返回语言资源的Json格式对象或者语言字符串 * @example * ```javascript * editor.getLang('contextMenu.delete'); //如果当前是中文,那返回是的是'删除' * ``` */ getLang: function(path) { var lang = UE.I18N[this.options.lang]; if (!lang) { throw Error("not import language file"); } path = (path || "").split("."); for (var i = 0, ci; (ci = path[i++]); ) { lang = lang[ci]; if (!lang) break; } return lang; }, /** * 计算编辑器html内容字符串的长度 * @method getContentLength * @return { Number } 返回计算的长度 * @example * ```javascript * //编辑器html内容132
* editor.getContentLength() //返回27 * ``` */ /** * 计算编辑器当前纯文本内容的长度 * @method getContentLength * @param { Boolean } ingoneHtml 传入true时,只按照纯文本来计算 * @return { Number } 返回计算的长度,内容中有hr/img/iframe标签,长度加1 * @example * ```javascript * //编辑器html内容132
* editor.getContentLength() //返回3 * ``` */ getContentLength: function(ingoneHtml, tagNames) { var count = this.getContent(false, false, true).length; if (ingoneHtml) { tagNames = (tagNames || []).concat(["hr", "img", "iframe"]); count = this.getContentTxt().replace(/[\t\r\n]+/g, "").length; for (var i = 0, ci; (ci = tagNames[i++]); ) { count += this.document.getElementsByTagName(ci).length; } } return count; }, /** * 注册输入过滤规则 * @method addInputRule * @param { Function } rule 要添加的过滤规则 * @example * ```javascript * editor.addInputRule(function(root){ * $.each(root.getNodesByTagName('div'),function(i,node){ * node.tagName="p"; * }); * }); * ``` */ addInputRule: function(rule) { this.inputRules.push(rule); }, /** * 执行注册的过滤规则 * @method filterInputRule * @param { UE.uNode } root 要过滤的uNode节点 * @remind 执行editor.setContent方法和执行'inserthtml'命令后,会运行该过滤函数 * @example * ```javascript * editor.filterInputRule(editor.body); * ``` * @see UE.Editor:addInputRule */ filterInputRule: function(root) { for (var i = 0, ci; (ci = this.inputRules[i++]); ) { ci.call(this, root); } }, /** * 注册输出过滤规则 * @method addOutputRule * @param { Function } rule 要添加的过滤规则 * @example * ```javascript * editor.addOutputRule(function(root){ * $.each(root.getNodesByTagName('p'),function(i,node){ * node.tagName="div"; * }); * }); * ``` */ addOutputRule: function(rule) { this.outputRules.push(rule); }, /** * 根据输出过滤规则,过滤编辑器内容 * @method filterOutputRule * @remind 执行editor.getContent方法的时候,会先运行该过滤函数 * @param { UE.uNode } root 要过滤的uNode节点 * @example * ```javascript * editor.filterOutputRule(editor.body); * ``` * @see UE.Editor:addOutputRule */ filterOutputRule: function(root) { for (var i = 0, ci; (ci = this.outputRules[i++]); ) { ci.call(this, root); } }, /** * 根据action名称获取请求的路径 * @method getActionUrl * @remind 假如没有设置serverUrl,会根据imageUrl设置默认的controller路径 * @param { String } action action名称 * @example * ```javascript * editor.getActionUrl('config'); //返回 "/ueditor/php/controller.php?action=config" * editor.getActionUrl('image'); //返回 "/ueditor/php/controller.php?action=uplaodimage" * editor.getActionUrl('scrawl'); //返回 "/ueditor/php/controller.php?action=uplaodscrawl" * editor.getActionUrl('imageManager'); //返回 "/ueditor/php/controller.php?action=listimage" * ``` */ getActionUrl: function(action) { var actionName = this.getOpt(action) || action, imageUrl = this.getOpt("imageUrl"), serverUrl = this.getOpt("serverUrl"); if (!serverUrl && imageUrl) { serverUrl = imageUrl.replace(/^(.*[\/]).+([\.].+)$/, "$1controller$2"); } if (serverUrl) { serverUrl = serverUrl + (serverUrl.indexOf("?") == -1 ? "?" : "&") + "action=" + (actionName || ""); return utils.formatUrl(serverUrl); } else { return ""; } } }; utils.inherits(Editor, EventBase); })();