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
|
---|
21 | bootstrap.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 |
|
---|