/* * * @(#) $Id: html_editor.js,v 1.51 2014/09/28 02:58:16 mlemos Exp $ * */ /*jslint browser: true, devel: true, plusplus: true, sloppy: true, white: true */ var ML; if(ML === undefined) { ML = {}; } if(ML.HTMLEditor === undefined) { ML.HTMLEditor = {}; } if(ML.HTMLEditor.Editor === undefined) { ML.HTMLEditor.HTMLEditors = {}; ML.HTMLEditor.defaultVisualToolbar = { 'character': [ { type: 'bold' }, { type: 'italic' }, { type: 'underline' }, { type: 'strikethrough' }, { type: 'createlink' }, { type: 'unlink' } ], 'paragraph': [ { type: 'justifyleft' }, { type: 'justifycenter' }, { type: 'justifyright' }, { type: 'justifyfull' }, { type: 'space' }, { type: 'formatblock' }, { type: 'inserttemplate' } ], 'document': [ { type: 'copy' }, { type: 'cut' }, { type: 'paste' }, { type: 'delete' }, { type: 'space' }, { type: 'undo' }, { type: 'redo' }, { type: 'space' }, { type: 'html' } ] }; ML.HTMLEditor.defaultHTMLToolbar = { 'document': [ { type: 'visual' } ] }; ML.HTMLEditor.linkEmulationStyle = 'color: #0000FF; text-decoration: underline'; ML.HTMLEditor.Editor = function() { this.id = ''; this.error = ''; this.debug = true; this.editorStyle = 'background-color: #ffffff; border-style: solid; border-width: 1px; margin: 0px; border-color: #707070 #e0e0e0 #e0e0e0 #707070'; this.templateVariableStyle = 'border-style: dashed; border-width: 1px; margin: 1px; padding: 1px'; this.templateMarkStyle = 'background-color: #cedee6; border-style: solid; border-width: 1px; margin: 0px; padding: 2px; border-color: #e0e0e0 #707070 #707070 #e0e0e0; font-size: 8pt; opacity: 0.75; filter:alpha(opacity=75)'; this.menuStyle = 'background-color: #d0d0d0; border-style: solid; border-width: 1px; margin: 0px; border-color: #e0e0e0 #707070 #707070 #e0e0e0'; this.itemStyle = 'padding: 4px; color: #000000'; this.itemSelectStyle = 'padding: 4px; color: #ffffff; background-color: #000080'; this.mode = 'visual'; this.showToolbars = true; this.templateVariables = {}; this.externalCSS = []; this.openVariable = '{'; this.closeVariable = '}'; this.alternativesMark = '+'; /* * private variables */ this.editor = null; this.textarea = null; this.iframe = null; this.editorDocument = null; this.htmlEditor = null; this.visualEditor = null; this.lastMenu = ''; this.lastMenuTime = 0; this.menuDelayTime = 100; this.lastOpenedMenu = null; var hasAttribute = function (element, attribute) { if(element.hasAttribute) { return(element.hasAttribute(attribute)); } return element.attributes[attribute] !== undefined; }, getElementsByName = function(element, name, tags) { var l = [], t, e, i; for(t = 0; t < tags.length; ++t) { e = element.getElementsByTagName(tags[t]); for(i = 0; i < e.length; ++i) { if(e[i].getAttribute('name') === name) { l[l.length] = e[i]; } } } return l; }, getElementBox = function (element) { var b, s, o, p, box = {}, d = element.ownerDocument, win = d.defaultView || d.parentWindow; if(element.getBoundingClientRect) { b = element.getBoundingClientRect(); box.x = b.left + d.body.scrollLeft; box.y = b.top + d.body.scrollTop; box.width = b.right - b.left + 1; box.height = b.bottom - b.top + 1; } else { if(d.getBoxObjectFor) { b = d.getBoxObjectFor(element); box.x = b.x; box.y = b.y; box.width = b.width; box.height = b.height; if(win.getComputedStyle) { s = win.getComputedStyle(element, null); o = parseInt(s.borderLeftWidth, 10) + parseInt(s.borderRightWidth, 10); box.x -= o; box.width += o; o = parseInt(s.borderTopWidth, 10) + parseInt(s.borderBottomWidth, 10); box.y -= o; box.height += o; } } else { p = element.style.position; element.style.position = 'relative'; box.x = element.offsetLeft; box.y = element.offsetTop; box.width = element.offsetWidth; box.height = element.offsetHeight; element.style.position = p; } } return box; }, getElementSize = function (element) { var box = getElementBox(element); return { width: box.width, height: box.height }; }, repositionElement = function (element, parent, frame) { var s, b = getElementBox(parent), x = b.x, y = b.y + b.height, w = parseInt(b.width, 10); if(!isNaN(w)) { s = getElementSize(element); if(!isNaN(parseInt(s.width, 10))) { x += (w - parseInt(s.width, 10)) / 2; if(x < 0) { x = 0; } } } if(frame) { b = getElementBox(frame); x += b.x; y += b.y; if(typeof(frame.contentWindow.pageXOffset) === 'number') { x -= frame.contentWindow.pageXOffset; y -= frame.contentWindow.pageYOffset; } else { if(frame.contentWindow.document.documentElement && frame.contentWindow.document.compatMode && frame.contentWindow.document.compatMode !== "BackCompat") { x -= frame.contentWindow.document.documentElement.scrollLeft; y -= frame.contentWindow.document.documentElement.scrollTop; } else { x -= frame.contentWindow.document.body.scrollLeft; y -= frame.contentWindow.document.body.scrollTop; } } } element.style.left = x + 'px'; element.style.top = y + 'px'; }, addEventListener = function(element, event, listener, capture) { if(element.addEventListener) { element.addEventListener(event, listener, capture); } else { if(element.attachEvent) { element.attachEvent('on' + event, listener); } } }, replaceStrings = function(value, replace) { var v, c, p, f; for(v in replace) { c = ''; p = 0; while(p < value.length) { f = value.indexOf(v, p); if(f === -1) { c += value.substring(p); break; } c += value.substring(p, f) + replace[v]; p = f + v.length; } value = c; } return value; }, encodeHTML = function(value) { return replaceStrings(value, { '&': '&', '"': '"', '<': '<', '>': '>' }); }, escapeHTML = function(value) { return value.replace(/]*)>([^<]*)<\/a>/gi, '$2'); }, encodeString = function(value) { return "'" + replaceStrings(value, { "'": "\\'" }) + "'"; }, expandTemplates = function(e, value, insert) { var v, r, i, n, p, b, f, s, tv, style, a, h, t, expand, c, variables = {}, created = {}; if(insert) { for(v in e.templateVariables) { v = e.openVariable + v + e.closeVariable; r = getElementsByName(e.editorDocument, v, ['div', 'span']); for(i = 0; i < r.length; ++i) { n = (variables[v] ? variables[v].length : 0); if(n === 0) { variables[v] = []; } variables[v][n] = r[i].id; } } } style = e.templateVariableStyle.length ? ' style="' + encodeHTML(e.templateVariableStyle) + '"' : ''; v = value; value = ''; p = 0; while(p < v.length) { b = v.indexOf(e.openVariable, p); if(b === -1) { value += v.substring(p); break; } b += e.openVariable.length; f = v.indexOf(e.closeVariable, b); if(f === -1) { value += v.substring(p); break; } s = v.indexOf(' ', b); if(s === -1 || s > f) { s = f; } tv = v.substring(b, s); if(e.templateVariables[tv]) { if(e.templateVariables[tv].alternatives) { a = (s === f ? null : v.substring(s + 1, f)); if(a && !e.templateVariables[tv].alternatives[a]) { a = null; } } else { a = null; } h = (e.templateVariables[tv].inline !== undefined); if(h) { t = (e.templateVariables[tv].inline ? 'span' : 'div'); i = encodeHTML(e.openVariable + tv + e.closeVariable); n = (variables[i] ? variables[i].length : 0); expand = '<' + t + ' id="' + i + '_' + n +'" name="' + i + '"' + style + ' contentEditable="false"' + (e.templateVariables[tv].title ? ' title="' + encodeHTML(e.templateVariables[tv].title) + '"' : '') + (a ? ' data="' + encodeHTML(a) + '"' : '') + '>'; } else { expand = (a ? e.templateVariables[tv].alternatives[a].value : e.templateVariables[tv].value); } value += v.substring(p, b - e.openVariable.length) + expand; p = f + e.closeVariable.length; if(h) { if(n === 0) { variables[i] = []; } c = (created[tv] ? created[tv].length : 0); if(c === 0) { created[tv] = []; } variables[i][n] = created[tv][c] = i + '_' + n; } } else { value += v.substring(p, b); p = b; } } return { value: value, created: created }; }, handleMenus = function(e) { return function(event) { var id, parent, now, menu; if(event.target) { parent = event.target; id = parent.id; } else { if(event.srcElement) { parent = event.srcElement; id = parent.id; } else { return; } } if(id.length === 0 || !parent) { return; } now = (new Date()).getTime(); if(e.lastMenu !== id || now - e.lastMenuTime > e.menuDelayTime) { menu = e.visualEditor.ownerDocument.getElementById(id + '_menu'); if(e.toggleMenu(menu, parent, e.iframe)) { menu.setAttribute('data', parent.parentNode.id); } e.lastMenu = id; e.lastMenuTime = now; } if(event) { if(event.preventDefault) { event.preventDefault(); } event.cancelBubble = true; event.returnValue = false; } }; }, convertValue = function(e, toEditor) { var value, v, n, r, t, a, replace; if(toEditor) { value = expandTemplates(e, e.textarea.value, false).value; e.editorDocument.body.innerHTML = value; return value; } t = document.getElementById(e.id + '_temporary'); t.innerHTML = e.editorDocument.body.innerHTML; for(v in e.templateVariables) { if(e.templateVariables[v].inline !== undefined) { r = getElementsByName(t, e.openVariable + v + e.closeVariable, ['div', 'span']); for(n = 0; n < r.length; ++n) { r[n].parentNode.replaceChild(document.createTextNode(e.openVariable + v + (hasAttribute(r[n], 'data') ? ' ' + r[n].getAttribute('data') : '') + e.closeVariable), r[n]); } } } value = t.innerHTML; t.innerHTML = ''; for(v in e.templateVariables) { if(e.templateVariables[v].inline === undefined) { replace = {}; replace[e.templateVariables[v].value] = e.openVariable + v + e.closeVariable; if(e.templateVariables[v].alternatives) { for(a in e.templateVariables[v].alternatives) { replace[e.templateVariables[v].alternatives[a].value] = e.openVariable + v + ' ' + a + e.closeVariable; } } value = replaceStrings(value, replace); } } return value; }, renderToolbar = function(e, toolbars) { var t, b, type, setup, action, o, oo, menu, comma, id, v, content, title, render = ''; setup = 'var e = ML.HTMLEditor.HTMLEditors[\'' + e.id + '\']; '; for(t in toolbars) { render += ''; } render += '
'; for(b = 0; b < toolbars[t].length; ++b) { type = toolbars[t][b].type; switch(type) { case 'bold': case 'italic': case 'underline': case 'strikethrough': case 'createlink': case 'unlink': case 'copy': case 'cut': case 'paste': case 'delete': case 'undo': case 'redo': case 'html': case 'visual': case 'justifyleft': case 'justifycenter': case 'justifyright': case 'justifyfull': action = setup + 'if(!e.execCommand("' + type + '", true) && !e.debug) { alert("Could not execute the ' + type + ' command in this browser.") }'; switch(type) { case 'bold': content = 'B'; title = 'Bold'; break; case 'italic': content = 'I'; title = 'Italic'; break; case 'underline': content = 'U'; title = 'Underline'; break; case 'strikethrough': content = 'S'; title = 'Strike-through'; break; case 'createlink': content = 'www'; title = 'Create link'; action = 'var url = prompt("Link URL:", "http://www."); if(url === null || url === "") return false; ' + setup + 'if(!e.execCommand("' + type + '", url) && !e.debug) { alert("Could not execute the ' + type + ' command in this browser.") }'; break; case 'unlink': content = 'www'; title = 'Remove link'; break; case 'copy': content = 'Copy'; title = 'Copy selected'; break; case 'cut': content = 'Cut'; title = 'Cut selected'; break; case 'paste': content = 'Paste'; title = 'Paste copied'; break; case 'delete': content = 'Delete'; title = 'Delete selected'; break; case 'undo': content = 'Undo'; title = 'Undo'; break; case 'redo': content = 'Redo'; title = 'Redo'; break; case 'justifyleft': content = 'Left'; title = 'Justify left'; break; case 'justifycenter': content = 'Center'; title = 'Justify center'; break; case 'justifyright': content = 'Right'; title = 'Justify right'; break; case 'justifyfull': content = 'Full'; title = 'Justify full'; break; case 'visual': content = 'Visual'; title = 'Edit visually'; action = setup + 'e.setEditMode("visual");'; break; case 'html': content = 'HTML'; title = 'Edit HTML'; action = setup + 'e.setEditMode("html");'; break; default: if(e.debug) { alert('toolbar element of type ' + type + ' is not implemented'); } action = ''; content = title = type; break; } if(action.length) { render += ''; } break; case 'justify': case 'inserttemplate': case 'formatblock': switch(type) { case 'justify': menu = 'Justify'; o = { 'justifyleft': 'Left', 'justifycenter': 'Center', 'justifyright': 'Right', 'justifyfull': 'Full' }; action = 'if(!e.execCommand({item}, true) && !e.debug) { alert("Could not execute the justify command in this browser.") }'; break; case 'inserttemplate': menu = 'Insert template'; o = {}; if(e.templateVariables.length === 0) { break; } comma = ''; for(v in e.templateVariables) { if(e.templateVariables[v].inline !== undefined) { o[e.openVariable + v + e.closeVariable] = (e.templateVariables[v].title || v); } comma = ', '; } action = 'if(!e.execCommand("inserthtml", {item}) && !e.debug) { alert("Could not execute the inserthtml command in this browser.") }; e.loadTemplates();'; break; case 'formatblock': menu = 'Format block'; o = { 'p': '

Paragraph

', 'pre': '
Preformatted
', 'address': '
Address
', 'h1': '

Heading 1

', 'h2': '

Heading 2

', 'h3': '

Heading 3

', 'h4': '

Heading 4

', 'h5': '
Heading 5
', 'h6': '
Heading 6
' }; action = 'if(!e.execCommand("formatblock", {item} ) && !e.debug) { alert("Could not execute the formatblock command in this browser.") }'; break; } id = e.id + '_' + type; oo = ''; for(v in o) { oo += '
' + o[v] + '
'; } render += ''; break; case 'separator': render += (toolbars[t][b].content || ' | '); break; case 'space': render += (toolbars[t][b].content || ' '); break; default: if(e.debug) { alert('toolbar element of type ' + type + ' is not implemented'); } break; } } render += '
'; return render; }; this.formatMark = function(variable, alternative) { var e = this; return '' + encodeHTML(e.openVariable + variable + (alternative ? ' ' + alternative : '') + e.closeVariable + (e.templateVariables[variable].alternatives ? ' ' + e.alternativesMark : '')) + '
' + escapeHTML(alternative ? e.templateVariables[variable].alternatives[alternative].preview : e.templateVariables[variable].preview); }; this.changeMark = function(mark, variable, alternative) { var e = this, s, i = e.editorDocument.createElement('button'); i.setAttribute('id', e.id + '_' + variable); s = (e.templateVariables[variable].inline ? '' : 'display: block; width: 100%; text-align: left; ') + 'background-color: inherit; border-style: none; border-width: 0px; padding: 0px; font-family: inherit; font-size: inherit; color: inherit'; if(i.currentStyle) { i.style.cssText = s; } else { i.setAttribute('style', s); } i.setAttribute('contentEditable', 'false'); i.innerHTML = e.formatMark(variable, alternative); mark.innerHTML = ''; mark.appendChild(i); }; this.setAlternative = function(mark, variable, alternative) { var e = this, p; p = e.editorDocument.getElementById(mark); if(alternative) { p.setAttribute('data', alternative); } else { p.setAttribute('data', ''); p.removeAttribute('data'); } e.changeMark(p, variable, alternative); }; this.loadTemplates = function() { var v, r, n, i, s, o, a, oo, id, e = this, setup = 'var e = ML.HTMLEditor.HTMLEditors[\'' + e.id + '\']; '; for(v in e.templateVariables) { r = getElementsByName(e.editorDocument, e.openVariable + v + e.closeVariable, [ 'div', 'span' ]); for(n = 0; n < r.length; ++n) { id = e.id + '_' + v; r[n].innerHTML = ''; if(e.templateVariables[v].alternatives) { i = e.visualEditor.ownerDocument.createElement('div'); i.setAttribute('id', id + '_menu'); e.visualEditor.appendChild(i); s = e.menuStyle + '; position: absolute; visibility: hidden'; if(i.currentStyle) { i.style.cssText = s; } else { i.setAttribute('style', s); } o = {}; o[v] = e.templateVariables[v].title; for(a in e.templateVariables[v].alternatives) { o[a] = e.templateVariables[v].alternatives[a].title; } oo = ''; for(a in o) { oo += '
' + o[a] + '
'; } i.innerHTML = oo; } e.changeMark(r[n], v, (e.templateVariables[v].alternatives && hasAttribute(r[n],'data')) ? r[n].getAttribute('data') : null); if(e.templateVariables[v].alternatives) { addEventListener(r[n], 'click', handleMenus(e), true); } } } }; this.hideMenu = function(menu) { if(menu) { menu.style.visibility = 'hidden'; this.lastOpenedMenu = null; } }; this.showMenu = function(menu, parent, frame) { if(menu) { if(this.lastOpenedMenu && this.lastOpenedMenu.id !== menu.id) { this.hideMenu(this.lastOpenedMenu); } repositionElement(menu, parent, frame); menu.style.visibility = 'visible'; this.lastOpenedMenu = menu; return true; } return false; }; this.toggleMenu = function(menu, parent, frame) { if(menu) { if(menu.style.visibility === 'visible') { this.hideMenu(menu); } else { return this.showMenu(menu, parent, frame); } } return false; }; this.synchronize = function() { var e = this, converted; converted = convertValue(e, false); if(converted !== e.textarea.value) { if(e.mode === 'visual') { e.textarea.value = converted; if(typeof(e.textarea.onchange) === 'function') { e.textarea.onchange(); } } else { convertValue(e, true); } } }; this.setError = function(error) { this.error = error; if(this.debug) { alert(error); } return false; }; this.insertEditor = function(editor, textarea) { var l, e = this, editor_document, html_editor, visual_editor, c, html_size, visual_size, size, i; l = document.getElementById(editor); if(!l) { return e.setError('editor block "' + editor + '" was not found in this document.'); } e.id = textarea.id; editor_document = editor + '_iframe'; html_editor = editor + '_html'; visual_editor = editor + '_visual'; l.innerHTML = '
' + renderToolbar(e, ML.HTMLEditor.defaultHTMLToolbar) + '
' + renderToolbar(e, ML.HTMLEditor.defaultVisualToolbar) + '
'; e.htmlEditor = document.getElementById(html_editor); if(!e.htmlEditor) { return e.setError('could not create the HTML editor section'); } e.visualEditor = document.getElementById(visual_editor); if(!e.visualEditor) { return e.setError('could not create the visual editor section'); } e.textarea = document.getElementById(textarea.id); if(!e.textarea) { return e.setError('could not create the editor textarea'); } l = e.iframe = document.getElementById(editor_document); if(!l) { return e.setError('could not create the editor iframe'); } if(!l.contentWindow || !l.contentWindow.document) { return e.setError('could not access the editor iframe'); } e.editorDocument = l.contentWindow.document; e.editorDocument.open(); e.editorDocument.write('Editor'); if(e.externalCSS.length) { for(c = 0; c < e.externalCSS.length; ++c) { e.editorDocument.write(''); } } e.editorDocument.write(''); e.editorDocument.close(); html_size = getElementSize(e.htmlEditor); visual_size = getElementSize(e.visualEditor); size = getElementSize(e.textarea); i = getElementSize(e.iframe); l.style.width = size.width + 'px'; l.style.height = (i.height + html_size.height - visual_size.height) + 'px'; if(e.mode === 'visual') { e.htmlEditor.style.display = 'none'; } else { e.visualEditor.style.display = 'none'; } convertValue(e, true); if(typeof(e.editorDocument.designMode) === 'string' && e.editorDocument.designMode === 'off') { e.editorDocument.designMode = 'on'; } else { if(e.editorDocument.body.contentEditable !== undefined) { e.editorDocument.body.contentEditable = true; } else { return e.setError('HTML editing in this browser is not yet supported'); } } addEventListener(e.textarea, 'change', function() { e.synchronize(); }, false ); addEventListener(e.editorDocument.body, 'blur', function() { e.synchronize(); }, false ); ML.HTMLEditor.HTMLEditors[textarea.id] = e; e.loadTemplates(); return true; }; this.execCommand = function(command, argument) { var e = this, enabled, expanded, r; if(e.editorDocument === null) { return e.setError('the HTML editor elements are not yet setup'); } try { switch(command) { case 'inserthtml': try { enabled = e.editorDocument.queryCommandEnabled(command); } catch(exception) { enabled = false; } expanded = expandTemplates(e, argument, true); argument = expanded.value; break; default: enabled = true; } if(enabled) { e.editorDocument.execCommand(command, false, argument); } switch(command) { case 'copy': case 'cut': case 'paste': if(!e.editorDocument.queryCommandSupported(command)) { throw('Not supported'); } break; case 'inserthtml': if(!enabled) { e.editorDocument.body.focus(); r = e.editorDocument.selection.createRange(); r.pasteHTML(argument); } /* for(var v in expanded.created) { if(e.templateVariables[v].alternatives) { for(var i = 0; i < expanded.created[v].length; ++i) { addEventListener(e.editorDocument.getElementById(expanded.created[v][i]), 'click', handleMenus(e), true); } } } */ break; } } catch(exception) { return e.setError(command + ' command is not allowed in this browser: ' + exception.message); } return true; }; this.setEditMode = function(mode) { switch(mode) { case 'visual': this.textarea.blur(); this.htmlEditor.style.display = 'none'; this.synchronize(); this.visualEditor.style.display = 'block'; this.loadTemplates(); this.editorDocument.body.focus(); break; case 'html': this.editorDocument.body.blur(); this.visualEditor.style.display = 'none'; this.synchronize(); this.htmlEditor.style.display = 'block'; this.textarea.focus(); break; default: return this.setError(mode + ' is not valid edit mode'); } this.mode = mode; return true; }; this.setValue = function(value) { var e = this; e.textarea.value = value; convertValue(e, true); }; }; }