Changeset 1335 for trunk/modules/Gecko
- Timestamp:
- 02/03/18 07:37:27 (23 months ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/modules/Gecko/paraHandlerBest.js
r694 r1335 30 30 // "constants" 31 31 32 // Set up the node type constants for browsers that don't support them. 33 var ELEMENT_NODE = ELEMENT_NODE || 1; 34 var ATTRIBUTE_NODE = ATTRIBUTE_NODE || 2; 35 var TEXT_NODE = TEXT_NODE || 3; 36 var COMMENT_NODE = COMMENT_NODE || 8; 37 var DOCUMENT_NODE = DOCUMENT_NODE || 9; 32 38 /** 33 39 * Whitespace Regex … … 47 53 48 54 EnterParagraphs.prototype._pContainers = /^(body|del|div|fieldset|form|ins|map|noscript|object|td|th)$/i; 55 EnterParagraphs.prototype._pWrapper = /^(body|d[ltd]|table|[uo]l|div|p|h[1-6]|li|t[hrd]|del|fieldset|ins|form|map|noscript|object|address|blockquote|pre)$/i; 49 56 50 57 /** … … 67 74 68 75 /** 69 * Elements which should get a new P, before or after, when enter is pressed at either end76 * When the cursor is at the inside edge of one of these elements, we will move the cursor just outside the element, and insert a P element there. 70 77 */ 71 78 … … 74 81 75 82 /** 76 * Elements which should get a new P, before or after a close parent, when enter is pressed at either end 83 * When the cursor is at the inside edge of one of these elements, and this element's outside edge is just at the inside edge of its immediate parent, 84 * we will move the cursor to the outside edge of the immediate parent, and insert a P element there. 77 85 */ 78 86 … … 430 438 { 431 439 432 var next = function(element, search_direction) 433 { 440 var next = function(element, search_direction) { 434 441 return ( search_direction == "left" ? element.previousSibling : element.nextSibling ); 435 442 }; 436 443 437 444 var node = search_direction == "left" ? rng.startContainer : rng.endContainer; … … 442 449 // be on the exclusion list and we wouldn't know until it was too late 443 450 444 while ( start.nodeType == 1 && !this._permEmpty.test(start.nodeName) ) 445 { 451 while ( start.nodeType == 1 && !this._permEmpty.test(start.nodeName) ) { 446 452 start = ( offset ? start.lastChild : start.firstChild ); 447 453 } … … 458 464 // sometimes this loop finds a blank text node, sometimes it doesn't. 459 465 460 while ( roam = roam ? ( next(roam,search_direction) ? next(roam,search_direction) : roam.parentNode ) : start )461 {462 466 roam = roam ? ( next(roam,search_direction) ? next(roam,search_direction) : roam.parentNode ) : start; 467 while (roam) { 468 463 469 // next() is an inline function defined above that returns the next node depending 464 470 // on the direction we're searching. 465 466 if ( next(roam,search_direction) ) 467 { 468 471 472 if ( next(roam,search_direction) ) { 473 469 474 // If the next sibling's on the exclusion list, stop before it 470 471 if ( this._pExclusions.test(next(roam,search_direction).nodeName) ) 472 { 473 475 476 if ( this._pExclusions.test(next(roam,search_direction).nodeName) ) { 477 474 478 return this.processRng(rng, search_direction, roam, next(roam,search_direction), (search_direction == "left"?'AfterEnd':'BeforeBegin'), true, false); 475 479 } 476 } 477 else 478 { 479 480 } else { 481 480 482 // If our parent's on the container list, stop inside it 481 482 if (this._pContainers.test(roam.parentNode.nodeName)) 483 { 484 483 484 if (this._pContainers.test(roam.parentNode.nodeName)) { 485 485 486 return this.processRng(rng, search_direction, roam, roam.parentNode, (search_direction == "left"?'AfterBegin':'BeforeEnd'), true, false); 486 } 487 else if (this._pExclusions.test(roam.parentNode.nodeName)) 488 { 489 487 } else if (this._pExclusions.test(roam.parentNode.nodeName)) { 488 490 489 // chop without wrapping 491 492 if (this._pBreak.test(roam.parentNode.nodeName)) 493 { 494 490 491 if (this._pBreak.test(roam.parentNode.nodeName)) { 492 495 493 return this.processRng(rng, search_direction, roam, roam.parentNode, 496 494 (search_direction == "left"?'AfterBegin':'BeforeEnd'), false, (search_direction == "left" ?true:false)); 497 } 498 else 499 { 500 495 } else { 496 501 497 // the next(roam,search_direction) in this call is redundant since we know it's false 502 498 // because of the "if next(roam,search_direction)" above. … … 504 500 // the final false prevents this range from being wrapped in <p>'s most likely 505 501 // because it's already wrapped. 506 502 507 503 return this.processRng(rng, 508 504 search_direction, … … 515 511 } 516 512 } 513 roam = roam ? ( next(roam,search_direction) ? next(roam,search_direction) : roam.parentNode ) : start; 517 514 } 518 515 … … 781 778 if (ev.keyCode == 13 && !ev.shiftKey && this.editor._iframe.contentWindow.getSelection) 782 779 { 783 return this. handleEnter(ev);780 return this.breakLine(ev, this.editor._doc); 784 781 } 785 782 … … 789 786 790 787 /** 788 * Helper function to find the index of the given node with its parent's 789 * childNodes array. If there is any problem with the lookup, we'll return 790 * NULL. 791 */ 792 793 EnterParagraphs.prototype.indexInParent = function (el) 794 { 795 if (!el.parentNode || !el.parentNode.childNodes) 796 { 797 // The element is at the root of the tree, or it's a broken node. 798 return null; 799 } 800 801 for (var index=0; index<el.parentNode.childNodes.length; ++index) 802 { 803 if (el == el.parentNode.childNodes[index]) 804 { 805 return index; 806 } 807 } 808 809 // This will only happen if the DOM node is broken... 810 return null; 811 } 812 813 /* 814 * Determine if a cursor points to the end of it's containing node. 815 */ 816 EnterParagraphs.prototype.cursorAtEnd = function (cursorNode, cursorOffset) 817 { 818 if (cursorNode.nodeType == TEXT_NODE) 819 { 820 if (cursorOffset == cursorNode.nodeValue.length) 821 { 822 return true; 823 } 824 // We're in the middle of a text node. If the node is a whitespace node, 825 // we'll ignore it and treat it as if the cursor were after the node, and 826 // not in it. 827 if (/\S/.test(cursorNode.nodeValue)) 828 { 829 return false; 830 } 831 cursorOffset = this.indexInParent(cursorNode) + 1; 832 cursorNode = cursorNode.parentNode; 833 834 // We need to make sure we there wasn't an error in indexInParent 835 if (cursorOffset === null) 836 { 837 return false; 838 } 839 } 840 // The easy case, it's after the last node... 841 if (cursorOffset == cursorNode.childNodes.length) 842 { 843 return true; 844 } 845 // At this point, if the pointed to node is a whitespace node, and all of 846 // it's nextSiblings are also whitespace node, then the cursor is at the end 847 // of the node. 848 for (var node = cursorNode.childNodes[cursorOffset]; node; node = node.nextSibling) 849 { 850 if ((node.nodeType != TEXT_NODE) || (/\S/.test(node.nodeValue))) 851 { 852 return false; 853 } 854 } 855 return true; 856 } 857 /* 858 * Test suite for this, because it's really tough to get right. 859 */ 860 EnterParagraphs.RunTests = function(xinha, debug) 861 { 862 var test = function(message, before, cursorBefore, after, cursorAfter, cursorAfter2) { 863 console.group('Test: ', message); 864 if (before !== null) { 865 xinha.setHTML(before); 866 } 867 // Do something 868 var cAnchor, cOffset; 869 870 var mockEvent = { 871 preventDefault: function() {if (debug) console.log("Preventing default.");}, 872 stopPropagation: function() {if (debug) console.log("Stopping propagation.");}, 873 } 874 function setCursor(commands) { 875 cAnchor = xinha._doc.body; 876 cOffset = 0; 877 try { 878 for (var index=0; index<commands.length; ++index) { 879 var command = commands[index]; 880 if ('id' == command[0]) { 881 cAnchor = xinha._doc.getElementById(command[1]); 882 } else if ('firsttag' == command[0]) { 883 cAnchor = xinha._doc.getElementsByTagName(command[1])[0]; 884 } else if ('child' == command[0]) { 885 cAnchor = cAnchor.childNodes[command[1]]; 886 } else if ('next' == command[0]) { 887 for (var next=command[1]; next > 0 && cAnchor.nextSibling; --next) { 888 cAnchor = cAnchor.nextSibling; 889 } 890 } else if ('previous' == command[0]) { 891 for (var previous=command[1]; previous > 0 && cAnchor.previousSibling; --previous) { 892 cAnchor = cAnchor.previousSibling; 893 } 894 } else if ('offset' == command[0]) { 895 if (command[1] == 'length') { 896 if (TEXT_NODE == cAnchor.nodeType) { 897 cOffset = cAnchor.nodeValue.length; 898 } else { 899 cOffset = cAnchor.childNodes.length; 900 } 901 } else if (command[1] < 0) { 902 if (TEXT_NODE == cAnchor.nodeType) { 903 cOffset = cAnchor.nodeValue.length + command[1]; 904 } else { 905 cOffset = cAnchor.childNodes.length + command[1]; 906 } 907 } else { 908 cOffset = command[1]; 909 } 910 } 911 } 912 } catch(e) { 913 cAnchor = null; 914 cOffset = null; 915 } 916 } 917 918 setCursor(cursorBefore); 919 920 var selection = xinha.getSelection(); 921 var range = xinha.createRange(selection); 922 923 range.setStart(cAnchor, cOffset); 924 range.setEnd(cAnchor, cOffset); 925 selection.removeAllRanges(); 926 selection.addRange(range); 927 928 // Breakline 929 try { 930 xinha.plugins['EnterParagraphs'].instance.breakLine(mockEvent, xinha._doc); 931 } catch (e) { 932 console.error('Breakline threw exception ', e); 933 console.groupEnd(); 934 return; 935 } 936 937 var selection = xinha.getSelection(); 938 var range = xinha.createRange(selection); 939 940 setCursor(cursorAfter); 941 942 if ((selection.anchorNode != cAnchor) || (selection.anchorOffset != cOffset)) { 943 // Sometimes there are multiple equivalent selection, let's see if we received alternatives. 944 if (typeof cursorAfter2 != 'undefined') { 945 setCursor(cursorAfter2); 946 if ((selection.anchorNode != cAnchor) || (selection.anchorOffset != cOffset)) { 947 console.error('Actual anchor: ' + selection.anchorNode + 948 '\nActual offset: ' + selection.anchorOffset + 949 '\nExpected anchor: ' + cAnchor + 950 '\nExpected offset: ' + cOffset); 951 } 952 } else { 953 console.error('Actual anchor: ' + selection.anchorNode + 954 '\nActual offset: ' + selection.anchorOffset + 955 '\nExpected anchor: ' + cAnchor + 956 '\nExpected offset: ' + cOffset); 957 } 958 } 959 960 result = xinha.getInnerHTML(); 961 if (result.trim() == after.trim()) { 962 console.info('Success!'); 963 } else { 964 console.error('Was: \n`' + before + 965 '`\nExpected: \n`' + after + 966 '`\nGot: \n`' + result + '`'); 967 } 968 console.groupEnd(); 969 } 970 contentBackup = xinha.getInnerHTML(); 971 xinha.setHTML(""); 972 console.group('Running tests:'); 973 /* 974 The initial content on browser load seems to be: 975 <body><br />\n</body> 976 That's a break tag and a whitespace text node containing a newline character. 977 */ 978 test('Initial Xinha Content', 979 null, [], 980 '<p> </p><p><br></p>\n', [['child', 1]]); // Mozilla kicks off a trailing newline. Do I care about this? 981 test('Initial Xinha Content: Recreated', 982 '<br>\n', [], 983 '<p> </p><p><br></p>\n', [['child', 1]]); // Mozilla kicks off a trailing newline. Do I care about this? 984 985 test('Empty Body', 986 '', [], 987 '<p> </p><p><br></p>', [['child', 1]], 988 [['child', 1], ['child', 0]]); 989 990 test('Text node in body: text node', 991 'Hi', [], // Point to text node 992 '<p> </p><p>Hi</p>', [['child', 1]], 993 [['child', 1], ['child', 0]]); 994 test('Text node in body: first char', 995 'Hi', [['child', 0]], // Point to 'H' 996 '<p> </p><p>Hi</p>', [['child', 1]], 997 [['child', 1], ['child', 0]]); 998 test('Text node in body: split text', 999 'Hi', [['child', 0], ['offset', 1]], // Point to 'i' 1000 '<p>H</p><p>i</p>', [['child', 1]], 1001 [['child', 1], ['child', 0]]); 1002 test('Text node in body: after text', 1003 'Hi', [['child', 0], ['offset', 'length']], // Point after 'i' 1004 '<p>Hi</p><p> </p>', [['child', 1]], 1005 [['child', 1], ['child', 0]]); 1006 test('Text node in body: after text node', 1007 'Hi', [['offset', 'length']], // Point after text node 1008 'Hi<p> </p>', [['child', 1]], // This is not ideal output, but the line breaker never sees the text node and 1009 [['child', 1], ['child', 0]]); // so can't do anything about it. 1010 1011 // For the next two tests, Douglas thinks the ideal output would be (either of): 1012 // [['child', 1], ['child', 0]], 1013 // [['child', 1], ['child', 0], ['child', 0]]); 1014 // That is, with the cursor inside the <em> node. 1015 // I (ejucovy) think that the output [['child', 1]], inside the second <p> 1016 // but outside the <em>, is better. It's also what we're actually getting back 1017 // from the browser, so let's go with that and leave this here for posterity.. 1018 test('Body with inline tag: em node', 1019 '<em>hi</em>', [], // Point to document body 1020 '<p> </p><p><em>hi</em></p>', [['child', 1]]); 1021 test('Body with inline em: inside em node', 1022 '<em>hi</em>', [['child', 0]], // Point to beginning of em node (before h) 1023 '<p> </p><p><em>hi</em></p>', [['child', 1]]); 1024 1025 test('Body with inline tag: text node', 1026 '<em>hi</em>', [['child', 0]], 1027 '<p> </p><p><em>hi</em></p>', [['child', 1], ['child', 0]], 1028 [['child', 1], ['child', 0], ['child', 0]]); 1029 test('Body with inline tag: first char', 1030 '<em>hi</em>', [['child', 0], ['child', 0]], 1031 '<p> </p><p><em>hi</em></p>', [['child', 1], ['child', 0]], 1032 [['child', 1], ['child', 0], ['child', 0]]); 1033 test('Body with inline tag: split text', 1034 '<em>hi</em>', [['child', 0], ['child', 0], ['offset', 1]], 1035 '<p><em>h</em></p><p><em>i</em></p>', [['child', 1], ['child', 0]], 1036 [['child', 1], ['child', 0], ['child', 0]]); 1037 test('Body with inline tag: after text', 1038 '<em>hi</em>', [['child', 0], ['child', 0], ['offset', 'length']], 1039 '<p><em>hi</em></p><p> </p>', [['child', 1], ['child', 0]], 1040 [['child', 1], ['child', 0], ['child', 0]]); 1041 // I hate that this is expected behavior, but the split code doesn't see the em tag in these two cases. 1042 test('Body with inline tag: after text node', 1043 '<em>hi</em>', [['child', 0], ['offset', 'length']], 1044 '<em>hi</em><p> </p>', [['child', 1], ['child', 0]], 1045 [['child', 1], ['child', 0], ['child', 0]]); 1046 test('Body with inline tag: after em node', 1047 '<em>hi</em>', [['offset', 'length']], 1048 '<em>hi</em><p> </p>', [['child', 1], ['child', 0]], 1049 [['child', 1], ['child', 0], ['child', 0]]); 1050 1051 /*************** Repeat the header block for each header level once the tests are passing *********************/ 1052 test('Split header 1: h1 node', 1053 '<h1>hi</h1>', [], 1054 '<p> </p><h1>hi</h1>', [['child', 1]], 1055 [['child', 1], ['child', 0]]); 1056 test('Split header 1: text node', 1057 '<h1>hi</h1>', [['child', 0]], 1058 '<p> </p><h1>hi</h1>', [['child', 1]], 1059 [['child', 1], ['child', 0]]); 1060 test('Split header 1: first char', 1061 '<h1>hi</h1>', [['child', 0], ['child', 0]], 1062 '<p> </p><h1>hi</h1>', [['child', 1]], 1063 [['child', 1], ['child', 0]]); 1064 test('Split header 1: split text', 1065 '<h1>hi</h1>', [['child', 0], ['child', 0], ['offset', 1]], 1066 '<h1>h</h1><h1>i</h1>', [['child', 1]], 1067 [['child', 1], ['child', 0]]); 1068 test('Split header 1: after text', 1069 '<h1>hi</h1>', [['child', 0], ['child', 0], ['offset', 'length']], 1070 '<h1>hi</h1><p> </p>', [['child', 1]], 1071 [['child', 1], ['child', 0]]); 1072 test('Split header 1: after text node', 1073 '<h1>hi</h1>', [['child', 0], ['offset', 'length']], 1074 '<h1>hi</h1><p> </p>', [['child', 1]], 1075 [['child', 1], ['child', 0]]); 1076 test('Split header 1: after h1 node', 1077 '<h1>hi</h1>', [['offset', 'length']], 1078 '<h1>hi</h1><p> </p>', [['child', 1]], 1079 [['child', 1], ['child', 0]]); 1080 1081 console.groupEnd(); 1082 xinha.setHTML(contentBackup); 1083 // EnterParagraphs.RunTests(xinha_editors['myTextArea']) 1084 } 1085 /* 1086 * Determine if a cursor points to the end of it's containing node. 1087 */ 1088 EnterParagraphs.prototype.cursorAtBeginning = function (cursorNode, cursorOffset) 1089 { 1090 if (cursorOffset == 0) 1091 { 1092 return true; 1093 } 1094 if (cursorNode.nodeType == TEXT_NODE) 1095 { 1096 // We're in the middle of a text node. If the node is a whitespace node, 1097 // we'll ignore it and treat it as if the cursor were at the beginning of 1098 // the node, and not in it. 1099 if (/\S/.test(cursorNode.nodeValue)) 1100 { 1101 return false; 1102 } 1103 cursorOffset = this.indexInParent(cursorNode); 1104 cursorNode = cursorNode.parentNode; 1105 1106 // We need to make sure we there wasn't an error in indexInParent 1107 if (cursorOffset === null) 1108 { 1109 return false; 1110 } 1111 1112 // We have to check the new offset for the easy case. 1113 if (cursorOffset == 0) 1114 { 1115 return true; 1116 } 1117 } 1118 // At this point, if all of the nodes before the cursor are white space 1119 // nodes, then the cursor is at the beginning of the node. 1120 for (var node = cursorNode.childNodes[cursorOffset-1]; node; node = node.previousSibling) 1121 { 1122 if ((node.nodeType != TEXT_NODE) || (/\S/.test(node.nodeValue))) 1123 { 1124 return false; 1125 } 1126 } 1127 return true; 1128 } 1129 /** 791 1130 * Handles the pressing of an unshifted enter for Gecko 792 1131 */ 1132 1133 EnterParagraphs.prototype.breakLine = function(ev, doc) 1134 { 1135 // Helper function that copies a DOM element and its attributes (except the 1136 // id) without any of the contents. 1137 var safeShallowCopy = function(node, doc) 1138 { 1139 var copy = doc.createElement(node.nodeName); 1140 for (var index=0; index < node.attributes.length; ++index) 1141 { 1142 var attr = node.attributes[index]; 1143 if ('id' != attr.name.toLowerCase()) 1144 { 1145 copy.setAttribute(attr.name, attr.value); 1146 } 1147 } 1148 return copy; 1149 } 1150 1151 // Helper function that will get the node immediately following the current 1152 // node, but without descending into children nodes. When looking at the 1153 // markup of the document, this means that if a node to the right of this 1154 // node in the text is at a lower depth in the DOM tree, than we will return 1155 // it's first parent that is at our depth our higher in the tree. 1156 var nextRootNode = function(node) 1157 { 1158 if (node.nextSibling) 1159 { 1160 return node.nextSibling; 1161 } 1162 for (var nextRoot = node.parentNode;nextRoot;nextRoot = nextRoot.parentNode) 1163 { 1164 if (nextRoot.nextSibling) 1165 { 1166 return nextRoot.nextSibling; 1167 } 1168 } 1169 } 1170 1171 // A cursor is specified by a node and an offset, so we will split at that 1172 // location. It should be noted that if splitNode is a text node, 1173 // splitOffset is an offset into the text contents. If not, it is an index 1174 // into the childNodes array. 1175 var splitTree = function(root, splitNode, splitOffset, doc) 1176 { 1177 // Split root into two. 1178 var breaker = safeShallowCopy(root, doc); 1179 if (root.nextSibling) 1180 { 1181 breaker = root.parentNode.insertBefore(breaker,root.nextSibling); 1182 } 1183 else 1184 { 1185 breaker = root.parentNode.appendChild(breaker); 1186 } 1187 1188 var insertNode = breaker; 1189 // XXX TODO don't use a closure to access this, pass it in... 1190 for (;recreateStack.length>0;) 1191 { 1192 var stackEl = safeShallowCopy(recreateStack.pop(), doc) 1193 insertNode.appendChild(stackEl); 1194 // Move content here 1195 insertNode = stackEl; 1196 } 1197 1198 // We need to keep track of the new cursor location. When our cursor is in 1199 // the middle of a text node, the new cursor will be at the beginning of 1200 // the text node we create to contain the text to the right of the cursor. 1201 // Otherwise, the cursor will point to a node, and the new cursor needs to 1202 // point to that node in it's new location. 1203 var newCursorNode = null; 1204 1205 var sourceNode = splitNode; 1206 var sourceOffset = splitOffset; 1207 if (TEXT_NODE == sourceNode.nodeType) 1208 { 1209 var textNode = doc.createTextNode(sourceNode.nodeValue.substring(splitOffset,sourceNode.nodeValue.length)); 1210 newCursorNode = textNode = insertNode.appendChild(textNode); 1211 sourceNode.nodeValue = sourceNode.nodeValue.substring(0,splitOffset); 1212 } 1213 1214 // When splitting a tree, we need to take any nodes that are after the 1215 // split and move them into their location in the new tree. We can have 1216 // siblings at each level of the tree, so we need to walk from the inside 1217 // of the source outwards, and move the offending nodes to the equivalent 1218 // position on the newly duplicated tree. 1219 1220 // Move insertNode from the inside outwards towards the root, moving any 1221 // content nodes as we go. We'll make sure that we can do the same with 1222 // sourceNode 1223 while (insertNode != root.parentNode) 1224 { 1225 for (var moveNode=sourceNode.childNodes[sourceOffset];moveNode;) 1226 { 1227 // We have to take a reference to the next sibling before cutting out 1228 // of the tree, or we will lose our place. 1229 1230 // nextNode can potentially be null. This is not a problem. 1231 var nextNode = moveNode.nextSibling; 1232 var cutNode = moveNode.parentNode.removeChild(moveNode); 1233 insertNode.appendChild(cutNode); 1234 moveNode = nextNode; 1235 } 1236 1237 // Move both of our node pointers one step closer to the root node. 1238 sourceOffset = EnterParagraphs.prototype.indexInParent(sourceNode); 1239 sourceNode = sourceNode.parentNode; 1240 insertNode = insertNode.parentNode; 1241 } 1242 1243 // Below code needs to check for element node with empty text node. 1244 // An empty node is an text node of zero length or an element node with no 1245 // children, or whose only children are zero-length text nodes. 1246 var emptyNode = function(node) 1247 { 1248 if ((TEXT_NODE == node.nodeType) && (0 == node.nodeValue.length)) 1249 { 1250 // Text nodes are empty if there is no text. 1251 return true; 1252 } 1253 1254 if (ELEMENT_NODE == node.nodeType) 1255 { 1256 for (var child = node.firstChild; child; child = child.nextSibling) 1257 { 1258 if ((ELEMENT_NODE == child.nodeType) || (0 != child.nodeValue.length)) 1259 { 1260 // If there are any element children, or text nodes with text in 1261 // them, this node is not empty. 1262 return false; 1263 } 1264 } 1265 1266 // node has no childNodes that are elements and no childNodes that are 1267 // text nodes with text in them. 1268 return true; 1269 } 1270 1271 return false; 1272 } 1273 1274 var stuffEmptyNode = function(node, doc) 1275 { 1276 if (!emptyNode(node)) 1277 { 1278 return; 1279 } 1280 1281 if (TEXT_NODE == node.nodeType) 1282 { 1283 // Unicode equivalent of non breaking whitespace. 1284 node.nodeValue = '\u00a0'; 1285 } 1286 else if (0 == node.childNodes.length) 1287 { 1288 // Unicode equivalent of non breaking whitespace. 1289 node.appendChild(doc.createTextNode('\u00a0')); 1290 } 1291 else 1292 { 1293 // Unicode equivalent of non breaking whitespace. The node is empty, 1294 // but it has child nodes, so firstChild is guaranteed to be an empty 1295 // text node. 1296 node.firstChild.nodeValue = '\u00a0'; 1297 } 1298 } 1299 1300 // If the cursor node wasn't created by the split (it was moved), then that 1301 // means we need to point to the inside of our brand new tree. 1302 if (!newCursorNode) 1303 { 1304 newCursorNode = breaker.childNodes[0]; 1305 } 1306 1307 // Make sure when we split the tree that we don't leave any empty nodes, as 1308 // that would have visual glitches. 1309 stuffEmptyNode(splitNode, doc); 1310 stuffEmptyNode(newCursorNode, doc); 1311 1312 // So that we can correctly set the selection, we'll return a reference to 1313 // the inserted subtree. 1314 return newCursorNode; 1315 } 1316 var insertLineBreak = function(cursorParent, cursorOffset, useNewline, doc) 1317 { 1318 if (TEXT_NODE == cursorParent.nodeType) 1319 { 1320 // The cursor points inside of a text node, we insert the newline 1321 // directly into the text. 1322 var splitNode = cursorParent; 1323 var splitOffset = cursorOffset; 1324 if (useNewline) 1325 { 1326 splitNode.nodeValue = splitNode.nodeValue.substring(0,splitOffset) + '\n' + splitNode.nodeValue.substring(splitOffset,splitNode.nodeValue.length); 1327 } 1328 else 1329 { 1330 var newTextNode = doc.createTextNode(splitNode.nodeValue.substring(splitOffset,splitNode.nodeValue.length)); 1331 var newBreakNode = doc.createElement('br'); 1332 splitNode.nodeValue = splitNode.nodeValue.substring(0,splitOffset); 1333 1334 var appendIndex = EnterParagraphs.prototype.indexInParent(cursorParent); 1335 if (appendIndex == cursorParent.parentNode.length-1) 1336 { 1337 newBreakNode = cursorParent.appendChild(newBreakNode); 1338 newTextNode = cursorParent.appendChild(newTextNode); 1339 } 1340 else 1341 { 1342 newTextNode = cursorParent.insertBefore(newTextNode, cursorParent.parentNode.childNodes[appendIndex+1]); 1343 newBreakNode = cursorParent.insertBefore(newBreakNode, newTextNode); 1344 } 1345 return newBreakNode; 1346 } 1347 } 1348 else if (0 == cursorParent.childNodes.length) 1349 { 1350 // The cursor is inside an empty element or document node, so we insert a txt node or break element as necessary. 1351 if (useNewline) 1352 { 1353 var breakingNode = doc.createTextNode('\n'); 1354 cursorParent.appendChild(breakingNode); 1355 } 1356 else 1357 { 1358 var breakingNode = doc.createElement('br'); 1359 return cursorParent.appendChild(breakingNode); 1360 } 1361 } 1362 else if ((cursorOffset == cursorParent.childNodes.length) && (TEXT_NODE == cursorParent.childNodes[cursorOffset-1].nodeType)) 1363 { 1364 // The cursor is at the after the last node, and the previous node is a 1365 // text node where we can insert the newline. 1366 if (useNewline) 1367 { 1368 var lastTextNode = cursorParent.childNodes[cursorOffset-1]; 1369 lastTextNode.nodeValue = lastTextNode.nodeValue + '\n'; 1370 } 1371 else 1372 { 1373 var breakingNode = doc.createElement('br'); 1374 return cursorParent.appendChild(breakingNode); 1375 } 1376 } 1377 else if (cursorOffset == cursorParent.childNodes.length) 1378 { 1379 // The cursor is at the after the last node, and the previous node is an 1380 // not text, so we must insert a text node. 1381 if (useNewline) 1382 { 1383 var breakingNode = doc.createTextNode('\n'); 1384 cursorParent.appendChild(breakingNode); 1385 } 1386 else 1387 { 1388 var breakingNode = doc.createElement('br'); 1389 return cursorParent.appendChild(breakingNode); 1390 } 1391 } 1392 else if (TEXT_NODE == cursorParent.childNodes[cursorOffset].nodeType) 1393 { 1394 // The cursor points to a text node, insert our newline there. 1395 if (useNewline) 1396 { 1397 var splitNode = cursorParent.childNodes[cursorOffset]; 1398 splitNode.nodeValue = '\n' + splitNode.nodeValue; 1399 } 1400 else 1401 { 1402 var breakingNode = doc.createElement('br'); 1403 return cursorParent.insertBefore(breakingNode, cursorParent[cursorOffset]); 1404 } 1405 } 1406 else if (TEXT_NODE == cursorParent.childNodes[cursorOffset-1].nodeType) 1407 { 1408 // The cursor points to an non-text node, but there is a text node just 1409 // before where we can insert a newline. 1410 if (useNewline) 1411 { 1412 var splitNode = cursorParent.childNodes[cursorOffset-1]; 1413 splitNode.nodeValue = splitNode.nodeValue + '\n'; 1414 } 1415 else 1416 { 1417 var breakingNode = doc.createElement('br'); 1418 return cursorParent.insertBefore(breakingNode, cursorParent[cursorOffset]); 1419 } 1420 } 1421 else 1422 { 1423 // The cursor points between two non-text nodes, so we must insert a text 1424 // node. 1425 if (useNewline) 1426 { 1427 var breakingNode = doc.createTextNode('\n'); 1428 cursorParent.insertBefore(breakingNode, cursorParent.childNodes[cursorOffset]); 1429 } 1430 else 1431 { 1432 var breakingNode = doc.createElement('br'); 1433 return cursorParent.insertBefore(breakingNode, cursorParent.childNodes[cursorOffset]); 1434 } 1435 } 1436 } 1437 1438 /* *********************************************************************** 1439 CODE 1440 *********************************************************************** */ 1441 // In the case of the user pressing enter, we have to break the line somehow. 1442 // If there is anything already selected, we interpret that the user wishes 1443 // for the content to be deleted. 1444 1445 var selection = this.editor.getSelection(); 1446 var range = this.editor.createRange(selection); 1447 1448 selection.collapseToStart(); 1449 range.deleteContents(); 1450 1451 // We do some magic manipulation to help with user intent. 1452 this.moveCursorOnEdge(selection); 1453 1454 // Take a reference to the cursor. 1455 var cursorParent = selection.anchorNode; 1456 var cursorOffset = selection.anchorOffset; 1457 1458 // Now that we have an empty selection, the process of breaking the line is a 1459 // bit simpler. Our strategy for breaking the line is as follows: 1460 1461 // We will modify the cursor position in an attempt to guess the user's 1462 // intent. When the cursor is at the inside edge of certain elements, we 1463 // work under the assumption that the user wished to select just outside of 1464 // that element. As such, we will move the cursor to just outside the 1465 // element, and then continue. 1466 1467 // Next, we find the first non-inline element that contains our cursor. 1468 // These can be broken into four types: 1469 // 1) Definition lists and their elements (dl, dt, dd) 1470 // 2) Other lists (ul, ol) 1471 // 3) Other containers (body, div, tr, pre, etc.) 1472 // 4) Other block elements (p, h3, li, th, td, etc.) 1473 // 1474 // If we are inside a definition (1) list, we try to guess the users intent 1475 // as to whether they want to insert* a new term or a new definition. 1476 // If we are inside any other list (2) element, we will insert* an li element. 1477 // If we are in any other container (3), we will insert* a p element. 1478 // If we are in any other block (4) element, we split the block into two 1479 // pieces and move* anything after the cursor to the second block. 1480 // 1481 // *When inserting or moving content, we must be sure to look at any 1482 // inline elements that wrap the cursor, properly close them off, and create 1483 // the same group of wrapping inline elements in the inserted/moved 1484 // element. This logic is incorporated into splitTree. 1485 1486 // Find the first wrapping non-inline element. (1-5 above) 1487 if (ELEMENT_NODE == cursorParent.nodeType) 1488 { 1489 // When the cursor is on an element node, it's before that element in the 1490 // document, and so we only want to consider its parent for deciding what 1491 // to do. The same is true when the cursor points to just before a text 1492 // node, so we only need to check the cursorParent. 1493 var wrapNode = cursorParent; 1494 } 1495 else if (TEXT_NODE == cursorParent.nodeType) 1496 { 1497 // Since we know that a text node is not the wrapper, we'll start with 1498 // its parent. 1499 var wrapNode = cursorParent.parentNode; 1500 } 1501 else 1502 { 1503 // We are dealing with an XML document. This should be expanded to 1504 // handle these cases. 1505 // http://www.w3schools.com/Dom/dom_nodetype.asp 1506 alert('You have selected a node from an XML document, type ' + 1507 cursorParent.nodeType + '.\nXML documents are not ' + 1508 'yet supported.'); 1509 // Let the browser deal with it. 1510 return true; 1511 } 1512 1513 // This is an array used as a stack for recreating the current 'state' of 1514 // the cursor. (eg. If the cursor is inside of an em tag inside of a p, 1515 // we'll add the em to the stack so that we can recreate it while splitting 1516 // the p.) 1517 var recreateStack = []; 1518 1519 while (!EnterParagraphs.prototype._pWrapper.test(wrapNode.nodeName)) 1520 { 1521 recreateStack.push(wrapNode); 1522 wrapNode = wrapNode.parentNode; 1523 1524 if (!wrapNode) 1525 { 1526 // Broken DOM, let the browser handle it. 1527 return true; 1528 } 1529 } 1530 1531 if (wrapNode.nodeName.toLowerCase() in {pre:''}) 1532 { 1533 insertLineBreak(cursorParent, cursorOffset, true, doc); 1534 this.editor.updateToolbar(); 1535 1536 Xinha._stopEvent(ev); 1537 1538 range.setStart(cursorParent, cursorOffset+1); 1539 range.setEnd(cursorParent, cursorOffset+1); 1540 selection.removeAllRanges(); 1541 selection.addRange(range); 1542 return false; 1543 } 1544 else if (wrapNode.nodeName.toLowerCase() in {body:'',div:'',fieldset:'',form:'',map:'',noscript:'','object':'',blockquote:''}) 1545 { 1546 // We know that the there are no block elements between the cursor and the 1547 // wrapNode, but there may be inline elements. What we'll do is take 1548 // everything in the tree below wrapNode, embed it into a P element, and 1549 // then split the whole thing. 1550 1551 // The cursor might be at the ending edge of the wrapNode. 1552 // 0. Pointing inside a completely empty element <body></body> 1553 // 1. Pointing to a text node <body>^This is text</body> 1554 // 2. Pointing to an inline node that is the child of the wrapNode.<body>^<em>text</em></body> 1555 // 3. Pointing to an inline node that is a non-direct descendant of the wrapNode.<body><q>^<em>text</em></q></body> 1556 // 4. Pointing to an inline node that is a non-direct descendant of the wrapNode.<body><q><em>text</em> this^</q></body> 1557 // 5. Pointing to the end of the wrapNode.<body>Here is some text.^</body> 1558 // 6. Pointing to a block node that is just inside of the wrapNode.<body>^<p>text</p></body> 1559 1560 // In the special case of a completely empty node, the cursor is still 1561 // visible, and the user expects to have two lines after hitting the enter 1562 // key. We'll add two paragraphs to any of these nodes if they are empty. 1563 if (!wrapNode.firstChild) { 1564 var embedNode1 = doc.createElement('p'); 1565 var embedNode2 = doc.createElement('p'); 1566 embedNode1 = wrapNode.appendChild(embedNode1); 1567 embedNode2 = wrapNode.appendChild(embedNode2); 1568 // The unicode character below is a representation of a non-breaking 1569 // space we use to prevent the paragraph from having visual glitches. 1570 var emptyTextNode1 = doc.createTextNode('\u00a0'); 1571 var emptyTextNode2 = doc.createTextNode('\u00a0'); 1572 emptyTextNode1 = embedNode1.appendChild(emptyTextNode1); 1573 emptyTextNode2 = embedNode2.appendChild(emptyTextNode2); 1574 1575 Xinha._stopEvent(ev); 1576 1577 range.setStart(emptyTextNode2, 0); 1578 range.setEnd(emptyTextNode2, 0); 1579 selection.removeAllRanges(); 1580 selection.addRange(range); 1581 1582 return false; 1583 } 1584 1585 var startNode = cursorParent; 1586 for (;(startNode != wrapNode) && (startNode.parentNode != wrapNode);) 1587 { 1588 startNode = startNode.parentNode; 1589 } 1590 1591 if (TEXT_NODE == cursorParent.nodeType) 1592 { 1593 var treeRoot = cursorParent; 1594 } 1595 else if (cursorOffset == cursorParent.childNodes.length) 1596 { 1597 var embedNode = doc.createElement('p'); 1598 embedNode = wrapNode.appendChild(embedNode); 1599 // The unicode character below is a representation of a non-breaking 1600 // space we use to prevent the paragraph from having visual glitches. 1601 var emptyTextNode = doc.createTextNode('\u00a0'); 1602 emptyTextNode = embedNode.appendChild(emptyTextNode); 1603 1604 Xinha._stopEvent(ev); 1605 1606 range.setStart(emptyTextNode, 0); 1607 range.setEnd(emptyTextNode, 0); 1608 selection.removeAllRanges(); 1609 selection.addRange(range); 1610 1611 return false; 1612 } 1613 else 1614 { 1615 var treeRoot = cursorParent.childNodes[cursorOffset]; 1616 } 1617 1618 for (;wrapNode != treeRoot.parentNode;) 1619 { 1620 treeRoot = treeRoot.parentNode; 1621 } 1622 1623 // At this point, treeRoot points to the root of the subtree inside 1624 // wrapNode that containes our cursor. If this happens to be a block level 1625 // element, we'll just insert a P node here. Otherwise, we'll replace this 1626 // node with an empty P node, and then embed it into that P node. 1627 1628 if (EnterParagraphs.prototype._pWrapper.test(treeRoot.nodeName)) 1629 { 1630 var embedNode = doc.createElement('p'); 1631 embedNode = wrapNode.insertBefore(embedNode, treeRoot); 1632 // The unicode character below is a representation of a non-breaking 1633 // space we use to prevent the paragraph from having visual glitches. 1634 var emptyTextNode = doc.createTextNode('\u00a0'); 1635 emptyTextNode = embedNode.appendChild(emptyTextNode); 1636 1637 Xinha._stopEvent(ev); 1638 1639 range.setStart(treeRoot, 0); 1640 range.setEnd(treeRoot, 0); 1641 selection.removeAllRanges(); 1642 selection.addRange(range); 1643 1644 return false; 1645 } 1646 var embedNode = doc.createElement('p'); 1647 1648 treeRoot = wrapNode.replaceChild(embedNode, treeRoot); 1649 1650 treeRoot = embedNode.appendChild(treeRoot); 1651 1652 if ((TEXT_NODE == treeRoot.nodeType) && !/\S/.test(treeRoot.nodeValue)) 1653 { 1654 var newCursor = treeRoot; 1655 } 1656 else if (TEXT_NODE == treeRoot.nodeType) 1657 { 1658 var newCursor = splitTree(embedNode, treeRoot, cursorOffset, doc); 1659 } 1660 else 1661 { 1662 var parentOffset = this.indexInParent(treeRoot); 1663 if (null === parentOffset) 1664 { 1665 // We can't do anything with this cursor, so return. 1666 return; 1667 } 1668 var newCursor = splitTree(embedNode, treeRoot.parentNode, parentOffset, doc); 1669 } 1670 } 1671 else if (wrapNode.nodeName.toLowerCase() in {td:'',address:''}) 1672 { 1673 // Line breaks BR element 1674 var newCursor = insertLineBreak(cursorParent, cursorOffset, false, doc); 1675 } 1676 else if (wrapNode.nodeName.toLowerCase() in {dl:''}) 1677 { 1678 // Find the leftSibling of the cursorParent. If none, insert dt (term) followed by dd (definition), 1679 // otherwise insert same as cursorParent followed by same as leftSibling. 1680 // Check to see if the leftSibling and rightSibling are the same and then just insert the one term. 1681 // XXX TODO 1682 } 1683 else if (wrapNode.nodeName.toLowerCase() in {h1:'',h2:'',h3:'',h4:'',h5:'',h6:'',p:''}) 1684 { 1685 // Split wrapNode into two. 1686 var newCursor = splitTree(wrapNode, cursorParent, cursorOffset, doc); 1687 } 1688 else if (wrapNode.nodeName.toLowerCase() in {dt:'',dd:'',li:''}) 1689 { 1690 // To the bane of software developers the world over, users expect to be 1691 // able to hit enter twice to end a list, whether at the end or in the 1692 // middle. This means that we need to have special handling for list items 1693 // to check for the second return. We do this by testing to see if the 1694 // current list item is empty, and if so, deleting it, splitting the list 1695 // into two if necessary, and inserting a paragraph. 1696 var newCursor = splitTree(wrapNode, cursorParent, cursorOffset, doc); 1697 } 1698 else if (wrapNode.nodeName.toLowerCase() in {ol:'',ul:''}) 1699 { 1700 // Insert li 1701 var breaker = doc.createElement('li'); 1702 if (TEXT_NODE == cursorParent.nodeType) 1703 { 1704 var newCursor = wrapNode.insertBefore(breaker,cursorParent); 1705 } 1706 else 1707 { 1708 var newCursor = wrapNode.insertBefore(breaker,cursorParent.childNodes[cursorOffset]); 1709 } 1710 } 1711 1712 this.editor.updateToolbar(); 1713 1714 Xinha._stopEvent(ev); 1715 1716 // We turn the newCursor node into a cursor and offset into the parent. 1717 var newOffset = 0; 1718 while (newCursor.parentNode.childNodes[newOffset] != newCursor) 1719 { 1720 newOffset++; 1721 } 1722 newCursor = newCursor.parentNode; 1723 1724 // Monkey the new cursor position into somewhere the user should actually be 1725 // typing. 1726 1727 1728 Xinha._stopEvent(ev); 1729 range.setStart(newCursor, newOffset); 1730 range.setEnd(newCursor, newOffset); 1731 selection.removeAllRanges(); 1732 selection.addRange(range); 1733 return false; 1734 } 1735 1736 /** 1737 * If the cursor is on the edge of certain elements, we reposition it so that we 1738 * can break the line in a way that's more useful to the user. 1739 */ 1740 1741 EnterParagraphs.prototype.moveCursorOnEdge = function(selection) 1742 { 1743 // We'll only move the cursor if the selection is collapsed (ie. no contents) 1744 if ((selection.anchorNode != selection.focusNode) || 1745 (selection.anchorOffset != selection.focusOffset)) 1746 { 1747 return; 1748 } 1749 1750 // We now need to filter based on the element we are inside of. If the 1751 // cursor is on a text node, we look at the parent of the node. 1752 var wrapNode = selection.anchorNode; 1753 if (TEXT_NODE == wrapNode.nodeType) 1754 { 1755 wrapNode = wrapNode.parentNode; 1756 } 1757 1758 // Check the wrapper against our lists of trigger nodes. 1759 if (!EnterParagraphs.prototype._pifyParent.test(wrapNode.nodeName) && 1760 !EnterParagraphs.prototype._pifySibling.test(wrapNode.nodeName)) 1761 { 1762 // We're lucky, no need to check for edges, let's just return. 1763 return; 1764 } 1765 1766 // Okay, time to perform edge checking. If the cursor is inside of a text 1767 // node, the rules for edge detection are quite specialized, so we'll deal 1768 // with that first. Since text nodes can't contain other nodes, we only have 1769 // to perform this check once. We won't actually move the cursor here, just 1770 // our copy of it, because we won't know where it belongs until we're dealing 1771 // with the nodes themselves, rather than the text. 1772 1773 var cursorParent = selection.anchorNode; 1774 var cursorOffset = selection.anchorOffset; 1775 1776 for (;this.cursorAtEnd(cursorParent, cursorOffset);) 1777 { 1778 if (TEXT_NODE == cursorParent.nodeType) 1779 { 1780 // If we're at the end and stuck inside of a text node, we move out of 1781 // the text node, which is a simpler case, than continue. 1782 var parentOffset = this.indexInParent(cursorParent); 1783 if (null === parentOffset) 1784 { 1785 // We can't do anything with this cursor, so return. 1786 return; 1787 } 1788 1789 cursorParent = cursorParent.parentNode; 1790 cursorOffset = parentOffset + 1; 1791 continue; 1792 } 1793 1794 var parentOffset = this.indexInParent(cursorParent); 1795 if (null === parentOffset) 1796 { 1797 // We can't do anything with this cursor, so return. 1798 return; 1799 } 1800 1801 cursorParent = cursorParent.parentNode; 1802 cursorOffset = parentOffset + 1; 1803 1804 // If we are no longer inside of one of our trigger nodes, we're done. 1805 if (!this._pifyParent.test(cursorParent.nodeName) && 1806 !this._pifySibling.test(cursorParent.nodeName)) 1807 { 1808 // Move the real cursor. 1809 selection.removeAllRanges(); 1810 var range = this.editor.createRange(selection); 1811 range.setStart(cursorParent, cursorOffset); 1812 range.setEnd(cursorParent, cursorOffset); 1813 selection.addRange(range); 1814 return; 1815 } 1816 } 1817 1818 for (;this.cursorAtBeginning(cursorParent, cursorOffset);) 1819 { 1820 if (TEXT_NODE == cursorParent.nodeType) 1821 { 1822 // If we're at the beginning and stuck inside of a text node, we move out 1823 // of the text node, which is a simpler case, than continue. 1824 var parentOffset = this.indexInParent(cursorParent); 1825 if (null === parentOffset) 1826 { 1827 // We can't do anything with this cursor, so return. 1828 return; 1829 } 1830 1831 cursorParent = cursorParent.parentNode; 1832 cursorOffset = parentOffset; 1833 continue; 1834 } 1835 1836 var parentOffset = this.indexInParent(cursorParent); 1837 if (null === parentOffset) 1838 { 1839 // We can't do anything with this cursor, so return. 1840 return; 1841 } 1842 1843 cursorParent = cursorParent.parentNode; 1844 cursorOffset = parentOffset; 1845 1846 // If we are no longer inside of one of our trigger nodes, we're done. 1847 if (!this._pifyParent.test(cursorParent.nodeName) && 1848 !this._pifySibling.test(cursorParent.nodeName)) 1849 { 1850 // Move the real cursor. 1851 selection.removeAllRanges(); 1852 var range = this.editor.createRange(selection); 1853 range.setStart(cursorParent, cursorOffset); 1854 range.setEnd(cursorParent, cursorOffset); 1855 selection.addRange(range); 1856 return; 1857 } 1858 } 1859 } 793 1860 794 1861 EnterParagraphs.prototype.handleEnter = function(ev) … … 807 1874 if ( this.isNormalListItem(rng) ) 808 1875 { 809 810 1876 return true; 811 1877 }
Note: See TracChangeset
for help on using the changeset viewer.