source: gs3-extensions/seaweed-debug/trunk/src/Selection.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: 49.2 KB
Line 
1/*
2 * file: Selection.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 */
19bootstrap.provides("Selection");
20
21var _toggleSectionHighlight;
22
23(function(){
24
25 /*
26 * The selection model
27 *
28 * Users can select anything in the document, even GUI's etc. The selection is not native, but is a emulated
29 * model which works on all browsers. The reason while it is emulated is because when the cursor module manipulates
30 * the dom around the clicked nodes - the native selection models fall over and the selection goes haywire on every platform.
31 *
32 * Usually in a typical content editor the cursor follows the end-of-selection. However this is confusing for users
33 * when a blinking cursor is outside editable sections since it suggests that users can edit non-editable html.
34 * To avoid this confusion the cursor is hidden when the selection contains non-editable content.
35 */
36
37 /*
38 * The selection start/end node/indexes are virtual. Virtual node/indexes are nodes/index in the document
39 * when there is no highlighed dom. Actual node/indexes are nodes/indexes in the document at the current state
40 * which is effected by highlighted dom.
41 */
42 var selStartNode = null,
43 selStartIndex = null,
44 selEndNode = null,
45 selEndIndex = null,
46 highlightFragment = null,
47 fragmentOpList = null,
48 formatOpList = null,
49 hightlightCSS = {
50 /*high: "#1C1C1C",
51 low: "#FFFFFF"*/
52 high: "#3B4B5B",
53 low: "#DFFFFF"
54 },
55 settingCursor = false,
56
57 /* Used for raising selection start/end MVC events. */
58 supressSelectionEvents = false,
59
60 /*
61 * Determines how many pixels away from a cursor's charactor/element
62 * the mouse pointer should be to re-evaluate a new position.
63 */
64 CURSOR_REEVALAUTE_TOLERANCE = 3,
65
66 /* Elements to let clicks fall through */
67 fallthroughElements = $createLookupMap("button,input,select,textarea"),
68
69 /* Inline elements which should not be included/bundled with a word (for word selection) */
70 wordBreakerInlines = $createLookupMap("br,button,img,iframe,map,object,select,textarea,applet"), /* TODO: REFACTOR- SHARE WITH WHITESPACE INTERNALS */
71
72 wordBreakerChars = /^\W$/, // TODO: Multilingual support - not just latin alphabet
73
74 /* Elements used for focus/selection stealing */
75 focusContainer, focusStealerEle,
76
77 /* True if the last mouse down was in a protected node. False if not. */
78 clickedProtectedNode;
79
80 $enqueueInit("Selection", function(){
81
82 // Make as subject
83 _model(de.selection);
84
85 // Disable selection in IE
86 if (typeof docBody.onselectstart != "undefined")
87 docBody.onselectstart = function(){
88 return de.events.consume(window.event)
89 };
90
91 var target = _engine == _Platform.GECKO ? window : document;
92
93 _addHandler(target, "mousedown", onMouseDown);
94 _addHandler(target, "mouseup", onMouseUp);
95 _addHandler(target, "mousemove", onMouseMove);
96 _addHandler(target, "dblclick", onDoubleClick);
97
98 // Consume ACCEL+A events to prevent select-all
99 _addHandler(document, "keydown", function(e) {
100
101 if (de.events.Keyboard.isAcceleratorDown(e)) {
102 if (e.keyCode == 65)
103 return false; // NB: Doesn't work in presto
104 }
105 });
106
107 // Whenever the cursor is set outside of this module, keep the selection synchronized.
108 de.cursor.addObserver({
109 onCursorChanged: function(cDesc){
110 if (settingCursor)
111 return;
112 if (cDesc) {
113 // Get the cursors virtual node/index
114 var vni = getVirtualNodeIndex(cDesc.domNode, getSelectionIndexFromCDesc(cDesc));
115
116 // Update the selection: If shift is down then set the new range, otherwise
117 // set selection as a single point.
118 if (de.events.current && de.events.current.shiftKey && selStartNode)
119 de.selection.setSelection(selStartNode, selStartIndex, vni.node, vni.index, false);
120 else
121 de.selection.setSelection(vni.node, vni.index, null, null, false);
122
123 } else
124 de.selection.clear();
125 }
126 });
127
128 // Always ensure that the selection is cleared before an action is executed/redone/undone
129 function onBeforeAction() {
130 de.selection.clear();
131 }
132
133 de.UndoMan.addObserver({
134 onBeforeExec : onBeforeAction,
135 onBeforeUndo : onBeforeAction,
136 onBeforeRedo : onBeforeAction
137 });
138
139 // Setup the focus steal element
140 focusContainer = $createElement("div");
141 _setClassName(focusContainer, _PROTECTED_CLASS);
142 focusContainer.innerHTML = '<input type="text" style="border-style:none"\>';
143 _setFullStyle(focusContainer, "position:absolute;width:1px;height:1px;display:none;z-index:-500");
144 focusStealerEle = focusContainer.firstChild;
145 docBody.appendChild(focusContainer);
146
147 }, "Cursor", "UndoMan");
148
149 /**
150 * @param {Event} e A mouse down dom event
151 */
152 function onMouseDown(e){
153
154 clickedProtectedNode = 0;
155
156 var targetNode = de.events.getEventTarget(e);
157
158 // Test if should let event fall through
159 var nodeName = _nodeName(targetNode);
160 if (targetNode && fallthroughElements[nodeName]) {
161
162 // Alow selection in text boxes
163 if (nodeName == "textarea" || (nodeName == "input" && targetNode.type == "text")) {
164
165 // Get ride of dedit selection/cursor - switch edit paradigm to native text box
166 de.selection.clear(); // clear selection
167 de.cursor.setCursor(null); // clear focus
168
169 }
170 return;
171 }
172
173 if (de.events.Mouse.isLeftDown()) {
174
175 // Ignore clicks in protected nodes
176 if (de.doc.isProtectedNode(targetNode)) {
177 clickedProtectedNode = 1;
178 return;
179 }
180
181 // Get the cursor position at the mouse x/y coord
182 var mousePos = de.events.getXYInWindowFromEvent(e),
183 targetCursorDesc = de.cursor.getCursorDescAtXY(mousePos.x, mousePos.y, targetNode);
184
185 if (!targetCursorDesc) {
186 // Clear any cursor/selection
187 de.cursor.setCursor(null); // Triggers MVC event and selection will update
188 return false;
189 }
190
191 // Is the user ranging a selection via the shift key?
192 if (e.shiftKey && selStartNode) {
193
194 // Try and the cursor at the click position
195 settingCursor = true; // Prevent updating selection due to cursor MVC events
196 de.cursor.setCursor(targetCursorDesc);
197 settingCursor = false;
198
199 // Update the selection
200 var vni = getVirtualNodeIndex(targetCursorDesc.domNode, getSelectionIndexFromCDesc(targetCursorDesc));
201 setSelection(selStartNode, selStartIndex, vni.node, vni.index, false);
202
203 } else { // User is clicking in the document
204
205 // Set the cursor at the click position
206 de.cursor.setCursor(targetCursorDesc); // Triggers MVC event and selection will update
207
208 // If the cursor was not supported at the target node, then the selection will have cleared... however
209 // we want to allow for the user to select outside of editable sections
210 if (!de.cursor.exists() && !de.doc.isProtectedNode(targetCursorDesc.domNode)) {
211 var vni = getVirtualNodeIndex(targetCursorDesc.domNode, getSelectionIndexFromCDesc(targetCursorDesc));
212 setSelection(vni.node, vni.index, null, null, false);
213 }
214
215 }
216
217 // Ensure that the document has focus... this will ensure that any input controls within the
218 // document or in the browser loses focus so user input is forwarded to direct edit
219
220 // Get the scrollbar state and set the focus stealer position in the viewport
221 // to avoid scrolling the document
222 var scrollPos = _getDocumentScrollPos();
223
224 // Position the float (container) at the top left of the viewport,
225 // but if the scroll bars are at zero, then place the float
226 // outside of the document... this will completely conceal the float
227 focusContainer.style.left = (scrollPos.left == 0 ? -50 : scrollPos.left + 10) + "px";
228 focusContainer.style.top = (scrollPos.top == 0 ? -50 : scrollPos.top + 10) + "px";
229 focusContainer.style.display = "";
230
231 focusStealerEle.focus();
232 focusStealerEle.select();
233
234 focusContainer.style.display = "none";
235
236 // Disable native selection
237 return false;
238 }
239
240 }
241
242 /**
243 * Implements manipulating of selection via dragging the mouse.
244 * @param {Event} e A mouse move dom event
245 */
246 function onMouseMove(e){
247
248 if (de.events.Mouse.isLeftDown() && selStartNode && !clickedProtectedNode) { // Is the user dragging the mouse - and changing the selection?
249
250 // Avoid firing many selection event whenever the selection changes while dragging
251 supressSelectionEvents = true;
252
253 var mousePos = de.events.getXYInWindowFromEvent(e),
254 curCDesc = de.cursor.getCurrentCursorDesc();
255
256 // Quick-check to see if the mouse pointer is not far from the current cursor
257 // to avoid re-evaluting the cursors poistion via the relatively expensive dual binsearch
258 // at every mouse move event:
259 if (curCDesc &&
260 mousePos.x >= (curCDesc.x - CURSOR_REEVALAUTE_TOLERANCE) &&
261 mousePos.x <= (curCDesc.x + curCDesc.width + CURSOR_REEVALAUTE_TOLERANCE) &&
262 mousePos.y >= (curCDesc.y - CURSOR_REEVALAUTE_TOLERANCE) &&
263 mousePos.y <= (curCDesc.y + curCDesc.height + CURSOR_REEVALAUTE_TOLERANCE)) {
264
265 var updateSelection = 0;
266
267 // See is isRightOf flag needs flipping
268 if (Math.abs(mousePos.x - curCDesc.x) < Math.abs(mousePos.x - (curCDesc.x + curCDesc.width))) {
269
270 // The mouse is closer to the left of charactor/element that the cursor is currently at
271 if (curCDesc.isRightOf) {
272 // Need to flip the rightOf flag
273 curCDesc.isRightOf = false; // flip
274 settingCursor = true;
275 de.cursor.setCursor(curCDesc);
276 settingCursor = false;
277 updateSelection = 1;
278
279 }
280
281 } else if (!curCDesc.isRightOf) {
282 // The mouse is closer to the right of charactor/element that the cursor is currently at..
283 // but the current cursor is to the left of it.
284 curCDesc.isRightOf = true; // flip
285 settingCursor = true;
286 de.cursor.setCursor(curCDesc);
287 settingCursor = false;
288 updateSelection = 1;
289 }
290
291 // Update the selection
292 if (updateSelection) {
293 var vni = getVirtualNodeIndex(curCDesc.domNode, getSelectionIndexFromCDesc(curCDesc));
294 setSelection(selStartNode, selStartIndex, vni.node, vni.index, false);
295 }
296
297 } else { // Re-evaluate the cursor's position via coordinates from the mouse event
298
299 curCDesc = de.cursor.getCursorDescAtXY(mousePos.x, mousePos.y, de.events.getEventTarget(e));
300
301 if (curCDesc && !de.doc.isProtectedNode(curCDesc.domNode)) {
302
303 var vni = getVirtualNodeIndex(curCDesc.domNode, getSelectionIndexFromCDesc(curCDesc));
304
305 // Update the selection
306 setSelection(selStartNode, selStartIndex, vni.node, vni.index);
307
308 }
309
310 }
311 }
312 }
313
314
315 /**
316 * Raises selection changed events for dragged selection
317 * @param {Object} e
318 */
319 function onMouseUp(e) {
320
321 // Was there any selection due to dragging the moust pointer?
322 if (selStartNode && supressSelectionEvents)
323 de.selection.fireEvent("SelectionChanged");
324
325 // Restore selection supression flag
326 supressSelectionEvents = false;
327 }
328
329
330 // TODO: Triple-click to select full phraise
331 function onDoubleClick(e) {
332
333 // Ignore clicks in protected nodes
334 if (!de.doc.isProtectedNode(de.events.getEventTarget(e))) {
335
336 de.selection.clear();
337
338 var mousePos = de.events.getXYInWindowFromEvent(e);
339 var cDesc = de.cursor.getCursorDescAtXY(mousePos.x, mousePos.y, de.events.getEventTarget(e));
340
341 // Double-clicked on anything to select?
342 if (cDesc) {
343
344 // Get the word the user selected on if any
345 var range = de.selection.getWordRangeAt(cDesc.domNode, cDesc.relIndex);
346 if (range) { // double clicked on word / space
347 // Set the new selection to select the word
348 setSelection(range.startNode, range.startIndex, range.endNode, range.endIndex);
349
350 }
351
352 }
353 }
354
355 return false; // TODO: DOES Disable selection in safari?
356 }
357
358 // See namespace docs
359 function getVirtualNodeIndex(node, index) {
360 if (highlightFragment)
361 return highlightFragment.getOriginalNodeIndex(node, index);
362 return {node:node,index:index};
363 }
364
365 // See namespace docs
366 function getActualNodeIndex(node, index) {
367
368 if (highlightFragment)
369 return highlightFragment.getAdjustedNodeIndex(node, index);
370 return {node:node,index:index};
371
372 }
373
374 /**
375 * To be used when wanting the highlighted DOM cleared.
376 * ONLY TO BE USED IN BRIEF MOMENTS: Always call the false then truee, never leave
377 * in uneven state.
378 *
379 * @param {Boolean} on True to restore highlighting, false to clear any.
380 */
381 _toggleSectionHighlight = function(on) {
382
383 if (on && highlightFragment) {
384 // Restore highlight css
385 _redoOperations(formatOpList);
386 } else if (!on && highlightFragment) {
387 // Remove highlight formatting
388 _undoOperations(formatOpList);
389 }
390
391 }
392
393
394 /**
395 * Visually highlights the current selection. Assumes that there is no selection highlight formatting.
396 * Updates the cursor.
397 */
398 function highlightSelection(){
399
400 debug.assert(formatOpList == null && fragmentOpList == null && highlightFragment == null);
401 debug.assert(selStartNode != null && selEndNode != null);
402
403 // Gets range in left-to-right order
404 var selRange = de.selection.getRange(true);
405
406 debug.assert(!_getOperations());
407
408 try {
409
410 // Get the cursor cursor state
411 var curCDesc = de.cursor.getCurrentCursorDesc();
412
413 highlightFragment = _buildFragment(_getCommonAncestor(selRange.startNode, selRange.endNode),
414 selRange.startNode,
415 selRange.startIndex,
416 selRange.endNode,
417 selRange.endIndex);
418
419 // Get fragment build operations
420 fragmentOpList = _getOperations() || [];
421
422 // Apply the CSS Highlighting
423 highlightFragment.visit(function(frag){
424
425 if (!frag.isShared) {
426
427 var domNode = frag.node, highlightNode = null;
428
429 if (domNode.nodeType == Node.ELEMENT_NODE)
430 highlightNode = domNode;
431
432 else if (domNode.nodeType == Node.TEXT_NODE && frag.parent.isShared && _doesTextSupportNonWS(domNode)) {
433
434 // If this non shared fragment is a text node who's parent is shared, then
435 // in order to format this node then spans will be added as its parent
436 highlightNode = $createElement("span");
437 highlightNode.className = "dehighlight-node";
438
439 // Add the format span and move the text node into it
440 _execOp(_Operation.INSERT_NODE, highlightNode, domNode.parentNode, _indexInParent(domNode));
441 _execOp(_Operation.REMOVE_NODE, domNode);
442 _execOp(_Operation.INSERT_NODE, domNode, highlightNode);
443 }
444
445 if (highlightNode) {
446
447 // TODO : Use Classes instead?? although setting actual style will have best precedence
448
449 // Get background color for this element
450 /*var bgColor, bgNode = highlightNode;
451 do {
452 bgColor = _getComputedStyle(bgNode, "background-color");
453 bgNode = bgNode.parentNode;
454 } while (bgNode && bgColor == "transparent"); // CSS Defaut for background color
455
456 if (bgColor && bgColor != "") {
457 bgColor = _getColorRGB(bgColor);
458 } else
459 bgColor = [255, 255, 255];
460
461 // Get bg color brightness
462 var intensity = ((bgColor[0] / 255) + (bgColor[1] / 255) + (bgColor[2] / 255)) / 3;
463
464
465 // Override/set background and foreground color style for this element
466 _execOp(_Operation.SET_CSS_STYLE, highlightNode, "backgroundColor", intensity >= 0.5 ? hightlightCSS.high : hightlightCSS.low);
467 _execOp(_Operation.SET_CSS_STYLE, highlightNode, "color", intensity >= 0.5 ? hightlightCSS.low : hightlightCSS.high);
468 */
469 // ABOVE KILLS PERFORMANCE
470
471 // if (!_isBlockLevel(highlightNode) || !_isAncestor(highlightNode, selRange.endNode)) {
472 _execOp(_Operation.SET_CSS_STYLE, highlightNode, "backgroundColor", hightlightCSS.high);
473 _execOp(_Operation.SET_CSS_STYLE, highlightNode, "color", hightlightCSS.low);
474 // }
475
476 }
477 }
478
479 }); // End visiting all fragments
480
481 // Get the formatting operations
482 formatOpList = _getOperations() || [];
483
484 // Update the new cursor pos -If there is a cursor, then its node/index may need updating
485 if (curCDesc) {
486 var cursorANI = getActualNodeIndex(curCDesc.domNode, curCDesc.relIndex);
487 if (cursorANI.node != curCDesc.domNode || cursorANI.index != curCDesc.relIndex) {
488 curCDesc.domNode = cursorANI.node;
489 curCDesc.relIndex = cursorANI.index;
490 settingCursor = true;
491 de.cursor.setCursor(curCDesc);
492 settingCursor = false;
493 }
494 }
495
496 } catch (e) {
497 settingHighlight = false;
498 selStartNode = selEndNode = highlightFragment = null;
499 formatOpList = null;
500 throw e;
501 }
502
503 }
504
505 /**
506 * Un-highlights any previous highlighting if there is any.
507 * Updates the cursor.
508 */
509 function unHighlightSelection(){
510
511 if (highlightFragment) {
512
513 // Keep the cursor updated
514 var curCDesc = de.cursor.getCurrentCursorDesc();
515 var cursorVNI = curCDesc ? getVirtualNodeIndex(curCDesc.domNode, curCDesc.relIndex) : null;
516
517 _undoOperations(formatOpList);
518 _undoOperations(fragmentOpList);
519 formatOpList = fragmentOpList = highlightFragment = null;
520
521 // If there was a cursor, then update its node / index due to highlighting
522 if (cursorVNI && (cursorVNI.node != curCDesc.domNode || cursorVNI.index != curCDesc.relIndex)) {
523 curCDesc.domNode = cursorVNI.node;
524 curCDesc.relIndex = cursorVNI.index;
525 settingCursor = true;
526 de.cursor.setCursor(curCDesc);
527 settingCursor = false;
528 }
529 }
530 }
531
532 /*
533 * See namespace docs
534 */
535 function setSelection(startNode, startIndex, endNode, endIndex, updateCursor) {
536
537 debug.assert(!startNode || (startNode && typeof(startIndex) == "number"));
538 debug.assert(!endNode || (endNode && typeof(endIndex) == "number"));
539
540 // Should the selection be cleared?
541 if (!startNode) {
542 de.selection.clear(updateCursor); // Fires selection changed
543 return;
544 }
545
546 // If the start and end node/index is the same, then nullify the end point.
547 if (startNode == endNode && startIndex == endIndex)
548 endNode = null;
549
550 // See if selection needs updating
551 if (startNode == selStartNode && startIndex == selStartIndex &&
552 ((!endNode && !selEndNode) || (endNode == selEndNode && endIndex == selEndIndex)))
553 return;
554
555 // Clear selection highlight if any
556 unHighlightSelection();
557
558 // Set selection model (all virtual)
559 selStartNode = startNode;
560 selStartIndex = startIndex;
561 selEndNode = endNode;
562 selEndIndex = endIndex;
563
564 // If the selection range has an end point then highlight it,
565 // and adjust the range to exclude any protected nodes
566 if (selEndNode) {
567
568 var startOccursFirst = doesStartOccurFirst(),
569 procContainer;
570
571 // Adjust selection start/end to exclude protected nodes
572 while(selStartNode) {
573 procContainer = de.doc.getProtectedNodeContainer(selStartNode);
574 if (procContainer) {
575 selStartNode = (startOccursFirst ? procContainer.nextSibling : procContainer.previousSibling);
576 if (selStartNode)
577 selStartIndex = startOccursFirst ? 0 : _nodeLength(selStartNode, 1);
578 } else break;
579 }
580 while(selEndNode) {
581 procContainer = de.doc.getProtectedNodeContainer(selEndNode);
582 if (procContainer) {
583 selEndNode = (startOccursFirst ? procContainer.previousSibling : procContainer.nextSibling);
584 if (selEndNode)
585 selEndIndex = startOccursFirst ? _nodeLength(selEndNode, 1) : 0;
586 } else break;
587 }
588
589 var isValid = selStartNode && selEndNode;
590 if (isValid) {
591
592 isValid = false; // Verify valid range
593
594 // Relocate protected nodes if they fall into the range
595 var relocateProcContainers = [];
596 _visitAllNodes(_getCommonAncestor(selStartNode, selEndNode), selStartNode, startOccursFirst, function(domNode){
597
598 procContainer = de.doc.getProtectedNodeContainer(domNode);
599
600 debug.assert(!procContainer || (procContainer && (domNode != selStartNode && domNode != selEndNode)));
601
602 if (procContainer) relocateProcContainers.push(procContainer);
603
604 // Stop the traversal when reached the selection end node
605 if (domNode == selEndNode) {
606 isValid = true; // Flag as valid
607 return false;
608 }
609 });
610 }
611
612 if (!isValid) {
613 //debug.println("WARNING: Attempt to set selection range within a protected node");
614 de.selection.clear(updateCursor);
615 return;
616 }
617
618 // Relocate protected containers so they are outside of the selection
619 for (var i in relocateProcContainers) {
620 var pc = relocateProcContainers[i];
621 if (pc.parentNode) // TODO: AND NOT DOCUMENT_FRAGMENT_NODE ??
622 pc.parentNode.removeChild(pc);
623 }
624
625 for (var i in relocateProcContainers) {
626 var pc = relocateProcContainers[i];
627 if (!pc.parentNode)
628 docBody.appendChild(pc);
629 }
630
631 // Visually highlight range
632 highlightSelection();
633
634 }
635
636 // Should the cursor be adjusted to be placed at the start/end of the new range?
637 if (updateCursor !== false) {
638
639 var newCursor = null;
640
641 // Only set cursor if the range is editable
642 if (de.selection.isRangeEditable()) {
643
644 var ani = selEndNode ?
645 getActualNodeIndex(selEndNode, selEndIndex) :
646 getActualNodeIndex(selStartNode, selStartIndex);
647
648 // Adjust index if at end of text run
649 var isRightOf = false;
650 if (ani.node.nodeType == Node.TEXT_NODE && ani.index >= _nodeLength(ani.node)) {
651 isRightOf = true;
652 ani.index--;
653 }
654
655 //newCursor = de.cursor.createCursorDesc(ani.node, ani.index, isRightOf);
656 newCursor = de.cursor.getNearestCursorDesc(ani.node, ani.index, isRightOf, true);
657 }
658
659 // Set the cursor
660 settingCursor = true;
661 de.cursor.setCursor(newCursor);
662 settingCursor = false;
663
664 }
665
666 // Fire selection ended event
667 if (!supressSelectionEvents)
668 de.selection.fireEvent("SelectionChanged");
669
670
671 }
672
673 /**
674 * @param {de.cursor.CursorDescriptor} cDesc A cursor descrptor
675 * @return {Number} The selection index of the given cursor.
676 */
677 function getSelectionIndexFromCDesc(cDesc){
678 var index = cDesc.relIndex;
679 if (cDesc.isRightOf && cDesc.domNode.nodeType == Node.TEXT_NODE)
680 index ++;
681 return index;
682 }
683
684 /**
685 * @return {Boolean} True if the selection start occurs before the selection end.
686 */
687 function doesStartOccurFirst() {
688 if (!selEndNode)
689 return true;
690
691 if (selStartNode == selEndNode)
692 return selStartIndex < selEndIndex;
693
694 // Convert sel start/end virtual range to actual dom nodes
695 var actualStart = getActualNodeIndex(selStartNode, selStartIndex).node,
696 actualEnd = getActualNodeIndex(selEndNode, selEndIndex).node;
697
698 var startOccursFirst = false;
699 _visitAllNodes(docBody, actualStart, true, function(domNode){
700 startOccursFirst = (domNode == actualEnd);
701 return !startOccursFirst;
702 });
703
704 return startOccursFirst;
705 }
706
707
708 /**
709 * @namespace
710 * Cross-browser DEdit-specific selection. Implemented as a continuous selection model.
711 */
712 de.selection = {
713
714 /**
715 * Sets a new selection.
716 *
717 * @param {Node} startNode The starting dom node of the selection range.
718 *
719 * @param {Number} startIndex The inclusive start index in the start node.
720 * Ranges from 0 to the text length for text nodes.
721 * Where 0 indicates that the range begins at the first char, and text length
722 * indicates that the range begins directly after the text node, but not including it.
723 * <br>
724 * Ranges from 0 to 1 for elements.
725 * Where 0 indicates that the range includes the element and its decendants,
726 * and 1 indicates that the range excludes the element and its decendants.
727 *
728 *
729 * @param {Node} endNode The ending dom node of the selection range.
730 *
731 * @param {Number} endIndex The inclusive end index in the end node.
732 * Ranges from 0 to the text length for text nodes.
733 * Where 0 indicates that the range ends at the first char, and text length
734 * indicates that the range ends directly after the text node, but not including it.
735 * <br>
736 * Ranges from 0 to 1 for elements.
737 * Where 0 indicates that the range includes the element and its decendants,
738 * and 1 indicates that the range excludes the element and its decendants.
739 */
740 setSelection: setSelection,
741
742 /**
743 * Clears any current selection in the document.
744 * Implementaion Note: If there is highlighting, the the DOM will be manipulated and the cursor will be
745 * updated.
746 * @param {Boolean} updateCursor (optional) False to supress updating the cursor. Otherwise will destroy the current cursor.
747 */
748 clear: function(updateCursor){
749
750 // Restore any highlighting
751 unHighlightSelection();
752 // Nullify range
753 selStartNode = selEndNode = null;
754
755 // Update the cursor
756 if (updateCursor !== false) {
757 settingCursor = true;
758 de.cursor.setCursor(null);
759 settingCursor = false;
760 }
761
762 if (!supressSelectionEvents)
763 de.selection.fireEvent("SelectionChanged");
764
765 },
766
767 /**
768 * Retreives the selection range.
769 *
770 * @param {Boolean} inOrder True to get the range in left-to-right traversal order,
771 * False to get the range as it is (i.e. the selection end may physically appear before the selection start).
772 *
773 * @return {Object} The current selections range in the document. Null if there is none.
774 * The selection range will have the following members:
775 * <br>
776 * startNode - the dom node of the beginning of the selection
777 * <br>
778 * startIndex - the index of the beginning of the selection.
779 * <br>
780 * endNode - the dom node of the end of the selection.
781 * May not be present - if not, then the selection does not range for more than one charactor/element
782 * (i.e. no highlighting present).
783 * <br>
784 * endIndex - the index of the end of the selection.
785 * May not be present - if not, then the selection does not range for more than one charactor/element
786 * (i.e. no highlighting present).
787 * <br>
788 * inOrder - True if the start tuple occurs before the end tuple wrt in-order traversal.
789 *
790 */
791 getRange: function(inOrder) {
792
793 if (!selStartNode) return null;
794
795 if (selEndNode) { // Is there selection ranging beyond one charactor/element?
796
797 var startOccursFirst = doesStartOccurFirst();
798
799 // Determine whether the start point occurs before the end point
800 var declareStartFirst = !inOrder || startOccursFirst;
801
802
803 range = {
804 inOrder : inOrder || (startOccursFirst == declareStartFirst),
805 startNode: declareStartFirst ? selStartNode : selEndNode,
806 startIndex: declareStartFirst ? selStartIndex : selEndIndex,
807 endNode: declareStartFirst ? selEndNode : selStartNode,
808 endIndex: declareStartFirst ? selEndIndex : selStartIndex
809 };
810
811 } else {
812 range = {
813 startNode: selStartNode,
814 startIndex: selStartIndex
815 };
816 }
817
818 return range;
819
820 },
821
822 /**
823 * @return {Boolean} True if there is selection which is all editable. False if there is no selection,
824 * or the selection contains non-editable content.
825 */
826 isRangeEditable : function() {
827
828 if (selStartNode) {
829 var actualStart = getActualNodeIndex(selStartNode, selStartIndex).node;
830 var actualEnd = selEndNode ? getActualNodeIndex(selEndNode, selEndIndex).node : actualStart;
831 var ca = _getCommonAncestor(actualStart, actualEnd, true);
832 return (actualStart != actualEnd && de.doc.isEditSection(ca)) || de.doc.isNodeEditable(ca);
833 }
834
835 return false;
836 },
837
838 /**
839 * @return {String} "highlight" if there is a highlighted selection,
840 * "single" if their is only a cursor present (no highlighted range)
841 * Null if there is nothing selected.
842 */
843 getState : function() {
844 if (highlightFragment)
845 return "range";
846 if (selStartNode)
847 return "single";
848 return null;
849 },
850
851 /**
852 * Converts the given node index into an actual tuple.
853 *
854 * An actual tuple is a node and index which is valid while selectoin highlighting is present.
855 * (Selection highlighting can create new nodes).
856 *
857 * @param {Node} node The node to convert
858 *
859 * @param {Number} index The index to convert
860 *
861 * @return {Object} An node/index tuple of the actual values.
862 */
863 getActualNodeIndex : getActualNodeIndex,
864
865 /**
866 * Converts the given node index into a virtual tuple.
867 *
868 * A virtual tuple is a node and index which is valid when all selection highlighting is removed.
869 * (Selection highlighting can create new nodes).
870 *
871 * @param {Node} node The node to convert
872 *
873 * @param {Number} index The index to convert
874 *
875 * @return {Object} An node/index tuple of the virtual values.
876 *
877 */
878 getVirtualNodeIndex : getVirtualNodeIndex,
879
880
881 setHightlightCSS: function(){
882
883 // TODO..
884
885 },
886
887
888 /**
889 * Removes the current selection from the document (which is undoable - automatically added to undo manager).
890 * @return {Boolean} True if there was something selected and therefore removed.
891 * False if there was nothing to remove.
892 *
893 * @throws Error if range is not editable.
894 *
895 * @see de.selection.isRangeEditable
896 */
897 remove : function() {
898
899 var selRange = this.getRange(true);
900
901 if (selRange && selRange.endNode) {
902
903 if (!this.isRangeEditable())
904 _error("Attempt to remove selection which contains uneditable content");
905
906 // Execute the removal action
907 de.UndoMan.execute(
908 "RemoveDOM",
909 selRange.startNode,
910 selRange.startIndex,
911 selRange.endNode,
912 selRange.endIndex);
913
914 return true;
915 }
916
917 return false;
918
919 },
920
921 /**
922 * @return {Node} A copy of the selection in the document if there is anything highlighted.
923 * Null if there is nothing highlighted.
924 */
925 getHighlightedDOM : function() {
926
927 // Is there anything highlighted?
928 if (highlightFragment) {
929
930 // Get rid of highlight CSS
931 _undoOperations(formatOpList);
932
933 var highlightRoot = (function trav(frag){
934
935 // Shallow copy the fragment's dom node
936 var clonedNode = frag.node.cloneNode(false);
937
938 // Recurse into fragments children and build up cloned dom tree
939 for (var i in frag.children) {
940 var child = trav(frag.children[i]);
941 clonedNode.appendChild(child);
942 }
943
944 return clonedNode;
945
946 })(highlightFragment);
947
948 // Restore highlight CSS
949 _redoOperations(formatOpList);
950
951 return highlightRoot;
952
953 }
954
955 return null;
956 },
957
958 /**
959 * Selects all content in an editable section.
960 *
961 * @param {Node} targetES (Optional) An editable selection to select. If not provided the
962 * current cursor's editable section owner will be selected.
963 */
964 selectAll: function(targetES) {
965
966 // If target editable section not provided then get ES at current cursor position
967 if (!targetES) {
968 var cDesc = de.cursor.getCurrentCursorDesc();
969 if (cDesc)
970 targetES = de.doc.getEditSectionContainer(cDesc.domNode);
971 }
972
973 // Anything to select?
974 if (targetES) {
975
976 de.selection.setSelection(
977 targetES.firstChild,
978 0,
979 targetES.lastChild,
980 _nodeLength(targetES.lastChild, 1));
981 }
982 },
983
984 /**
985 * Selects the start or end of an editable sectoin
986 * @param {Element} esEle An editable sectoin
987 * @param {Boolean} start True to select the beginning of the editable section, false for the end
988 */
989 selectES : function(esEle, start) {
990
991 debug.assert(de.doc.isEditSection(esEle));
992
993 var deepNode = esEle;
994 if (start) {
995 while (deepNode.firstChild) {
996 deepNode = deepNode.firstChild;
997 }
998 } else {
999 while (deepNode.lastChild) {
1000 deepNode = deepNode.lastChild;
1001 }
1002 }
1003
1004 var cDesc = de.cursor.getNearestCursorDesc(deepNode, start ? 0 : _nodeLength(deepNode, 1), !start, !start);
1005
1006 if (cDesc)
1007 de.cursor.setCursor(cDesc);
1008 },
1009
1010 /**
1011 * @param {Node} node A dome node
1012 * @param {Index} index An index (ranges from 0-text-length-1 for text nodes).
1013 * @return {Object} The range of a word at the given index - null if there is no word.
1014 */
1015 getWordRangeAt : function(node, index) {
1016
1017 if (node.nodeType == Node.TEXT_NODE) {
1018 var range = {
1019 startNode:node,
1020 startIndex:index,
1021 endNode:node,
1022 endIndex:index
1023 };
1024
1025 var checkESBoundry = de.doc.isNodeEditable(node);
1026
1027 // Expand start backward to first occurance whitespace / block exclusive
1028 _visitAllNodes(docBody, node, false, function(domNode) {
1029
1030 if (domNode != node && _findAncestor(domNode, _getCommonAncestor(domNode, node, true), isWordBreakElement, true))
1031 return false; // Abort when break out of inline group
1032 else if (domNode.nodeType == Node.TEXT_NODE) { // Scan for whitespace - extend range backward
1033 for (var i = domNode == node ? index : _nodeLength(domNode) - 1; i >= 0; i--) {
1034 var c = domNode.nodeValue.charAt(i);
1035 //if (_isAllWhiteSpace(c) || c == _NBSP)
1036 if (wordBreakerChars.test(c))
1037 return false;
1038 range.startNode = domNode;
1039 range.startIndex = i;
1040 }
1041
1042 }
1043
1044 });
1045
1046 // Expand start forward to first occurance whitespace / block exclusive
1047 _visitAllNodes(docBody, node, true, function(domNode) {
1048
1049 if (domNode != node && (isWordBreakElement(domNode) || _findAncestor(node, _getCommonAncestor(domNode, node, true), isWordBreakElement, true)))
1050 return false; // Abort when break out of inline group
1051
1052 if (domNode.nodeType == Node.TEXT_NODE) { // Scan for whitespace - extend range backward
1053 for (var i = domNode == node ? index : 0; i < _nodeLength(domNode); i++) {
1054 var c = domNode.nodeValue.charAt(i);
1055 //if (_isAllWhiteSpace(c) || c == _NBSP)
1056 if (wordBreakerChars.test(c))
1057 return false;
1058 range.endNode = domNode;
1059 range.endIndex = i;
1060 }
1061
1062 }
1063
1064 });
1065
1066 range.endIndex++;
1067
1068 return range;
1069
1070 }
1071
1072 function isWordBreakElement(domNode) {
1073 return _isBlockLevel(domNode) || wordBreakerInlines[_nodeName(domNode)] || (checkESBoundry && de.doc.isEditSection(domNode));
1074 }
1075 },
1076
1077 /**
1078 *
1079 * @param {[String]} formatTypes The format types to poll. Case insensitive. See format dom action for format type list
1080 *
1081 * @return {Object} The edit state
1082 */
1083 getEditState : function(formatTypes) {
1084
1085 var state = {
1086 formatStates : {},
1087 inlineContainerType : null,
1088 textAlign : null,
1089 blockQuote : false
1090 };
1091
1092 // Is highlight?
1093 if (highlightFragment) {
1094
1095 // Remove highlight formatting
1096 _undoOperations(formatOpList);
1097
1098 // Traverse through nodes ... checking inlines for computed CSS and for block-parent types / links
1099 (function trav(frag){
1100 updateState(frag.node);
1101 })(highlightFragment);
1102
1103 // Restore highlight css
1104 _redoOperations(formatOpList);
1105
1106 }
1107
1108 // Is there any selection - and is it a textnode/inline node?
1109 if (selStartNode && (selStartNode.nodeType == Node.TEXT_NODE || _isInlineLevel(selStartNode)))
1110 updateState(selStartNode.nodeType == Node.TEXT_NODE ? selStartNode.parentNode : selStartNode);
1111
1112 // Return the state
1113 return state;
1114
1115 function isBlockQuote(domNode) {
1116 return _nodeName(domNode) == "blockquote";
1117 }
1118
1119 function updateState(node) {
1120
1121 if (_isInlineLevel(node)) {
1122
1123 var eProps = de.doc.getEditProperties(node) || {};
1124
1125 // TODO: Format filtering here? Or at another place - i.e. via keystrokes wouldnt use this function...
1126 // Could have in both places... could always do it in actions
1127
1128
1129 // Determine format states
1130 for (var i in formatTypes) {
1131 var fType = formatTypes[i].toLowerCase();
1132
1133 // Determine formattings
1134 if (!state.formatStates[fType] || state.formatStates[fType] != "mixed") {
1135
1136 var current = node, wasFound = false;
1137 while (current != docBody && de.doc.isNodeEditable(current)) { // for each editable ancestor up to the document body
1138 // Evaluate specific format state on specific node
1139 var res = _formatEnvironment[fType + "Eval"](current);
1140
1141 if (res) {
1142 if (typeof state.formatStates[fType] == "undefined")
1143 state.formatStates[fType] = res.value; // Set first time
1144
1145 else if (state.formatStates[fType] !== res.value) state.formatStates[fType] = "mixed";
1146
1147 wasFound = true;
1148 break;
1149 }
1150 current = current.parentNode;
1151
1152 } // End loop: scanning ancestors formatting
1153
1154 // Was the current format type found?
1155 if (!wasFound)
1156 state.formatStates[fType] = (state.formatStates[fType] === null || typeof state.formatStates[fType] == "undefined") ? null : "mixed";
1157
1158
1159 }
1160
1161 } // End loop: evaluating format states
1162
1163
1164 }
1165
1166 if (node.nodeType == Node.ELEMENT_NODE) {
1167
1168 // Evaluate text alignment
1169 if (state.textAlign != "mixed") {
1170
1171 var alignment = _getComputedStyle(node, "text-align");
1172
1173 if (!alignment)
1174 alignment = "start";
1175
1176 // Left/Right value based on start/end is conditional - depends on if browser is LTR OR RTL
1177 if (alignment == "start")
1178 alignment = _localeDirection == "rtl" ? "right" : "left";
1179 else if (alignment == "start") alignment = _localeDirection == "rtl" ? "left" : "right";
1180
1181 if (!state.textAlign)
1182 state.textAlign = alignment;
1183 else if (alignment != state.textAlign) state.textAlign = "mixed";
1184
1185 }
1186
1187
1188 // Evaluate inline container type
1189 if (state.inlineContainerType != "mixed") {
1190
1191 var iCon = _isBlockLevel(node) ? node : _findAncestor(node, docBody, _isBlockLevel, true);
1192
1193 if (!de.doc.isNodeEditable(iCon))
1194 iCon = null;
1195
1196 var cType = iCon ? _nodeName(iCon) : "none";
1197
1198 if (!state.inlineContainerType)
1199 state.inlineContainerType = cType;
1200
1201 else if (cType != state.inlineContainerType)
1202 state.inlineContainerType = "mixed";
1203
1204 }
1205
1206 // Evaluate blockquote state
1207 if (!state.blockQuote) {
1208
1209 var bq = isBlockQuote(node) ? node : _findAncestor(node, docBody, isBlockQuote, true);
1210
1211 if (de.doc.isNodeEditable(bq))
1212 state.blockQuote = true;
1213 }
1214
1215
1216 }
1217
1218
1219
1220 }
1221
1222 }
1223
1224
1225 };
1226
1227})();
Note: See TracBrowser for help on using the repository browser.