/*! 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); } }); /** * @requires Xquared.js * @requires Editor.js */ xq.moduleName = "Minimal"