1 /** 2 * RichDom for W3C Standard Engine 3 */ 4 xq.RichDomW3 = xq.Class(xq.RichDom, { 5 insertNode: function(node) { 6 var rng = this.rng(); 7 rng.insertNode(node); 8 rng.selectNode(node); 9 rng.collapse(false); 10 return node; 11 }, 12 13 removeTrailingWhitespace: function(block) { 14 // TODO: do nothing 15 }, 16 17 getOuterHTML: function(element) { 18 var div = element.ownerDocument.createElement("div"); 19 div.appendChild(element.cloneNode(true)); 20 return div.innerHTML; 21 }, 22 23 correctEmptyElement: function(element) { 24 if(!element || element.nodeType != 1 || this.tree.isAtomic(element)) return; 25 26 if(element.firstChild) 27 this.correctEmptyElement(element.firstChild); 28 else 29 element.appendChild(this.makePlaceHolder()); 30 }, 31 32 correctParagraph: function() { 33 if(this.hasSelection()) return false; 34 35 var block = this.getCurrentElement(); 36 var modified = false; 37 38 if(this.tree.isBlockOnlyContainer(block)) { 39 this.execCommand("InsertParagraph"); 40 41 // check for atomic block element such as HR 42 var newBlock = this.getCurrentElement(); 43 if(this.tree.isAtomic(newBlock.previousSibling)) { 44 var nextBlock = this.tree.findForward( 45 newBlock, 46 function(node) {return this.tree.isBlock(node) && !this.tree.isBlockOnlyContainer(node)}.bind(this) 47 ); 48 if(nextBlock) { 49 this.deleteNode(newBlock); 50 this.placeCaretAtStartOf(nextBlock); 51 } 52 } 53 modified = true; 54 } else if(this.tree.hasMixedContents(block)) { 55 this.wrapAllInlineOrTextNodesAs("P", block, true); 56 modified = true; 57 } 58 59 block = this.getCurrentElement(); 60 if(this.tree.isBlock(block) && !this._hasPlaceHolderAtEnd(block)) { 61 block.appendChild(this.makePlaceHolder()); 62 modified = true; 63 } 64 65 if(this.tree.isBlock(block)) { 66 var parentsLastChild = block.parentNode.lastChild; 67 if(this.isPlaceHolder(parentsLastChild)) { 68 this.deleteNode(parentsLastChild); 69 modified = true; 70 } 71 } 72 73 return modified; 74 }, 75 76 _hasPlaceHolderAtEnd: function(block) { 77 if(!block.hasChildNodes()) return false; 78 return this.isPlaceHolder(block.lastChild) || this._hasPlaceHolderAtEnd(block.lastChild); 79 }, 80 81 applyBackgroundColor: function(color) { 82 this.execCommand("styleWithCSS", "true"); 83 this.execCommand("hilitecolor", color); 84 this.execCommand("styleWithCSS", "false"); 85 86 // 0. Save current selection 87 var bookmark = this.saveSelection(); 88 89 // 1. Get selected blocks 90 var blocks = this.getSelectedBlockElements(); 91 if(blocks.length == 0) return; 92 93 // 2. Apply background-color to all adjust inline elements 94 // 3. Remove background-color from blocks 95 for(var i = 0; i < blocks.length; i++) { 96 if((i == 0 || i == blocks.length-1) && !blocks[i].style.backgroundColor) continue; 97 98 var spans = this.wrapAllInlineOrTextNodesAs("SPAN", blocks[i], true); 99 for(var j = 0; j < spans.length; j++) { 100 spans[j].style.backgroundColor = color; 101 } 102 blocks[i].style.backgroundColor = ""; 103 } 104 105 // 4. Restore selection 106 this.restoreSelection(bookmark); 107 }, 108 109 110 111 112 ////// 113 // Commands 114 execCommand: function(commandId, param) { 115 return this.doc.execCommand(commandId, false, param || null); 116 }, 117 118 saveSelection: function() { 119 var rng = this.rng(); 120 return [rng.startContainer, rng.startOffset, rng.endContainer, rng.endOffset]; 121 }, 122 123 restoreSelection: function(bookmark) { 124 var rng = this.rng(); 125 rng.setStart(bookmark[0], bookmark[1]); 126 rng.setEnd(bookmark[2], bookmark[3]); 127 }, 128 129 applyRemoveFormat: function() { 130 this.execCommand("RemoveFormat"); 131 this.execCommand("Unlink"); 132 }, 133 applyEmphasis: function() { 134 // Generate <i> tag. It will be replaced with <emphasis> tag during cleanup phase. 135 this.execCommand("styleWithCSS", "false"); 136 this.execCommand("italic"); 137 }, 138 applyStrongEmphasis: function() { 139 // Generate <b> tag. It will be replaced with <strong> tag during cleanup phase. 140 this.execCommand("styleWithCSS", "false"); 141 this.execCommand("bold"); 142 }, 143 applyStrike: function() { 144 // Generate <strike> tag. It will be replaced with <style class="strike"> tag during cleanup phase. 145 this.execCommand("styleWithCSS", "false"); 146 this.execCommand("strikethrough"); 147 }, 148 applyUnderline: function() { 149 // Generate <u> tag. It will be replaced with <em class="underline"> tag during cleanup phase. 150 this.execCommand("styleWithCSS", "false"); 151 this.execCommand("underline"); 152 }, 153 execHeading: function(level) { 154 this.execCommand("Heading", "H" + level); 155 }, 156 157 158 159 ////// 160 // Focus/Caret/Selection 161 162 focus: function() { 163 setTimeout(this._focus.bind(this), 0); 164 }, 165 166 /** @private */ 167 _focus: function() { 168 this.win.focus(); 169 if(!this.hasSelection() && this.getCurrentElement().nodeName == "HTML") { 170 this.selectElement(this.doc.body.firstChild); 171 this.collapseSelection(true); 172 } 173 }, 174 175 sel: function() { 176 return this.win.getSelection(); 177 }, 178 179 rng: function() { 180 var sel = this.sel(); 181 return (sel == null || sel.rangeCount == 0) ? null : sel.getRangeAt(0); 182 }, 183 184 hasSelection: function() { 185 var sel = this.sel(); 186 return sel && !sel.isCollapsed; 187 }, 188 189 deleteSelection: function() { 190 this.rng().deleteContents(); 191 this.sel().collapseToStart(); 192 }, 193 194 selectElement: function(element, entireElement) {throw "Not implemented yet"}, 195 196 selectBlocksBetween: function(start, end) { 197 // required to avoid FF selection bug. 198 try { 199 if(!xq.Browser.isMac) this.doc.execCommand("SelectAll", false, null); 200 } catch(ignored) {} 201 202 var rng = this.rng(); 203 rng.setStart(start.firstChild, 0); 204 rng.setEnd(end, end.childNodes.length); 205 }, 206 207 collapseSelection: function(toStart) { 208 this.rng().collapse(toStart); 209 }, 210 211 placeCaretAtStartOf: function(element) { 212 while(this.tree.isBlock(element.firstChild)) { 213 element = element.firstChild; 214 } 215 this.selectElement(element, false); 216 this.collapseSelection(true); 217 }, 218 219 getSelectionAsHtml: function() { 220 var container = document.createElement("div"); 221 container.appendChild(this.rng().cloneContents()); 222 return container.innerHTML; 223 }, 224 225 getSelectionAsText: function() { 226 return this.rng().toString() 227 }, 228 229 hasImportantAttributes: function(element) { 230 return !!(element.id || element.className || element.style.cssText); 231 }, 232 233 isEmptyBlock: function(element) { 234 if(!element.hasChildNodes()) return true; 235 var children = element.childNodes; 236 for(var i = 0; i < children.length; i++) { 237 if(!this.isPlaceHolder(children[i]) && !this.isEmptyTextNode(children[i])) return false; 238 } 239 return true; 240 }, 241 242 getLastChild: function(element) { 243 if(!element || !element.hasChildNodes()) return null; 244 245 var nodes = xq.$A(element.childNodes).reverse(); 246 247 for(var i = 0; i < nodes.length; i++) { 248 if(!this.isPlaceHolder(nodes[i]) && !this.isEmptyTextNode(nodes[i])) return nodes[i]; 249 } 250 return null; 251 }, 252 253 getCurrentElement: function() { 254 var rng = this.rng(); 255 if(!rng) return null; 256 257 var container = rng.startContainer; 258 return container.nodeType == 3 ? container.parentNode : container; 259 }, 260 261 getBlockElementsAtSelectionEdge: function(naturalOrder, ignoreEmptyEdges) { 262 var start = this.getBlockElementAtSelectionStart(); 263 var end = this.getBlockElementAtSelectionEnd(); 264 265 var reversed = false; 266 267 if(naturalOrder && start != end && this.tree.checkTargetBackward(start, end)) { 268 var temp = start; 269 start = end; 270 end = temp; 271 272 reversed = true; 273 } 274 275 if(ignoreEmptyEdges && start != end) { 276 // TODO - Firefox sometimes selects one more block. 277 /* 278 279 var sel = this.sel(); 280 if(reversed) { 281 if(sel.focusNode.nodeType == 1) start = start.nextSibling; 282 if(sel.anchorNode.nodeType == 3 && sel.focusOffset == 0) end = end.previousSibling; 283 } else { 284 if(sel.anchorNode.nodeType == 1) start = start.nextSibling; 285 if(sel.focusNode.nodeType == 3 && sel.focusOffset == 0) end = end.previousSibling; 286 } 287 */ 288 } 289 290 return [start, end]; 291 }, 292 293 getBlockElementAtSelectionStart: function() { 294 var block = this.getParentBlockElementOf(this.sel().anchorNode); 295 296 // find bottom-most first block child 297 while(this.tree.isBlockContainer(block) && block.firstChild && this.tree.isBlock(block.firstChild)) { 298 block = block.firstChild; 299 } 300 301 return block; 302 }, 303 304 getBlockElementAtSelectionEnd: function() { 305 var block = this.getParentBlockElementOf(this.sel().focusNode); 306 307 // find bottom-most last block child 308 while(this.tree.isBlockContainer(block) && block.lastChild && this.tree.isBlock(block.lastChild)) { 309 block = block.lastChild; 310 } 311 312 return block; 313 }, 314 315 isCaretAtBlockStart: function() { 316 if(this.isCaretAtEmptyBlock()) return true; 317 if(this.hasSelection()) return false; 318 var rng = this.rng(); 319 var node = this.getCurrentBlockElement(); 320 var isTrue = false; 321 322 if(node == rng.startContainer) { 323 var marker = this.pushMarker(); 324 while (node = this.getFirstChild(node)) { 325 if (node == marker) { 326 isTrue = true; 327 break; 328 } 329 } 330 this.popMarker(); 331 } else { 332 while (node = node.firstChild) { 333 if (node == rng.startContainer && rng.startOffset == 0) { 334 isTrue = true; 335 break; 336 } 337 } 338 } 339 340 return isTrue; 341 }, 342 343 isCaretAtBlockEnd: function() { 344 if(this.isCaretAtEmptyBlock()) return true; 345 if(this.hasSelection()) return false; 346 347 var rng = this.rng(); 348 var node = this.getCurrentBlockElement(); 349 var isTrue = false; 350 351 if(node == rng.startContainer) { 352 var marker = this.pushMarker(); 353 while (node = this.getLastChild(node)) { 354 if ((node == marker) || (this.isPlaceHolder(node) && node.previousSibling == marker)) { 355 isTrue = true; 356 break; 357 } 358 } 359 this.popMarker(); 360 } else { 361 while (node = this.getLastChild(node)) { 362 if (node == rng.endContainer && rng.endContainer.nodeType == 1) { 363 isTrue = true; 364 break; 365 } else if (node == rng.endContainer && rng.endOffset == node.nodeValue.length) { 366 isTrue = true; 367 break; 368 } 369 } 370 } 371 372 return isTrue; 373 } 374 }); 375