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