source: gs3-extensions/seaweed-debug/trunk/src/actions/FormatAction.js@ 25160

Last change on this file since 25160 was 25160, checked in by sjm84, 12 years ago

Initial cut at a version of seaweed for debugging purposes. Check it out live into the web/ext folder

File size: 23.9 KB
Line 
1/*
2 * file: FormatAction.js
3 *
4 * @BEGINLICENSE
5 * Copyright 2010 Brook Novak (email : [email protected])
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
17 * @ENDLICENSE
18 */
19
20// @DEPENDS: UndoMan
21bootstrap.provides("actions.FormatAction");
22
23_registerAction("Format", {
24
25 /**
26 * An undoable formatting action. Formats a range of DOM nodes in the document by
27 * adjusting their CSS styles.
28 *
29 * @param {String} type The type of formatting you want to apply. Case insensitive.
30 * See TODO for a list of standard formatting actions available.
31 *
32 * @param {Object} value The value to set - dependant on format type. null or empty to clear formatting.
33 *
34 * @param {Object} range (Optional) The DOM Range to apply the formatting to. If omitted then the current
35 * selection will be used. Must have members:
36 *
37 * - startNode The starting dom node of the fragments range.
38 *
39 * - startIndex The inclusive start index in the start node.
40 * Ranges from 0 to the text length for text nodes.
41 * Where 0 indicates that the range begins at the first char, and text length
42 * indicates that the range begins directly after the text node, but not including it.
43 * <br>
44 * Ranges from 0 to 1 for elements.
45 * Where 0 indicates that the range includes the element and it's decendants,
46 * and 1 indicates that the range excludes the element and it's decendants.
47 *
48 *
49 * - (optional) endNode The ending dom node of the fragments range. Omit to select word at given start node/index.
50 *
51 * - (optional) endIndex The inclusive end index in the end node. Omit to select word at given start node/index.
52 * Ranges from 0 to the text length for text nodes.
53 * Where 0 indicates that the range ends just before the text node, but not including it,
54 * and text length indicates that the range ends at the last charactor in the text run.
55 * <br>
56 * Ranges from 0 to 1 for elements.
57 * Where 0 indicates that tmFragment(frag) {
58 * and 1 indicates that the range includes the element and it's decendants.
59 *
60 * @return {_Fragment} The fragment which represents the range in the DOM which was formatted. Undefined if nothing formatted.
61 */
62 exec:function(type, value, range) {
63
64 type = type.toLowerCase();
65
66 // Check if the format type exists
67 if (typeof _formatEnvironment[type + "Wrapper"] == "undefined") {
68 debug.assert(false, 'Unknown format request: "' + type + '"');
69 return;
70 }
71
72 var clear = value ? false : true, isWordRange = false, orderSelAfter = true, formatRange;
73
74 // Auto-set range to current selectoin if none is provided
75 if (!range) {
76
77 if (!this.selBefore)
78 return; // Nothing to format
79 range = _clone(this.selBeforeOrdered);
80 formatRange = _clone(range);
81
82 // Because the range is based on the current selection, set the selection
83 // after to maintain the selection before ordering (i.e. keep the cursor
84 // at the same end of the selection).
85 orderSelAfter = this.selBefore.inOrder;
86
87 } else
88 formatRange = _clone(range);
89
90 // Auto-select word-range - if the range is a single node/index tuple
91 if (!formatRange.endNode) {
92
93 var anchorRange = null;
94
95 // Special case: instead of selecting word for links, select the whole anchor
96 if (type == "link") {
97
98 var current = formatRange.startNode;
99
100 while (current && de.doc.isNodeEditable(current)) {
101 if (current.nodeType == Node.ELEMENT_NODE && _nodeName(current) == "a") {
102 anchorRange = {
103 startNode : current,
104 startIndex : 0,
105 endNode : current,
106 endIndex : 1 /* Since a element, 1 indicates complete right-most deepest descandant */
107 };
108 break;
109 }
110 current = current.parentNode;
111 }
112
113 }
114
115 formatRange = anchorRange ?
116 anchorRange
117 : de.selection.getWordRangeAt(formatRange.startNode, formatRange.startIndex >= _nodeLength(formatRange.startNode) ? _nodeLength(formatRange.startNode) - 1 : formatRange.startIndex);
118
119 if (!formatRange)
120 return; // Nothing to format
121
122 isWordRange = orderSelAfter = true;
123 }
124
125 // Execute the action
126 var fragmentRoot = formatDOMExec(
127 formatRange.startNode,
128 formatRange.startIndex,
129 formatRange.endNode,
130 formatRange.endIndex,
131 _formatEnvironment[type + "Wrapper"](value || ""),
132 clear,
133 _formatEnvironment[type + "Eval"]);
134
135 // Update selection if requested
136 if (this.flags & de.UndoMan.ExecFlag.UPDATE_SELECTION) {
137
138 if (isWordRange) {
139
140 // Must get adjusted node/index since fragment will most likely of split some text nodes
141 var adjustedRange = fragmentRoot.getAdjustedNodeIndex(range.startNode, range.startIndex);
142
143 // Set the selection to be the single point
144 this.selAfter = {
145 startNode: adjustedRange.node,
146 startIndex: adjustedRange.index
147 };
148
149 } else {
150
151 var startFrag = fragmentRoot.getStartFragment(), endFrag = fragmentRoot.getEndFragment();
152
153 var startIndex = 0,
154 endIndex = _nodeLength(endFrag.node, 1);
155
156 this.selAfter = {
157 startNode: orderSelAfter ? startFrag.node : endFrag.node,
158 startIndex: orderSelAfter ? startIndex : endIndex,
159 endNode: orderSelAfter ? endFrag.node : startFrag.node,
160 endIndex: orderSelAfter ? endIndex : startIndex
161 };
162 }
163
164 }
165
166
167 /**
168 * The workhorse for the format action.
169 */
170 function formatDOMExec(startNode, startIndex, endNode, endIndex, inlineWrapper, clear, evalElement) {
171
172 var isRecursing = arguments.length > 7,
173 startExclusive = startIndex == _nodeLength(startNode, 1),
174 endExclusive = endIndex == 0;
175
176 // Build up fragment - split any text nodes where neccessary
177 var fragmentRoot = _buildFragment(_getCommonAncestor(startNode, endNode, false),
178 startNode,
179 startIndex,
180 endNode,
181 endIndex);
182
183 var startFrag = fragmentRoot.getStartFragment(),
184 endFrag = fragmentRoot.getEndFragment(),
185 rootNode = fragmentRoot.node; // Keep track of the root node... it may change if it is removed
186
187 // Mark fragments which should not be included for formatting
188 if (startExclusive)
189 markFrags(startFrag);
190 if (endExclusive)
191 markFrags(endFrag);
192
193 function markFrags(markFrag) {
194 do {
195 markFrag.dontFormat = 1;
196 markFrag = markFrag.parent;
197 } while(markFrag && markFrag.children.length == 1);
198 }
199
200 // PASS 1: Clear all related formatting
201
202 // Clear all related formatting within fragment
203 fragmentRoot.visit(function(frag){
204 if (!frag.dontFormat)
205 removeFormatting(frag.node);
206 });
207
208 // Clear all related formatting up to the document root (include body since can have styles/classes)
209 if (!isRecursing) {
210 var domNode = rootNode; // NB: Include root in removal since it may no longer be the fragment root
211 while (domNode) {
212 removeFormatting(domNode);
213 if (domNode == docBody) break;
214 domNode = domNode.parentNode;
215 } // End clear formatting to to root element
216 }
217
218 if (!clear) { // Should new formatting be applied? I.E. Not just clearing formatting
219
220 // PASS 2: Set new formatting
221
222 // Rebuild fragment since structure may have changed (this won't create any extra operations)
223 startNode = startFrag.node;
224 endNode = endFrag.node;
225 var newFragmentRoot = _buildFragment(
226 rootNode,
227 startNode,
228 startExclusive ? _nodeLength(startNode, 1) : 0,
229 endNode,
230 endExclusive ? 0 : _nodeLength(endNode, 1)
231 );
232
233 if (startExclusive)
234 markFrags(newFragmentRoot.getStartFragment());
235 if (endExclusive)
236 markFrags(newFragmentRoot.getEndFragment());
237
238 // NB: Keeping old fragment root in order to determine new cursor position (see below)
239
240 // Traverse the fragment structure and encapsulate inline groups where needed
241 (function trav(frag){
242
243 var igroups = [];
244
245 // For all sub tress
246 for (var i in frag.children) {
247
248 var child = frag.children[i];
249
250 if (shouldEncap(child))
251 igroups.push(child.node);
252 else {
253 // If cannot wrap the subtree due to containing at least one block level element, then
254 // recurse deeper
255 encapsulateIGroup(); // wrap anything pending
256 trav(child);
257 }
258 }
259
260 // Encap remaining run if any
261 encapsulateIGroup();
262
263 /**
264 * Checks the local igroups array and moves any nodes in the array into a wrapper
265 * then clears the array.
266 */
267 function encapsulateIGroup(){
268
269 if (igroups.length > 0) {
270
271 // Create inline wrapper and inset at beginning of inline group run
272 var wrapper = inlineWrapper.cloneNode(false);
273 _execOp(_Operation.INSERT_NODE, wrapper, frag.node, _indexInParent(igroups[0]));
274
275 // Move run of inline groups into the wrapper
276 for (var i in igroups) {
277 var toEncap = igroups[i];
278 _execOp(_Operation.REMOVE_NODE, toEncap);
279 _execOp(_Operation.INSERT_NODE, toEncap, wrapper);
280 }
281
282 // Reset igroup run
283 igroups = [];
284
285 }
286 } // End encapsulateIGroup function
287
288 /**
289 * @param {Node} node The node to test
290 * @return {Boolean} True iff the fragments node should be encapsulated.
291 */
292 function shouldEncap(frag) {
293
294 // Should this fragment be excluded from formatting?
295 if (frag.dontFormat)
296 return false;
297
298 // Check that all node inclusive don't contain block-levels
299 var allin = true;
300 _visitAllNodes(frag.node, frag.node, true, function(domNode) {
301 allin = !_isBlockLevel(domNode);
302 return allin;
303 })
304
305 if (allin) {
306
307 // Last check: the fragment mimics the actual document structure.. i.e. not a partial range.
308 // This avoids encapsulating nodes which are not in the format range
309 var isStructureSame = true;
310 (function checkStructure(curFrag, curNode) {
311
312 // Avoid including fragments which aren't actually in the format range
313 if (curFrag.dontFormat) {
314 isStructureSame = false;
315 return;
316 }
317
318 if (curNode.firstChild) {
319
320 isStructureSame = curNode.childNodes.length == curFrag.children.length;
321
322 if (isStructureSame) {
323 for (var j = 0; j < curNode.childNodes.length; j++) {
324 checkStructure(curFrag.children[j], curNode.childNodes[j]);
325 if (!isStructureSame)
326 break;
327 }
328 }
329
330 } else {
331 isStructureSame = curFrag.children.length == 0;
332 }
333
334 })(frag, frag.node);
335
336 return isStructureSame;
337
338 }
339
340 return false;
341 } // End isAllInline function
342
343 })(newFragmentRoot); // End encapsulating inline groups
344
345 } // End if: not clearing formatting
346
347 return fragmentRoot;
348
349 /**
350 * Removes immediate formatting for the given node.
351 * May result in removal of node... the local range may be adjusted when removals occur
352 *
353 * @param {Node} node a node to remove formatting from
354 */
355 function removeFormatting(node) {
356
357 if (node.nodeType == Node.ELEMENT_NODE) {
358
359 var shouldRemove = false,
360 res = evalElement(node); // Determine the actions to take to strip formatting at this element
361
362 if (res) { // Does this element contain related formatting?
363
364 for (var i in res.strip) {
365
366 // Strip formatting
367 switch (res.strip[i].type) {
368 case 1: // Name match
369 shouldRemove = true;
370 break;
371
372 case 2: // Class match
373
374 // Remove class... but keep extra classes
375 var cls = _getClassName(node);
376 if (cls) { // Strip matched class
377 var clss = cls.split(' ');
378 for (var j in clss) {
379 if (clss[j] == res.strip[i].match) {
380 clss.splice(j, 1);
381 break;
382 }
383 }
384 cls = cls.length > 0 ? clss.join(' ') : "";
385
386 _execOp(_Operation.SET_CLASS, node, cls);
387 }
388 break;
389
390 case 3: // Style match
391 // Erase the matched style
392 _execOp(_Operation.SET_CSS_STYLE, node, res.strip[i].match, "");
393 break;
394
395 // @DEBUG ON
396 default:
397 debug.assert(false, 'Unknown result type "' + res.strip[i].type + '" for stripping formatting');
398 // @DEBUG OFF
399 }
400
401 // If going to remove then no need to strip anything.
402 if (shouldRemove) break;
403
404 } // End loop: stripping formatting
405
406 // Get left-most child of the current sub tree
407 var leftMost = node.firstChild;
408 while (leftMost.firstChild)
409 leftMost = leftMost.firstChild;
410
411 // Get right-most child of the current sub tree
412 var rightMost = node.lastChild;
413 while (rightMost.lastChild)
414 rightMost = rightMost.lastChild;
415
416 // Check if the node should be auto-removed - only if the element is a generic span and has no class/styles.
417 if (!shouldRemove && _nodeName(node) == "span") {
418 shouldRemove = !_getClassName(node);
419 if (!shouldRemove)
420 shouldRemove = _doesHaveElementStyle(node);
421 }
422
423 if (shouldRemove) {
424
425 // Remove all children and connect with parent
426 var migrations = [];
427 while (node.firstChild) {
428 var migrant = node.firstChild;
429 _execOp(_Operation.REMOVE_NODE, migrant);
430 _execOp(_Operation.INSERT_NODE,
431 migrant,
432 node.parentNode,
433 _indexInParent(migrations.length > 0 ? migrations[migrations.length - 1] : node) + 1
434 );
435 migrations.push(migrant);
436 }
437
438 // If has no children, then ignore ... code wll bloat and it's too complex to bother tidying such cases
439 if (migrations.length> 0) {
440
441 // If node is the current root node then the root must be adjusted
442 if (node == rootNode)
443 rootNode = node.parentNode;
444
445 // Remove the actual node
446 _execOp(_Operation.REMOVE_NODE, node);
447
448 }
449
450 }
451
452 // Ensure that all of the elements descendants that it not within the formatting range
453 // still inherits the formatting that was just stripped using recursion
454 if (!isRecursing) {
455
456 // Recurse on left-range?
457 var isInFormatRange = false;
458 fragmentRoot.visit(function(f){
459 isInFormatRange = (!f.dontFormat && f.node == leftMost);
460 return !isInFormatRange;
461 });
462
463 if (!isInFormatRange) { // Do recursion
464
465 formatDOMExec(
466 leftMost,
467 0,
468 startFrag.node,
469 startExclusive ? _nodeLength(startFrag.node, 1) : 0,
470 res.inline,
471 false,
472 evalElement,
473 true);
474 }
475
476 // Recurse on right-range?
477 isInFormatRange = false;
478 fragmentRoot.visit(function(f){
479 isInFormatRange = (!f.dontFormat && f.node == rightMost);
480 return !isInFormatRange;
481 });
482
483 if (!isInFormatRange) { // Do recursion
484
485 formatDOMExec(
486 endFrag.node,
487 endExclusive ? 0 : _nodeLength(endFrag.node, 1),
488 rightMost,
489 _nodeLength(rightMost, 1),
490 res.inline,
491 false,
492 evalElement,
493 true);
494 }
495 }
496
497 }
498
499 }
500
501 } // End inner function removeFormatting
502
503 } // End inner function formatDOMExec
504
505 } // End exec
506});
507
508
509
510
Note: See TracBrowser for help on using the repository browser.