/*! Xquared is copyrighted free software by Alan Kang . * For more information, see http://xquared.springbook.playmaru.net/ */ if(!window.xq) { /** * @namespace Contains all variables. */ var xq = {}; } xq.majorVersion = '0.7'; xq.minorVersion = '20080402'; /** * Compiles regular expression pattern if possible. * * @param {String} p Regular expression. * @param {String} f Flags. */ xq.compilePattern = function(p, f) { if(!RegExp.prototype.compile) return new RegExp(p, f); var r = new RegExp(); r.compile(p, f); return r; } /** * @class Simple class based OOP framework */ xq.Class = function() { var parent = null, properties = xq.$A(arguments), key; if (typeof properties[0] === "function") { parent = properties.shift(); } function klass() { this.initialize.apply(this, arguments); } if(parent) { for (key in parent.prototype) { klass.prototype[key] = parent.prototype[key]; } } for (key in properties[0]) if(properties[0].hasOwnProperty(key)){ klass.prototype[key] = properties[0][key]; } if (!klass.prototype.initialize) { klass.prototype.initialize = function() {}; } klass.prototype.constructor = klass; return klass; }; /** * Registers event handler * * @param {Element} element Target element. * @param {String} eventName Name of event. For example "keydown". * @param {Function} handler Event handler. */ xq.observe = function(element, eventName, handler) { if (element.addEventListener) { element.addEventListener(eventName, handler, false); } else { element.attachEvent('on' + eventName, handler); } element = null; }; /** * Unregisters event handler */ xq.stopObserving = function(element, eventName, handler) { if (element.removeEventListener) { element.removeEventListener(eventName, handler, false); } else { element.detachEvent("on" + eventName, handler); } element = null; }; /** * Predefined event handler which simply cancels given event * * @param {Event} e Event to cancel. */ xq.cancelHandler = function(e) {xq.stopEvent(e); return false;}; /** * Stops event propagation. * * @param {Event} e Event to stop. */ xq.stopEvent = function(e) { if(e.preventDefault) { e.preventDefault(); } if(e.stopPropagation) { e.stopPropagation(); } e.returnValue = false; e.cancelBubble = true; e.stopped = true; }; xq.isButton = function(event, code) { return event.which ? (event.which === code + 1) : (event.button === code); }; xq.isLeftClick = function(event) {return xq.isButton(event, 0);}; xq.isMiddleClick = function(event) {return xq.isButton(event, 1);}; xq.isRightClick = function(event) {return xq.isButton(event, 2);}; xq.getEventPoint = function(event) { return { x: event.pageX || (event.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft)), y: event.pageY || (event.clientY + (document.documentElement.scrollTop || document.body.scrollTop)) }; }; xq.getCumulativeOffset = function(element, until) { var top = 0, left = 0; do { top += element.offsetTop || 0; left += element.offsetLeft || 0; element = element.offsetParent; } while (element && element != until); return {top:top, left:left}; }; xq.$ = function(id) { return document.getElementById(id); }; xq.isEmptyHash = function(h) { for(var key in h) if(h.hasOwnProperty(key)){ return false; } return true; }; xq.emptyFunction = function() {}; xq.$A = function(arraylike) { var len = arraylike.length, a = []; while (len--) { a[len] = arraylike[len]; } return a; }; xq.addClassName = function(element, className) { if (!xq.hasClassName(element, className)) { element.className += (element.className ? ' ' : '') + className; } return element; }; xq.removeClassName = function(element, className) { if (xq.hasClassName(element, className)) { element.className = element.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); } return element; }; xq.hasClassName = function(element, className) { var classNames = element.className; return (classNames.length > 0 && (classNames === className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(classNames))); }; xq.serializeForm = function(f) { var options = {hash: true}; var data = {}; var elements = f.getElementsByTagName("*"); for(var i = 0; i < elements.length; i++) { var element = elements[i]; var tagName = element.tagName.toLowerCase(); if(element.disabled || !element.name || ['input', 'textarea', 'option', 'select'].indexOf(tagName) === -1) { continue; } var key = element.name; var value = xq.getValueOfElement(element); if(value === undefined) { continue; } if(key in data) { if(data[key].constructor === Array) { data[key] = [data[key]]; } data[key].push(value); } else { data[key] = value; } } return data; }; xq.getValueOfElement = function(e) { var type = e.type.toLowerCase(); if(type === 'checkbox' || type === 'radio') { return e.checked ? e.value : undefined; } else { return e.value; } }; /** * Find elements by class name (and tag name) * * @param {Element} element Root element * @param {String} className Target class name * @param {String} tagName Optional tag name */ xq.getElementsByClassName = function(element, className, tagName) { if(!tagName && element.getElementsByClassName) { return element.getElementsByClassName(className); } var elements = element.getElementsByTagName(tagName || "*"); var len = elements.length; var result = []; var p = xq.compilePattern("(^|\\s)" + className + "($|\\s)", "i"); for(var i = 0; i < len; i++) { var cur = elements[i]; if(p.test(cur.className)) { result.push(cur); } } return result; }; if(!window.Prototype) { if(!Function.prototype.bind) { Function.prototype.bind = function() { var m = this, arg = xq.$A(arguments), o = arg.shift(); return function() { return m.apply(o, arg.concat(xq.$A(arguments))); }; }; } if(!Function.prototype.bindAsEventListener) { Function.prototype.bindAsEventListener = function() { var m = this, arg = xq.$A(arguments), o = arg.shift(); return function(event) { return m.apply(o, [event || window.event].concat(arg)); }; }; } Array.prototype.find = function(f) { for(var i = 0; i < this.length; i++) { if(f(this[i])) { return this[i]; } } }; Array.prototype.findAll = function(f) { var result = []; for(var i = 0; i < this.length; i++) { if(f(this[i])) { result.push(this[i]); } } return result; }; Array.prototype.first = function() {return this[0];}; Array.prototype.last = function() {return this[this.length - 1];}; Array.prototype.flatten = function() { var result = []; var recursive = function(array) { for(var i = 0; i < array.length; i++) { if(array[i].constructor === Array) { recursive(array[i]); } else { result.push(array[i]); } } }; recursive(this); return result; }; xq.pStripTags = xq.compilePattern("]+>", "gi"); String.prototype.stripTags = function() { return this.replace(xq.pStripTags, ''); }; String.prototype.escapeHTML = function() { xq.textNode.data = this; return xq.divNode.innerHTML; }; xq.textNode = document.createTextNode(''); xq.divNode = document.createElement('div'); xq.divNode.appendChild(xq.textNode); xq.pStrip1 = xq.compilePattern("^\\s+"); xq.pStrip2 = xq.compilePattern("\\s+$"); String.prototype.strip = function() { return this.replace(xq.pStrip1, '').replace(xq.pStrip2, ''); }; Array.prototype.indexOf = function(n) { for(var i = 0; i < this.length; i++) { if(this[i] === n) { return i; } } return -1; }; } Array.prototype.includeElement = function(o) { if (this.indexOf(o) !== -1) { return true; } var found = false; for(var i = 0; i < this.length; i++) { if(this[i] === o) { return true; } } return false; }; /** * Make given object as event source * * @param {Object} object target object * @param {String} prefix prefix for generated functions * @param {Array} events array of string which contains name of events */ xq.asEventSource = function(object, prefix, events) { object.autoRegisteredEventListeners = []; object.registerEventFirer = function(prefix, name) { this["_fireOn" + name] = function() { for(var i = 0; i < this.autoRegisteredEventListeners.length; i++) { var listener = this.autoRegisteredEventListeners[i]; var func = listener["on" + prefix + name]; if(func) { func.apply(listener, xq.$A(arguments)); } } }; }; object.addListener = function(l) { this.autoRegisteredEventListeners.push(l); }; for(var i = 0; i < events.length; i++) { object.registerEventFirer(prefix, events[i]); } }; /** * JSON to Element mapper */ xq.json2element = function(json, doc) { var div = doc.createElement("DIV"); div.innerHTML = xq.json2html(json); return div.firstChild || {}; }; /** * Element to JSON mapper */ xq.element2json = function(element) { var o, i, childElements; if(element.nodeName === 'DL') { o = {}; childElements = xq.findChildElements(element); for(i = 0; i < childElements.length; i++) { var dt = childElements[i]; var dd = childElements[++i]; o[dt.innerHTML] = xq.element2json(xq.findChildElements(dd)[0]); } return o; } else if (element.nodeName === 'OL') { o = []; childElements = xq.findChildElements(element); for(i = 0; i < childElements.length; i++) { var li = childElements[i]; o[i] = xq.element2json(xq.findChildElements(li)[0]); } } else if(element.nodeName === 'SPAN' && element.className === 'number') { return parseFloat(element.innerHTML); } else if(element.nodeName === 'SPAN' && element.className === 'string') { return element.innerHTML; } else { // ignore textnode or unknown tag return null; } }; /** * JSON to HTML string mapper */ xq.json2html = function(json) { var sb = []; xq._json2html(json, sb); return sb.join(''); }; xq._json2html = function(o, sb) { if(typeof o === 'number') { sb.push('' + o + ''); } else if(typeof o === 'string') { sb.push('' + o.escapeHTML() + ''); } else if(o.constructor === Array) { sb.push('
    '); for(var i = 0; i < o.length; i++) { sb.push('
  1. '); xq._json2html(o[i], sb); sb.push('
  2. '); } sb.push('
'); } else { // Object sb.push('
'); for (var key in o) if (o.hasOwnProperty(key)) { sb.push('
' + key + '
'); sb.push('
'); xq._json2html(o[key], sb); sb.push('
'); } sb.push('
'); } }; xq.findChildElements = function(parent) { var childNodes = parent.childNodes; var elements = []; for(var i = 0; i < childNodes.length; i++) { if(childNodes[i].nodeType === 1) { elements.push(childNodes[i]); } } return elements; }; Date.preset = null; Date.pass = function(msec) { if(Date.preset !== null) { Date.preset = new Date(Date.preset.getTime() + msec); } }; Date.get = function() { return Date.preset === null ? new Date() : Date.preset; }; Date.prototype.elapsed = function(msec, curDate) { return (curDate || Date.get()).getTime() - this.getTime() >= msec; }; String.prototype.merge = function(data) { var newString = this; for(var k in data) if(data.hasOwnProperty(k)) { newString = newString.replace("{" + k + "}", data[k]); } return newString; }; xq.pBlank = xq.compilePattern("^\\s*$"); String.prototype.isBlank = function() { return xq.pBlank.test(this); }; xq.pURL = xq.compilePattern("((((\\w+)://(((([^@:]+)(:([^@]+))?)@)?([^:/\\?#]+)?(:(\\d+))?))?([^\\?#]+)?)(\\?([^#]+))?)(#(.+))?"); String.prototype.parseURL = function() { var m = this.match(xq.pURL); var includeAnchor = m[0]; var includeQuery = m[1] || undefined; var includePath = m[2] || undefined; var includeHost = m[3] || undefined; var includeBase = null; var protocol = m[4] || undefined; var user = m[8] || undefined; var password = m[10] || undefined; var domain = m[11] || undefined; var port = m[13] || undefined; var path = m[14] || undefined; var query = m[16] || undefined; var anchor = m[18] || undefined; if(!path || path === '/') { includeBase = includeHost + '/'; } else { var index = path.lastIndexOf('/'); includeBase = includeHost + path.substring(0, index + 1); } return { includeAnchor: includeAnchor, includeQuery: includeQuery, includePath: includePath, includeBase: includeBase, includeHost: includeHost, protocol: protocol, user: user, password: password, domain: domain, port: port, path: path, query: query, anchor: anchor }; }; xq.commonAttrs = ['title', 'class', 'id', 'style'];; /** * Pre-defined whitelist */ xq.predefinedWhitelist = { 'a': xq.commonAttrs.concat('href', 'charset', 'rev', 'rel', 'type', 'hreflang', 'tabindex'), 'abbr': xq.commonAttrs.concat(), 'acronym': xq.commonAttrs.concat(), 'address': xq.commonAttrs.concat(), 'blockquote': xq.commonAttrs.concat('cite'), 'br': xq.commonAttrs.concat(), 'button': xq.commonAttrs.concat('disabled', 'type', 'name', 'value'), 'caption': xq.commonAttrs.concat(), 'cite': xq.commonAttrs.concat(), 'code': xq.commonAttrs.concat(), 'dd': xq.commonAttrs.concat(), 'dfn': xq.commonAttrs.concat(), 'div': xq.commonAttrs.concat(), 'dl': xq.commonAttrs.concat(), 'dt': xq.commonAttrs.concat(), 'em': xq.commonAttrs.concat(), 'embed': xq.commonAttrs.concat('src', 'width', 'height', 'allowscriptaccess', 'type', 'allowfullscreen', 'bgcolor'), 'h1': xq.commonAttrs.concat(), 'h2': xq.commonAttrs.concat(), 'h3': xq.commonAttrs.concat(), 'h4': xq.commonAttrs.concat(), 'h5': xq.commonAttrs.concat(), 'h6': xq.commonAttrs.concat(), 'hr': xq.commonAttrs.concat(), 'iframe': xq.commonAttrs.concat('name', 'src', 'frameborder', 'scrolling', 'width', 'height', 'longdesc'), 'input': xq.commonAttrs.concat('type', 'name', 'value', 'size', 'checked', 'readonly', 'src', 'maxlength'), 'img': xq.commonAttrs.concat('alt', 'width', 'height', 'src', 'longdesc'), 'label': xq.commonAttrs.concat('for'), 'kbd': xq.commonAttrs.concat(), 'li': xq.commonAttrs.concat(), 'object': xq.commonAttrs.concat('align', 'classid', 'codetype', 'archive', 'width', 'type', 'codebase', 'height', 'data', 'name', 'standby', 'declare'), 'ol': xq.commonAttrs.concat(), 'option': xq.commonAttrs.concat('disabled', 'selected', 'laabel', 'value'), 'p': xq.commonAttrs.concat(), 'param': xq.commonAttrs.concat('name', 'value', 'valuetype', 'type'), 'pre': xq.commonAttrs.concat(), 'q': xq.commonAttrs.concat('cite'), 'samp': xq.commonAttrs.concat(), 'script': xq.commonAttrs.concat('src', 'type'), 'select': xq.commonAttrs.concat('disabled', 'size', 'multiple', 'name'), 'span': xq.commonAttrs.concat(), 'sup': xq.commonAttrs.concat(), 'sub': xq.commonAttrs.concat(), 'strong': xq.commonAttrs.concat(), 'table': xq.commonAttrs.concat('summary', 'width'), 'thead': xq.commonAttrs.concat(), 'textarea': xq.commonAttrs.concat('cols', 'disabled', 'rows', 'readonly', 'name'), 'tbody': xq.commonAttrs.concat(), 'th': xq.commonAttrs.concat('colspan', 'rowspan'), 'td': xq.commonAttrs.concat('colspan', 'rowspan'), 'tr': xq.commonAttrs.concat(), 'tt': xq.commonAttrs.concat(), 'ul': xq.commonAttrs.concat(), 'var': xq.commonAttrs.concat() }; /** * Automatic finalization queue */ xq.autoFinalizeQueue = []; /** * Automatic finalizer */ xq.addToFinalizeQueue = function(obj) { xq.autoFinalizeQueue.push(obj); }; /** * Finalizes given object */ xq.finalize = function(obj) { if(typeof obj.finalize === "function") { try {obj.finalize();} catch(ignored) {} } for(var key in obj) if(obj.hasOwnProperty(key)) { obj[key] = null; } }; xq.observe(window, "unload", function() { // "xq" and "xq.autoFinalizeQueue" could be removed by another libraries' clean-up mechanism. if(xq && xq.autoFinalizeQueue) { for(var i = 0; i < xq.autoFinalizeQueue.length; i++) { xq.finalize(xq.autoFinalizeQueue[i]); } xq = null; } }); /** * Finds Xquared's '); }; /** * Returns all Xquared script file names */ xq.getXquaredScriptFileNames = function() { return [ 'Xquared.js', 'Browser.js', 'DomTree.js', 'rdom/Base.js', 'rdom/W3.js', 'rdom/Gecko.js', 'rdom/Webkit.js', 'rdom/Trident.js', 'rdom/Factory.js', 'validator/Base.js', 'validator/W3.js', 'validator/Gecko.js', 'validator/Webkit.js', 'validator/Trident.js', 'validator/Factory.js', 'macro/Base.js', 'macro/Factory.js', 'macro/FlashMovieMacro.js', 'macro/IFrameMacro.js', 'macro/JavascriptMacro.js', 'EditHistory.js', 'plugin/Base.js', 'RichTable.js', 'Timer.js', 'Layer.js', 'ui/Base.js', 'ui/Control.js', 'ui/Toolbar.js', 'ui/_templates.js', 'Json2.js', 'Shortcut.js', 'Editor.js' ]; }; xq.getXquaredScriptBasePath = function() { var script = xq.findXquaredScript(); return script.src.match(/(.*\/)xquared\.js.*/i)[1]; }; xq.loadOthers = function() { var basePath = xq.getXquaredScriptBasePath(); var others = xq.getXquaredScriptFileNames(); // Xquared.js(this file) should not be loaded again. So the value of "i" starts with 1 instead of 0 for(var i = 1; i < others.length; i++) { xq.loadScript(basePath + others[i]); } }; if(xq.shouldLoadOthers()) { xq.loadOthers(); } /** * @namespace Contains browser detection codes * * @requires Xquared.js */ xq.Browser = new function() { // By Rendering Engines /** * True if rendering engine is Trident * @type boolean */ this.isTrident = navigator.appName === "Microsoft Internet Explorer", /** * True if rendering engine is Webkit * @type boolean */ this.isWebkit = navigator.userAgent.indexOf('AppleWebKit/') > -1, /** * True if rendering engine is Gecko * @type boolean */ this.isGecko = navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') === -1, /** * True if rendering engine is KHTML * @type boolean */ this.isKHTML = navigator.userAgent.indexOf('KHTML') !== -1, /** * True if rendering engine is Presto * @type boolean */ this.isPresto = navigator.appName === "Opera", // By Platforms /** * True if platform is Mac * @type boolean */ this.isMac = navigator.userAgent.indexOf("Macintosh") !== -1, /** * True if platform is Ubuntu Linux * @type boolean */ this.isUbuntu = navigator.userAgent.indexOf('Ubuntu') !== -1, /** * True if platform is Windows * @type boolean */ this.isWin = navigator.userAgent.indexOf('Windows') !== -1, // By Browsers /** * True if browser is Internet Explorer * @type boolean */ this.isIE = navigator.appName === "Microsoft Internet Explorer", /** * True if browser is Internet Explorer 6 * @type boolean */ this.isIE6 = navigator.userAgent.indexOf('MSIE 6') !== -1, /** * True if browser is Internet Explorer 7 * @type boolean */ this.isIE7 = navigator.userAgent.indexOf('MSIE 7') !== -1, /** * True if browser is Internet Explorer 8 * @type boolean */ this.isIE8 = navigator.userAgent.indexOf('MSIE 8') !== -1, /** * True if browser is Firefox * @type boolean */ this.isFF = navigator.userAgent.indexOf('Firefox') !== -1, /** * True if browser is Firefox 2 * @type boolean */ this.isFF2 = navigator.userAgent.indexOf('Firefox/2') !== -1, /** * True if browser is Firefox 3 * @type boolean */ this.isFF3 = navigator.userAgent.indexOf('Firefox/3') !== -1, /** * True if browser is Safari * @type boolean */ this.isSafari = navigator.userAgent.indexOf('Safari') !== -1 }; /** * @requires Xquared.js */ xq.Timer = xq.Class(/** @lends xq.Timer.prototype */{ /** * @constructs * * @param {Number} precision precision in milliseconds */ initialize: function(precision) { xq.addToFinalizeQueue(this); this.precision = precision; this.jobs = {}; this.nextJobId = 0; this.checker = null; }, finalize: function() { this.stop(); }, /** * starts timer */ start: function() { this.stop(); this.checker = window.setInterval(function() { this.executeJobs(); }.bind(this), this.precision); }, /** * stops timer */ stop: function() { if(this.checker) window.clearInterval(this.checker); }, /** * registers new job * * @param {Function} job function to execute * @param {Number} interval interval in milliseconds * * @return {Number} job id */ register: function(job, interval) { var jobId = this.nextJobId++; this.jobs[jobId] = { func:job, interval: interval, lastExecution: Date.get() }; return jobId; }, /** * unregister job by job id * * @param {Number} job id */ unregister: function(jobId) { delete this.jobs[jobId]; }, /** * Execute all expired jobs immedialty. This method will be called automatically by interval timer. */ executeJobs: function() { var curDate = new Date(); for(var id in this.jobs) { var job = this.jobs[id]; if(job.lastExecution.elapsed(job.interval, curDate)) { try { job.lastReturn = job.func(); } catch(e) { job.lastException = e; } finally { job.lastExecution = curDate; } } } } }); /** * @requires Xquared.js */ xq.DomTree = xq.Class(/** @lends xq.DomTree.prototype */{ /** * Provides various tree operations. * * TODO: Add specs * * @constructs */ initialize: function() { xq.addToFinalizeQueue(this); this._blockTags = ["DIV", "DD", "LI", "ADDRESS", "CAPTION", "DT", "H1", "H2", "H3", "H4", "H5", "H6", "HR", "P", "BODY", "BLOCKQUOTE", "PRE", "PARAM", "DL", "OL", "UL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; this._blockContainerTags = ["DIV", "DD", "LI", "BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"]; this._listContainerTags = ["OL", "UL", "DL"]; this._tableCellTags = ["TH", "TD"]; this._blockOnlyContainerTags = ["BODY", "BLOCKQUOTE", "UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR"]; this._atomicTags = ["IMG", "OBJECT", "PARAM", "BR", "HR"]; }, getBlockTags: function() { return this._blockTags; }, /** * Find common ancestor(parent) and his immediate children(left and right).
*
* A --- B -+- C -+- D -+- E
* |
* +- F -+- G
*
* For example:
* > findCommonAncestorAndImmediateChildrenOf("E", "G")
*
* will return
*
* > {parent:"B", left:"C", right:"F"} */ findCommonAncestorAndImmediateChildrenOf: function(left, right) { if(left.parentNode === right.parentNode) { return { left:left, right:right, parent:left.parentNode }; } else { var parentsOfLeft = this.collectParentsOf(left, true); var parentsOfRight = this.collectParentsOf(right, true); var ca = this.getCommonAncestor(parentsOfLeft, parentsOfRight); var leftAncestor = parentsOfLeft.find(function(node) {return node.parentNode === ca}); var rightAncestor = parentsOfRight.find(function(node) {return node.parentNode === ca}); return { left:leftAncestor, right:rightAncestor, parent:ca }; } }, /** * Find leaves at edge.
*
* A --- B -+- C -+- D -+- E
* |
* +- F -+- G
*
* For example:
* > getLeavesAtEdge("A")
*
* will return
*
* > ["E", "G"] */ getLeavesAtEdge: function(element) { if(!element.hasChildNodes()) return [null, null]; var findLeft = function(el) { for (var i = 0; i < el.childNodes.length; i++) { if (el.childNodes[i].nodeType === 1 && this.isBlock(el.childNodes[i])) return findLeft(el.childNodes[i]); } return el; }.bind(this); var findRight=function(el) { for (var i = el.childNodes.length; i--;) { if (el.childNodes[i].nodeType === 1 && this.isBlock(el.childNodes[i])) return findRight(el.childNodes[i]); } return el; }.bind(this); var left = findLeft(element); var right = findRight(element); return [left === element ? null : left, right === element ? null : right]; }, getCommonAncestor: function(parents1, parents2) { for(var i = 0; i < parents1.length; i++) { for(var j = 0; j < parents2.length; j++) { if(parents1[i] === parents2[j]) return parents1[i]; } } }, collectParentsOf: function(node, includeSelf, exitCondition) { var parents = []; if(includeSelf) parents.push(node); while((node = node.parentNode) && (node.nodeName !== "HTML") && !(typeof exitCondition === "function" && exitCondition(node))) parents.push(node); return parents; }, isDescendantOf: function(parent, child) { if(parent.length > 0) { for(var i = 0; i < parent.length; i++) { if(this.isDescendantOf(parent[i], child)) return true; } return false; } if(parent === child) return false; while (child = child.parentNode) if (child === parent) return true; return false; }, /** * Perform tree walking (foreward) */ walkForward: function(node) { var target = node.firstChild; if(target) return target; // intentional assignment for micro performance turing if(target = node.nextSibling) return target; while(node = node.parentNode) { // intentional assignment for micro performance turing if(target = node.nextSibling) return target; } return null; }, /** * Perform tree walking (backward) */ walkBackward: function(node) { if(node.previousSibling) { node = node.previousSibling; while(node.hasChildNodes()) {node = node.lastChild;} return node; } return node.parentNode; }, /** * Perform tree walking (to next siblings) */ walkNext: function(node) {return node.nextSibling}, /** * Perform tree walking (to next siblings) */ walkPrev: function(node) {return node.previousSibling}, /** * Returns true if target is followed by start */ checkTargetForward: function(start, target) { return this._check(start, this.walkForward, target); }, /** * Returns true if start is followed by target */ checkTargetBackward: function(start, target) { return this._check(start, this.walkBackward, target); }, findForward: function(start, condition, exitCondition) { return this._find(start, this.walkForward, condition, exitCondition); }, findBackward: function(start, condition, exitCondition) { return this._find(start, this.walkBackward, condition, exitCondition); }, _check: function(start, direction, target) { if(start === target) return false; while(start = direction(start)) { if(start === target) return true; } return false; }, _find: function(start, direction, condition, exitCondition) { while(start = direction(start)) { if(exitCondition && exitCondition(start)) return null; if(condition(start)) return start; } return null; }, /** * Walks Forward through DOM tree from start to end, and collects all nodes that matches with a filter. * If no filter provided, it just collects all nodes. * * @param {Element} start Starting element. * @param {Element} end Ending element. * @param {Function} filter A filter function. */ collectNodesBetween: function(start, end, filter) { if(start === end) return [start, end].findAll(filter || function() {return true}); var nodes = this.collectForward(start, function(node) {return node === end}, filter); if( start !== end && typeof filter === "function" && filter(end) ) nodes.push(end); return nodes; }, collectForward: function(start, exitCondition, filter) { return this.collect(start, this.walkForward, exitCondition, filter); }, collectBackward: function(start, exitCondition, filter) { return this.collect(start, this.walkBackward, exitCondition, filter); }, collectNext: function(start, exitCondition, filter) { return this.collect(start, this.walkNext, exitCondition, filter); }, collectPrev: function(start, exitCondition, filter) { return this.collect(start, this.walkPrev, exitCondition, filter); }, collect: function(start, next, exitCondition, filter) { var nodes = [start]; while(true) { start = next(start); if( (start === null) || (typeof exitCondition === "function" && exitCondition(start)) ) break; nodes.push(start); } return (typeof filter === "function") ? nodes.findAll(filter) : nodes; }, hasBlocks: function(element) { var nodes = element.childNodes; for(var i = 0; i < nodes.length; i++) { if(this.isBlock(nodes[i])) return true; } return false; }, hasMixedContents: function(element) { if(!this.isBlock(element)) return false; if(!this.isBlockContainer(element)) return false; var hasTextOrInline = false; var hasBlock = false; for(var i = 0; i < element.childNodes.length; i++) { var node = element.childNodes[i]; if(!hasTextOrInline && this.isTextOrInlineNode(node)) hasTextOrInline = true; if(!hasBlock && this.isBlock(node)) hasBlock = true; if(hasTextOrInline && hasBlock) break; } if(!hasTextOrInline || !hasBlock) return false; return true; }, isBlockOnlyContainer: function(element) { if(!element) return false; return this._blockOnlyContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; }, isTableCell: function(element) { if(!element) return false; return this._tableCellTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; }, isBlockContainer: function(element) { if(!element) return false; return this._blockContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; }, isHeading: function(element) { if(!element) return false; return (typeof element === 'string' ? element : element.nodeName).match(/H\d/); }, isBlock: function(element) { if(!element) return false; return this._blockTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; }, isAtomic: function(element) { if(!element) return false; return this._atomicTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; }, isListContainer: function(element) { if(!element) return false; return this._listContainerTags.indexOf(typeof element === 'string' ? element : element.nodeName) !== -1; }, isTextOrInlineNode: function(node) { return node && (node.nodeType === 3 || !this.isBlock(node)); } }); /** * @namespace */ xq.rdom = {} /** * @requires Xquared.js * @requires DomTree.js */ xq.rdom.Base = xq.Class(/** @lends xq.rdom.Base.prototype */{ /** * Encapsulates browser incompatibility problem and provides rich set of DOM manipulation API.
*
* Base provides basic CRUD + Advanced DOM manipulation API, various query methods and caret/selection management API. * * @constructs */ initialize: function() { xq.addToFinalizeQueue(this); /** * Instance of DomTree * @type xq.DomTree */ this.tree = new xq.DomTree(); this.focused = false; this._lastMarkerId = 0; }, /** * Initialize Base instance using window object. * Reads document and body from window object and sets them as a property * * @param {Window} win Browser's window object */ setWin: function(win) { if(!win) throw "[win] is null"; this.win = win; }, /** * Initialize Base instance using root element. * Reads window and document from root element and sets them as a property. * * @param {Element} root Root element */ setRoot: function(root) { if(!root) throw "[root] is null"; this.root = root; }, /** * @returns Browser's window object. */ getWin: function() { return this.win || (this.root ? (this.root.ownerDocument.defaultView || this.root.ownerDocument.parentWindow) : window); }, /** * @returns Root element. */ getRoot: function() { return this.root || this.win.document.body; }, /** * @returns Document object of root element. */ getDoc: function() { return this.getWin().document || this.getRoot().ownerDocument; }, ///////////////////////////////////////////// // CRUDs clearRoot: function() { this.getRoot().innerHTML = ""; this.getRoot().appendChild(this.makeEmptyParagraph()); }, /** * Removes place holders and empty text nodes of given element. * * @param {Element} element target element */ removePlaceHoldersAndEmptyNodes: function(element) { if(!element.hasChildNodes()) return; var stopAt = this.getBottommostLastChild(element); if(!stopAt) return; stopAt = this.tree.walkForward(stopAt); while(element && element !== stopAt) { if( this.isPlaceHolder(element) || (element.nodeType === 3 && (element.nodeValue === "" || (!element.nextSibling && element.nodeValue.isBlank()))) ) { var deleteTarget = element; element = this.tree.walkForward(element); this.deleteNode(deleteTarget); } else { element = this.tree.walkForward(element); } } }, /** * Sets multiple attributes into element at once * * @param {Element} element target element * @param {Object} map key-value pairs */ setAttributes: function(element, map) { for(var key in map) element.setAttribute(key, map[key]); }, /** * Creates textnode by given node value. * * @param {String} value value of textnode * @returns {Node} Created text node */ createTextNode: function(value) {return this.getDoc().createTextNode(value);}, /** * Creates empty element by given tag name. * * @param {String} tagName name of tag * @returns {Element} Created element */ createElement: function(tagName) {return this.getDoc().createElement(tagName);}, /** * Creates element from HTML string * * @param {String} html HTML string * @returns {Element} Created element */ createElementFromHtml: function(html) { var node = this.createElement("div"); node.innerHTML = html; if(node.childNodes.length !== 1) { throw "Illegal HTML fragment"; } return this.getFirstChild(node); }, /** * Deletes node from DOM tree. * * @param {Node} node Target node which should be deleted * @param {boolean} deleteEmptyParentsRecursively Recursively delete empty parent elements * @param {boolean} correctEmptyParent Call #correctEmptyElement on empty parent element after deletion */ deleteNode: function(node, deleteEmptyParentsRecursively, correctEmptyParent) { if(!node || !node.parentNode) return; if(node.nodeName === "BODY") throw "Cannot delete BODY"; var parent = node.parentNode; parent.removeChild(node); if(deleteEmptyParentsRecursively) { while(!parent.hasChildNodes()) { node = parent; parent = node.parentNode; if(!parent || this.getRoot() === node) break; parent.removeChild(node); } } if(correctEmptyParent && this.isEmptyBlock(parent)) { parent.innerHTML = ""; this.correctEmptyElement(parent); } }, /** * Inserts given node into current caret position * * @param {Node} node Target node * @returns {Node} Inserted node. It could be different with given node. */ insertNode: function(node) {throw "Not implemented"}, /** * Inserts given html into current caret position * * @param {String} html HTML string * @returns {Node} Inserted node. It could be different with given node. */ insertHtml: function(html) { return this.insertNode(this.createElementFromHtml(html)); }, /** * Creates textnode from given text and inserts it into current caret position * * @param {String} text Value of textnode * @returns {Node} Inserted node */ insertText: function(text) { this.insertNode(this.createTextNode(text)); }, /** * Places given node nearby target. * * @param {Node} node Node to be inserted. * @param {Node} target Target node. * @param {String} where Possible values: "before", "start", "end", "after" * @param {boolean} performValidation Validate node if needed. For example when P placed into UL, its tag name automatically replaced with LI * * @returns {Node} Inserted node. It could be different with given node. */ insertNodeAt: function(node, target, where, performValidation) { if( ["HTML", "HEAD"].indexOf(target.nodeName) !== -1 || "BODY" === target.nodeName && ["before", "after"].indexOf(where) !== -1 ) throw "Illegal argument. Cannot move node[" + node.nodeName + "] to '" + where + "' of target[" + target.nodeName + "]" var object; var message; var secondParam; switch(where.toLowerCase()) { case "before": object = target.parentNode; message = 'insertBefore'; secondParam = target; break case "start": if(target.firstChild) { object = target; message = 'insertBefore'; secondParam = target.firstChild; } else { object = target; message = 'appendChild'; } break case "end": object = target; message = 'appendChild'; break case "after": if(target.nextSibling) { object = target.parentNode; message = 'insertBefore'; secondParam = target.nextSibling; } else { object = target.parentNode; message = 'appendChild'; } break } if(performValidation && this.tree.isListContainer(object) && node.nodeName !== "LI") { var li = this.createElement("LI"); li.appendChild(node); node = li; object[message](node, secondParam); } else if(performValidation && !this.tree.isListContainer(object) && node.nodeName === "LI") { this.wrapAllInlineOrTextNodesAs("P", node, true); var div = this.createElement("DIV"); this.moveChildNodes(node, div); this.deleteNode(node); object[message](div, secondParam); node = this.unwrapElement(div, true); } else { object[message](node, secondParam); } return node; }, /** * Creates textnode from given text and places given node nearby target. * * @param {String} text Text to be inserted. * @param {Node} target Target node. * @param {String} where Possible values: "before", "start", "end", "after" * * @returns {Node} Inserted node. */ insertTextAt: function(text, target, where) { return this.insertNodeAt(this.createTextNode(text), target, where); }, /** * Creates element from given HTML string and places given it nearby target. * * @param {String} html HTML to be inserted. * @param {Node} target Target node. * @param {String} where Possible values: "before", "start", "end", "after" * * @returns {Node} Inserted node. */ insertHtmlAt: function(html, target, where) { return this.insertNodeAt(this.createElementFromHtml(html), target, where); }, /** * Replaces element's tag by removing current element and creating new element by given tag name. * * @param {String} tag New tag name * @param {Element} element Target element * * @returns {Element} Replaced element */ replaceTag: function(tag, element) { if(element.nodeName === tag) return null; if(this.tree.isTableCell(element)) return null; var newElement = this.createElement(tag); this.moveChildNodes(element, newElement); this.copyAttributes(element, newElement, true); element.parentNode.replaceChild(newElement, element); if(!newElement.hasChildNodes()) this.correctEmptyElement(newElement); return newElement; }, /** * Unwraps unnecessary paragraph. * * Unnecessary paragraph is P which is the only child of given container element. * For example, P which is contained by LI and is the only child is the unnecessary paragraph. * But if given container element is a block-only-container(BLOCKQUOTE, BODY), this method does nothing. * * @param {Element} element Container element * @returns {boolean} True if unwrap performed. */ unwrapUnnecessaryParagraph: function(element) { if(!element) return false; if(!this.tree.isBlockOnlyContainer(element) && element.childNodes.length === 1 && element.firstChild.nodeName === "P" && !this.hasImportantAttributes(element.firstChild)) { var p = element.firstChild; this.moveChildNodes(p, element); this.deleteNode(p); return true; } return false; }, /** * Unwraps element by extracting all children out and removing the element. * * @param {Element} element Target element * @param {boolean} wrapInlineAndTextNodes Wrap all inline and text nodes with P before unwrap * @returns {Node} First child of unwrapped element */ unwrapElement: function(element, wrapInlineAndTextNodes) { if(wrapInlineAndTextNodes) this.wrapAllInlineOrTextNodesAs("P", element); var nodeToReturn = element.firstChild; while(element.firstChild) this.insertNodeAt(element.firstChild, element, "before"); this.deleteNode(element); return nodeToReturn; }, /** * Wraps element by given tag * * @param {String} tag tag name * @param {Element} element target element to wrap * @returns {Element} wrapper */ wrapElement: function(tag, element) { var wrapper = this.insertNodeAt(this.createElement(tag), element, "before"); wrapper.appendChild(element); return wrapper; }, /** * Tests #smartWrap with given criteria but doesn't change anything */ testSmartWrap: function(endElement, criteria) { return this.smartWrap(endElement, null, criteria, true); }, /** * Create inline element with given tag name and wraps nodes nearby endElement by given criteria * * @param {Element} endElement Boundary(end point, exclusive) of wrapper. * @param {String} tag Tag name of wrapper. * @param {Object} function which returns text index of start boundary. * @param {boolean} testOnly just test boundary and do not perform actual wrapping. * * @returns {Element} wrapper */ smartWrap: function(endElement, tag, criteria, testOnly) { var block = this.getParentBlockElementOf(endElement); tag = tag || "SPAN"; criteria = criteria || function(text) {return -1}; // check for empty wrapper if(!testOnly && (!endElement.previousSibling || this.isEmptyBlock(block))) { var wrapper = this.insertNodeAt(this.createElement(tag), endElement, "before"); return wrapper; } // collect all textnodes var textNodes = this.tree.collectForward(block, function(node) {return node === endElement}, function(node) {return node.nodeType === 3}); // find textnode and break-point var nodeIndex = 0; var nodeValues = []; for(var i = 0; i < textNodes.length; i++) { nodeValues.push(textNodes[i].nodeValue); } var textToWrap = nodeValues.join(""); var textIndex = criteria(textToWrap) var breakPoint = textIndex; if(breakPoint === -1) { breakPoint = 0; } else { textToWrap = textToWrap.substring(breakPoint); } for(var i = 0; i < textNodes.length; i++) { if(breakPoint > nodeValues[i].length) { breakPoint -= nodeValues[i].length; } else { nodeIndex = i; break; } } if(testOnly) return {text:textToWrap, textIndex:textIndex, nodeIndex:nodeIndex, breakPoint:breakPoint}; // break textnode if necessary if(breakPoint !== 0) { var splitted = textNodes[nodeIndex].splitText(breakPoint); nodeIndex++; textNodes.splice(nodeIndex, 0, splitted); } var startElement = textNodes[nodeIndex] || block.firstChild; // split inline elements up to parent block if necessary var family = this.tree.findCommonAncestorAndImmediateChildrenOf(startElement, endElement); var ca = family.parent; if(ca) { if(startElement.parentNode !== ca) startElement = this.splitElementUpto(startElement, ca, true); if(endElement.parentNode !== ca) endElement = this.splitElementUpto(endElement, ca, true); var prevStart = startElement.previousSibling; var nextEnd = endElement.nextSibling; // remove empty inline elements if(prevStart && prevStart.nodeType === 1 && this.isEmptyBlock(prevStart)) this.deleteNode(prevStart); if(nextEnd && nextEnd.nodeType === 1 && this.isEmptyBlock(nextEnd)) this.deleteNode(nextEnd); // wrap var wrapper = this.insertNodeAt(this.createElement(tag), startElement, "before"); while(wrapper.nextSibling !== endElement) wrapper.appendChild(wrapper.nextSibling); return wrapper; } else { // wrap var wrapper = this.insertNodeAt(this.createElement(tag), endElement, "before"); return wrapper; } }, /** * Wraps all adjust inline elements and text nodes into block element. * * TODO: empty element should return empty array when it is not forced and (at least) single item array when forced * * @param {String} tag Tag name of wrapper * @param {Element} element Target element * @param {boolean} force Force wrapping. If it is set to false, this method do not makes unnecessary wrapper. * * @returns {Array} Array of wrappers. If nothing performed it returns empty array */ wrapAllInlineOrTextNodesAs: function(tag, element, force) { var wrappers = []; if(!force && !this.tree.hasMixedContents(element)) return wrappers; var node = element.firstChild; while(node) { if(this.tree.isTextOrInlineNode(node)) { var wrapper = this.wrapInlineOrTextNodesAs(tag, node); wrappers.push(wrapper); node = wrapper.nextSibling; } else { node = node.nextSibling; } } return wrappers; }, /** * Wraps node and its adjust next siblings into an element */ wrapInlineOrTextNodesAs: function(tag, node) { var wrapper = this.createElement(tag); var from = node; from.parentNode.replaceChild(wrapper, from); wrapper.appendChild(from); // move nodes into wrapper while(wrapper.nextSibling && this.tree.isTextOrInlineNode(wrapper.nextSibling)) wrapper.appendChild(wrapper.nextSibling); return wrapper; }, /** * Turns block element into list item * * @param {Element} element Target element * @param {String} type One of "UL", "OL". * @param {String} className CSS class name. * * @return {Element} LI element */ turnElementIntoListItem: function(element, type, className) { type = type.toUpperCase(); className = className || ""; var container = this.createElement(type); if(className) container.className = className; if(this.tree.isTableCell(element)) { var p = this.wrapAllInlineOrTextNodesAs("P", element, true)[0]; container = this.insertNodeAt(container, element, "start"); var li = this.insertNodeAt(this.createElement("LI"), container, "start"); li.appendChild(p); } else { container = this.insertNodeAt(container, element, "after"); var li = this.insertNodeAt(this.createElement("LI"), container, "start"); li.appendChild(element); } this.unwrapUnnecessaryParagraph(li); this.mergeAdjustLists(container); return li; }, /** * Extracts given element out from its parent element. * * @param {Element} element Target element */ extractOutElementFromParent: function(element) { if(element === this.getRoot() || element.parentNode === this.getRoot() || !element.offsetParent) return null; if(element.nodeName === "LI") { this.wrapAllInlineOrTextNodesAs("P", element, true); element = element.firstChild; } var container = element.parentNode; var nodeToReturn = null; if(container.nodeName === "LI" && container.parentNode.parentNode.nodeName === "LI") { // nested list item if(element.previousSibling) { this.splitContainerOf(element, true); this.correctEmptyElement(element); } this.outdentListItem(element); nodeToReturn = element; } else if(container.nodeName === "LI") { // not-nested list item if(this.tree.isListContainer(element.nextSibling)) { // 1. split listContainer var listContainer = container.parentNode; this.splitContainerOf(container, true); this.correctEmptyElement(element); // 2. extract out LI's children nodeToReturn = container.firstChild; while(container.firstChild) { this.insertNodeAt(container.firstChild, listContainer, "before"); } // 3. remove listContainer and merge adjust lists var prevContainer = listContainer.previousSibling; this.deleteNode(listContainer); if(prevContainer && this.tree.isListContainer(prevContainer)) this.mergeAdjustLists(prevContainer); } else { // 1. split LI this.splitContainerOf(element, true); this.correctEmptyElement(element); // 2. split list container var listContainer = this.splitContainerOf(container); // 3. extract out this.insertNodeAt(element, listContainer.parentNode, "before"); this.deleteNode(listContainer.parentNode); nodeToReturn = element; } } else if(this.tree.isTableCell(container) || this.tree.isTableCell(element)) { // do nothing } else { // normal block this.splitContainerOf(element, true); this.correctEmptyElement(element); nodeToReturn = this.insertNodeAt(element, container, "before"); this.deleteNode(container); } return nodeToReturn; }, /** * Insert new block above or below given element. * * @param {Element} block Target block * @param {boolean} before Insert new block above(before) target block * @param {String} forceTag New block's tag name. If omitted, target block's tag name will be used. * * @returns {Element} Inserted block */ insertNewBlockAround: function(block, before, forceTag) { var isListItem = block.nodeName === "LI" || block.parentNode.nodeName === "LI"; this.removeTrailingWhitespace(block); if(this.isFirstLiWithNestedList(block) && !forceTag && before) { var li = this.getParentElementOf(block, ["LI"]); var newBlock = this._insertNewBlockAround(li, before); return newBlock; } else if(isListItem && !forceTag) { var li = this.getParentElementOf(block, ["LI"]); var newBlock = this._insertNewBlockAround(block, before); if(li !== block) newBlock = this.splitContainerOf(newBlock, false, "prev"); return newBlock; } else if(this.tree.isBlockContainer(block)) { this.wrapAllInlineOrTextNodesAs("P", block, true); return this._insertNewBlockAround(block.firstChild, before, forceTag); } else { return this._insertNewBlockAround(block, before, this.tree.isHeading(block) ? "P" : forceTag); } }, /** * @private * * TODO: Rename */ _insertNewBlockAround: function(element, before, tagName) { var newElement = this.createElement(tagName || element.nodeName); this.copyAttributes(element, newElement, false); this.correctEmptyElement(newElement); newElement = this.insertNodeAt(newElement, element, before ? "before" : "after"); return newElement; }, /** * Wrap or replace element with given tag name. * * @param {String} [tag] Tag name. If not provided, it does not modify tag name. * @param {Element} element Target element * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. * * @return {Element} wrapper element or replaced element. */ applyTagIntoElement: function(tag, element, className) { if(!tag && !className) return null; var result = element; if(tag) { if(this.tree.isBlockOnlyContainer(tag)) { result = this.wrapBlock(tag, element); } else if(this.tree.isBlockContainer(element)) { var wrapper = this.createElement(tag); this.moveChildNodes(element, wrapper); result = this.insertNodeAt(wrapper, element, "start"); } else if(this.tree.isBlockContainer(tag) && this.hasImportantAttributes(element)) { result = this.wrapBlock(tag, element); } else { result = this.replaceTag(tag, element); } } if(className) { result.className = className; } return result; }, /** * Wrap or replace elements with given tag name. * * @param {String} [tag] Tag name. If not provided, it does not modify tag name. * @param {Element} from Start boundary (inclusive) * @param {Element} to End boundary (inclusive) * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. * * @returns {Array} Array of wrappers or replaced elements */ applyTagIntoElements: function(tagName, from, to, className) { if(!tagName && !className) return [from, to]; var applied = []; if(tagName) { if(this.tree.isBlockContainer(tagName)) { var family = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to); var node = family.left; var wrapper = this.insertNodeAt(this.createElement(tagName), node, "before"); var coveringWholeList = family.parent.nodeName === "LI" && family.parent.parentNode.childNodes.length === 1 && !family.left.previousSilbing && !family.right.nextSibling; if(coveringWholeList) { var ul = node.parentNode.parentNode; this.insertNodeAt(wrapper, ul, "before"); wrapper.appendChild(ul); } else { while(node !== family.right) { next = node.nextSibling; wrapper.appendChild(node); node = next; } wrapper.appendChild(family.right); } applied.push(wrapper); } else { // is normal tagName var elements = this.getBlockElementsBetween(from, to); for(var i = 0; i < elements.length; i++) { if(this.tree.isBlockContainer(elements[i])) { var wrappers = this.wrapAllInlineOrTextNodesAs(tagName, elements[i], true); for(var j = 0; j < wrappers.length; j++) { applied.push(wrappers[j]); } } else { applied.push(this.replaceTag(tagName, elements[i]) || elements[i]); } } } } if(className) { var elements = this.tree.collectNodesBetween(from, to, function(n) {return n.nodeType == 1;}); for(var i = 0; i < elements.length; i++) { elements[i].className = className; } } return applied; }, /** * Moves block up or down * * @param {Element} block Target block * @param {boolean} up Move up if true * * @returns {Element} Moved block. It could be different with given block. */ moveBlock: function(block, up) { // if block is table cell or contained by table cell, select its row as mover block = this.getParentElementOf(block, ["TR"]) || block; // if block is only child, select its parent as mover while(block.nodeName !== "TR" && block.parentNode !== this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) { block = block.parentNode; } // find target and where var target, where; if (up) { target = block.previousSibling; if(target) { var singleNodeLi = target.nodeName === 'LI' && ((target.childNodes.length === 1 && this.tree.isBlock(target.firstChild)) || !this.tree.hasBlocks(target)); var table = ['TABLE', 'TR'].indexOf(target.nodeName) !== -1; where = this.tree.isBlockContainer(target) && !singleNodeLi && !table ? "end" : "before"; } else if(block.parentNode !== this.getRoot()) { target = block.parentNode; where = "before"; } } else { target = block.nextSibling; if(target) { var singleNodeLi = target.nodeName === 'LI' && ((target.childNodes.length === 1 && this.tree.isBlock(target.firstChild)) || !this.tree.hasBlocks(target)); var table = ['TABLE', 'TR'].indexOf(target.nodeName) !== -1; where = this.tree.isBlockContainer(target) && !singleNodeLi && !table ? "start" : "after"; } else if(block.parentNode !== this.getRoot()) { target = block.parentNode; where = "after"; } } // no way to go? if(!target) return null; if(["TBODY", "THEAD"].indexOf(target.nodeName) !== -1) return null; // normalize this.wrapAllInlineOrTextNodesAs("P", target, true); // make placeholder if needed if(this.isFirstLiWithNestedList(block)) { this.insertNewBlockAround(block, false, "P"); } // perform move var parent = block.parentNode; var moved = this.insertNodeAt(block, target, where, true); // cleanup if(!parent.hasChildNodes()) this.deleteNode(parent, true); this.unwrapUnnecessaryParagraph(moved); this.unwrapUnnecessaryParagraph(target); // remove placeholder if(up) { if(moved.previousSibling && this.isEmptyBlock(moved.previousSibling) && !moved.previousSibling.previousSibling && moved.parentNode.nodeName === "LI" && this.tree.isListContainer(moved.nextSibling)) { this.deleteNode(moved.previousSibling); } } else { if(moved.nextSibling && this.isEmptyBlock(moved.nextSibling) && !moved.previousSibling && moved.parentNode.nodeName === "LI" && this.tree.isListContainer(moved.nextSibling.nextSibling)) { this.deleteNode(moved.nextSibling); } } this.correctEmptyElement(moved); return moved; }, /** * Remove given block * * @param {Element} block Target block * @returns {Element} Nearest block of remove element */ removeBlock: function(block) { var blockToMove; // if block is only child, select its parent as mover while(block.parentNode !== this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) { block = block.parentNode; } var finder = function(node) {return this.tree.isBlock(node) && !this.tree.isAtomic(node) && !this.tree.isDescendantOf(block, node) && !this.tree.hasBlocks(node);}.bind(this); var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this); if(this.isFirstLiWithNestedList(block)) { blockToMove = this.outdentListItem(block.nextSibling.firstChild); this.deleteNode(blockToMove.previousSibling, true); } else if(this.tree.isTableCell(block)) { var rtable = new xq.RichTable(this, this.getParentElementOf(block, ["TABLE"])); blockToMove = rtable.getBelowCellOf(block); // should not delete row when there's thead and the row is the only child of tbody if( block.parentNode.parentNode.nodeName === "TBODY" && rtable.hasHeadingAtTop() && rtable.getDom().tBodies[0].rows.length === 1) return blockToMove; blockToMove = blockToMove || this.tree.findForward(block, finder, exitCondition) || this.tree.findBackward(block, finder, exitCondition); this.deleteNode(block.parentNode, true); } else { blockToMove = blockToMove || this.tree.findForward(block, finder, exitCondition) || this.tree.findBackward(block, finder, exitCondition); if(!blockToMove) blockToMove = this.insertNodeAt(this.makeEmptyParagraph(), block, "after"); this.deleteNode(block, true); } if(!this.getRoot().hasChildNodes()) { blockToMove = this.createElement("P"); this.getRoot().appendChild(blockToMove); this.correctEmptyElement(blockToMove); } return blockToMove; }, /** * Removes trailing whitespaces of given block * * @param {Element} block Target block */ removeTrailingWhitespace: function(block) {throw "Not implemented"}, /** * Extract given list item out and change its container's tag * * @param {Element} element LI or P which is a child of LI * @param {String} type "OL", "UL" * @param {String} className CSS class name * * @returns {Element} changed element */ changeListTypeTo: function(element, type, className) { type = type.toUpperCase(); className = className || ""; var li = this.getParentElementOf(element, ["LI"]); if(!li) throw "IllegalArgumentException"; var container = li.parentNode; this.splitContainerOf(li); var newContainer = this.insertNodeAt(this.createElement(type), container, "before"); if(className) newContainer.className = className; this.insertNodeAt(li, newContainer, "start"); this.deleteNode(container); this.mergeAdjustLists(newContainer); return element; }, /** * Split container of element into (maxium) three pieces. */ splitContainerOf: function(element, preserveElementItself, dir) { if([element, element.parentNode].indexOf(this.getRoot()) !== -1) return element; var container = element.parentNode; if(element.previousSibling && (!dir || dir.toLowerCase() === "prev")) { var prev = this.createElement(container.nodeName); this.copyAttributes(container, prev); while(container.firstChild !== element) { prev.appendChild(container.firstChild); } this.insertNodeAt(prev, container, "before"); this.unwrapUnnecessaryParagraph(prev); } if(element.nextSibling && (!dir || dir.toLowerCase() === "next")) { var next = this.createElement(container.nodeName); this.copyAttributes(container, next); while(container.lastChild !== element) { this.insertNodeAt(container.lastChild, next, "start"); } this.insertNodeAt(next, container, "after"); this.unwrapUnnecessaryParagraph(next); } if(!preserveElementItself) element = this.unwrapUnnecessaryParagraph(container) ? container : element; return element; }, /** * TODO: Add specs */ splitParentElement: function(seperator) { var parent = seperator.parentNode; if(["HTML", "HEAD", "BODY"].indexOf(parent.nodeName) !== -1) throw "Illegal argument. Cannot seperate element[" + parent.nodeName + "]"; var previousSibling = seperator.previousSibling; var nextSibling = seperator.nextSibling; var newElement = this.insertNodeAt(this.createElement(parent.nodeName), parent, "after"); var next; while(next = seperator.nextSibling) newElement.appendChild(next); this.insertNodeAt(seperator, newElement, "start"); this.copyAttributes(parent, newElement); return newElement; }, /** * TODO: Add specs */ splitElementUpto: function(seperator, element, excludeElement) { while(seperator.previousSibling !== element) { if(excludeElement && seperator.parentNode === element) break; seperator = this.splitParentElement(seperator); } return seperator; }, /** * Merges two adjust elements * * @param {Element} element base element * @param {boolean} withNext merge base element with next sibling * @param {boolean} skip skip merge steps */ mergeElement: function(element, withNext, skip) { this.wrapAllInlineOrTextNodesAs("P", element.parentNode, true); // find two block if(withNext) { var prev = element; var next = this.tree.findForward( element, function(node) {return this.tree.isBlock(node) && !this.tree.isListContainer(node) && node !== element.parentNode}.bind(this) ); } else { var next = element; var prev = this.tree.findBackward( element, function(node) {return this.tree.isBlock(node) && !this.tree.isListContainer(node) && node !== element.parentNode}.bind(this) ); } // normalize next block if(next && this.tree.isDescendantOf(this.getRoot(), next)) { var nextContainer = next.parentNode; if(this.tree.isBlockContainer(next)) { nextContainer = next; this.wrapAllInlineOrTextNodesAs("P", nextContainer, true); next = nextContainer.firstChild; } } else { next = null; } // normalize prev block if(prev && this.tree.isDescendantOf(this.getRoot(), prev)) { var prevContainer = prev.parentNode; if(this.tree.isBlockContainer(prev)) { prevContainer = prev; this.wrapAllInlineOrTextNodesAs("P", prevContainer, true); prev = prevContainer.lastChild; } } else { prev = null; } try { var containersAreTableCell = prevContainer && (this.tree.isTableCell(prevContainer) || ['TR', 'THEAD', 'TBODY'].indexOf(prevContainer.nodeName) !== -1) && nextContainer && (this.tree.isTableCell(nextContainer) || ['TR', 'THEAD', 'TBODY'].indexOf(nextContainer.nodeName) !== -1); if(containersAreTableCell && prevContainer !== nextContainer) return null; // if next has margin, perform outdent if((!skip || !prev) && next && nextContainer.nodeName !== "LI" && this.outdentElement(next)) return element; // nextContainer is first li and next of it is list container ([I] represents caret position): // // * A[I] // * B // * C if(nextContainer && nextContainer.nodeName === 'LI' && this.tree.isListContainer(next.nextSibling)) { // move child nodes and... this.moveChildNodes(nextContainer, prevContainer); // merge two paragraphs this.removePlaceHoldersAndEmptyNodes(prev); this.moveChildNodes(next, prev); this.deleteNode(next); return prev; } // merge two list containers if(nextContainer && nextContainer.nodeName === 'LI' && this.tree.isListContainer(nextContainer.parentNode.previousSibling)) { this.mergeAdjustLists(nextContainer.parentNode.previousSibling, true, "next"); return prev; } if(next && !containersAreTableCell && prevContainer && prevContainer.nodeName === 'LI' && nextContainer && nextContainer.nodeName === 'LI' && prevContainer.parentNode.nextSibling === nextContainer.parentNode) { var nextContainerContainer = nextContainer.parentNode; this.moveChildNodes(nextContainer.parentNode, prevContainer.parentNode); this.deleteNode(nextContainerContainer); return prev; } // merge two containers if(next && !containersAreTableCell && prevContainer && prevContainer.nextSibling === nextContainer && ((skip && prevContainer.nodeName !== "LI") || (!skip && prevContainer.nodeName === "LI"))) { this.moveChildNodes(nextContainer, prevContainer); return prev; } // unwrap container if(nextContainer && nextContainer.nodeName !== "LI" && !this.getParentElementOf(nextContainer, ["TABLE"]) && !this.tree.isListContainer(nextContainer) && nextContainer !== this.getRoot() && !next.previousSibling) { return this.unwrapElement(nextContainer, true); } // delete table if(withNext && nextContainer && nextContainer.nodeName === "TABLE") { this.deleteNode(nextContainer, true); return prev; } else if(!withNext && prevContainer && this.tree.isTableCell(prevContainer) && !this.tree.isTableCell(nextContainer)) { this.deleteNode(this.getParentElementOf(prevContainer, ["TABLE"]), true); return next; } // if prev is same with next, do nothing if(prev === next) return null; // if there is a null block, do nothing if(!prev || !next || !prevContainer || !nextContainer) return null; // if two blocks are not in the same table cell, do nothing if(this.getParentElementOf(prev, ["TD", "TH"]) !== this.getParentElementOf(next, ["TD", "TH"])) return null; var prevIsEmpty = false; // cleanup empty block before merge // 1. cleanup prev node which ends with marker +   if( xq.Browser.isTrident && prev.childNodes.length >= 2 && this.isMarker(prev.lastChild.previousSibling) && prev.lastChild.nodeType === 3 && prev.lastChild.nodeValue.length === 1 && prev.lastChild.nodeValue.charCodeAt(0) === 160 ) { this.deleteNode(prev.lastChild); } // 2. cleanup prev node (if prev is empty, then replace prev's tag with next's) this.removePlaceHoldersAndEmptyNodes(prev); if(this.isEmptyBlock(prev)) { // replace atomic block with normal block so that following code don't need to care about atomic block if(this.tree.isAtomic(prev)) prev = this.replaceTag("P", prev); prev = this.replaceTag(next.nodeName, prev) || prev; prev.innerHTML = ""; } else if(prev.firstChild === prev.lastChild && this.isMarker(prev.firstChild)) { prev = this.replaceTag(next.nodeName, prev) || prev; } // 3. cleanup next node if(this.isEmptyBlock(next)) { // replace atomic block with normal block so that following code don't need to care about atomic block if(this.tree.isAtomic(next)) next = this.replaceTag("P", next); next.innerHTML = ""; } // perform merge this.moveChildNodes(next, prev); this.deleteNode(next); return prev; } finally { // cleanup if(prevContainer && this.isEmptyBlock(prevContainer)) this.deleteNode(prevContainer, true); if(nextContainer && this.isEmptyBlock(nextContainer)) this.deleteNode(nextContainer, true); if(prevContainer) this.unwrapUnnecessaryParagraph(prevContainer); if(nextContainer) this.unwrapUnnecessaryParagraph(nextContainer); } }, /** * Merges adjust list containers which has same tag name * * @param {Element} container target list container * @param {boolean} force force adjust list container even if they have different list type * @param {String} dir Specify merge direction: PREV or NEXT. If not supplied it will be merged with both direction. */ mergeAdjustLists: function(container, force, dir) { var prev = container.previousSibling; var isPrevSame = prev && (prev.nodeName === container.nodeName && prev.className === container.className); if((!dir || dir.toLowerCase() === 'prev') && (isPrevSame || (force && this.tree.isListContainer(prev)))) { while(prev.lastChild) { this.insertNodeAt(prev.lastChild, container, "start"); } this.deleteNode(prev); } var next = container.nextSibling; var isNextSame = next && (next.nodeName === container.nodeName && next.className === container.className); if((!dir || dir.toLowerCase() === 'next') && (isNextSame || (force && this.tree.isListContainer(next)))) { while(next.firstChild) { this.insertNodeAt(next.firstChild, container, "end"); } this.deleteNode(next); } }, /** * Moves child nodes from one element into another. * * @param {Elemet} from source element * @param {Elemet} to target element */ moveChildNodes: function(from, to) { if(this.tree.isDescendantOf(from, to) || ["HTML", "HEAD"].indexOf(to.nodeName) !== -1) throw "Illegal argument. Cannot move children of element[" + from.nodeName + "] to element[" + to.nodeName + "]"; if(from === to) return; while(from.firstChild) to.appendChild(from.firstChild); }, /** * Copies attributes from one element into another. * * @param {Element} from source element * @param {Element} to target element * @param {boolean} copyId copy ID attribute of source element */ copyAttributes: function(from, to, copyId) { // IE overrides this var attrs = from.attributes; if(!attrs) return; for(var i = 0; i < attrs.length; i++) { if(attrs[i].nodeName === "class" && attrs[i].nodeValue) { to.className = attrs[i].nodeValue; } else if((copyId || "id" !== attrs[i].nodeName) && attrs[i].nodeValue) { to.setAttribute(attrs[i].nodeName, attrs[i].nodeValue); } } }, _indentElements: function(node, blocks, affect) { for (var i=0; i < affect.length; i++) { if (affect[i] === node || this.tree.isDescendantOf(affect[i], node)) return; } leaves = this.tree.getLeavesAtEdge(node); if (blocks.includeElement(leaves[0])) { var affected = this.indentElement(node, true); if (affected) { affect.push(affected); return; } } if (blocks.includeElement(node)) { var affected = this.indentElement(node, true); if (affected) { affect.push(affected); return; } } var children=xq.$A(node.childNodes); for (var i=0; i < children.length; i++) this._indentElements(children[i], blocks, affect); return; }, indentElements: function(from, to) { var blocks = this.getBlockElementsBetween(from, to); var top = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to); var affect = []; leaves = this.tree.getLeavesAtEdge(top.parent); if (blocks.includeElement(leaves[0])) { var affected = this.indentElement(top.parent); if (affected) return [affected]; } var children = xq.$A(top.parent.childNodes); for (var i=0; i < children.length; i++) { this._indentElements(children[i], blocks, affect); } affect = affect.flatten() return affect.length > 0 ? affect : blocks; }, outdentElementsCode: function(node) { if (node.tagName === 'LI') node = node.parentNode; if (node.tagName === 'OL' && node.className === 'code') return true; return false; }, _outdentElements: function(node, blocks, affect) { for (var i=0; i < affect.length; i++) { if (affect[i] === node || this.tree.isDescendantOf(affect[i], node)) return; } leaves = this.tree.getLeavesAtEdge(node); if (blocks.includeElement(leaves[0]) && !this.outdentElementsCode(leaves[0])) { var affected = this.outdentElement(node, true); if (affected) { affect.push(affected); return; } } if (blocks.includeElement(node)) { var children = xq.$A(node.parentNode.childNodes); var isCode = this.outdentElementsCode(node); var affected = this.outdentElement(node, true, isCode); if (affected) { if (children.includeElement(affected) && this.tree.isListContainer(node.parentNode) && !isCode) { for (var i=0; i < children.length; i++) { if (blocks.includeElement(children[i]) && !affect.includeElement(children[i])) affect.push(children[i]); } }else affect.push(affected); return; } } var children=xq.$A(node.childNodes); for (var i=0; i < children.length; i++) this._outdentElements(children[i], blocks, affect); return; }, outdentElements: function(from, to) { var start, end; if (from.parentNode.tagName === 'LI') start=from.parentNode; if (to.parentNode.tagName === 'LI') end=to.parentNode; var blocks = this.getBlockElementsBetween(from, to); var top = this.tree.findCommonAncestorAndImmediateChildrenOf(from, to); var affect = []; leaves = this.tree.getLeavesAtEdge(top.parent); if (blocks.includeElement(leaves[0]) && !this.outdentElementsCode(top.parent)) { var affected = this.outdentElement(top.parent); if (affected) return [affected]; } var children = xq.$A(top.parent.childNodes); for (var i=0; i < children.length; i++) { this._outdentElements(children[i], blocks, affect); } if (from.offsetParent && to.offsetParent) { start = from; end = to; }else if (blocks.first().offsetParent && blocks.last().offsetParent) { start = blocks.first(); end = blocks.last(); } affect = affect.flatten() if (!start || !start.offsetParent) start = affect.first(); if (!end || !end.offsetParent) end = affect.last(); return this.getBlockElementsBetween(start, end); }, /** * Performs indent by increasing element's margin-left */ indentElement: function(element, noParent, forceMargin) { if( !forceMargin && (element.nodeName === "LI" || (!this.tree.isListContainer(element) && !element.previousSibling && element.parentNode.nodeName === "LI")) ) return this.indentListItem(element, noParent); var root = this.getRoot(); if(!element || element === root) return null; if (element.parentNode !== root && !element.previousSibling && !noParent) element=element.parentNode; var margin = element.style.marginLeft; var cssValue = margin ? this._getCssValue(margin, "px") : {value:0, unit:"em"}; cssValue.value += 2; element.style.marginLeft = cssValue.value + cssValue.unit; return element; }, /** * Performs outdent by decreasing element's margin-left */ outdentElement: function(element, noParent, forceMargin) { if(!forceMargin && element.nodeName === "LI") return this.outdentListItem(element, noParent); var root = this.getRoot(); if(!element || element === root) return null; var margin = element.style.marginLeft; var cssValue = margin ? this._getCssValue(margin, "px") : {value:0, unit:"em"}; if(cssValue.value === 0) { return element.previousSibling || forceMargin ? null : this.outdentElement(element.parentNode, noParent); } cssValue.value -= 2; element.style.marginLeft = cssValue.value <= 0 ? "" : cssValue.value + cssValue.unit; if(element.style.cssText === "") element.removeAttribute("style"); return element; }, /** * Performs indent for list item */ indentListItem: function(element, treatListAsNormalBlock) { var li = this.getParentElementOf(element, ["LI"]); var container = li.parentNode; var prev = li.previousSibling; if(!li.previousSibling) return this.indentElement(container); if(li.parentNode.nodeName === "OL" && li.parentNode.className === "code") return this.indentElement(li, treatListAsNormalBlock, true); if(!prev.lastChild) prev.appendChild(this.makePlaceHolder()); var targetContainer = this.tree.isListContainer(prev.lastChild) ? // if there's existing list container, select it as target container prev.lastChild : // if there's nothing, create new one this.insertNodeAt(this.createElement(container.nodeName), prev, "end"); this.wrapAllInlineOrTextNodesAs("P", prev, true); // perform move targetContainer.appendChild(li); // flatten nested list if(!treatListAsNormalBlock && li.lastChild && this.tree.isListContainer(li.lastChild)) { var childrenContainer = li.lastChild; var child; while(child = childrenContainer.lastChild) { this.insertNodeAt(child, li, "after"); } this.deleteNode(childrenContainer); } this.unwrapUnnecessaryParagraph(li); return li; }, /** * Performs outdent for list item * * @return {Element} outdented list item or null if no outdent performed */ outdentListItem: function(element, treatListAsNormalBlock) { var li = this.getParentElementOf(element, ["LI"]); var container = li.parentNode; if(!li.previousSibling) { var performed = this.outdentElement(container); if(performed) return performed; } if(li.parentNode.nodeName === "OL" && li.parentNode.className === "code") return this.outdentElement(li, treatListAsNormalBlock, true); var parentLi = container.parentNode; if(parentLi.nodeName !== "LI") return null; if(treatListAsNormalBlock) { while(container.lastChild !== li) { this.insertNodeAt(container.lastChild, parentLi, "after"); } } else { // make next siblings as children if(li.nextSibling) { var targetContainer = li.lastChild && this.tree.isListContainer(li.lastChild) ? // if there's existing list container, select it as target container li.lastChild : // if there's nothing, create new one this.insertNodeAt(this.createElement(container.nodeName), li, "end"); this.copyAttributes(container, targetContainer); var sibling; while(sibling = li.nextSibling) { targetContainer.appendChild(sibling); } } } // move current LI into parent LI's next sibling li = this.insertNodeAt(li, parentLi, "after"); // remove empty container if(container.childNodes.length === 0) this.deleteNode(container); if(li.firstChild && this.tree.isListContainer(li.firstChild)) { this.insertNodeAt(this.makePlaceHolder(), li, "start"); } this.wrapAllInlineOrTextNodesAs("P", li); this.unwrapUnnecessaryParagraph(parentLi); return li; }, /** * Performs justification * * @param {Element} block target element * @param {String} dir one of "LEFT", "CENTER", "RIGHT", "BOTH" */ justifyBlock: function(block, dir) { // if block is only child, select its parent as mover while(block.parentNode !== this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) { block = block.parentNode; } var styleValue = dir.toLowerCase() === "both" ? "justify" : dir; if(styleValue === "left") { block.style.textAlign = ""; if(block.style.cssText === "") block.removeAttribute("style"); } else { block.style.textAlign = styleValue; } return block; }, justifyBlocks: function(blocks, dir) { for(var i = 0; i < blocks.length; i++) { this.justifyBlock(blocks[i], dir); } return blocks; }, /** * Turn given element into list. If the element is a list already, it will be reversed into normal element. * * @param {Element} element target element * @param {String} type one of "UL", "OL" * @param {String} className CSS className * @returns {Element} affected element */ applyList: function(element, type, className) { type = type.toUpperCase(); className = className || ""; var containerTag = type; if(element.nodeName === "LI" || (element.parentNode.nodeName === "LI" && !element.previousSibling)) { var element = this.getParentElementOf(element, ["LI"]); var container = element.parentNode; if(container.nodeName === containerTag && container.className === className) { return this.extractOutElementFromParent(element); } else { return this.changeListTypeTo(element, type, className); } } else { return this.turnElementIntoListItem(element, type, className); } }, applyLists: function(from, to, type, className) { type = type.toUpperCase(); className = className || ""; var containerTag = type; var blocks = this.getBlockElementsBetween(from, to); // LIs or Non-containing blocks var whole = blocks.findAll(function(e) { return e.nodeName === "LI" || !this.tree.isBlockContainer(e); }.bind(this)); // LIs var listItems = whole.findAll(function(e) {return e.nodeName === "LI"}.bind(this)); // Non-containing blocks which is not a descendant of any LIs selected above(listItems). var normalBlocks = whole.findAll(function(e) { return e.nodeName !== "LI" && !(e.parentNode.nodeName === "LI" && !e.previousSibling && !e.nextSibling) && !this.tree.isDescendantOf(listItems, e) }.bind(this)); var diffListItems = listItems.findAll(function(e) { return e.parentNode.nodeName !== containerTag; }.bind(this)); // Conditions needed to determine mode var hasNormalBlocks = normalBlocks.length > 0; var hasDifferentListStyle = diffListItems.length > 0; var blockToHandle = null; if(hasNormalBlocks) { blockToHandle = normalBlocks; } else if(hasDifferentListStyle) { blockToHandle = diffListItems; } else { blockToHandle = listItems; } // perform operation for(var i = 0; i < blockToHandle.length; i++) { var block = blockToHandle[i]; // preserve original index to restore selection var originalIndex = blocks.indexOf(block); blocks[originalIndex] = this.applyList(block, type, className); } return blocks; }, /** * Insert place-holder for given empty element. Empty element does not displayed and causes many editing problems. * * @param {Element} element empty element */ correctEmptyElement: function(element) {throw "Not implemented"}, /** * Corrects current block-only-container to do not take any non-block element or node. */ correctParagraph: function() {throw "Not implemented"}, /** * Makes place-holder for empty element. * * @returns {Node} Platform specific place holder */ makePlaceHolder: function() {throw "Not implemented"}, /** * Makes place-holder string. * * @returns {String} Platform specific place holder string */ makePlaceHolderString: function() {throw "Not implemented"}, /** * Makes empty paragraph which contains only one place-holder */ makeEmptyParagraph: function() {throw "Not implemented"}, /** * Applies background color to selected area * * @param {Object} color valid CSS color value */ applyBackgroundColor: function(color) {throw "Not implemented";}, /** * Applies foreground color to selected area * * @param {Object} color valid CSS color value */ applyForegroundColor: function(color) { this.execCommand("forecolor", color); }, /** * Applies font face to selected area * * @param {String} face font face */ applyFontFace: function(face) { this.execCommand("fontname", face); }, /** * Applies font size to selected area * * @param {Number} size font size (px) */ applyFontSize: function(size) { this.execCommand("fontsize", size); }, execCommand: function(commandId, param) {throw "Not implemented";}, applyRemoveFormat: function() {throw "Not implemented";}, applyEmphasis: function() {throw "Not implemented";}, applyStrongEmphasis: function() {throw "Not implemented";}, applyStrike: function() {throw "Not implemented";}, applyUnderline: function() {throw "Not implemented";}, applySuperscription: function() { this.execCommand("superscript"); }, applySubscription: function() { this.execCommand("subscript"); }, indentBlock: function(element, treatListAsNormalBlock) { return (!element.previousSibling && element.parentNode.nodeName === "LI") ? this.indentListItem(element, treatListAsNormalBlock) : this.indentElement(element); }, outdentBlock: function(element, treatListAsNormalBlock) { while(true) { if(!element.previousSibling && element.parentNode.nodeName === "LI") { element = this.outdentListItem(element, treatListAsNormalBlock); return element; } else { var performed = this.outdentElement(element); if(performed) return performed; // first-child can outdent container if(!element.previousSibling) { element = element.parentNode; } else { break; } } } return null; }, wrapBlock: function(tag, start, end) { if(this.tree._blockTags.indexOf(tag) === -1) throw "Unsuppored block container: [" + tag + "]"; if(!start) start = this.getCurrentBlockElement(); if(!end) end = start; // Check if the selection captures valid fragement var validFragment = false; if(start === end) { // are they same block? validFragment = true; } else if(start.parentNode === end.parentNode && !start.previousSibling && !end.nextSibling) { // are they covering whole parent? validFragment = true; start = end = start.parentNode; } else { // are they siblings of non-LI blocks? validFragment = (start.parentNode === end.parentNode) && (start.nodeName !== "LI"); } if(!validFragment) return null; var wrapper = this.createElement(tag); if(start === end) { // They are same. if(this.tree.isBlockContainer(start) && !this.tree.isListContainer(start)) { // It's a block container. Wrap its contents. if(this.tree.isBlockOnlyContainer(wrapper)) { this.correctEmptyElement(start); this.wrapAllInlineOrTextNodesAs("P", start, true); } this.moveChildNodes(start, wrapper); start.appendChild(wrapper); } else { // It's not a block container. Wrap itself. wrapper = this.insertNodeAt(wrapper, start, "after"); wrapper.appendChild(start); } this.correctEmptyElement(wrapper); } else { // They are siblings. Wrap'em all. wrapper = this.insertNodeAt(wrapper, start, "before"); var node = start; while(node !== end) { next = node.nextSibling; wrapper.appendChild(node); node = next; } wrapper.appendChild(node); } return wrapper; }, ///////////////////////////////////////////// // Focus/Caret/Selection /** * Gives focus to root element's window */ focus: function() {throw "Not implemented";}, /** * Returns selection object */ sel: function() {throw "Not implemented";}, /** * Returns range object */ rng: function() {throw "Not implemented";}, /** * Returns true if DOM has selection */ hasSelection: function() {throw "Not implemented";}, /** * Returns true if root element's window has selection */ hasFocus: function() { return this.focused; }, /** * Adjust scrollbar to make the element visible in current viewport. * * @param {Element} element Target element * @param {boolean} toTop Align element to top of the viewport * @param {boolean} moveCaret Move caret to the element */ scrollIntoView: function(element, toTop, moveCaret) { element.scrollIntoView(toTop); if(moveCaret) this.placeCaretAtStartOf(element); }, /** * Select all document */ selectAll: function() { return this.execCommand('selectall'); }, /** * Select specified element. * * @param {Element} element element to select * @param {boolean} entireElement true to select entire element, false to select inner content of element */ selectElement: function(node, entireElement) {throw "Not implemented"}, /** * Select all elements between two blocks(inclusive). * * @param {Element} start start of selection * @param {Element} end end of selection */ selectBlocksBetween: function(start, end) {throw "Not implemented"}, /** * Delete selected area */ deleteSelection: function() {throw "Not implemented"}, /** * Collapses current selection. * * @param {boolean} toStart true to move caret to start of selected area. */ collapseSelection: function(toStart) {throw "Not implemented"}, /** * Returns selected area as HTML string */ getSelectionAsHtml: function() {throw "Not implemented"}, /** * Returns selected area as text string */ getSelectionAsText: function() {throw "Not implemented"}, /** * Places caret at start of the element * * @param {Element} element Target element */ placeCaretAtStartOf: function(element) {throw "Not implemented"}, /** * Checks if the caret is place at start of the block */ isCaretAtBlockStart: function() { if(this.isCaretAtEmptyBlock()) return true; if(this.hasSelection()) return false; var node = this.getCurrentBlockElement(); var marker = this.pushMarker(); var isTrue = false; while (node = this.getFirstChild(node)) { if (node === marker) { isTrue = true; break; } } this.popMarker(); return isTrue; }, /** * Checks if the caret is place at end of the block */ isCaretAtBlockEnd: function() {throw "Not implemented"}, /** * Checks if the node is empty-text-node or not */ isEmptyTextNode: function(node) { return node.nodeType === 3 && (node.nodeValue.length === 0 || (node.nodeValue.length === 1 && (node.nodeValue.charAt(0) === 32 || node.nodeValue.charAt(0) === 160))); }, /** * Checks if the caret is place in empty block element */ isCaretAtEmptyBlock: function() { return this.isEmptyBlock(this.getCurrentBlockElement()); }, /** * Saves current selection info * * @returns {Object} Bookmark for selection */ saveSelection: function() {throw "Not implemented"}, /** * Restores current selection info * * @param {Object} bookmark Bookmark */ restoreSelection: function(bookmark) {throw "Not implemented"}, /** * Create marker */ createMarker: function() { var marker = this.createElement("SPAN"); marker.id = "xquared_marker_" + (this._lastMarkerId++); marker.className = "xquared_marker"; return marker; }, /** * Create and insert marker into current caret position. * Marker is an inline element which has no child nodes. It can be used with many purposes. * For example, You can push marker to mark current caret position. * * @returns {Element} marker */ pushMarker: function() { var marker = this.createMarker(); return this.insertNode(marker); }, /** * Removes last marker * * @params {boolean} moveCaret move caret into marker before delete. */ popMarker: function(moveCaret) { var id = "xquared_marker_" + (--this._lastMarkerId); var marker = this.$(id); if(!marker) return; if(moveCaret) { this.selectElement(marker, true); this.collapseSelection(false); } this.deleteNode(marker); }, ///////////////////////////////////////////// // Query methods isMarker: function(node) { return (node.nodeType === 1 && node.nodeName === "SPAN" && node.className === "xquared_marker"); }, isFirstBlockOfBody: function(block) { var root = this.getRoot(); if(this.isFirstLiWithNestedList(block)) block = block.parentNode; var found = this.tree.findBackward( block, function(node) { return node === root || (this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)); }.bind(this) ); return found === root; }, /** * Returns outer HTML of given element */ getOuterHTML: function(element) {throw "Not implemented"}, /** * Returns inner text of given element * * @param {Element} element Target element * @returns {String} Text string */ getInnerText: function(element) { return element.innerHTML.stripTags(); }, /** * Checks if given node is place holder or not. * * @param {Node} node DOM node */ isPlaceHolder: function(node) {throw "Not implemented"}, /** * Checks if given block is the first LI whose next sibling is a nested list. * * @param {Element} block Target block */ isFirstLiWithNestedList: function(block) { return !block.previousSibling && block.parentNode.nodeName === "LI" && this.tree.isListContainer(block.nextSibling); }, /** * Search all links within given element * * @param {Element} [element] Container element. If not given, the root element will be used. * @param {Array} [found] if passed, links will be appended into this array. * @returns {Array} Array of anchors. It returns empty array if there's no links. */ searchAnchors: function(element, found) { if(!element) element = this.getRoot(); if(!found) found = []; var anchors = element.getElementsByTagName("A"); for(var i = 0; i < anchors.length; i++) { found.push(anchors[i]); } return found; }, /** * Search all headings within given element * * @param {Element} [element] Container element. If not given, the root element will be used. * @param {Array} [found] if passed, headings will be appended into this array. * @returns {Array} Array of headings. It returns empty array if there's no headings. */ searchHeadings: function(element, found) { if(!element) element = this.getRoot(); if(!found) found = []; var regexp = /^h[1-6]/ig; var nodes = element.childNodes; if (!nodes) return []; for(var i = 0; i < nodes.length; i++) { var isContainer = nodes[i] && this.tree._blockContainerTags.indexOf(nodes[i].nodeName) !== -1; var isHeading = nodes[i] && nodes[i].nodeName.match(regexp); if (isContainer) { this.searchHeadings(nodes[i], found); } else if (isHeading) { found.push(nodes[i]); } } return found; }, /** * Collect structure and style informations of given element. * * @param {Element} element target element * @returns {Object} object that contains information: {em: true, strong: false, block: "p", list: "ol", ...} */ collectStructureAndStyle: function(element) { if(!element || element.nodeName === "#document") return {}; var block = this.getParentBlockElementOf(element); if(block === null || (xq.Browser.isTrident && ["ready", "complete"].indexOf(block.readyState) === -1)) return {}; var parents = this.tree.collectParentsOf(element, true, function(node) {return block.parentNode === node}); var blockName = block.nodeName; var info = {}; var doc = this.getDoc(); var em = doc.queryCommandState("Italic"); var strong = doc.queryCommandState("Bold"); var strike = doc.queryCommandState("Strikethrough"); var underline = doc.queryCommandState("Underline") && !this.getParentElementOf(element, ["A"]); var superscription = doc.queryCommandState("superscript"); var subscription = doc.queryCommandState("subscript"); var foregroundColor = doc.queryCommandValue("forecolor"); var fontName = doc.queryCommandValue("fontname"); var fontSize = doc.queryCommandValue("fontsize"); // @WORKAROUND: Trident's fontSize value is affected by CSS if(xq.Browser.isTrident && fontSize === "5" && this.getParentElementOf(element, ["H1", "H2", "H3", "H4", "H5", "H6"])) fontSize = ""; // @TODO: remove conditional var backgroundColor; if(xq.Browser.isGecko) { this.execCommand("styleWithCSS", "true"); try { backgroundColor = doc.queryCommandValue("hilitecolor"); } catch(e) { // if there's selection and the first element of the selection is // an empty block... backgroundColor = ""; } this.execCommand("styleWithCSS", "false"); } else { backgroundColor = doc.queryCommandValue("backcolor"); } // if block is only child, select its parent while(block.parentNode && block.parentNode !== this.getRoot() && !block.previousSibling && !block.nextSibling && !this.tree.isListContainer(block.parentNode)) { block = block.parentNode; } var list = false; if(block.nodeName === "LI") { var parent = block.parentNode; var isCode = parent.nodeName === "OL" && parent.className === "code"; var hasClass = parent.className.length > 0; if(isCode) { list = "CODE"; } else if(hasClass) { list = false; } else { list = parent.nodeName; } } var justification = block.style.textAlign || "left"; return { block:blockName, em: em, strong: strong, strike: strike, underline: underline, superscription: superscription, subscription: subscription, list: list, justification: justification, foregroundColor: foregroundColor, backgroundColor: backgroundColor, fontSize: fontSize, fontName: fontName }; }, /** * Checks if the element has one or more important attributes: id, class, style * * @param {Element} element Target element */ hasImportantAttributes: function(element) {throw "Not implemented"}, /** * Checks if the element is empty or not. Place-holder is not counted as a child. * * @param {Element} element Target element */ isEmptyBlock: function(element) {throw "Not implemented"}, /** * Returns element that contains caret. */ getCurrentElement: function() {throw "Not implemented"}, /** * Returns block element that contains caret. Trident overrides this method. */ getCurrentBlockElement: function() { var cur = this.getCurrentElement(); if(!cur) return null; var block = this.getParentBlockElementOf(cur); if(!block) return null; return (block.nodeName === "BODY") ? null : block; }, /** * Returns parent block element of parameter. * If the parameter itself is a block, it will be returned. * * @param {Element} element Target element * * @returns {Element} Element or null */ getParentBlockElementOf: function(element) { while(element) { if(this.tree._blockTags.indexOf(element.nodeName) !== -1) return element; element = element.parentNode; } return null; }, /** * Returns parent element of parameter which has one of given tag name. * If the parameter itself has the same tag name, it will be returned. * * @param {Element} element Target element * @param {Array} tagNames Array of string which contains tag names * * @returns {Element} Element or null */ getParentElementOf: function(element, tagNames) { while(element) { if(tagNames.indexOf(element.nodeName) !== -1) return element; element = element.parentNode; } return null; }, /** * Collects all block elements between two elements * * @param {Element} from Start element(inclusive) * @param {Element} to End element(inclusive) */ getBlockElementsBetween: function(from, to) { return this.tree.collectNodesBetween(from, to, function(node) { return node.nodeType === 1 && this.tree.isBlock(node); }.bind(this)); }, /** * Returns block element that contains selection start. * * This method will return exactly same result with getCurrentBlockElement method * when there's no selection. */ getBlockElementAtSelectionStart: function() {throw "Not implemented"}, /** * Returns block element that contains selection end. * * This method will return exactly same result with getCurrentBlockElement method * when there's no selection. */ getBlockElementAtSelectionEnd: function() {throw "Not implemented"}, /** * Returns blocks at each edge of selection(start and end). * * TODO: implement ignoreEmptyEdges for FF * * @param {boolean} naturalOrder Mak the start element always comes before the end element * @param {boolean} ignoreEmptyEdges Prevent some browser(Gecko) from selecting one more block than expected */ getBlockElementsAtSelectionEdge: function(naturalOrder, ignoreEmptyEdges) {throw "Not implemented"}, /** * Returns array of selected block elements */ getSelectedBlockElements: function() { var selectionEdges = this.getBlockElementsAtSelectionEdge(true, true); var start = selectionEdges[0]; var end = selectionEdges[1]; return this.tree.collectNodesBetween(start, end, function(node) { return node.nodeType === 1 && this.tree.isBlock(node); }.bind(this)); }, /** * Get element by ID * * @param {String} id Element's ID * @returns {Element} element or null */ getElementById: function(id) {return this.getDoc().getElementById(id)}, /** * Shortcut for #getElementById */ $: function(id) {return this.getElementById(id)}, /** * Returns first "valid" child of given element. It ignores empty textnodes. * * @param {Element} element Target element * @returns {Node} first child node or null */ getFirstChild: function(element) { if(!element) return null; var nodes = xq.$A(element.childNodes); return nodes.find(function(node) {return !this.isEmptyTextNode(node)}.bind(this)); }, /** * Returns last "valid" child of given element. It ignores empty textnodes and place-holders. * * @param {Element} element Target element * @returns {Node} last child node or null */ getLastChild: function(element) {throw "Not implemented"}, getNextSibling: function(node) { while(node = node.nextSibling) { if(node.nodeType !== 3 || !node.nodeValue.isBlank()) break; } return node; }, getBottommostFirstChild: function(node) { while(node.firstChild && node.nodeType === 1) node = node.firstChild; return node; }, getBottommostLastChild: function(node) { while(node.lastChild && node.nodeType === 1) node = node.lastChild; return node; }, /** @private */ _getCssValue: function(str, defaultUnit) { if(!str || str.length === 0) return {value:0, unit:defaultUnit}; var tokens = str.match(/(\d+)(.*)/); return { value:parseInt(tokens[1]), unit:tokens[2] || defaultUnit }; } }); /** * @requires Xquared.js * @requires rdom/Base.js */ xq.rdom.Trident = xq.Class(xq.rdom.Base, /** * @name xq.rdom.Trident * @lends xq.rdom.Trident.prototype * @extends xq.rdom.Base * @constructor */ { makePlaceHolder: function() { return this.createTextNode(" "); }, makePlaceHolderString: function() { return ' '; }, makeEmptyParagraph: function() { return this.createElementFromHtml("

 

"); }, isPlaceHolder: function(node) { return false; }, getOuterHTML: function(element) { return element.outerHTML; }, getCurrentBlockElement: function() { var cur = this.getCurrentElement(); if(!cur) return null; var block = this.getParentBlockElementOf(cur); if(!block) return null; if(block.nodeName === "BODY") { // Atomic block such as HR var newParagraph = this.insertNode(this.makeEmptyParagraph()); var next = newParagraph.nextSibling; if(this.tree.isAtomic(next)) { this.deleteNode(newParagraph); return next; } } else { return block; } }, insertNode: function(node) { if(this.hasSelection()) this.collapseSelection(true); this.rng().pasteHTML(''); var marker = this.$('xquared_temp'); if(node.id === 'xquared_temp') return marker; if(marker) marker.replaceNode(node); return node; }, removeTrailingWhitespace: function(block) { if(!block) return; // @TODO: reimplement to handle atomic tags and so on. (use DomTree) if(this.tree.isBlockOnlyContainer(block)) return; if(this.isEmptyBlock(block)) return; var text = block.innerText; var html = block.innerHTML; var lastCharCode = text.charCodeAt(text.length - 1); if(text.length <= 1 || [32,160].indexOf(lastCharCode) === -1) return; // shortcut for most common case if(text == html.replace(/ /g, " ")) { block.innerHTML = html.replace(/ $/, ""); return; } var node = block; while(node && node.nodeType !== 3) node = node.lastChild; if(!node) return; // DO NOT REMOVE OR MODIFY FOLLOWING CODE. Modifying following code will crash IE7 var nodeValue = node.nodeValue; if(nodeValue.length <= 1) { this.deleteNode(node, true); } else { node.nodeValue = nodeValue.substring(0, nodeValue.length - 1); } }, correctEmptyElement: function(element) { if(!element || element.nodeType !== 1 || this.tree.isAtomic(element)) return; if(element.firstChild) { this.correctEmptyElement(element.firstChild); } else { element.innerHTML = " "; } }, copyAttributes: function(from, to, copyId) { to.mergeAttributes(from, !copyId); }, correctParagraph: function() { if(!this.hasFocus()) return false; if(this.hasSelection()) return false; var block = this.getCurrentElement(); // if caret is at // * atomic block level elements(HR) or // * ... // then following is true if(this.tree.isBlockOnlyContainer(block)) { // check for atomic block element such as HR block = this.insertNode(this.makeEmptyParagraph()); if(this.tree.isAtomic(block.nextSibling)) { // @WORKAROUND: // At this point, HR has a caret but getCurrentElement() doesn't return the HR and // I couldn't find a way to get this HR. So I have to keep this reference. // I will be used in Editor._handleEnter. this.recentHR = block.nextSibling; this.deleteNode(block); return false; } else { // I can't remember exactly when following is executed and what it does :-( // * Case 1: Performing Ctrl+A and Ctrl+X repeatedly // * ... var nextBlock = this.tree.findForward( block, function(node) {return this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)}.bind(this) ); if(nextBlock) { this.deleteNode(block); this.placeCaretAtStartOf(nextBlock); } else { this.placeCaretAtStartOf(block); } return true; } } else { block = this.getCurrentBlockElement(); if(block.nodeType === 3) block = block.parentNode; if(this.tree.hasMixedContents(block)) { var marker = this.pushMarker(); this.wrapAllInlineOrTextNodesAs("P", block, true); this.popMarker(true); return true; } else if((this.tree.isTextOrInlineNode(block.previousSibling) || this.tree.isTextOrInlineNode(block.nextSibling)) && this.tree.hasMixedContents(block.parentNode)) { // @WORKAROUND: // IE?서??Blockê³?Inline/Textê°??¸ì ‘??경우 getCurrentElement ?±ì´ ?¤ìž‘?™í•œ?? // ?°ë¼???„재 Block 주ë?까ì? ?œë²ˆ???¡ì•„주어???œë‹¤. this.wrapAllInlineOrTextNodesAs("P", block.parentNode, true); return true; } else { return false; } } }, ////// // Commands execCommand: function(commandId, param) { return this.getDoc().execCommand(commandId, false, param); }, applyBackgroundColor: function(color) { this.execCommand("BackColor", color); }, applyEmphasis: function() { // Generate tag. It will be replaced with tag during cleanup phase. this.execCommand("Italic"); }, applyStrongEmphasis: function() { // Generate tag. It will be replaced with tag during cleanup phase. this.execCommand("Bold"); }, applyStrike: function() { // Generate tag. It will be replaced with ' ); // create designmode iframe for WYSIWYG editor this.editorFrame = this._createIFrame(outerDoc); outerDoc.body.appendChild(this.editorFrame); var editorDoc = this._createDoc( this.editorFrame, '' + (!xq.Browser.isTrident ? '' : '') + // @WORKAROUND: it is needed to force href of pasted content to be an absolute url (this.config.changeCursorOnLink ? '' : ''), this.config.contentCssList, this.config.bodyId, this.config.bodyClass, '' ); this.rdom.setWin(this.editorFrame.contentWindow); this.editHistory = new xq.EditHistory(this.rdom); // turn on designmode this.rdom.getDoc().designMode = "On"; // turn off Firefox's table editing feature if(xq.Browser.isGecko) { try {this.rdom.getDoc().execCommand("enableInlineTableEditing", false, "false")} catch(ignored) {} } // register event handlers this._registerEventHandlers(); // hook onsubmit of form if(this.config.automaticallyHookSubmitEvent && this.contentElement.form) { var original = this.contentElement.form.onsubmit; this.contentElement.form.onsubmit = function() { this.contentElement.value = this.getCurrentContent(); return original ? original.bind(this.contentElement.form)() : true; }.bind(this); } }, ///////////////////////////////////////////// // Event Management _registerEventHandlers: function() { var events = [this.platformDepedentKeyEventType, 'click', 'keyup', 'mouseup', 'contextmenu']; if(xq.Browser.isTrident && this.config.changeCursorOnLink) events.push('mousemove'); var handler = this._handleEvent.bindAsEventListener(this); for(var i = 0; i < events.length; i++) { xq.observe(this.getDoc(), events[i], handler); } if(xq.Browser.isGecko) { xq.observe(this.getDoc(), "focus", handler); xq.observe(this.getDoc(), "blur", handler); xq.observe(this.getDoc(), "scroll", handler); xq.observe(this.getDoc(), "dragdrop", handler); } else { xq.observe(this.getWin(), "focus", handler); xq.observe(this.getWin(), "blur", handler); xq.observe(this.getWin(), "scroll", handler); } }, _handleEvent: function(e) { this._fireOnBeforeEvent(this, e); if(e.stopProcess) { xq.stopEvent(e); return false; } // Trident only if(e.type === 'mousemove') { if(!this.config.changeCursorOnLink) return true; var link = !!this.rdom.getParentElementOf(e.srcElement, ["A"]); var editable = this.getBody().contentEditable; editable = editable === 'inherit' ? false : editable; if(editable !== link && !this.rdom.hasSelection()) this.getBody().contentEditable = !link; return true; } var stop = false; var modifiedByCorrection = false; if(e.type === this.platformDepedentKeyEventType) { var undoPerformed = false; modifiedByCorrection = this.rdom.correctParagraph(); for(var key in this.config.shortcuts) { if(!this.config.shortcuts[key].event.matches(e)) continue; var handler = this.config.shortcuts[key].handler; var xed = this; stop = (typeof handler === "function") ? handler(this) : eval(handler); if(key === "undo") undoPerformed = true; } } else if(e.type === 'click' && e.button === 0 && this.config.enableLinkClick) { var a = this.rdom.getParentElementOf(e.target || e.srcElement, ["A"]); if(a) stop = this.handleClick(e, a); } else if(["keyup", "mouseup"].indexOf(e.type) !== -1) { modifiedByCorrection = this.rdom.correctParagraph(); } else if(["contextmenu"].indexOf(e.type) !== -1) { this._handleContextMenu(e); } else if("focus" == e.type) { this.rdom.focused = true; } else if("blur" == e.type) { this.rdom.focused = false; } if(stop) xq.stopEvent(e); this._fireOnCurrentContentChanged(this); this._fireOnAfterEvent(this, e); if(!undoPerformed && !modifiedByCorrection) this.editHistory.onEvent(e); return !stop; }, /** * TODO: remove dup with handleAutocompletion */ handleAutocorrection: function() { var block = this.rdom.getCurrentBlockElement(); // TODO: use complete unescape algorithm var text = this.rdom.getInnerText(block).replace(/ /gi, " "); var acs = this.config.autocorrections; var performed = false; var stop = false; for(var key in acs) { var ac = acs[key]; if(ac.criteria(text)) { try { this.editHistory.onCommand(); this.editHistory.disable(); if(typeof ac.handler === "String") { var xed = this; var rdom = this.rdom; eval(ac.handler); } else { stop = ac.handler(this, this.rdom, block, text); } this.editHistory.enable(); } catch(ignored) {} block = this.rdom.getCurrentBlockElement(); text = this.rdom.getInnerText(block); performed = true; if(stop) break; } } return stop; }, /** * TODO: remove dup with handleAutocorrection */ handleAutocompletion: function() { var acs = this.config.autocompletions; if(xq.isEmptyHash(acs)) return; if(this.rdom.hasSelection()) { var text = this.rdom.getSelectionAsText(); this.rdom.deleteSelection(); var wrapper = this.rdom.insertNode(this.rdom.createElement("SPAN")); wrapper.innerHTML = text; var marker = this.rdom.pushMarker(); var filtered = []; for(var key in acs) { filtered.push([key, acs[key].criteria(text)]); } filtered = filtered.findAll(function(elem) { return elem[1] !== -1; }); if(filtered.length === 0) { this.rdom.popMarker(true); return; } var minIndex = 0; var min = filtered[0][1]; for(var i = 0; i < filtered.length; i++) { if(filtered[i][1] < min) { minIndex = i; min = filtered[i][1]; } } var ac = acs[filtered[minIndex][0]]; this.editHistory.disable(); this.rdom.selectElement(wrapper); } else { var marker = this.rdom.pushMarker(); var filtered = []; for(var key in acs) { filtered.push([key, this.rdom.testSmartWrap(marker, acs[key].criteria).textIndex]); } filtered = filtered.findAll(function(elem) { return elem[1] !== -1; }); if(filtered.length === 0) { this.rdom.popMarker(true); return; } var minIndex = 0; var min = filtered[0][1]; for(var i = 0; i < filtered.length; i++) { if(filtered[i][1] < min) { minIndex = i; min = filtered[i][1]; } } var ac = acs[filtered[minIndex][0]]; this.editHistory.disable(); var wrapper = this.rdom.smartWrap(marker, "SPAN", ac.criteria); } var block = this.rdom.getCurrentBlockElement(); // TODO: use complete unescape algorithm var text = this.rdom.getInnerText(wrapper).replace(/ /gi, " "); try { // call handler if(typeof ac.handler === "String") { var xed = this; var rdom = this.rdom; eval(ac.handler); } else { ac.handler(this, this.rdom, block, wrapper, text); } } catch(ignored) {} try { this.rdom.unwrapElement(wrapper); } catch(ignored) {} if(this.rdom.isEmptyBlock(block)) this.rdom.correctEmptyElement(block); this.editHistory.enable(); this.editHistory.onCommand(); this.rdom.popMarker(true); }, /** * Handles click event * * @param {Event} e click event * @param {Element} target target element(usually has A tag) */ handleClick: function(e, target) { var href = decodeURI(target.href); if(!xq.Browser.isTrident) { if(!e.ctrlKey && !e.shiftKey && e.button !== 1) { window.location.href = href; return true; } } else { if(e.shiftKey) { window.open(href, "_blank"); } else { window.location.href = href; } return true; } return false; }, /** * Show link dialog * * TODO: should support modify/unlink * TODO: Add selenium test */ handleLink: function() { var text = this.rdom.getSelectionAsText() || ''; var dialog = new xq.ui.FormDialog( this, xq.ui_templates.basicLinkDialog, function(dialog) { if(text) { dialog.form.text.value = text; dialog.form.url.focus(); dialog.form.url.select(); } }, function(data) { this.focus(); if(xq.Browser.isTrident) { var rng = this.rdom.rng(); rng.moveToBookmark(bm); rng.select(); } if(!data) return; this.handleInsertLink(false, data.url, data.text, data.text); }.bind(this) ); if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); dialog.show({position: 'centerOfEditor'}); return true; }, /** * Inserts link or apply link into selected area * @TODO Add selenium test * * @param {boolean} autoSelection if set true and there's no selection, automatically select word to link(if possible) * @param {String} url url * @param {String} title title of link * @param {String} text text of link. If there's a selection(manually or automatically), it will be replaced with this text * * @returns {Element} created element */ handleInsertLink: function(autoSelection, url, title, text) { if(autoSelection && !this.rdom.hasSelection()) { var marker = this.rdom.pushMarker(); var a = this.rdom.smartWrap(marker, "A", function(text) { var index = text.lastIndexOf(" "); return index === -1 ? index : index + 1; }); a.href = url; a.title = title; if(text) { a.innerHTML = "" a.appendChild(this.rdom.createTextNode(text)); } else if(!a.hasChildNodes()) { this.rdom.deleteNode(a); } this.rdom.popMarker(true); } else { text = text || (this.rdom.hasSelection() ? this.rdom.getSelectionAsText() : null); if(!text) return; this.rdom.deleteSelection(); var a = this.rdom.createElement('A'); a.href = url; a.title = title; a.appendChild(this.rdom.createTextNode(text)); this.rdom.insertNode(a); } var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * @TODO Add selenium test */ handleSpace: function() { // If it has selection, perform default action. if(this.rdom.hasSelection()) return false; // Trident performs URL replacing automatically if(!xq.Browser.isTrident) { this.replaceUrlToLink(); } return false; }, /** * Called when enter key pressed. * @TODO Add selenium test * * @param {boolean} skipAutocorrection if set true, skips autocorrection * @param {boolean} forceInsertParagraph if set true, inserts paragraph */ handleEnter: function(skipAutocorrection, forceInsertParagraph) { // If it has selection, perform default action. if(this.rdom.hasSelection()) return false; // @WORKAROUND: // If caret is in HR, default action should be performed and // this._handleEvent() will correct broken HTML if(xq.Browser.isTrident && this.rdom.tree.isBlockOnlyContainer(this.rdom.getCurrentElement()) && this.rdom.recentHR) { this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.recentHR, "before"); this.rdom.recentHR = null; return true; } // Perform autocorrection if(!skipAutocorrection && this.handleAutocorrection()) return true; var block = this.rdom.getCurrentBlockElement(); var info = this.rdom.collectStructureAndStyle(block); // Perform URL replacing. Trident performs URL replacing automatically if(!xq.Browser.isTrident) { this.replaceUrlToLink(); } var atEmptyBlock = this.rdom.isCaretAtEmptyBlock(); var atStart = atEmptyBlock || this.rdom.isCaretAtBlockStart(); var atEnd = atEmptyBlock || (!atStart && this.rdom.isCaretAtBlockEnd()); var atEdge = atEmptyBlock || atStart || atEnd; if(!atEdge) { var marker = this.rdom.pushMarker(); if(this.rdom.isFirstLiWithNestedList(block) && !forceInsertParagraph) { var parent = block.parentNode; this.rdom.unwrapElement(block); block = parent; } else if(block.nodeName !== "LI" && this.rdom.tree.isBlockContainer(block)) { block = this.rdom.wrapAllInlineOrTextNodesAs("P", block, true).first(); } this.rdom.splitElementUpto(marker, block); this.rdom.popMarker(true); } else if(atEmptyBlock) { this._handleEnterAtEmptyBlock(); if(!xq.Browser.isWebkit) { if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize); if(info.fontName) this.handleFontFace(info.fontName); } } else { this._handleEnterAtEdge(atStart, forceInsertParagraph); if(!xq.Browser.isWebkit) { if(info.fontSize && info.fontSize !== "2") this.handleFontSize(info.fontSize); if(info.fontName) this.handleFontFace(info.fontName); } } return true; }, /** * Moves current block upward or downward * * @param {boolean} up moves current block upward */ handleMoveBlock: function(up) { var block = this.rdom.moveBlock(this.rdom.getCurrentBlockElement(), up); if(block) { this.rdom.selectElement(block, false); if(this.rdom.isEmptyBlock(block)) this.rdom.collapseSelection(true); block.scrollIntoView(false); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } return true; }, /** * Called when tab key pressed * @TODO: Add selenium test */ handleTab: function() { var hasSelection = this.rdom.hasSelection(); var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]); if(hasSelection) { this.handleIndent(); } else if (table && table.className === "datatable") { this.handleMoveToNextCell(); } else if (this.rdom.isCaretAtBlockStart()) { this.handleIndent(); } else { this.handleInsertTab(); } return true; }, /** * Called when shift+tab key pressed * @TODO: Add selenium test */ handleShiftTab: function() { var hasSelection = this.rdom.hasSelection(); var table = this.rdom.getParentElementOf(this.rdom.getCurrentBlockElement(), ["TABLE"]); if(hasSelection) { this.handleOutdent(); } else if (table && table.className === "datatable") { this.handleMoveToPreviousCell(); } else { this.handleOutdent(); } return true; }, /** * Inserts three non-breaking spaces * @TODO: Add selenium test */ handleInsertTab: function() { this.rdom.insertHtml(' '); this.rdom.insertHtml(' '); this.rdom.insertHtml(' '); return true; }, /** * Called when delete key pressed * @TODO: Add selenium test */ handleDelete: function() { if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockEnd()) return false; return this._handleMerge(true); }, /** * Called when backspace key pressed * @TODO: Add selenium test */ handleBackspace: function() { if(this.rdom.hasSelection() || !this.rdom.isCaretAtBlockStart()) return false; return this._handleMerge(false); }, _handleMerge: function(withNext) { var block = this.rdom.getCurrentBlockElement(); if(this.rdom.isEmptyBlock(block) && !this.rdom.tree.isBlockContainer(block.nextSibling) && withNext) { var blockToMove = this.rdom.removeBlock(block); this.rdom.placeCaretAtStartOf(blockToMove); blockToMove.scrollIntoView(false); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; } else { // save caret position; var marker = this.rdom.pushMarker(); // perform merge var merged = this.rdom.mergeElement(block, withNext, withNext); if(!merged && !withNext) this.rdom.extractOutElementFromParent(block); // restore caret position this.rdom.popMarker(true); if(merged) this.rdom.correctEmptyElement(merged); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return !!merged; } }, /** * (in table) Moves caret to the next cell * @TODO: Add selenium test */ handleMoveToNextCell: function() { this._handleMoveToCell("next"); }, /** * (in table) Moves caret to the previous cell * @TODO: Add selenium test */ handleMoveToPreviousCell: function() { this._handleMoveToCell("prev"); }, /** * (in table) Moves caret to the above cell * @TODO: Add selenium test */ handleMoveToAboveCell: function() { this._handleMoveToCell("above"); }, /** * (in table) Moves caret to the below cell * @TODO: Add selenium test */ handleMoveToBelowCell: function() { this._handleMoveToCell("below"); }, _handleMoveToCell: function(dir) { var block = this.rdom.getCurrentBlockElement(); var cell = this.rdom.getParentElementOf(block, ["TD", "TH"]); var table = this.rdom.getParentElementOf(cell, ["TABLE"]); var rtable = new xq.RichTable(this.rdom, table); var target = null; if(["next", "prev"].indexOf(dir) !== -1) { var toNext = dir === "next"; target = toNext ? rtable.getNextCellOf(cell) : rtable.getPreviousCellOf(cell); } else { var toBelow = dir === "below"; target = toBelow ? rtable.getBelowCellOf(cell) : rtable.getAboveCellOf(cell); } if(!target) { var finder = function(node) {return ['TD', 'TH'].indexOf(node.nodeName) === -1 && this.tree.isBlock(node) && !this.tree.hasBlocks(node);}.bind(this.rdom); var exitCondition = function(node) {return this.tree.isBlock(node) && !this.tree.isDescendantOf(this.getRoot(), node)}.bind(this.rdom); target = (toNext || toBelow) ? this.rdom.tree.findForward(cell, finder, exitCondition) : this.rdom.tree.findBackward(table, finder, exitCondition); } if(target) this.rdom.placeCaretAtStartOf(target); }, /** * Applies STRONG tag * @TODO: Add selenium test */ handleStrongEmphasis: function() { this.rdom.applyStrongEmphasis(); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Applies EM tag * @TODO: Add selenium test */ handleEmphasis: function() { this.rdom.applyEmphasis(); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Applies EM.underline tag * @TODO: Add selenium test */ handleUnderline: function() { this.rdom.applyUnderline(); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Applies SPAN.strike tag * @TODO: Add selenium test */ handleStrike: function() { this.rdom.applyStrike(); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Removes all style * @TODO: Add selenium test */ handleRemoveFormat: function() { this.rdom.applyRemoveFormat(); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Remove link * @TODO: Add selenium test */ handleRemoveLink: function() { this.rdom.applyRemoveLink(); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Inserts table * @TODO: Add selenium test * * @param {Number} cols number of columns * @param {Number} rows number of rows * @param {String} headerPosition position of THs. "T" or "L" or "TL". "T" means top, "L" means left. */ handleTable: function(cols, rows, headerPositions) { var cur = this.rdom.getCurrentBlockElement(); if(this.rdom.getParentElementOf(cur, ["TABLE"])) return true; var rtable = xq.RichTable.create(this.rdom, cols, rows, headerPositions); if(this.rdom.tree.isBlockContainer(cur)) { var wrappers = this.rdom.wrapAllInlineOrTextNodesAs("P", cur, true); cur = wrappers.last(); } var tableDom = this.rdom.insertNodeAt(rtable.getDom(), cur, "after"); this.rdom.placeCaretAtStartOf(rtable.getCellAt(0, 0)); if(this.rdom.isEmptyBlock(cur)) this.rdom.deleteNode(cur, true); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, handleInsertNewRowAt: function(where) { var cur = this.rdom.getCurrentBlockElement(); var tr = this.rdom.getParentElementOf(cur, ["TR"]); if(!tr) return true; var table = this.rdom.getParentElementOf(tr, ["TABLE"]); var rtable = new xq.RichTable(this.rdom, table); var row = rtable.insertNewRowAt(tr, where); this.rdom.placeCaretAtStartOf(row.cells[0]); return true; }, /** * @TODO: Add selenium test */ handleInsertNewColumnAt: function(where) { var cur = this.rdom.getCurrentBlockElement(); var td = this.rdom.getParentElementOf(cur, ["TD"], true); if(!td) return true; var table = this.rdom.getParentElementOf(td, ["TABLE"]); var rtable = new xq.RichTable(this.rdom, table); rtable.insertNewCellAt(td, where); this.rdom.placeCaretAtStartOf(cur); return true; }, /** * @TODO: Add selenium test */ handleDeleteRow: function() { var cur = this.rdom.getCurrentBlockElement(); var tr = this.rdom.getParentElementOf(cur, ["TR"]); if(!tr) return true; var table = this.rdom.getParentElementOf(tr, ["TABLE"]); var rtable = new xq.RichTable(this.rdom, table); var blockToMove = rtable.deleteRow(tr); this.rdom.placeCaretAtStartOf(blockToMove); return true; }, /** * @TODO: Add selenium test */ handleDeleteColumn: function() { var cur = this.rdom.getCurrentBlockElement(); var td = this.rdom.getParentElementOf(cur, ["TD"], true); if(!td) return true; var table = this.rdom.getParentElementOf(td, ["TABLE"]); var rtable = new xq.RichTable(this.rdom, table); rtable.deleteCell(td); //this.rdom.placeCaretAtStartOf(table); return true; }, /** * Performs block indentation * @TODO: Add selenium test */ handleIndent: function() { if(this.rdom.hasSelection()) { var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); if(blocks.first() !== blocks.last()) { var affected = this.rdom.indentElements(blocks.first(), blocks.last()); this.rdom.selectBlocksBetween(affected.first(), affected.last()); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; } } var block = this.rdom.getCurrentBlockElement(); var affected = this.rdom.indentElement(block); if(affected) { this.rdom.placeCaretAtStartOf(affected); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } return true; }, /** * Performs block outdentation * @TODO: Add selenium test */ handleOutdent: function() { if(this.rdom.hasSelection()) { var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); if(blocks.first() !== blocks.last()) { var affected = this.rdom.outdentElements(blocks.first(), blocks.last()); this.rdom.selectBlocksBetween(affected.first(), affected.last()); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; } } var block = this.rdom.getCurrentBlockElement(); var affected = this.rdom.outdentElement(block); if(affected) { this.rdom.placeCaretAtStartOf(affected); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } return true; }, /** * Applies list. * @TODO: Add selenium test * * @param {String} type "UL" or "OL" * @param {String} CSS class name */ handleList: function(type, className) { if(this.rdom.hasSelection()) { var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); if(blocks.first() !== blocks.last()) { blocks = this.rdom.applyLists(blocks.first(), blocks.last(), type, className); } else { blocks[0] = blocks[1] = this.rdom.applyList(blocks.first(), type, className); } this.rdom.selectBlocksBetween(blocks.first(), blocks.last()); } else { var block = this.rdom.applyList(this.rdom.getCurrentBlockElement(), type, className); this.rdom.placeCaretAtStartOf(block); } var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Applies justification * @TODO: Add selenium test * * @param {String} dir "left", "center", "right" or "both" */ handleJustify: function(dir) { if(this.rdom.hasSelection()) { var blocks = this.rdom.getSelectedBlockElements(); var dir = (dir === "left" || dir === "both") && (blocks[0].style.textAlign === "left" || blocks[0].style.textAlign === "") ? "both" : dir; this.rdom.justifyBlocks(blocks, dir); this.rdom.selectBlocksBetween(blocks.first(), blocks.last()); } else { var block = this.rdom.getCurrentBlockElement(); var dir = (dir === "left" || dir === "both") && (block.style.textAlign === "left" || block.style.textAlign === "") ? "both" : dir; this.rdom.justifyBlock(block, dir); } var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Removes current block element * @TODO: Add selenium test */ handleRemoveBlock: function() { var block = this.rdom.getCurrentBlockElement(); var blockToMove = this.rdom.removeBlock(block); this.rdom.placeCaretAtStartOf(blockToMove); blockToMove.scrollIntoView(false); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Applies background color * @TODO: Add selenium test * * @param {String} color CSS color string */ handleBackgroundColor: function(color) { if(color) { this.rdom.applyBackgroundColor(color); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } else { var dialog = new xq.ui.FormDialog( this, xq.ui_templates.basicColorPickerDialog, function(dialog) {}, function(data) { this.focus(); if(xq.Browser.isTrident) { var rng = this.rdom.rng(); rng.moveToBookmark(bm); rng.select(); } if(!data) return; this.handleBackgroundColor(data.color); }.bind(this) ); if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); dialog.show({position: 'centerOfEditor'}); } return true; }, /** * Applies foreground color * @TODO: Add selenium test * * @param {String} color CSS color string */ handleForegroundColor: function(color) { if(color) { this.rdom.applyForegroundColor(color); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } else { var dialog = new xq.ui.FormDialog( this, xq.ui_templates.basicColorPickerDialog, function(dialog) {}, function(data) { this.focus(); if(xq.Browser.isTrident) { var rng = this.rdom.rng(); rng.moveToBookmark(bm); rng.select(); } if(!data) return; this.handleForegroundColor(data.color); }.bind(this) ); if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); dialog.show({position: 'centerOfEditor'}); } return true; }, /** * Applies font face * @TODO: Add selenium test * * @param {String} face font face */ handleFontFace: function(face) { if(face) { this.rdom.applyFontFace(face); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } else { //TODO: popup font dialog } return true; }, /** * Applies font size * * @param {Number} font size (1 to 6) */ handleFontSize: function(size) { if(size) { this.rdom.applyFontSize(size); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } else { //TODO: popup font dialog } return true; }, /** * Applies superscription * @TODO: Add selenium test */ handleSuperscription: function() { this.rdom.applySuperscription(); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Applies subscription * @TODO: Add selenium test */ handleSubscription: function() { this.rdom.applySubscription(); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Change or wrap current block(or selected blocks)'s tag * @TODO: Add selenium test * * @param {String} [tagName] Name of tag. If not provided, it does not modify current tag name * @param {String} [className] Class name of tag. If not provided, it does not modify current class name, and if empty string is provided, class attribute will be removed. */ handleApplyBlock: function(tagName, className) { if(!tagName && !className) return true; // if current selection contains multi-blocks if(this.rdom.hasSelection()) { var blocks = this.rdom.getBlockElementsAtSelectionEdge(true, true); if(blocks.first() !== blocks.last()) { var applied = this.rdom.applyTagIntoElements(tagName, blocks.first(), blocks.last(), className); this.rdom.selectBlocksBetween(applied.first(), applied.last()); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; } } // else var block = this.rdom.getCurrentBlockElement(); this.rdom.pushMarker(); var applied = this.rdom.applyTagIntoElement(tagName, block, className) || block; this.rdom.popMarker(true); if(this.rdom.isEmptyBlock(applied)) { this.rdom.correctEmptyElement(applied); this.rdom.placeCaretAtStartOf(applied); } var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Inserts seperator (HR) * @TODO: Add selenium test */ handleSeparator: function() { this.rdom.collapseSelection(); var curBlock = this.rdom.getCurrentBlockElement(); var atStart = this.rdom.isCaretAtBlockStart(); if(this.rdom.tree.isBlockContainer(curBlock)) curBlock = this.rdom.wrapAllInlineOrTextNodesAs("P", curBlock, true)[0]; this.rdom.insertNodeAt(this.rdom.createElement("HR"), curBlock, atStart ? "before" : "after"); this.rdom.placeCaretAtStartOf(curBlock); // add undo history var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); return true; }, /** * Performs UNDO * @TODO: Add selenium test */ handleUndo: function() { var performed = this.editHistory.undo(); this._fireOnCurrentContentChanged(this); var curBlock = this.rdom.getCurrentBlockElement(); if(!xq.Browser.isTrident && curBlock) { curBlock.scrollIntoView(false); } return true; }, /** * Performs REDO * @TODO: Add selenium test */ handleRedo: function() { var performed = this.editHistory.redo(); this._fireOnCurrentContentChanged(this); var curBlock = this.rdom.getCurrentBlockElement(); if(!xq.Browser.isTrident && curBlock) { curBlock.scrollIntoView(false); } return true; }, _handleContextMenu: function(e) { if (xq.Browser.isWebkit) { if (e.metaKey || xq.isLeftClick(e)) return false; } else if (e.shiftKey || e.ctrlKey || e.altKey) { return false; } var point = xq.getEventPoint(e); var x = point.x; var y = point.y; var pos = xq.getCumulativeOffset(this.wysiwygEditorDiv); x += pos.left; y += pos.top; this._contextMenuTargetElement = e.target || e.srcElement; if (!xq.Browser.isTrident) { var doc = this.getDoc(); var body = this.getBody(); x -= doc.documentElement.scrollLeft; y -= doc.documentElement.scrollTop; x -= body.scrollLeft; y -= body.scrollTop; } for(var cmh in this.config.contextMenuHandlers) { var stop = this.config.contextMenuHandlers[cmh].handler(this, this._contextMenuTargetElement, x, y); if(stop) { xq.stopEvent(e); return true; } } return false; }, showContextMenu: function(menuItems, x, y) { if (!menuItems || menuItems.length <= 0) return; if (!this.contextMenuContainer) { this.contextMenuContainer = this.doc.createElement('UL'); this.contextMenuContainer.className = 'xqContextMenu'; this.contextMenuContainer.style.display='none'; xq.observe(this.doc, 'click', this._contextMenuClicked.bindAsEventListener(this)); xq.observe(this.rdom.getDoc(), 'click', this.hideContextMenu.bindAsEventListener(this)); this.body.appendChild(this.contextMenuContainer); } else { while (this.contextMenuContainer.childNodes.length > 0) this.contextMenuContainer.removeChild(this.contextMenuContainer.childNodes[0]); } for (var i=0; i < menuItems.length; i++) { menuItems[i]._node = this._addContextMenuItem(menuItems[i]); } this.contextMenuContainer.style.display='block'; this.contextMenuContainer.style.left = Math.min(Math.max(this.doc.body.scrollWidth, this.doc.documentElement.clientWidth) - this.contextMenuContainer.offsetWidth, x) + 'px'; this.contextMenuContainer.style.top = Math.min(Math.max(this.doc.body.scrollHeight, this.doc.documentElement.clientHeight) - this.contextMenuContainer.offsetHeight, y) + 'px'; this.contextMenuItems = menuItems; }, hideContextMenu: function() { if (this.contextMenuContainer) this.contextMenuContainer.style.display='none'; }, _addContextMenuItem: function(item) { if (!this.contextMenuContainer) throw "No conext menu container exists"; var node = this.doc.createElement('LI'); if (item.disabled) node.className += ' disabled'; if (item.title === '----') { node.innerHTML = ' '; node.className = 'separator'; } else { if(item.handler) { node.innerHTML = ''+(item.title.toString().escapeHTML())+''; } else { node.innerHTML = (item.title.toString().escapeHTML()); } } if(item.className) node.className = item.className; this.contextMenuContainer.appendChild(node); return node; }, _contextMenuClicked: function(e) { this.hideContextMenu(); if (!this.contextMenuContainer) return; var node = e.srcElement || e.target; while(node && node.nodeName !== "LI") { node = node.parentNode; } if (!node || !this.rdom.tree.isDescendantOf(this.contextMenuContainer, node)) return; for (var i=0; i < this.contextMenuItems.length; i++) { if (this.contextMenuItems[i]._node === node) { var handler = this.contextMenuItems[i].handler; if (!this.contextMenuItems[i].disabled && handler) { var xed = this; var element = this._contextMenuTargetElement; if(typeof handler === "function") { handler(xed, element); } else { eval(handler); } } break; } } }, /** * Inserts HTML template * @TODO: Add selenium test * * @param {String} html Template string. It should have single root element * @returns {Element} inserted element */ insertTemplate: function(html) { return this.rdom.insertHtml(this._processTemplate(html)); }, /** * Places given HTML template nearby target. * @TODO: Add selenium test * * @param {String} html Template string. It should have single root element * @param {Node} target Target node. * @param {String} where Possible values: "before", "start", "end", "after" * * @returns {Element} Inserted element. */ insertTemplateAt: function(html, target, where) { return this.rdom.insertHtmlAt(this._processTemplate(html), target, where); }, _processTemplate: function(html) { // apply template processors var tps = this.getTemplateProcessors(); for(var key in tps) { var value = tps[key]; html = value.handler(html); } // remove all whitespace characters between block tags return this.removeUnnecessarySpaces(html); }, /** @private */ _handleEnterAtEmptyBlock: function() { var block = this.rdom.getCurrentBlockElement(); if(this.rdom.tree.isTableCell(block) && this.rdom.isFirstBlockOfBody(block)) { block = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start"); } else { block = this.rdom.outdentElement(block) || this.rdom.extractOutElementFromParent(block) || this.rdom.replaceTag("P", block) || this.rdom.insertNewBlockAround(block); } this.rdom.placeCaretAtStartOf(block); if(!xq.Browser.isTrident) block.scrollIntoView(false); }, /** @private */ _handleEnterAtEdge: function(atStart, forceInsertParagraph) { var block = this.rdom.getCurrentBlockElement(); var blockToPlaceCaret; if(atStart && this.rdom.isFirstBlockOfBody(block)) { blockToPlaceCaret = this.rdom.insertNodeAt(this.rdom.makeEmptyParagraph(), this.rdom.getRoot(), "start"); } else { if(this.rdom.tree.isTableCell(block)) forceInsertParagraph = true; var newBlock = this.rdom.insertNewBlockAround(block, atStart, forceInsertParagraph ? "P" : null); blockToPlaceCaret = !atStart ? newBlock : newBlock.nextSibling; } this.rdom.placeCaretAtStartOf(blockToPlaceCaret); if(!xq.Browser.isTrident) blockToPlaceCaret.scrollIntoView(false); }, /** * Replace URL text nearby caret into a link * @TODO: Add selenium test */ replaceUrlToLink: function() { // If there's link nearby caret, nothing happens if(this.rdom.getParentElementOf(this.rdom.getCurrentElement(), ["A"])) return; var marker = this.rdom.pushMarker(); var criteria = function(text) { var m = /(http|https|ftp|mailto)\:\/\/[^\s]+$/.exec(text); return m ? m.index : -1; }; var test = this.rdom.testSmartWrap(marker, criteria); if(test.textIndex !== -1) { var a = this.rdom.smartWrap(marker, "A", criteria); a.href = encodeURI(test.text); } this.rdom.popMarker(true); } }); /** * @namespace */ xq.macro = {}; /** * @requires Xquared.js */ xq.macro.Base = xq.Class(/** @lends xq.macro.Base.prototype */{ /** * @constructs * * @param {Object} Parameters or HTML fragment. * @param {String} URL to place holder image. */ initialize: function(id, paramsOrHtml, placeHolderImgSrc) { this.id = id; this.placeHolderImgSrc = placeHolderImgSrc; if(typeof paramsOrHtml === "string") { this.html = paramsOrHtml; this.params = {}; this.initFromHtml(); } else { this.html = null; this.params = paramsOrHtml; this.initFromParams(); } }, initFromHtml: function() {}, initFromParams: function() {}, createHtml: function() {throw "Not implemented";}, onLayerInitialzied: function(layer) {}, createPlaceHolderHtml: function() { var size = {width: 5, height: 5}; var def = {}; def.id = this.id; def.params = this.params; sb = []; sb.push(''); return sb.join(''); } }) /** * @requires Xquared.js * @requires macro/Base.js */ xq.macro.Factory = xq.Class(/** @lends xq.macro.Factory.prototype */{ /** * @constructs * * @param {String} URL to place holder image. */ initialize: function(placeHolderImgSrc) { this.placeHolderImgSrc = placeHolderImgSrc; this.macroClazzes = {}; }, /** * Registers new macro by ID. * * @param {String} id Macro id. */ register: function(id) { var clazz = xq.macro[id + "Macro"]; if(!clazz) throw "Unknown macro id: [" + id + "]"; this.macroClazzes[id] = clazz; }, /** * Creates macro instance by given HTML fragment. * * @param {String} html HTML fragment. * @returns {xq.macro.Base} Macro instance or null if recognization of the HTML fragment fails. */ createMacroFromHtml: function(html) { for(var id in this.macroClazzes) { var clazz = this.macroClazzes[id]; if(clazz.recognize(html)) return new clazz(id, html, this.placeHolderImgSrc); } return null; }, /** * Creates macro instance by given macro definition. * * @param {Object} def Macro definition. * @returns {xq.macro.Base} Macro instance * @throws If macro not found by def[id]. */ createMacroFromDefinition: function(def) { var clazz = this.macroClazzes[def.id]; if(!clazz) return null; return new clazz(def.id, def.params, this.placeHolderImgSrc); } }) /** * @requires Xquared.js * @requires Editor.js */ xq.Layer = xq.Class(/** @lends xq.Layer.prototype */{ /** * @constructs * * @param {xq.Editor} editor editor instance * @param {Element} element designMode document's element. Layer instance will be attached to this element * @param {String} html HTML for body. */ initialize: function(editor, element, html) { xq.addToFinalizeQueue(this); this.margin = 4; this.editor = editor; this.element = element; this.frame = this.editor._createIFrame(this.editor.getOuterDoc(), this.element.offsetWidth - (this.margin * 2) + "px", this.element.offsetHeight + (this.margin * 2) + "px"); this.editor.getOuterDoc().body.appendChild(this.frame); this.doc = editor._createDoc( this.frame, '', [], null, null, html ); this.frame.style.position = "absolute"; this.updatePosition(); }, getFrame: function() { return this.frame; }, getDoc: function() { return this.doc; }, getBody: function() { return this.doc.body; }, isValid: function() { return this.element && this.element.parentNode && this.element.offsetParent; }, detach: function() { this.frame.parentNode.removeChild(this.frame); this.frame = null; this.element = null; }, updatePosition: function() { // calculate element position var offset = xq.getCumulativeOffset(this.element, this.editor.rdom.getRoot()); // and scroll position var doc = this.editor.getDoc(); var body = this.editor.getBody(); offset.left -= doc.documentElement.scrollLeft + body.scrollLeft - this.margin; offset.top -= doc.documentElement.scrollTop + body.scrollTop - this.margin; // apply new position this.frame.style.left = offset.left + "px"; this.frame.style.top = offset.top + "px"; // perform autofit var newWidth = this.doc.body.scrollWidth + (this.margin - 1) * 2; var newHeight = this.doc.body.scrollHeight + (this.margin - 1) * 2; // without -1, the element increasing slowly. this.element.width = newWidth; this.element.height = newHeight; // resize frame this.frame.style.width = this.element.offsetWidth - (this.margin * 2) + "px"; this.frame.style.height = this.element.offsetHeight - (this.margin * 2) + "px"; } }); /* json2.js 2008-02-14 Public Domain No warranty expressed or implied. Use at your own risk. See http://www.JSON.org/js.html This file creates a global JSON object containing two methods: JSON.stringify(value, whitelist) value any JavaScript value, usually an object or array. whitelist an optional array parameter that determines how object values are stringified. This method produces a JSON text from a JavaScript value. There are three possible ways to stringify an object, depending on the optional whitelist parameter. If an object has a toJSON method, then the toJSON() method will be called. The value returned from the toJSON method will be stringified. Otherwise, if the optional whitelist parameter is an array, then the elements of the array will be used to select members of the object for stringification. Otherwise, if there is no whitelist parameter, then all of the members of the object will be stringified. Values that do not have JSON representaions, such as undefined or functions, will not be serialized. Such values in objects will be dropped; in arrays will be replaced with null. JSON.stringify(undefined) returns undefined. Dates will be stringified as quoted ISO dates. Example: var text = JSON.stringify(['e', {pluribus: 'unum'}]); // text is '["e",{"pluribus":"unum"}]' JSON.parse(text, filter) This method parses a JSON text to produce an object or array. It can throw a SyntaxError exception. The optional filter parameter is a function that can filter and transform the results. It receives each of the keys and values, and its return value is used instead of the original value. If it returns what it received, then structure is not modified. If it returns undefined then the member is deleted. Example: // Parse the text. If a key contains the string 'date' then // convert the value to a date. myData = JSON.parse(text, function (key, value) { return key.indexOf('date') >= 0 ? new Date(value) : value; }); This is a reference implementation. You are free to copy, modify, or redistribute. Use your own copy. It is extremely unwise to load third party code into your pages. */ /*jslint evil: true */ /*global JSON */ /*members "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, charCodeAt, floor, getUTCDate, getUTCFullYear, getUTCHours, getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, length, parse, propertyIsEnumerable, prototype, push, replace, stringify, test, toJSON, toString */ if (!this.JSON) { JSON = function () { function f(n) { // Format integers to have at least two digits. return n < 10 ? '0' + n : n; } Date.prototype.toJSON = function () { // Eventually, this method will be based on the date.toISOString method. return this.getUTCFullYear() + '-' + f(this.getUTCMonth() + 1) + '-' + f(this.getUTCDate()) + 'T' + f(this.getUTCHours()) + ':' + f(this.getUTCMinutes()) + ':' + f(this.getUTCSeconds()) + 'Z'; }; var m = { // table of character substitutions '\b': '\\b', '\t': '\\t', '\n': '\\n', '\f': '\\f', '\r': '\\r', '"' : '\\"', '\\': '\\\\' }; function stringify(value, whitelist) { var a, // The array holding the partial texts. i, // The loop counter. k, // The member key. l, // Length. r = /["\\\x00-\x1f\x7f-\x9f]/g, v; // The member value. switch (typeof value) { case 'string': // If the string contains no control characters, no quote characters, and no // backslash characters, then we can safely slap some quotes around it. // Otherwise we must also replace the offending characters with safe sequences. return r.test(value) ? '"' + value.replace(r, function (a) { var c = m[a]; if (c) { return c; } c = a.charCodeAt(); return '\\u00' + Math.floor(c / 16).toString(16) + (c % 16).toString(16); }) + '"' : '"' + value + '"'; case 'number': // JSON numbers must be finite. Encode non-finite numbers as null. return isFinite(value) ? String(value) : 'null'; case 'boolean': case 'null': return String(value); case 'object': // Due to a specification blunder in ECMAScript, // typeof null is 'object', so watch out for that case. if (!value) { return 'null'; } // If the object has a toJSON method, call it, and stringify the result. if (typeof value.toJSON === 'function') { return stringify(value.toJSON()); } a = []; if (typeof value.length === 'number' && !(value.propertyIsEnumerable('length'))) { // The object is an array. Stringify every element. Use null as a placeholder // for non-JSON values. l = value.length; for (i = 0; i < l; i += 1) { a.push(stringify(value[i], whitelist) || 'null'); } // Join all of the elements together and wrap them in brackets. return '[' + a.join(',') + ']'; } if (whitelist) { // If a whitelist (array of keys) is provided, use it to select the components // of the object. l = whitelist.length; for (i = 0; i < l; i += 1) { k = whitelist[i]; if (typeof k === 'string') { v = stringify(value[k], whitelist); if (v) { a.push(stringify(k) + ':' + v); } } } } else { // Otherwise, iterate through all of the keys in the object. for (k in value) { if (typeof k === 'string') { v = stringify(value[k], whitelist); if (v) { a.push(stringify(k) + ':' + v); } } } } // Join all of the member texts together and wrap them in braces. return '{' + a.join(',') + '}'; } } return { stringify: stringify, parse: function (text, filter) { var j; function walk(k, v) { var i, n; if (v && typeof v === 'object') { for (i in v) { if (Object.prototype.hasOwnProperty.apply(v, [i])) { n = walk(i, v[i]); if (n !== undefined) { v[i] = n; } else { delete v[i]; } } } } return filter(k, v); } // Parsing happens in three stages. In the first stage, we run the text against // regular expressions that look for non-JSON patterns. We are especially // concerned with '()' and 'new' because they can cause invocation, and '=' // because it can cause mutation. But just to be safe, we want to reject all // unexpected forms. // We split the first stage into 4 regexp operations in order to work around // crippling inefficiencies in IE's and Safari's regexp engines. First we // replace all backslash pairs with '@' (a non-JSON character). Second, we // replace all simple value tokens with ']' characters. Third, we delete all // open brackets that follow a colon or comma or that begin the text. Finally, // we look to see that the remaining characters are only whitespace or ']' or // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. if (/^[\],:{}\s]*$/.test(text.replace(/\\./g, '@'). replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { // In the second stage we use the eval function to compile the text into a // JavaScript structure. The '{' operator is subject to a syntactic ambiguity // in JavaScript: it can begin a block or an object literal. We wrap the text // in parens to eliminate the ambiguity. j = eval('(' + text + ')'); // In the optional third stage, we recursively walk the new structure, passing // each name/value pair to a filter function for possible transformation. return typeof filter === 'function' ? walk('', j) : j; } // If the text is not JSON parseable, then a SyntaxError is thrown. throw new SyntaxError('parseJSON'); } }; }(); } /** * @requires Xquared.js * @requires Browser.js * @requires Editor.js * * @requires macro/Factory.js * @requires Layer.js * @requires Json2.js * * @requires plugin/Base.js */ xq.plugin.MacroPlugin = xq.Class(xq.plugin.Base, /** * @name xq.plugin.MacroPlugin * @lends xq.plugin.MacroPlugin.prototype * @extends xq.plugin.Base * @constructor */ { isEventListener: function() {return true;}, onAfterLoad: function(xed) { this.xed = xed; this.xed.config.macroIds = []; this.layers = []; }, onEditorStartInitialization: function(xed) { this.xed.validator.addListener(this); this.xed.macroFactory = new xq.macro.Factory(this.xed.config.imagePathForContent + 'placeholder.gif'); for(var i = 0; i < this.xed.config.macroIds.length; i++) { this.xed.macroFactory.register(this.xed.config.macroIds[i]); } xed.timer.register(this.updateLayers.bind(this), 100); xed.timer.register(this.updateLayerList.bind(this), 2000); }, /** * @param {Element} [placeHolder] place holder element */ attachMacro: function(element) { var longdesc = element.getAttribute("longdesc") || element.longdesc; var def = JSON.parse(unescape(longdesc)); var macro = this.xed.macroFactory.createMacroFromDefinition(def); var layer = new xq.Layer(this.xed, element, macro.createHtml()); macro.onLayerInitialzied(layer); this.layers.push(layer); }, isAttachedPlaceHolder: function(element) { for(var i = 0; i < this.layers.length; i++) { if(this.layers[i].element === element) return true; } return false; }, updateLayerList: function() { if(this.xed.getCurrentEditMode() !== 'wysiwyg') { for(var i = 0; i < this.layers.length; i++) { this.layers[i].detach(); } this.layers = []; } else { var placeHolders = xq.getElementsByClassName(this.xed.rdom.getRoot(), "xqlayer", xq.Browser.isTrident ? "img" : null); for(var i = 0; i < placeHolders.length; i++) { if(!this.isAttachedPlaceHolder(placeHolders[i])) { this.attachMacro(placeHolders[i]); } } } }, /** * Updates all layers immediately. If there're invalid layers, detachs and removes them. */ updateLayers: function() { if(this.xed.getCurrentEditMode() !== 'wysiwyg') return; for(var i = 0; i < this.layers.length; i++) { var layer = this.layers[i]; if(layer.isValid()) { layer.updatePosition(); } else { layer.detach(); this.layers.splice(i, 1); } } }, onValidatorPreprocessing: function(html) { var p = xq.compilePattern("<(IFRAME|SCRIPT|OBJECT|EMBED)\\s+[^>]+(?:/>|>.*?)", "img"); html.value = html.value.replace(p, function(str, tag) { var macro = this.xed.macroFactory.createMacroFromHtml(str); return macro ? macro.createPlaceHolderHtml() : ""; }.bind(this)); }, onValidatorAfterStringValidation: function(html) { var p1 = /]*class="xqlayer"\s+[^>]*\/>/mg; var p2 = /]*longdesc="(.+?)"\s+[^>]*\/>/m; html.value = html.value.replace(p1, function(img) { var def = JSON.parse(unescape(img.match(p2)[1])); var macro = this.xed.macroFactory.createMacroFromDefinition(def); return macro.createHtml(); }.bind(this)); } }); /** * @requires macro/Base.js */ xq.macro.FlashMovieMacro = xq.Class(xq.macro.Base, /** * Flash movie macro * * @name xq.macro.FlashMovieMacro * @lends xq.macro.FlashMovieMacro.prototype * @extends xq.macro.Base * @constructor */ { initFromHtml: function() { this.params.html = this.html; }, initFromParams: function() { if(!xq.macro.FlashMovieMacro.recognize(this.params.html)) throw "Unknown src"; }, createHtml: function() { return this.params.html; } }); xq.macro.FlashMovieMacro.recognize = function(html) { var providers = { tvpot: /http:\/\/flvs\.daum\.net\/flvPlayer\.swf\?/, youtube: /http:\/\/(?:www\.)?youtube\.com\/v\//, pandoratv: /http:\/\/flvr\.pandora\.tv\/flv2pan\/flvmovie\.dll\?/, pandoratv2: /http:\/\/imgcdn\.pandora\.tv\/gplayer\/pandora\_EGplayer\.swf\?/, mncast: /http:\/\/dory\.mncast\.com\/mncHMovie\.swf\?/, yahoo: /http:\/\/d\.yimg\.com\// }; for(var id in providers) { if(html.match(providers[id])) return true; } return false; } /** * @requires Xquared.js * @requires Browser.js * @requires Editor.js * @requires plugin/Base.js * @requires ui/Control.js * @requires macro/Factory.js * @requires macro/FlashMovieMacro.js */ xq.plugin.FlashMovieMacroPlugin = xq.Class(xq.plugin.Base, /** * @name xq.plugin.FlashMovieMacroPlugin * @lends xq.plugin.FlashMovieMacroPlugin.prototype * @extends xq.plugin.Base * @constructor */ { onAfterLoad: function(xed) { xed.config.macroIds.push("FlashMovie"); xed.config.defaultToolbarButtonGroups.insert.push( {className:"movie", title:"Movie", handler:"xed.handleMovie()"} ) xed.handleInsertMovie = function(html) { var macro = this.macroFactory.createMacroFromDefinition({id:"FlashMovie", params:{html:html}}); if(macro) { var placeHolder = macro.createPlaceHolderHtml(); this.rdom.insertHtml(placeHolder); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } else { alert("Unknown URL pattern"); } return true; }; xed.handleMovie = function() { var dialog = new xq.ui.FormDialog( this, xq.ui_templates.basicMovieDialog, function(dialog) {}, function(data) { this.focus(); if(xq.Browser.isTrident) { var rng = this.rdom.rng(); rng.moveToBookmark(bm); rng.select(); } // cancel? if(!data) return; this.handleInsertMovie(data.html); }.bind(this) ); if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); dialog.show({position: 'centerOfEditor'}); return true; } } }); /** * @requires macro/Base.js */ xq.macro.IFrameMacro = xq.Class(xq.macro.Base, /** * IFrame macro * * @name xq.macro.IFrameMacro * @lends xq.macro.IFrameMacro.prototype * @extends xq.macro.Base * @constructor */ { initFromHtml: function() { this.params.html = this.html; }, initFromParams: function() { if(this.params.html) return; var sb = []; sb.push(''); this.params = {html:sb.join("")}; }, createHtml: function() { return this.params.html; } }); xq.macro.IFrameMacro.recognize = function(html) { var p = xq.compilePattern("]+(?:/>|>.*?)", "img"); return !!html.match(p); } /** * @requires Xquared.js * @requires Browser.js * @requires Editor.js * @requires plugin/Base.js * @requires ui/Control.js * @requires macro/Factory.js * @requires macro/IFrameMacro.js */ xq.plugin.IFrameMacroPlugin = xq.Class(xq.plugin.Base, /** * @name xq.plugin.IFrameMacroPlugin * @lends xq.plugin.IFrameMacroPlugin.prototype * @extends xq.plugin.Base * @constructor */ { onAfterLoad: function(xed) { xed.config.macroIds.push("IFrame"); xed.config.defaultToolbarButtonGroups.insert.push( {className:"iframe", title:"IFrame", handler:"xed.handleIFrame()"} ) xed.handleIFrame = function() { var dialog = new xq.ui.FormDialog( this, xq.ui_templates.basicIFrameDialog, function(dialog) {}, function(data) { this.focus(); // cancel? if(!data) return; var macro = this.macroFactory.createMacroFromDefinition({id:"IFrame", params:data}); if(macro) { var placeHolder = macro.createPlaceHolderHtml(); this.rdom.insertHtml(placeHolder); } else { alert("Unknown error"); } }.bind(this) ); dialog.show({position: 'centerOfEditor'}); return true; } } }); /** * @requires macro/Base.js */ xq.macro.JavascriptMacro = xq.Class(xq.macro.Base, /** * Javascript macro * * @name xq.macro.JavascriptMacro * @lends xq.macro.JavascriptMacro.prototype * @extends xq.macro.Base * @constructor */ { initFromHtml: function() { var p = xq.compilePattern("src=[\"'](.+?)[\"']", "img"); this.params.url = p.exec(this.html)[1]; }, initFromParams: function() { if(!xq.macro.JavascriptMacro.isSafeScript(this.params.url)) throw "Unknown src"; }, createHtml: function() {return ''}, onLayerInitialzied: function(layer) { layer.getDoc().write(this.createHtml()); } }); xq.macro.JavascriptMacro.recognize = function(html) { var p = xq.compilePattern("]*src=[\"']([^\"']+)[\"'][^>]*(?:/>|>.*?)", "img"); var m = p.exec(html); if(!m || !m[1]) return false; return this.isSafeScript(m[1]); } xq.macro.JavascriptMacro.isSafeScript = function(url) { var safeSrcs = { googleGadget: /http:\/\/gmodules\.com\/ig\/ifr\?/img }; for(var id in safeSrcs) { if(url.match(safeSrcs[id])) return true; } return false; } /** * @requires Xquared.js * @requires Browser.js * @requires Editor.js * @requires plugin/Base.js * @requires ui/Control.js * @requires macro/Factory.js * @requires macro/JavascriptMacro.js */ xq.plugin.JavascriptMacroPlugin = xq.Class(xq.plugin.Base, /** * @name xq.plugin.JavascriptMacroPlugin * @lends xq.plugin.JavascriptMacroPlugin.prototype * @extends xq.plugin.Base * @constructor */ { onAfterLoad: function(xed) { xed.config.macroIds.push("Javascript"); xed.config.defaultToolbarButtonGroups.insert.push( {className:"script", title:"Script", handler:"xed.handleScript()"} ) xed.handleInsertScript = function(url) { var params = {url: url}; var macro = this.macroFactory.createMacroFromDefinition({id:"Javascript", params:params}); if(macro) { var placeHolder = macro.createPlaceHolderHtml(); this.rdom.insertHtml(placeHolder); var historyAdded = this.editHistory.onCommand(); this._fireOnCurrentContentChanged(this); } else { alert("Unknown URL pattern"); } return true; }; xed.handleScript = function() { var dialog = new xq.ui.FormDialog( this, xq.ui_templates.basicScriptDialog, function(dialog) {}, function(data) { this.focus(); if(xq.Browser.isTrident) { var rng = this.rdom.rng(); rng.moveToBookmark(bm); rng.select(); } // cancel? if(!data) return; this.handleInsertScript(data.url); }.bind(this) ); if(xq.Browser.isTrident) var bm = this.rdom.rng().getBookmark(); dialog.show({position: 'centerOfEditor'}); return true; } } }); /** * @requires Xquared.js * @requires Editor.js * @requires plugin/MacroPlugin.js * @requires plugin/FlashMovieMacroPlugin.js * @requires plugin/IFrameMacroPlugin.js * @requires plugin/JavascriptMacroPlugin.js */ xq.moduleName = "Full";