/[drupal]/contributions/modules/collaborative_editor/drupal.collaborative_editor.js
ViewVC logotype

Contents of /contributions/modules/collaborative_editor/drupal.collaborative_editor.js

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.14 - (show annotations) (download) (as text)
Sat May 12 08:13:10 2007 UTC (2 years, 6 months ago) by ernest
Branch: MAIN
CVS Tags: HEAD
Changes since 1.13: +34 -126 lines
File MIME type: text/javascript
Bug http://drupal.org/node/78553 and other IE visual elements fixed
1 // globals
2
3 // The saved content we use to do the comparisons with the new content typed. We also update this variable when we store the new content returned
4 // from the server in the textarea
5 var oldContent = '';
6
7 // The same as above but used to store the revision id returned from the server. This will be used to send it in the next call
8 var lastRevision = '';
9
10 // This variable is updated before and after the asynchronous call using checkChanges function. If the values match then it
11 // means the user hasnt typed new content or he has stopped typing
12 var newChange = 0;
13
14 // The last content the client knows about used to set the newChange variable
15 var lastCheckedContent = '';
16
17 // If set to true we dont do the asynchronous call. This way the new content isn't updated unless the users stops typing. Therefore there are neither jumps of
18 // caret when the content is updated nor missed typed content during the call.
19 // Note: maybe a timeout needs to be set in case of the user never stops typing (?) and a lot of incoming changes are queuing to be update
20 var userIsTyping = false;
21
22 // If set to false we dont get the value from the server (only in use case 2) since the last content and revision id we uploaded are supposed to match
23 // with those ones in the server
24 var isDiff = false;
25
26 // If set to true the user clicks on submit button so no warnings of unsaved messages are required
27 var userSubmit = false;
28
29 // the ce content id
30 var cId = '';
31
32 // these are only used for debugging
33 var debughash = '';
34 var phase = '';
35 var newchangetest1 = 0;
36 var newchangetest2 = 0;
37 var phasejs = 0;
38 var usersdebug = '';
39
40 if (isJsEnabled()) {
41 addLoadEvent(start);
42 }
43
44 function start()
45 {
46 // set the handler to all submit buttons of the form. The aim is to have the same version in the ce_content table
47 var i = 0, el, els = document.getElementsByTagName("input");
48 // iterates each input element
49 while (el = els[i++]) {
50 if (el.getAttribute('type') == 'submit') {
51 // Note: leaveEditor(); or updateDocument(param); will NOT work here.
52 // Must be a reference to a function name, not a function call
53 el.onclick = leaveEditor;
54 }
55 }
56
57 var textArea = getTextArea();
58 // the common content overwrittes any other content when we start
59 var initContent = document.getElementById('edit-init').value;
60 if (initContent != '')
61 textArea.value = initContent;
62 // Get the initial values
63 // These functions will be simple with jQuery
64 oldContent = textArea.value;
65 lastCheckedContent = oldContent;
66 // we have to take the content id generated by the server from the initial form
67 cId = document.getElementById('edit-cid').value;
68 // we get the initial revision id from the hidden form field of the initial page
69 lastRevision = document.getElementById('edit-rev').value;
70
71
72 // we want to be sure the user doesnt exit the document without saving by mistake
73 // since our site is more a web app than a simple web site consider onbeforeunload morally right
74 if (cId != '') // only valid in the collaborative editor section
75 window.onbeforeunload = checkBeforeExit; // onbeforeunload handles better the messages than onunload
76
77 // this function is available in drupal.collaborative_editor_ui.js
78 addVisualElements();
79 // Activate asyncronous refresh loop
80 activateHeartBeat();
81 }
82
83
84
85 // Save contents before exit so the content in original table matches with the one in ce_content
86 function leaveEditor() {
87 userSubmit = true;
88 updateDocument(true); // true to avoid collision messages in this update. However we will need to log this error in some way
89 // Note: Possible bug, if we leave blank title the returned use case is 4 despite of the correct behaviour
90 }
91
92
93 // check if there are new changes. If any, ask user before exit
94 function checkBeforeExit() {
95 if (newChange != checkChanges() && !userSubmit) // false if the user clicks the submit button
96 return "You have unsaved changes. Click Submit to save them. Otherwise click OK to exit";
97 }
98
99
100
101 // get the textarea element
102 function getTextArea()
103 {
104 // we get the first textarea of the form. This need to be more general
105 var tas = document.getElementsByTagName("textarea");
106 return tas[0];
107 }
108
109
110 // Activate asyncronous refresh loop
111 // There are a lot of parameters to include into this function. Since the frequency of refresh ought to be different
112 // if a user is editing alone, is not typing, there has been an error, etc
113 // For now, it is set to 8 seconds for testing purposes
114 function activateHeartBeat()
115 {
116 // If user is typing we dont interfare and we'll do the update later on
117 if (!userIsTyping)
118 updateDocument(false);
119
120 // We set the update every 8 seconds
121 setTimeout(function () { activateHeartBeat(); }, 8000);
122 }
123
124
125 // a cross browser function to get the start and end of user caret
126 // Not tested in safari or OSX browsers
127 function getCaretPosition()
128 {
129 var textArea = getTextArea();
130 var start = 0;
131 var end = 0;
132 // a bit weird but we need the range object to set the caret in setCaretPosition() for IE
133 var rangeCopy = null;
134
135 if(document.getSelection) // FF
136 {
137 start = textArea.selectionStart;
138 end = textArea.selectionEnd;
139 }
140 else if(document.selection) // IE
141 {
142 // The current selection
143 var range = document.selection.createRange();
144 rangeCopy = range.duplicate();
145 // Select all text
146 rangeCopy.moveToElementText(textArea);
147 // Now move 'dummy' end point to end point of original range
148 rangeCopy.setEndPoint( 'EndToEnd', range );
149 // Now we can calculate start and end points
150 start = rangeCopy.text.length - range.text.length;
151 end = start + range.text.length;
152 }
153 return {start: start, end: end, rangeCopy: rangeCopy};
154 }
155
156
157 // We set the caret position, even in selection cases, with the values returned by getCaretPosition()
158 function setCaretPosition(start,end,rangeCopy)
159 {
160 var textArea = getTextArea();
161 if(document.getSelection) // FF
162 {
163 // what a pleasure in FF ;)
164 textArea.setSelectionRange(start,end);
165 }
166 else if(document.selection) // IE
167 {
168 rangeCopy.collapse(true);
169 rangeCopy.moveStart("character",start);
170 rangeCopy.moveEnd("character",end-start);
171 rangeCopy.select();
172 }
173 }
174
175
176 // The values to post are set and send to the server using XMLHTTPRequest
177 function updateDocument(exitDocument)
178 {
179 // we will send the parameters stored in this array and passed as third parameter of HTTPPost
180 var csend = [];
181 // This textArea should content the current value
182 var textArea = getTextArea();
183 var currentContent = textArea.value;
184
185 // Bug http://drupal.org/node/78553 fixed. We transform all carriage returns to \r\n before calculating the delta
186 currentContent = currentContent.replace(/\r/g,"").replace(/\n/g,"\r\n");
187
188 if (lastRevision != 0) {
189
190 // We get the difference block of the two strings (initial content and current content) and its parameters
191 // In this comparison the oldContent is the last version we got from the server. The currentContent is what we currently have in the textarea
192 var diffResult = getDiffBlock(oldContent, currentContent);
193
194 // set post vars to send to the server
195 csend['prefixLength'] = diffResult.prefixLength; // the previous common part of the two strings
196 csend['suffixLength'] = diffResult.suffixLength; // this is not used in the server. Could be deleted (not sent) in next versions
197 csend['replacementLength'] = diffResult.replacementLength; // it is 0 if there is no replacement
198 csend['diffText'] = diffResult.diffText; // the difference returned in the comparison
199 csend['clientRev'] = lastRevision; // the revision we send to the server. Usually is the same returned last time from the server
200
201 // set isDiff if nothing is sent. This will be used in handleresponse for use case 2
202 isDiff = (diffResult.diffText != "" && diffResult.replacementLength != 0) ? true : false;
203 }
204 else {
205 csend['diffText'] = currentContent;
206 csend['clientRev'] = lastRevision;
207 isDiff = true;
208 }
209
210 // write values into the debug field
211 if (diffResult) {
212 document.getElementById('edit-debug').value = "old text : " + oldContent
213 + "\ncurrent text : " + currentContent
214 + "\ndiff:" + diffResult.diffText
215 + " , diff length:" + diffResult.diffText.length
216 + "\nold-length:" + oldContent.length
217 + "\nrep:" + diffResult.replacementLength // three numeric values from de getDiffBlock()
218 + " pre:" + diffResult.prefixLength
219 + " suf:" + diffResult.suffixLength
220 + "\nrevisionid:" + lastRevision
221 // + "\nusersdebug: " + usersdebug.length
222 + "\nprevious phase: " + phase // notice it is the previous phase
223 + "\nold/new change flag: " + newchangetest1 // this values are returned by checkChanges()
224 + ":" + newchangetest2
225 + "\nDebughash: " + debughash; // additional debug vars
226
227 }
228
229 // ...ui.js
230 showSaveMessage();
231
232 // record the changes done so far so that we compare if it matches after the asynchronous call is done
233 newChange = checkChanges();
234 //debug var
235 newchangetest1 = newChange;
236
237 var cparameter = '';
238 var cback = function (r,s,t){handleResponse(r, currentContent, newChange, exitDocument);}; // drupal.js: callbackFunction(xmlHttp.responseText, xmlHttp, callbackParameter);
239 HTTPPost("collaborative_editor/" + cId + "/getAsyncContent", cback, cparameter, csend); // csend is the array with the parameters
240 }
241
242
243
244 // we use the response from the server to update the textarea
245 function handleResponse(r, contentUploaded, newChangeUploaded, exitDocument)
246 {
247 // check async errors
248 // (...)
249
250 // parse the object from server and get each value
251 var response = parseJson(r);
252 var textArea = getTextArea();
253 var clientContent = textArea.value;
254
255 var userList = response.currentusers;
256
257 userIsTyping = false;
258
259 // When there is new content from the server
260 // This will be only executed if the user is not typing. newChangeUploaded is the state of the textarea before the asynchronous call
261 // if it doesnt match with the current test returned by checkChanges it means the users has typed something new
262 if (response.return_rev != "" && newChangeUploaded == checkChanges())
263 {
264 //debug var
265 newchangetest2 = checkChanges();
266 // when there is something from others. cases 3, 4, 5. use case 1 is the exception
267 if (response.otherschange == 1) {
268 //debug vars
269 phasejs = 1;
270
271 // decode content to avoid errors
272 var serverContent = decodeURIComponent(response.content);
273 lastRevision = response.return_rev; // revision returned from the server. We will send it in our next call
274
275 var oldCaretPos = getCaretPosition(); // we get the current caret position of the user before updating the content of the textarea
276 var diffText = getDiffBlock(clientContent, serverContent); // get the difference to see where the new content starts
277 var changeLength = serverContent.length - clientContent.length; // if positive there is new content, otherwise something has been deleted
278
279 // we update the variables with the content from the server
280 oldContent = serverContent;
281 lastCheckedContent = oldContent;
282 textArea.value = serverContent; // the content of the textarea is updated
283
284 // the caret is set again to mantain the caret position in the same place
285 var newContentStart = diffText.prefixLength;
286 var newCaretPosStart = oldCaretPos.start;
287 var newCaretPosEnd = oldCaretPos.end;
288 // the offset we must add is the length of the new content if it is before our cursor position
289 if (newCaretPosStart > newContentStart)
290 newCaretPosStart += changeLength;
291 // if we have a selection done and the content is in between the selection will increase as well
292 if (newCaretPosEnd > newContentStart)
293 newCaretPosEnd += changeLength;
294 setCaretPosition(newCaretPosStart, newCaretPosEnd, oldCaretPos.rangeCopy); // set the caret position
295 }
296 // new revision id and otherschange == 0 , so no others' changes. case 1
297 else {
298 // the content will be the same we uploaded
299 oldContent = contentUploaded;
300 lastRevision = response.return_rev;
301 }
302 }
303 // Neither new revision nor others' change. case 2
304 else {
305 // debug var
306 phasejs = 2;
307
308 // if no diff uploaded we use the same values we uploaded last time
309 if (!isDiff) {
310 lastRevision = response.uploaded_rev;
311 oldContent = contentUploaded;
312 }
313 // we get here if the user is typing. Therefore we set the flag and call the updateDocument again three seconds later
314 // It may have a timeout
315 if (response.return_rev != "") {
316 userIsTyping = true;
317 setTimeout(function () { updateDocument(false); },3000);
318 }
319 }
320
321 // We alert the user that their last change is not valid. Meanwhile in the background the content from others is updated
322 // Note: if we exit the document the collision is not shown to the user. But something should be implemented to handle this anyway
323 if (response.collision == 1 && !exitDocument) {
324 alert("You have just typed some content where other users were writing. Your last changes have been undone");
325 }
326
327 showLastUpdate(response.collision, exitDocument) ;
328
329 showUserList(userList);
330
331 // debug vars
332 phase = response.phase;
333 usersdebug = response.currentusers;
334
335 // debug var
336 debughash = "phasejs: " + phasejs; // see which stage of handleresponse we are
337 /* Add this to the debughash if we want to see the caret variables
338 " server/client content length = " + serverContent.length // to check minuend and subtrahend
339 + ":" + clientContent.length
340 + " caretEnd = " + oldCaretPos.end
341 + " diffStart = " + newContentStart
342 + " lengthDiff = " + changeLength // the result of the subtraction
343 ;
344 */
345 }
346
347
348
349 function checkChanges()
350 {
351 var textArea = getTextArea();
352 var newContent = textArea.value;
353
354 // If the current content doesnt match with the last saved content we increment the new change var.
355 // newChange will be set to 0 next time we load the site
356 if (lastCheckedContent != newContent) {
357 lastCheckedContent = newContent;
358 newChange++;
359 }
360 return newChange;
361 }
362
363
364 // here we use an algorithm to extract the longest common subsequence of the two strings and then the difference block between them.
365 // Better algorithms are welcome!
366 // more info: http://cis.poly.edu/suel/papers/delta.pdf
367 function getDiffBlock(oldContent, newContent)
368 {
369 // Here we compare character by character to get the common prefix length
370 var prefixLength = 0;
371 var shortestLength = Math.min(oldContent.length, newContent.length);
372 while (prefixLength<shortestLength)
373 {
374 if (oldContent.charAt(prefixLength) == newContent.charAt(prefixLength))
375 prefixLength = prefixLength + 1;
376 else
377 shortestLength = prefixLength;
378 }
379
380 // Now we do the same to get the suffix length. The comparison starts in the end and then backwards
381 var suffixLength=0;
382 var oldRemainString = oldContent.slice(prefixLength, oldContent.length);
383 var newRemainString = newContent.slice(prefixLength, newContent.length);
384 shortestLength = Math.min(oldRemainString.length, newRemainString.length);
385 while (suffixLength<shortestLength)
386 {
387 if (oldRemainString.charAt(oldRemainString.length-suffixLength-1) == newRemainString.charAt(newRemainString.length-suffixLength-1))
388 suffixLength = suffixLength+1;
389 else
390 shortestLength = suffixLength;
391 }
392
393 // We also calculate if there has been any text replacement
394 var replacementLength = oldContent.length - prefixLength - suffixLength;
395
396 // And the diff content
397 var diffText = newContent.substr(prefixLength, newContent.length - prefixLength - suffixLength);
398
399 // We store the values in a hash
400 var diffResult = { prefixLength: prefixLength, suffixLength: suffixLength, replacementLength: replacementLength, diffText: diffText };
401 return diffResult;
402 }

  ViewVC Help
Powered by ViewVC 1.1.2