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 | */
|
---|
19 | bootstrap.provides("Selection");
|
---|
20 |
|
---|
21 | var _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 | })();
|
---|