PageSourceSearch

https://mynotebook.labarchives.com/javascripts/application.js?1789775860

js labarchives.com collected 2026-09-24 09:23:27 UTC 97,402 bytes, 3,093 lines download raw bytes

1/**
2 * Created with JetBrains RubyMine.
3 * User: kschneider
4 * Date: 2/4/13
5 * Time: 7:30 AM
6 * To change this template use File | Settings | File Templates.
7 */
8$(document).ajaxSend(function(e, xhr, options) {
9  var token = $("meta[name='csrf-token']").attr("content");
10  xhr.setRequestHeader("X-CSRF-Token", token);
11  xhr.setRequestHeader("X-TZ-Offset", getTimeZone());
12});
13  //method for allowing swapping, used to move entries on a page
14jQuery.fn.swap = function (b) {
15  if (b instanceof jQuery && b[0].nodeType === Node.ELEMENT_NODE) {
16    b = b[0];
17    var a = this[0];
18    var t = a.parentNode.insertBefore(document.createTextNode(''), a);
19    b.parentNode.insertBefore(a, b);
20    t.parentNode.insertBefore(b, t);
21    t.parentNode.removeChild(t);
22    return this;
23  } else {
24    throw new Error("Invalid swap element");
25  }
26};
27
28// Only call for parameters passed to functions
29function la_defined(v) {
30  //returns true if var is defined
31  return typeof v != 'undefined';
32}
33
34function la_escape_html(s) {
35  //escape HTML using jquery
36  //escape quotes in case this is in an attribute
37  return $('<div/>').text(s).html().replace(/"/g, '&quot;').replace(/'/g, '&#39;');
38}
39
40/**
41 * CSV Exploitability: We check the cell data and if it begins with =, +, - or @
42 * then we make it a string (rather than a formula) by prepending the data with
43 * "=" and enclosing the data in quotes
44 * NOTE: I tried to just prepend the data with a single quote which should
45 * also make it a string but when opened in Excel the quote was showing
46 * Here is that code:
47 *   s.replace(/^([\=\+\-\@])/, "'$1");
48*/
49function la_csv_escape(s) {
50  if (s.match(/^[\=\+\-\@]/) != null) {
51    return '="' + s.replace(/\"/g, '""') + '"';
52  }
53  return s;
54}
55
56/*!
57 * https://vanillajstoolkit.com/helpers/cleanhtml/
58 * Sanitize an HTML string
59 * (c) 2021 Chris Ferdinandi, MIT License, https://gomakethings.com
60 * @param  {String}          str   The HTML string to sanitize
61 * @param  {Boolean}         nodes If true, returns HTML nodes instead of a string
62 * @return {String|NodeList}       The sanitized string or nodes
63 */
64function cleanHTML (str, nodes) {
65
66  /**
67   * Convert the string to an HTML document
68   * @return {Node} An HTML document
69   */
70  function stringToHTML () {
71    let parser = new DOMParser();
72    let doc = parser.parseFromString(str, 'text/html');
73    return doc.body || document.createElement('body');
74  }
75
76  /**
77   * Remove <script> elements
78   * @param  {Node} html The HTML
79   */
80  function removeScripts (html) {
81    let scripts = html.querySelectorAll('script');
82    for (let script of scripts) {
83      script.remove();
84    }
85  }
86
87  /**
88   * Check if the attribute is potentially dangerous
89   * @param  {String}  name  The attribute name
90   * @param  {String}  value The attribute value
91   * @return {Boolean}       If true, the attribute is potentially dangerous
92   */
93  function isPossiblyDangerous (name, value) {
94    let val = value.replace(/\s+/g, '').toLowerCase();
95    if (['src', 'href', 'xlink:href'].includes(name)) {
96      if (val.includes('javascript:') || val.includes('data:')) return true;
97    }
98    if (name.startsWith('on')) return true;
99  }
100
101  /**
102   * Remove potentially dangerous attributes from an element
103   * @param  {Node} elem The element
104   */
105  function removeAttributes (elem) {
106
107    // Loop through each attribute
108    // If it's dangerous, remove it
109    let atts = elem.attributes;
110    for (let {name, value} of atts) {
111      if (!isPossiblyDangerous(name, value)) continue;
112      elem.removeAttribute(name);
113    }
114
115  }
116
117  /**
118   * Remove dangerous stuff from the HTML document's nodes
119   * @param  {Node} html The HTML document
120   */
121  function clean (html) {
122    let nodes = html.children;
123    for (let node of nodes) {
124      removeAttributes(node);
125      clean(node);
126    }
127  }
128
129  // Convert the string to HTML
130  let html = stringToHTML();
131
132  // Sanitize it
133  removeScripts(html);
134  clean(html);
135
136  // If the user wants HTML nodes back, return them
137  // Otherwise, pass a sanitized string back
138  return nodes ? html.childNodes : html.innerHTML;
139
140}
141
142function replaceTag(oldElem, newTag) {
143    const newElem = document.createElement(newTag);
144    for (const attr of oldElem.attributes) {
145        newElem.setAttribute(attr.name, attr.value);
146    }
147    newElem.innerHTML = oldElem.innerHTML;
148    oldElem.parentNode.replaceChild(newElem, oldElem);
149    return newElem;
150}
151
152function setAlertDialog($dlg) {
153  const $uiDialog = $dlg.closest('.ui-dialog');
154  $uiDialog.attr({
155    role: 'alertdialog',
156    'aria-modal': 'true',
157  });
158}
159
160/**
161 * Call this method instead of setting an element's
162 * innerHTML directly which is a XSS vulnerability
163 * @param {HTMLElement} ele
164 * @param {string} html_str
165 */
166function setInnerHtml(ele, html_str) {
167  // First, remove all the children in the element
168  while (ele.firstChild) {
169    ele.removeChild(ele.firstChild);
170  }
171  // Set the html for the ele
172  ele.append(...cleanHTML(html_str, true))
173}
174
175/**
176 * Decodes base64 encoded unicode string
177 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#The_Unicode_Problem}
178 * @param {string} str - The base64 encoded unicode string
179 * @returns {string} The decoded unicode string
180 */
181function b64DecodeUnicode(str) {
182  // Going backwards: from bytestream, to percent-encoding, to original string.
183  return decodeURIComponent(atob(str).split('').map(function(c) {
184    return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
185  }).join(''));
186}
187
188/**
189 * Decodes base64 encoded unicode JSON string and parses JSON
190 * @global
191 * @param {string} str - The base64 encoded unicode JSON string
192 * @returns {Object} The decoded JSON
193 */
194window.decodeBase64AndParseJSON = function(str) {
195  return JSON.parse(b64DecodeUnicode(str));
196};
197
198function splitterOnResize(event, ui) {
199  //console.log($('#page-bgtop').width());
200  $('#content').width(contentStartWidth - $('#sidebar').width());
201}
202
203function splitterOnStop(event, ui) {
204  $('#content').width(contentStartWidth - $('#sidebar').width());
205}
206var NavTreeSelectOnLoad = true; //used by the onload in navtree.  Set by various views to inhibit
207
208var contentStartWidth = -1;
209function splitterOnStart(event, ui) {
210}
211
212function setupResizeable() {
213  contentStartWidth = $('#content').width() + $('#sidebar').width();
214  $("#sidebar").resizable({resize: splitterOnResize, start: splitterOnStart, maxWidth: contentStartWidth - 400, minWidth: 150, stop: splitterOnStop});
215
216}
217
218
219function session_about_to_timeout () {
220
221
222  la_alert({msg:'This sessions is about to be ended due to inactivity.',title:'Session Ending',ok_text:'continue working',ok_call :
223    function() {$.ajax('ping')}})
224
225}
226
227
228var onResize_prev_window_width = 0;
229
230function onResizeAppWidth() {
231  if ($('#sidebar').is(':visible')) {
232    // need to reset to the default widths if the width of the browser window has changed
233    if (onResize_prev_window_width != $(window).width()) {
234      $('#wrapper').css('width', '');
235      $('#sidebar').css('width', '');
236      $('#content').css('width', '');
237      setupResizeable();
238      onResize_prev_window_width = $(window).width();
239    }
240  }
241}
242
243
244function set_page_bgtop_height() {
245  var header_height = $('#header').is(":visible") ? $('#header').outerHeight() : 0;
246  var window_height = $(window).outerHeight();
247  try {
248    if (window.course_grader_mode) {
249      window_height = $(parent).outerHeight() - ($('#docs-top', parent.document).is(":visible") ? $('#docs-top', parent.document).outerHeight() : 0);
250    }
251  }
252  catch (e) {
253
254  }
255  $('#page-bgtop').height(window_height - header_height);
256}
257
258function is_tall_window() {
259  return ($(window).height() > 500)
260}
261
262function isValidEmailAddress(emailAddress) {
263  const pattern = new RegExp(
264      /^(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0B\x0C\x0E-\x1F\x21\x23-\x5B\x5D-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)])$/i,
265  );
266  return pattern.test(emailAddress);
267}
268
269
270var share_properties_changed = false; //used below to know if we need to refresh the page after the share dialog closes
271function set_share_properties_change() {
272  share_properties_changed = true;
273}
274
275function generate_pdf(tree_root, is_leaf) {
276
277  var message = (Number(tree_root) == Number(TREE_NODE_ROOT)) ? '' : 'this portion of ';
278  var options =
279      '<p>' +
280        '<input id="generate_pdf_show_comments" type="checkbox" value="true" checked>' +
281        '<label for="generate_pdf_show_comments"> Show comments</label>' +
282      '</p>';
283  if (! is_leaf) {
284    options +=
285      '<p>' +
286        '<input id="generate_pdf_show_empty_folders" type="checkbox" value="true">' +
287        '<label for="generate_pdf_show_empty_folders"> Show empty folders</label>' +
288      '</p>';
289  }
290  la_confirm('Create a PDF file containing the contents of ' + message + 'this notebook?' + options, function() {
291    $('#pdf_tree_node_id').val(tree_root);
292    $('#pdf_show_comments').val($('#generate_pdf_show_comments').is(':checked'));
293    $('#pdf_show_empty_folders').val($('#generate_pdf_show_empty_folders').is(':checked'));
294    $('#pdf_tz_offset').val(getTimeZone());
295    $('#pdf_browser_language').val(navigator.language);
296    $('#pdf_generation_form').submit();
297  });
298  return false
299}
300
301function create_offline_notebook(tree_root) {
302
303  la_confirm('Create an offline version of this notebook?', function() {
304    $('#offline_tree_node_id').val(tree_root);
305    $('#offline_tz_offset').val(getTimeZone());
306    $('#offline_generation_form').submit();
307  });
308  return false
309}
310
311
312function last_action_failed() {
313  la_alert('An error occurred attempting to connect to the LabArchives Servers.  Please check your internet connection and try your last action again. You may need to refresh the LabArchives Browser Page to ensure it is displaying correctly')
314}
315
316
317function get_selected_notebook() {
318  return selected_notebook; // selected_notebook is a var in search_tools partial containing the current notebook
319}
320
321function onShare_properties(node, bEntry, default_tab, nb_id, title) {
322  share_properties_changed = false;
323  var sTitle;
324  if (!bEntry) {
325    if (typeof(node) == 'number') {
326      node = $('#' + UI_TREE_PREFIX + node);
327
328    }
329    $('#tree_node_id').val($(node).attr('id'));
330    // if title param is providied, use it
331    if (title) {
332      sTitle = 'Share "' + la_escape_html(title.trim()) + '"';
333    }
334    else {
335      // try and get the title from Redux
336      var tree_node_data = store.getState().current_notebook.by_id[$(node).attr('id')];
337      if (tree_node_data) {
338        sTitle = 'Share "' + la_escape_html(tree_node_data.title.trim()) + '"';
339      }
340      else {
341        sTitle = 'Share';
342      }
343    }
344  }
345  else {
346    sTitle = 'Share Entry';
347    $('#tree_node_id').val(node * -1);
348  }
349
350  $('#tree_nb_id').val(la_ensure_defined(nb_id, get_selected_notebook()));
351  $('#tree_share_tab').val(default_tab);
352  $('#tree_share_property_form').trigger('onsubmit');
353  $('#tree_share_property_div').attr("title", sTitle);
354  $("#tree_share_property_div").dialog({bgiframe: true,
355    dialogClass: 'la-ui-dialog--overflow-visible',
356    height: 434,
357    width: 700,
358    modal: true,
359    buttons: {
360      'Close': function () {
361        $(this).dialog('close');
362      }
363    },
364    close: function () {
365      if (share_properties_changed) {
366        if (bEntry) {
367          // dispatch to Redux to refresh the entry's info
368          store.dispatch(window.la_react.page.setEntryShare({ id: node }));
369        }
370        else {
371          // if the current page is a descendant of the tree node,
372          // dispatch to Redux to refresh all of the page's entry info
373          const currentPageId = store.getState().current_notebook.active_page.replace(UI_TREE_PREFIX, "");
374          const sharedNodeId = node[0].id.replace(UI_TREE_PREFIX, "");
375          if (sharedNodeId === currentPageId || window.la_tree_find.isAncestorId(
376              store.getState().current_notebook.by_id, sharedNodeId, currentPageId)) {
377            store.dispatch(window.la_react.page.setPageShare({ id: currentPageId }));
378          }
379        }
380        // dispatch to Redux to refesh the React tree
381        store.dispatch(getCurrentNotebookLevel({ id: 0 }));
382        share_properties_changed = false;
383      }
384      $("#tree_share_property_div").dialog('destroy').html('');
385    }
386  });
387
388}
389
390
391// nb_ids   Array of notebooks_ids to perform search, must be api_ids (encoded)
392function doSearch(newString, search_type, search_data, limit_search_to_user, nb_ids) {
393  nb_ids = la_ensure_defined(nb_ids, [get_selected_notebook()]);
394  // Generate a unique ID if doing a multinotebook search so we can group them in the Action table
395  var search_id = '';
396  if (nb_ids.length > 1) {
397    search_id = (new Date()).valueOf().toString() + Math.floor(Math.random() * 104).toString();
398  }
399
400  // dispatch to Redux to add search folder to React tree, if already added then clear folder
401  store.dispatch(addCurrentNotebookTreeSearchFolder({
402    id: SEARCH_TREE_ID,
403  }));
404
405    let parent_child_id = null;
406    let last_node_id = null;
407    const search_nodes = {};
408    // Create a results page for each notebook
409    $.each(nb_ids, function (idx, id) {
410
411    // create search node data
412    const search_node_id = `${SEARCH_TREE_ID}-${idx}`;
413    search_nodes[search_node_id] = {
414      count: null,
415      limit_search_to_user,
416      search_data,
417      search_id,
418      search_nb_id: la_ensure_defined(id, ''),
419      search_type,
420      title:
421          (search_type === 'advance_search')
422              ? store.getState().notebooks.by_id[id].name.trim()
423              : newString,
424      type: 'SP',
425    };
426    // The first search node will be the parent's child (flat data)
427    if (parent_child_id === null) {
428      parent_child_id = search_node_id;
429    }
430    // if last node id exists, set last node's next id to this iteration's search node id
431    if (last_node_id !== null) {
432      search_nodes[last_node_id]['next'] = search_node_id;
433    }
434    last_node_id = search_node_id;
435  });
436
437  // dispatch to Redux to add search "pages" to React tree
438  // Note: since this is a new search, the search page and sort is reset
439  store.dispatch(addCurrentNotebookTreeSearchNodes({
440    id: SEARCH_TREE_ID,
441    child: parent_child_id,
442    search_nodes,
443    search_page_num: '1',
444    search_sort: '',
445  }));
446
447  // dispatch to Redux to load the first search results "page"
448  store.dispatch(loadCurrentNotebookPage({
449    id: SEARCH_TREE_ID + '-0',
450  }));
451
452  // Update results with hit counts
453  NavTreeGetSearchCounts(SEARCH_TREE_ID+'-0');
454}
455
456
457function onSimpleSearch() {
458  if (($('#search_text').val().trim().length > 0) && ($('#search_text').val() != NOTEBOOK_SEARCH_TEXT)) {
459    doSearch($('#search_text').val(), 'quick_search', $('#search_text').val(), $('#search_mydataonly').is(':checked'));
460  }
461  return false;
462}
463
464
465function search_page_sort(sort) {
466  // dispatch to Redux to set search sort and load the search results "page"
467  store.dispatch(loadCurrentNotebookPage({
468    search_sort: sort,
469  }));
470
471  return false;
472
473
474}
475
476function search_page_next_prev(page_num) {
477  if (page_num > 0) {
478    // dispatch to Redux to set search page number and load the search results "page"
479    store.dispatch(loadCurrentNotebookPage({
480      search_page_num: page_num,
481    }));
482  }
483  return false;
484}
485
486
487window.reloading = false;
488function reload_page() {
489  window.reloading = true;
490  handleNotebookChange(); //reloads the page
491}
492
493
494function ajax_loading(ele) {
495  // TODO: Nav Tree - non-React loading uses "window.eCurAjaxEle" and "ele"
496  if (!ele) {
497    if (window.store) {
498      // dispatch to Redux to show a loding spinner in the React tree
499      store.dispatch(toggleLoadingSpinner({ visible: true }));
500      return;
501    }
502  }
503  ajax_loaded(); // turn off old call if there was one
504  window.eCurAjaxEle = ele;
505  if (window.eCurAjaxEle) {
506    $(window.eCurAjaxEle).addClass('la-loading');
507    $('body').addClass('la-body-loading');
508  }
509}
510
511function ajax_loaded() {
512  // TODO: Nav Tree - non-React loading uses "window.eCurAjaxEle"
513  if (window.eCurAjaxEle) {
514    $(window.eCurAjaxEle).removeClass('la-loading');
515    $('body').removeClass('la-body-loading');
516  }
517  window.eCurAjaxEle = null;
518
519  if (window.store) {
520    // dispatch to Redux to hide a loding spinner in the React tree
521    store.dispatch(toggleLoadingSpinner({ visible: false }));
522  }
523}
524
525function is_ajax_loading() {
526  // TODO: Nav Tree - non-React loading uses "window.eCurAjaxEle"
527  return (
528      (window.eCurAjaxEle != null)
529      || (window.store && (store.getState().loading_spinner === true))
530  );
531}
532
533function la_enable_content(enable) {
534  if (enable) {
535    $('#la_content_overlay').remove();
536  }
537  else if ($('#la_content_overlay').length < 1) {
538    $('<div id="la_content_overlay" class="ui-widget-overlay la_content_overlay"/>').appendTo('body');
539  }
540  return false;
541}
542
543function OPTION_DEFAULTS() {
544  return {
545    use_async: false,
546    redraw_page: true
547  };
548}
549
550function set_options($ele, options) {
551  options = $.extend(OPTION_DEFAULTS(), options);
552  $.each(options, function(key, value){
553    $ele.data(key, value);
554  });
555}
556
557function get_options($ele) {
558  options = {};
559  defaults = OPTION_DEFAULTS();
560  for (var valid_option in defaults) {
561    if ($ele.data(valid_option) === undefined) {
562      // undefined indicates, for our purposes, that the option isn't set
563      options[valid_option] = defaults[valid_option];
564    } else {
565      options[valid_option] = $ele.data(valid_option);
566    }
567  }
568  return options;
569}
570
571function reset_options($ele) {
572  $.each(OPTION_DEFAULTS(), function(key, value){
573    $ele.data(key, value);
574  });
575}
576
577
578function pop_widget(url, title) {
579  //escape HTML in widget name using jquery
580  title = la_escape_html(title);
581  var id = 'popup_widget_' + new Date().valueOf();
582  $('<iframe/>', {
583    id: id,
584    style: 'width:100%;border:0px',
585    scrolling: 'no',
586    src: url,
587    title: title
588  }).dialog({
589    autoOpen: true,
590    height: 'auto',
591    width: 'auto',
592    modal: false,
593    resizable: false,
594    create: function (event) {
595      var that = this;
596      var height, width;
597
598      $(event.target).parent().css('position', 'fixed');
599      $(this).parent().find('.ui-dialog-titlebar').append('<a href="#" class="dialog-minimize ui-dialog-titlebar-min ui-corner-all" role="button" style="position:absolute;right:24px;margin-top:-10px;top:50%;"><span class="ui-icon ui-icon-minusthick">minimize</span></a>');
600      $(this).parent().find('.ui-dialog-titlebar').append('<a href="#" class="dialog-restore ui-dialog-titlebar-rest ui-corner-all" role="button" style="position:absolute;right:24px;margin-top:-10px;top:50%;"><span class="ui-icon ui-icon-newwin">restore</span></a>');
601      $(this).parent().find('.dialog-restore').hide();
602
603      $(this).parent().find('.dialog-minimize').hover(
604          function () {
605            $(that).parent().find('.dialog-minimize').addClass('ui-state-hover');
606          },
607          function () {
608            $(that).parent().find('.dialog-minimize').removeClass('ui-state-hover');
609          }
610      ).focus(
611          function () {
612            $(that).parent().find('.dialog-minimize').addClass('ui-state-focus');
613          }
614      ).blur(
615          function () {
616            $(that).parent().find('.dialog-minimize').removeClass('ui-state-focus');
617          }
618      );
619
620      $(this).parent().find('.dialog-restore').hover(
621          function () {
622            $(that).parent().find('.dialog-restore').addClass('ui-state-hover');
623          },
624          function () {
625            $(that).parent().find('.dialog-restore').removeClass('ui-state-hover');
626          }
627      ).focus(
628          function () {
629            $(that).parent().find('.dialog-minimize').addClass('ui-state-focus');
630          }
631      ).blur(
632          function () {
633            $(that).parent().find('.dialog-minimize').removeClass('ui-state-focus');
634          }
635      );
636
637      //minimize
638      $(this).parent().find('.dialog-minimize').click(function () {
639        height = $(that).parent().height();
640        width = $(that).parent().width();
641        $(this).parents('.ui-dialog').animate({
642          height: '31px',
643          width: '200px'
644        }, 200);
645        $(this).parents().find('.ui-dialog-title').addClass('span_ellipsis');
646        $(that).parent().find('.dialog-minimize').hide();
647        $(that).parent().find('.dialog-restore').show();
648        $(`#${id}`).css('display', 'none');
649        return false;
650      });
651
652      //restore
653      $(this).parent().find('.dialog-restore').click(function () {
654        $(this).parents('.ui-dialog').animate({
655          height: height,
656          width: width
657        }, 200);
658        $(that).parent().find('.dialog-restore').hide();
659        $(that).parent().find('.dialog-minimize').show();
660        $(`#${id}`).css('display', 'block');
661        return false;
662      });
663    },
664    resizeStop: function (event, ui) {
665      $(event.target).parent().css('position', 'fixed');
666    },
667    open: function() {
668        replaceTag(document.querySelector(`[aria-describedby=${id}] .ui-dialog-title`), 'h5');
669    },
670    close: function (event, ui) {
671      $('#' + id).attr('src', '').remove();  //not clearing source causes the url to be called
672    }
673  })
674}
675
676function clean_up_ckeditor() {
677  //delete any existing ckeditor  controls - can get stranded by the Form_editor
678  try {
679    if (typeof CKEDITOR !== 'undefined') {
680      for (var x in LA_CKEDITOR().instances) {
681        LA_CKEDITOR().instances[x].destroy();
682      }
683    }
684  }
685  catch (e) {
686      la_alert('clean up error :' + e.message);
687  }
688
689}
690
691function pre_content_replace_cleanup() {
692  //this should be called prior to replacing  the entire content div via replace_html
693  // also unbinds the dragenter event that is added when a notebook page is displayed
694  //now destroy any existing spreadsheets
695  if ($.sheet && $.sheet.instance) {
696    $.sheet.killAll();
697  }
698  clean_up_ckeditor();
699
700  withPage(function(la_page) {
701    la_page.leavePage();
702  });
703
704  $("#content").off('dragenter');
705}
706
707
708function ep_flip_load_thumbs(thumb_id) {
709  var first_url = '';
710  $('#' + thumb_id + ' ul li img').each(function (n, obj) {
711    if (n == 0) {
712      first_url = $(obj).attr('src');
713    }
714    else
715      $(obj).attr('src', first_url.replace(/page_num=0/, 'page_num=' + Number(n)));
716  });
717}
718
719function ep_flip_load_all_thumbs() {
720  $(".docflip").each(function (bign, bigobj) {
721    ep_flip_load_thumbs(bigobj.id);
722  });
723}
724
725function LA_CKEDITOR() {
726  return CKEDITOR
727}
728
729function is_importing() {
730  return la_import_data != null;
731}
732
733function get_import_data() {
734  //returns import data
735  return la_import_data;
736}
737
738function clear_import_data() {
739  la_import_data = null;
740}
741//end import related methods
742
743
744function load_nb_entry(nb_id, tree_id, entry_id, comment_id) {
745  ajax_loading();
746  $('#load_nb_comment_id').val(comment_id);
747  $('#load_nb_entry_id').val(entry_id);
748  $('#load_nb_tree_id').val(tree_id);
749  $('#load_nb_nb_id').val(nb_id);
750  $('#load_nb_entry').submit();
751}
752
753function open_course_grader(nb_id, assignment_id, section_id) {
754    $('#open_course_grader_nb_id').val(nb_id);
755    $('#open_course_grader_as_id').val(assignment_id);
756    $('#open_course_grader_s_id').val(section_id);
757    $('#open_course_grader').submit();
758}
759
760//methods formally in show.js
761
762function ep_init_for_entry(entry_id, add_entry_pos, add_entry_specified) {
763  ep_current_entry_id = entry_id;
764  ep_add_entry_position = add_entry_pos;
765  ep_add_entry_specified = add_entry_specified;
766}
767
768
769function la_add_css(path) {
770  $("head").append("<link>");
771  var css = $("head").children(":last");
772  css.attr({
773    rel: "stylesheet",
774    type: "text/css",
775    href: path
776  });
777}
778
779function la_add_js(path) {
780  $.ajaxSetup({async: false}); //getScript defaults to async which means we might exit before we load
781  $.getScript(path);
782  $.ajaxSetup({async: true});
783}
784
785function ensure_sketch_tool() {
786  //called  as a sketch that needs to be displayed or edited
787  if (!$.ui.lasketch) {
788    la_add_css("/javascripts/la-sketch/resources/jquery-miniColors/jquery.minicolors.css");
789    la_add_css("/javascripts/la-sketch/resources/la-sketch/la-sketch.css");
790    $.ajaxSetup({async: false}); //getScript defaults to async which means we might exit before we load
791    $.getScript("/javascripts/la-sketch/resources/raphael.min.js");
792    $.getScript("/javascripts/la-sketch/resources/raphael.json.js");
793    $.getScript("/javascripts/la-sketch/resources/raphael.scale.js");
794    $.getScript("/javascripts/la-sketch/resources/jquery-miniColors/jquery.minicolors.min.js");
795    $.getScript("/javascripts/la-sketch/resources/legacyAnnotationConvert.js");
796    $.getScript("/javascripts/la-sketch/resources/la-sketch/jquery.ui.la-sketch.js");
797    $.getScript("/javascripts/la-sketch/resources/la-sketch/jquery.la-sketch.js");
798    $.getScript("/javascripts/la-sketch/resources/raphael.scale.js");
799    $.getScript("/javascripts/la-sketch/resources/la-sketch/la-sketch-viewer.js");
800    $.ajaxSetup({async: true});
801  }
802}
803
804
805function ensure_javascript_editor() {
806  if (typeof(CodeMirror) == 'undefined') {
807    la_add_css("/javascripts/codemirror-2.23/lib/codemirror.css");
808    $.ajaxSetup({async: false}); //getScript defaults to async which means we might exit before we load
809    $.getScript("/javascripts/codemirror-2.23/lib/codemirror.js");
810    $.getScript("/javascripts/codemirror-2.23/mode/javascript/javascript.js");
811    $.ajaxSetup({async: true});
812  }
813}
814
815function edit_reload_entry(bLoading_display) {//assumes that the tree_form_load_entry is present and set to our entry
816  try {
817    okToLoadPage(true, function() {
818      if (bLoading_display)
819        ajax_loading();
820
821      store.dispatch(loadCurrentNotebookPage({
822        id: store.getState().current_notebook.active_page,
823      }));
824    });
825  }
826  catch (e) {
827  }
828}
829
830
831var bIsEditingAPart = false;
832function editingAPart(bIsEditing) {
833  bIsEditingAPart = bIsEditing;
834  if (bIsEditingAPart) {
835    const ep_wrapper_active = document.querySelector(".ep_wrapper_active");
836    if (ep_wrapper_active) {
837      ep_wrapper_active.classList.add("entry-wrapper--editing");
838    }
839    $('#editing_entry_part').show();
840    window.la_react.ui.dispatchSetIsEditingAPart(true);
841  }
842  else {
843    $('*[class^=ep_editor_title_]').remove();
844    const entry_wrappers = document.querySelectorAll(".entry-wrapper");
845    if (entry_wrappers) {
846      entry_wrappers.forEach(entry_wrapper => {
847        entry_wrapper.classList.remove("entry-wrapper--editing");
848      });
849    }
850    $('#editing_entry_part').hide();
851    window.la_react.ui.dispatchSetIsEditingAPart(false);
852  }
853}
854
855
856function isEditingComment() {
857  var b_editing_comment = false;
858  //search for a comment text area that is visible  and that is not equal to the default text
859  $('.ep_comment_ta[id]').each(function (i) {
860    if (($.trim($(this).val()) != '') && ($(this).is(":visible"))) {
861      b_editing_comment = true;
862    }
863  });
864  return b_editing_comment;
865}
866
867function isEditingAPart() {
868  return bIsEditingAPart || isEditingComment();
869
870}
871
872function ok_toLoadPage_text(entry_msg) {
873  return (!la_defined(entry_msg) || entry_msg ? 'You are currently editing an entry.' : 'You have made changes.') + ' If you leave this page, you will abandon any edits and load the newly selected page.';
874}
875
876// ok_callback & cancel_callback called only if b_display_ui=true
877function okToLoadPage(b_display_ui, ok_callback, cancel_callback) {
878  if (window.isUserEditRequired) {
879    window.renderLaModalAlert({
880      title: "Please update your account information",
881      msg: 'Before continuing, fill out the required fields and click "Update".',
882    });
883    return;
884  }
885  var bOK = true;
886  try {
887    bOK = !(isEditingAPart());
888    if (b_display_ui) {
889      if (!bOK) {
890        la_confirm({
891            msg: ok_toLoadPage_text((typeof(la_auto_save) != 'undefined') && la_auto_save),
892            ok_text: 'Abandon Edits',
893            cancel_text: 'Keep Editing',
894            ok_call: function () {
895                //make sure this is for an entry edit and not something else, like a comment etc..
896                if ((typeof(la_auto_save) != 'undefined') && la_auto_save) {
897                    //if it is then
898                    la_auto_save.cancel();
899                }
900                if (la_defined(ok_callback)) {
901                    ok_callback();
902                }
903                return true;
904            },
905            cancel_call: function() {
906                if (la_defined(cancel_callback)) {
907                    cancel_callback();
908                }
909                return true;
910            }
911        });
912      }
913      else {
914          ok_callback();
915      }
916    }
917  }
918  catch (e) {
919  }
920  return bOK;
921}
922
923function la_auto_save_destroy() {
924  if (la_auto_save) {
925    la_auto_save.destroy();
926  }
927}
928
929
930function reload_timeout() {
931  window.setTimeout("reload_entry(true)", 60 * 10 * 1000)
932
933}
934
935function reload_entry(bRetry) {
936
937  if (!isEditingAPart()) {
938    loadAPage(false, ep_tree_id_prefix + ep_current_entry_id);
939  }
940  else if (bRetry) {
941    reload_timeout();
942  }
943}
944
945
946function new_office_doc(part_pos) {
947  var link = $('#od_create_new');
948  sURL = link.attr("href").replace(/pos=(.*)/, 'pos=' + part_pos);
949  window.open(sURL, '_blank');
950  ajax_loaded();
951  editingAPart(false);
952
953}
954
955function edit_part_id(ele) {
956  if (typeof (ele) != 'string')
957    ele = ele.id;
958  return String(ele).replace(/(^.*_)/, '');
959
960}
961
962
963function show_entry_revision(r_id, ele, part_only) {
964  var $entry_rev = $(ele).next();
965  if ($entry_rev.length > 0) {
966    $entry_rev.toggle();
967  }
968  else {
969    ajax_loading(ele);
970    s = part_only ? '_part' : '';
971    $("#ep_revision" + s + "_id").val(r_id);
972    $("#ep_show" + s + "_revision").trigger('onsubmit');
973  }
974}
975
976
977function toggle_aria_hidden(selector) {
978  var $ele = $(selector);
979  if ($ele.attr('aria-hidden') == 'true') {
980    $ele.attr('aria-hidden', 'false');
981  } else {
982    $ele.attr('aria-hidden', 'true');
983  }
984}
985
986
987function draw_math_formula(id) {
988  try {
989    MathJax.Hub.Queue(["Typeset", MathJax.Hub, id]);
990  }
991  catch (e) {
992  }
993}
994function edit_part(ele) {
995  var ep_id = edit_part_id(this);
996  var entry_part_model = LAPage.findEntryPart(ep_id);
997  var ep_locked = LAPage.entryPartLocked(entry_part_model);
998
999  if ((!ep_locked) && (!is_ajax_loading()) && (!bIsEditingAPart) && !window.entry_page_locked) {
1000    // comment dblclick does not cause editing
1001    if (ele.target
1002    &&  $(ele.target).closest('.entry_comments').length) {
1003      return false;
1004    }
1005    ajax_loading($(`#entry_header_portal_${ep_id} .entry-loader`));
1006    //make sure the ep has its active class set
1007    $('#epw_' + ep_id).addClass('ep_wrapper_active');
1008    //div panel and set to editing mode, so no other  controls can become visible
1009
1010    if (entry_part_model) {
1011      entry_part_model.edit('la:frontend');
1012    }
1013
1014    editingAPart(true);
1015    $("#ep_edit_part_id").val(ep_id);
1016    $("#ep_edit_part_form").trigger('onsubmit');  //note calling submit(); which calls the html form actions
1017  }
1018  return false;
1019}
1020
1021
1022function sk_json(data) {
1023  //to get around a rubymine inspection
1024  return data;
1025}
1026
1027// Emulating the opening in new tab part of generatePageUrlAndOpenInNewTab in navigation.ts
1028// you can get the endpoint param by calling navigation_endpoints in application_controller.rb
1029function open_navigation_endpoint(endpoint) {
1030  window.open(location.origin + "/" + endpoint, "_blank");
1031}
1032
1033/**
1034 * @deprecated
1035 */
1036function go_to_page(ele) {
1037  if (!bIsEditingAPart) {
1038    ajax_loading();
1039    $("#ep_goto_part_id").val(edit_part_id(this));
1040    $("#ep_goto_entry_form").trigger('onsubmit');
1041  }
1042  return false;
1043}
1044
1045function go_to_part(id, revision_id) {
1046  if (!bIsEditingAPart) {
1047    ajax_loading();
1048    $("#ep_goto_part_id").val(id);
1049    $("#ep_goto_part_revision").val((revision_id) ? revision_id : '');
1050    $("#ep_goto_entry_form").trigger('onsubmit');
1051  }
1052  return false;
1053}
1054
1055
1056function go_to_specific_page(id, revision_id) {
1057  if (!bIsEditingAPart) {
1058    ajax_loading();
1059    $("#ep_page_to_load").val(id);
1060    $("#ep_page_to_load_revision").val((revision_id) ? revision_id : '');
1061    $("#ep_load_specific_page").trigger('onsubmit');
1062  }
1063  return false;
1064}
1065
1066
1067function revert_part(part_id, version, isDelete) {
1068  if (!bIsEditingAPart && !window.entry_page_locked) {
1069
1070    sMessage = "Revert the entry to this version?";
1071    if (isDelete) {
1072      sMessage = "Undelete this Entry?";
1073    }
1074    la_confirm(sMessage, function () {
1075
1076      $("#ep_revert_part_id").val(part_id);
1077      $("#ep_revert_part_version").val(version);
1078      $("#ep_revert_part_form").trigger('onsubmit');
1079    });
1080  }
1081  else {
1082    la_alert('Cannot revert while you are currently editing this entry.')
1083  }
1084  return false;
1085}
1086
1087function revision_complete() {
1088  ajax_loaded();
1089  var id = $("#revision_part_id").val();
1090  $('#epr_' + id).toggle('blind', {direction: 'vertical'});
1091}
1092
1093
1094function comment_complete(bS, pid) {
1095  ajax_loaded();
1096  var id = (pid) ? pid : $("#comment_part_id").val();
1097  var com = $('#epcom_' + id);
1098  com.toggle('blind', {
1099    direction: 'vertical',
1100    complete: function() {
1101      if (bS) {
1102        scrollToSelector(`#${com[0].id}`);
1103        let scrollTimeout;
1104        const onScrollAfter = () => {
1105          clearTimeout(scrollTimeout);
1106          scrollTimeout = setTimeout(() => {
1107            window.removeEventListener("scroll", onScrollAfter);
1108            $('#entry_display').data('scrolling', false);
1109          }, 100);
1110        };
1111        window.addEventListener("scroll", onScrollAfter);
1112        setTimeout(() => {
1113          if (!scrollTimeout) {
1114            window.removeEventListener("scroll", onScrollAfter);
1115          }
1116        }, 100);
1117      } else {
1118        com[0].innerHTML = "";
1119      }
1120    }
1121  });
1122  return false;
1123}
1124
1125function ep_run_upload_info_passed_user(up) {
1126  var num_files = up.files.length;
1127  var bOk = true;
1128  if (up.total.uploaded == 0) {
1129    if (num_files < 21) {
1130      bOk = true;
1131    }
1132    else if (num_files < 51) {
1133      bOk = window.confirm('Are you sure you want to upload ' + num_files + ' files?')
1134    }
1135    else {
1136        la_alert('Your are attempting to upload ' + num_files + ' files.  Only a maximum of 50 files can be uploaded at any one time.');
1137      bOk = false;
1138    }
1139  }
1140  if (!bOk) {
1141    up.stop();
1142    ajax_loaded();
1143    editingAPart(false);
1144    reload_entry(false);
1145  }
1146  return bOk;
1147}
1148
1149function ep_unknown_edit_issue(err_msg, do_reload) {
1150  la_alert({
1151    msg:la_ensure_defined(err_msg, 'There was an issue attempting to bring up the entry editor. Please retry editing the entry after the page finishes reloading.'),
1152    ok_call :function () {
1153      if (bIsEditingAPart) {
1154        editingAPart(false);
1155      }
1156      if (la_ensure_defined(do_reload, true)) {
1157        reload_entry(false);
1158      }
1159      ajax_loaded();
1160    }
1161  });
1162}
1163
1164
1165function part_revisions(ele) {
1166  var p_id = edit_part_id(this);
1167  $("#revision_part_id").val(p_id);
1168  if ($('#epr_' + p_id).is(':visible')) {
1169    revision_complete(); //just close it
1170  }
1171  else if ((!bIsEditingAPart) && (!is_ajax_loading())) {
1172    ajax_loading($(`#entry_header_portal_${p_id} .entry-loader`));
1173    $('#revision_part_entry_locked').val(window.entry_page_locked);
1174    $("#ep_part_revisions_form").trigger('onsubmit');
1175  }
1176  return false;
1177}
1178
1179function part_comments(ele) {
1180  if (!bIsEditingAPart) {
1181    var p_id = edit_part_id(this);
1182    $("#comment_part_id").val(p_id);
1183    if ($('#epcom_' + p_id).is(':visible')) {
1184      comment_complete(false); //just close it
1185    }
1186    else {
1187      ajax_loading($(this));
1188      $("#ep_part_comment_form").trigger('onsubmit');
1189    }
1190  }
1191  return false;
1192}
1193
1194
1195function part_share(ele) {
1196  if ((!bIsEditingAPart) && (!is_ajax_loading())) {
1197    // get last loaded notebook from redux
1198    var last_loaded_notebook_id = store.getState().current_notebook.last_loaded_notebook_id;
1199    onShare_properties(edit_part_id(this), true, "invite", last_loaded_notebook_id);
1200  }
1201  return false;
1202}
1203
1204function part_deletion(ele) {
1205  if ((!is_ajax_loading()) && (!bIsEditingAPart) && !window.entry_page_locked) {
1206    _this = this;
1207    la_confirm('Delete this entry?', function() {
1208      ajax_loading();
1209      $("#delete_part_id").val(edit_part_id(_this));
1210      //ok, this is kind of bogus but we need to get the id of the next insert part panel .
1211      //  So we do this ()
1212      var sVal = '';
1213      try {
1214        var insert_panel = $(_this).parents('div.ep_wrapper_active, div.ep_wrapper').next();
1215        if (insert_panel.attr('class').indexOf('ep_ele_insert') > -1) {
1216          sVal = insert_panel.attr('id');
1217        }
1218      }
1219      catch (e) {
1220        //there was no text item (cause we were the last) so passing empty is the correct thing to do
1221        sVal = '';
1222      }
1223      $("#delete_part_control").val(sVal);
1224      const entryWrappers = [...document.querySelectorAll(".entry-wrapper")];
1225      const thisEntryWrapperIdx = entryWrappers.indexOf(_this.parentElement);
1226      const nextEntryWrapperToFocus = entryWrappers.length <= 0 ? null :
1227          thisEntryWrapperIdx >= entryWrappers.length - 1 ?
1228          entryWrappers[thisEntryWrapperIdx - 1] :
1229          entryWrappers[thisEntryWrapperIdx + 1];
1230      $("#ep_part_deletion_form").trigger('onsubmit');
1231      window.la_react.page.dispatchSetPageEntryLastDeleted(edit_part_id(_this), edit_part_id(_this));
1232      setTimeout(() => {
1233        const toast = document.querySelector("#del_toast_" + edit_part_id(_this));
1234        toast?.focus();
1235      }, 150);
1236      setTimeout(() => {
1237        // if focus has not changed after being reset to top of dom, set it to the edit button or header, otherwise just maintain current focus.
1238        if(document.activeElement.id === "del_toast_" + edit_part_id(_this)) {
1239          if (!nextEntryWrapperToFocus) {
1240            const page_header_wrapper = document.querySelector("#page_header_wrapper");
1241            const header = page_header_wrapper?.querySelector(".sr-only");
1242            header?.focus();
1243          }
1244          else {
1245            const editEntryButton = nextEntryWrapperToFocus.querySelector(".edit-entry");
1246            editEntryButton?.focus();
1247          }
1248        }
1249      }, 1500);
1250    }, () => {
1251      setTimeout(() => {
1252        _this.querySelector(".more-entry-options")?.focus();
1253      });
1254    });
1255  }
1256  return false;
1257}
1258
1259
1260function classToPartType(sClass) {
1261  if (!isNaN(sClass)) {
1262    return sClass;//hack so I can pass real part types as well, saves me in the import function
1263  }
1264  var s = 'unrecgnized_part_type';
1265  switch (sClass) {
1266    case 'ep_it_link':
1267      s = EPT_TEXT;
1268      break;
1269    case 'ep_pt_link':
1270      s = EPT_PLAIN_TEXT;
1271      break;
1272    case 'ep_at_link':
1273      s = EPT_ATTACHMENT;
1274      break;
1275    case 'ep_ht_link':
1276      s = EPT_HEADING;
1277      break;
1278    case 'ep_sh_link':
1279      s = EPT_SPREADSHEET;
1280      break;
1281    case 'ep_rf_link':
1282      s = EPT_REFERENCES;
1283      break;
1284    case 'ep_me_link':
1285      s = EPT_EQUATION;
1286      break;
1287    case 'ep_se_link':
1288      s = EPT_SKETCH;
1289      break;
1290    case 'ep_fr_link':
1291      s = EPT_FORM;
1292      break;
1293    case 'ep_cf_link':
1294      s = EPT_COLLECTED_FORM;
1295      break;
1296    case 'ep_as_link':
1297      s = EPT_ASSIGNMENT;
1298      break;
1299    case 'ep_in_link':
1300      s = EPT_INVENTORY;
1301      break;
1302    case 'ep_of_link':
1303    case 'ep_im_link':
1304      s = sClass;
1305      break;
1306  }
1307  return s;
1308}
1309
1310function tag_search() {
1311  doSearch('Tag = "' + $(this).text() + '"', 'tag_search', $(this).text());
1312  return false;
1313}
1314
1315function part_add_low(part_type) {
1316  if (!bIsEditingAPart && !window.entry_page_locked) {
1317    var insert_control = $('#' + ep_current_insert_control);
1318    ajax_loading();
1319    editingAPart(true);
1320    //locate the new parts position in the entry
1321    var nPartPos = 0;
1322    $('div[id*=epiw_]').each(function (i) {//ep_it_link
1323      nPartPos++;
1324      return this.id != insert_control.attr('id');
1325    });
1326    $("#ep_new_part_pos").val(nPartPos);
1327    $("#ep_part_type").val(part_type);
1328    $("#ep_new_part_page_loc").val('epi_portal_' + edit_part_id(insert_control.attr('id')));
1329    if (part_type == 'ep_of_link') {//office document
1330      new_office_doc(nPartPos);
1331    }
1332    else {
1333      $("#ep_new_part_form").trigger('onsubmit');
1334    }
1335    ep_insert_bar_reset();
1336  }
1337  return false;
1338}
1339
1340
1341function ep_insert_bar_reset() {
1342  document.querySelectorAll(".ep_ele_insert").forEach((ep_ele_insert) => {
1343    ep_ele_insert.dispatchEvent(new FocusEvent("blur"));
1344  });
1345}
1346
1347
1348function part_count(){
1349  return $('[id^="epw_"]').length;
1350}
1351
1352function has_assignment_parts() {
1353  return $('.as_entry_data_wrapper').length > 0;
1354}
1355
1356function part_insert(class_type, form_id, confirm_add) {
1357  if (la_ensure_defined(confirm_add, true)) {
1358    confirm_add_part(null, class_type, form_id, 'part_insert(class_type, form_id, false)');
1359  }
1360  else {
1361    try {$("#ep_new_part_form_id").val(form_id);} catch(e){}
1362    try {$('#' + ep_current_insert_control).attr('class', 'ep_ele_insert');} catch(e){}
1363    $("#ep_new_part_do_scroll").val('false');
1364    part_add_low(classToPartType(class_type));
1365  }
1366}
1367
1368function part_add(ele, class_type, form_id, confirm_add) {
1369  /*console.log( $(this).attr('form_id'));  */
1370  if (la_ensure_defined(confirm_add, true)) {
1371    confirm_add_part(ele, $(this).attr('class'), $(this).attr('form_id'), 'part_add(ele, class_type, form_id, false)');
1372    return false;
1373  }
1374  else {
1375    $("#ep_new_part_do_scroll").val('false');
1376    $("#ep_new_part_form_id").val(la_ensure_defined(form_id, $(this).attr('form_id')));
1377
1378    return part_add_low(classToPartType(la_ensure_defined(class_type, $(this).attr('class'))));
1379  }
1380}
1381
1382function part_add_new(ele, class_type, form_id, confirm_add) {
1383  //console.trace();
1384
1385  if (!ep_add_entry_specified && (part_count() > 0)) {
1386    popup_add_entry_option(ele, class_type, form_id);
1387    return false;
1388  }
1389  if (la_ensure_defined(confirm_add, true)) {
1390    confirm_add_part(
1391      ele,
1392      class_type,
1393      form_id,
1394      'part_add_new(ele, class_type, form_id, false)',
1395      false
1396    );
1397  }
1398  else {
1399    close_la_msg_dlg();
1400    var current_old = ep_current_insert_control;
1401    //adds part to top or bottom of page depending on ep_add_entry_position param
1402    ep_current_insert_control = (ep_add_entry_position == 'top' ? 'epiw_0' : $('[id^=epiw_]:last').attr('id'));
1403    $("#ep_new_part_do_scroll").val('true');
1404    $("#ep_new_part_form_id").val(form_id);
1405    part_add_low(classToPartType(class_type));
1406    ep_current_insert_control = current_old;
1407  }
1408  return false;
1409}
1410
1411function confirm_add_part(ele, class_type, form_id, add_code, close_dlg, force_confirm) {
1412  if (la_ensure_defined(force_confirm, false) || ((class_type == 'ep_as_link') && has_assignment_parts())) {
1413    var msg = [];
1414    msg.push('<div class="ui-state-highlight" style="padding: 8px; margin-bottom: 8px">');
1415    msg.push('<span class="ui-icon ui-icon-alert" style="float: left; margin-right: 4px;"></span>');
1416    msg.push('There is already an assignment entry on the page.  We recommend that you have only one assignment on a page; otherwise, the page-locking option in student notebooks will not work as intended.');
1417    msg.push('</div>');
1418    msg.push('Are you sure you want to add another assignment to this page?');
1419    la_confirm({
1420      title: 'Confirm Multiple Assignments',
1421      msg: msg.join(''),
1422      ok_text: 'Add Assignment',
1423      ok_call: function () {
1424        eval(add_code);
1425        return la_ensure_defined(close_dlg, true);
1426      }
1427    });
1428  }
1429  else {
1430    eval(add_code);
1431  }
1432  return false;
1433}
1434
1435function part_import(import_data) {
1436  try {
1437    $('#ep_new_import_data').val(JSON.stringify(import_data));
1438  }
1439  catch (e) {
1440  }
1441  part_add_new(null, import_data.part_type, 0);
1442}
1443
1444function part_import_csv(class_type, form_id, file_name, insert_part) {
1445  try {
1446    $('#ep_new_part_csv_file_name').val(file_name);
1447  }
1448  catch (e) {
1449  }
1450  if (insert_part) {
1451    part_insert(class_type, form_id);
1452  }
1453  else {
1454    part_add_new(null, class_type, form_id);
1455  }
1456}
1457
1458function part_move(ele) {
1459  //located in _navtree
1460  if ((is_ajax_loading()) || (bIsEditingAPart) || window.entry_page_locked)
1461    return false;
1462  $("#move_part_control").val($(this).parents('div.ep_wrapper_active').next().attr('id'));
1463  $("#move_part_id").val(edit_part_id(this));
1464  //if we got an empty id, just pretend we didn't get the click.
1465  //there is some strange bug that intermittently returns an empty string.  I'm not sure if its happening at the client or server
1466  // and, I cannot seem to duplicate it enough to solve it so...
1467  if ($("#move_part_id").val() == '')
1468    return false;
1469  popupSelectTree('tree_move_part_div', 'move_part_nav_tree', function (locNode, locTree) {
1470    if (nodeIsPage(locNode)) {
1471      la_confirm('Move entry to "' + la_escape_html(locTree.get_text(locNode)) + '"?', function() {
1472        var itemAdded = window.itemAdded; //declared in navtree
1473        if (window.itemAdded) window.itemAdded = false;//cause the controller does a reload so popupSelectTree dosn't have to
1474        ajax_loading();
1475        $("#tree_move_part_div").dialog('close');
1476        $("#move_part_dest_tree_id").val($(locNode).attr('id').substr(ep_tree_id_prefix.length));
1477        $("#move_part_paged_added").val(itemAdded); //declared in navtree
1478        $('#move_part_confirm_add_part').val(true);
1479        $("#ep_part_move_form").trigger('onsubmit');
1480      });
1481    }
1482    else if (is_folder(locNode)) {
1483      locTree.toggle_branch(locNode);
1484    }
1485  });
1486
1487  return false;
1488
1489
1490}
1491
1492function call_script_to_import_csv_file(data, widget_type) {
1493  const iframe_target = document.getElementById("ep_widget_iframe").contentWindow;
1494  $.widget_proxy.main_app.call_script_to_import_csv_file(iframe_target, data, widget_type);
1495}
1496
1497function ep_toggle_more_type_options(id) {
1498  //toggles the more types menu
1499  var parent = $("#" + id);
1500  $("#epl_more_parts").toggle();
1501  $("#epl_more_parts").position({
1502      my: 'left top',
1503      at: 'left bottom',
1504      of: parent
1505  });
1506  return false;
1507
1508}
1509
1510
1511function ep_save_error(entry_type) {
1512
1513    la_alert('There was an error saving your ' + entry_type + ' to the server.  Your last edits were not saved');
1514
1515  ajax_loaded();
1516  loadAPage(false, ep_tree_id_prefix + ep_current_entry_id);
1517
1518}
1519
1520
1521function move_ep_low(ep, adjacent_ep, direction) {
1522  if (!is_ajax_loading()) {
1523    var ok = true;
1524    try {
1525      ep.swap(adjacent_ep);
1526    }
1527    catch (e) {
1528      ok = false;
1529    }
1530    if (ok) {
1531      ajax_loading();
1532      $("#ep_nudge_part_id").val(edit_part_id(ep.attr('id')));
1533      $("#ep_nudge_part_direction").val(direction);
1534      $("#ep_nudge_part").trigger('onsubmit');
1535      scrollToSelector('#' + ep.attr('id'));
1536      ep.mouseover();
1537    }
1538  }
1539  return false;
1540}
1541
1542function move_ep_down() {
1543  var ep = $(this);
1544  var adjacent_ep = ep.nextAll('.ep_wrapper').first();
1545  return move_ep_low(ep, adjacent_ep, "down");
1546}
1547
1548function move_ep_up() {
1549  var ep = $(this);
1550  var adjacent_ep = ep.prevAll('.ep_wrapper').first();
1551  return move_ep_low(ep, adjacent_ep, "up");
1552}
1553
1554function getFormattedDate(value) {
1555  const ms = Date.parse(value);
1556  if (Number.isNaN(ms)) {
1557    return "";
1558  }
1559  return new Date(ms)
1560    .toLocaleString(navigator.languages, {
1561      year: "numeric",
1562      month: "short",
1563      day: "2-digit",
1564      hour: "numeric",
1565      minute: "2-digit",
1566      timeZoneName: "short",
1567    });
1568}
1569
1570/* returns the users offset, determines northern or southern hemisphere by taken the largest
1571  offset between jan or june.  Huh?
1572 */
1573function getTimeZone() {
1574  if (!window.tz_offset) {
1575    try {
1576      var jan = new Date(2013, 0, 1);
1577      var june = new Date(2013, 5, 1);
1578      window.tz_offset = Math.max(parseInt(jan.getTimezoneOffset()) * 60, parseInt(june.getTimezoneOffset()) * 60);
1579    } catch (e) {
1580      window.tz_offset = 0;
1581    }
1582  }
1583  return window.tz_offset;
1584}
1585
1586// No longer need to call this directly since it is called when la_alert or la_confirm is called
1587function init_la_msg_dlg() {
1588  // Add the html for the la_msg_dlg div, if not already there
1589  if ($('#la_msg_dlg').length == 0) {
1590    var $la_msg_dlg = $('<div id="la_msg_dlg"/>')
1591      .append($('<p/>')
1592          .append($('<span class="ui-icon ui-icon-info" style="float: left; margin: 0 7px 20px 0;"/>'))
1593          .append($('<span id="la_msg"/>')))
1594      .appendTo($('body'));
1595    // Initialize the message dialog used for alerts and confirms
1596    $la_msg_dlg.dialog({
1597      autoOpen: false,
1598      focus: false,
1599      modal: true,
1600      resizable: false,
1601    });
1602    $la_msg_dlg.on("dialogopen", function (event, ui) {
1603      const uiDialog = event.currentTarget.closest('.ui-dialog');
1604      uiDialog.focus();
1605    });
1606    $la_msg_dlg.on( "dialogclose", function (event, ui) {
1607      // tidy things up a bit: clear message from div and try to reset focus
1608      $('#la_msg_dlg #la_msg').empty();
1609      var la_opener_id = $la_msg_dlg.data('la_opener_id');
1610      $la_msg_dlg.data('la_opener_id', null);
1611      if (la_opener_id) {
1612        // taking hints from https://github.com/jquery/jquery-ui/blob/1.11.4/ui/dialog.js#L198
1613        $('#' + la_opener_id).filter(':focusable').focus();
1614      }
1615    });
1616    $la_msg_dlg.parent().attr({
1617      'aria-describedby': $la_msg_dlg.attr('id')
1618    });
1619  }
1620  return $('#la_msg_dlg');
1621}
1622
1623function close_la_msg_dlg() {
1624  //handy routine if you want to close the dialog programmically
1625  $("#la_msg_dlg").dialog('close');
1626}
1627
1628// option
1629//   title    Title of the dialog. Default is ''.
1630//   msg      Message to display in the dialog, can have html
1631//   icon     Icon, if any
1632//   buttons  Can be a string with values: "ok", "cancel", or "both"
1633//            or a collection of buttons (same is jQuery dialog "buttons" option.
1634//            Default is "both".
1635//   ok_text  Text for the ok button. Default is "Ok"
1636//   ok_title Text to use for the Ok button's title attribute. Default is what
1637//            ok_text is set to
1638//   ok_call  Function to call if the user selects the Ok button.
1639//            Note that after the function call, the dialog is
1640//            automatically closed.  If you don't want to close the dialog
1641//            after the call, have the function return false.  You will
1642//            need to do this when you call la_alert/la_confirm
1643//            in the ok_call or if you need to get the values in the
1644//            dialog.  Be sure to call close_la_msg_dlg to close the
1645//            dialog manually.
1646//   cancel_text  Text for the cancel button.  Default is "Cancel"
1647//   cancel_title Value for the cancel button's title attribute.  Default
1648//            to copy cancel_text's value and default if not provided.
1649//   cancel_call  Function to call if the user selects the Cancel button.
1650//            Note that after the function call, the dialog is
1651//            automatically closed.  If you don't want to close the dialog
1652//            after the call, have the function return false.  You will
1653//            need to do this when you call la_alert/la_confirm
1654//            in the cancel_call or if you need to get the values in the
1655//            dialog.  Be sure to call close_la_msg_dlg to close the
1656//            dialog manually.
1657//   all jquery ui dialog widget options are also supported
1658function la_msg_dlg(option) {
1659  option = $.extend({
1660    title: '',
1661    buttons: 'both',
1662    ok_text: 'Ok',
1663    ok_title: option['ok_text'] ? option['ok_text'] : 'Ok',
1664    cancel_text: 'Cancel',
1665    cancel_title: option['cancel_text'] ? option['cancel_text'] : 'Cancel',
1666    width: 400,
1667    modal: true
1668  }, option);
1669  var dlg_ele = init_la_msg_dlg();
1670  var btns = [];
1671  if ((option.buttons === 'ok') || (option.buttons === 'both')) {
1672    btns.push({
1673      text: option.ok_text,
1674      title: option.ok_title,
1675      click: function () {
1676        var do_close = true;
1677        if (option.ok_call) {
1678          do_close = option.ok_call();
1679        }
1680        if ((typeof(do_close) == 'undefined') || do_close) {
1681          $(this).dialog("close");
1682        }
1683      }
1684    });
1685  }
1686  if ((option.buttons === 'cancel') || (option.buttons === 'both')) {
1687    btns.push({
1688      text: option.cancel_text,
1689      title: option.cancel_title,
1690      click: function () {
1691        var do_close = true;
1692        if (option.cancel_call) {
1693          do_close = option.cancel_call();
1694        }
1695        if ((typeof(do_close) == 'undefined') || do_close) {
1696          $(this).dialog("close");
1697        }
1698      }
1699    });
1700  }
1701  dlg_ele.dialog("option", $.extend({}, $.ui.dialog.prototype.options, option, {
1702    buttons: (typeof(option.buttons) == 'string' ? btns : option.buttons)
1703  }));
1704
1705  $('#la_msg_dlg p .ui-icon').remove();
1706  if (option.icon) {
1707    let span = document.createElement("span");
1708    span.className = "ui-icon"
1709    span.style.float = "left";
1710    span.style.margin = "0 7px 20px 0";
1711    span.classList.add(option.icon);
1712    $('#la_msg_dlg p').prepend(span);
1713    //$('#la_msg_dlg p').prepend($('<span class="ui-icon" style="float: left; margin: 0 7px 20px 0;"/>').addClass(option.icon));
1714  }
1715  replaceTag(document.querySelector('.ui-dialog-title'), 'h5');
1716  // Add the msg but first remove all the children in the la_msg element
1717  const msg_ele = document.getElementById("la_msg");
1718  setInnerHtml(msg_ele, option.msg);
1719  //$('#la_msg_dlg #la_msg').html(option.msg);
1720  var la_opener_id = $(':focus').attr('id');
1721  dlg_ele.data('la_opener_id', (la_opener_id ? la_opener_id : null));
1722  setAlertDialog(dlg_ele);
1723  return dlg_ele.dialog("open");
1724}
1725
1726// option  If type string, the alert message
1727//         If type collection, see the option parameter for la_msg_dlg (above)
1728function la_alert(option) {
1729  if (typeof(option) == 'string') {
1730    option = {msg: option};
1731  }
1732  return la_msg_dlg($.extend({
1733    title: 'Alert',
1734    buttons: 'ok'
1735  }, option));
1736}
1737
1738// option  If type string, the confirm message
1739//         If type collection, see the option parameter for la_msg_dlg (above)
1740// Note: la_confirm doesn't return true/false like a window.confirm instead you need
1741//       to pass the ok_call parameter to specify the callback function when the
1742//       user clicks the "Ok" button.
1743function la_confirm(option, ok_call, cancel_call) {
1744  if (typeof(option) == 'string') {
1745    option = {msg: option};
1746  }
1747  if (ok_call) {
1748    option["ok_call"] = ok_call;
1749  }
1750  if (cancel_call) {
1751    option["cancel_call"] = cancel_call;
1752  }
1753  return la_msg_dlg($.extend({
1754    title: 'Confirm',
1755    buttons: 'both'
1756  }, option));
1757}
1758
1759function la_ensure_defined(v, default_value) {
1760  //returns a valid value for v;  if v is undefined, assigns it the default_value
1761  if (typeof v == 'undefined') {
1762    return default_value;
1763  }
1764  return v;
1765}
1766
1767
1768///Methods to aid in configuring the CKEditor
1769function rich_text_entry_special_options(math_jax_path) {
1770  return {toolbarGroups: [
1771    { name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
1772    { name: 'doc', groups: ['doctools', 'mode'] },
1773    { name: 'editing', groups: [ 'find', 'selection'] },
1774    { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
1775    { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align'] },
1776    '/',
1777    { name: 'la_insert', groups: ['links', 'insert', 'mediaembed','addImage', 'others']},
1778    { name: 'styles' },
1779    { name: 'colors' },
1780    { name: 'tools' }
1781  ],
1782    removeButtons: 'Flash,PageBreak,Maximize,Smiley,Templates,addFile',
1783
1784    mathJaxLib: math_jax_path}
1785}
1786
1787function widget_editor_special_options() {
1788  return {toolbarGroups: [
1789    { name: 'clipboard', groups: [ 'clipboard', 'undo' ] },
1790    { name: 'doc', groups: ['doctools', 'mode'] },
1791    { name: 'editing', groups: [ 'find', 'selection' ] },
1792    { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] },
1793    { name: 'paragraph', groups: [ 'list', 'indent', 'blocks', 'align'] },
1794    '/',
1795    { name: 'forms'},
1796    { name: 'la_insert', groups: ['links', 'insert', 'mediaembed', 'others']},
1797    { name: 'styles' },
1798    { name: 'colors' },
1799    { name: 'tools' }
1800  ],
1801    removeButtons: 'Flash,PageBreak,Maximize,Smiley,Mathjax,Templates,addFile,addImage,doksoft_image_embed'}
1802}
1803
1804//rotates doms with anomation , step controls animation
1805$.fn.rotate = function (degrees, step, current) {
1806  var self = $(this);
1807  current = current || 0;
1808  step = step || 5;
1809  current += step;
1810  self.css({
1811    '-webkit-transform': 'rotate(' + current + 'deg)',
1812    '-moz-transform': 'rotate(' + current + 'deg)',
1813    '-ms-transform': 'rotate(' + current + 'deg)',
1814    'transform': 'rotate(' + current + 'deg)'
1815  });
1816  if (current != degrees) {
1817    setTimeout(function () {
1818      self.rotate(degrees, step, current);
1819    }, 5);
1820  }
1821};
1822
1823/* jQuery plugin to give <a> elements used as buttons the button role and
1824 * necessary space keypress event handlers needed for enhanced accessibility.
1825 * usage: $('#page_tools a').la_button_role();
1826 */
1827(function($) {
1828  var space_keydown_handler = function(event) {
1829    if ( event.keyCode == 32) {
1830      // prevent space from scrolling or doing other things
1831      event.preventDefault();
1832    }
1833  };
1834
1835  var space_keyup_handler = function(event) {
1836    if ( event.keyCode == 32 ) {
1837      $(this).click();
1838    }
1839  };
1840
1841  $.fn.la_button_role = function() {
1842    this.filter('a').each(function() {
1843      $(this).attr('role', 'button')
1844             .on('keydown', space_keydown_handler)
1845             .on('keyup', space_keyup_handler);
1846    });
1847    return this;
1848  };
1849}(jQuery));
1850
1851
1852function toggle_header() {
1853  if (!is_ajax_loading()) {
1854    var b_maxed = !store.getState().navbar.visible;
1855    var width = '100%';
1856    $('#content').width(width);
1857    if (window.course_grader_mode) {
1858        $('#docs-top', parent.document).toggle('blind', {direction: 'vertical', complete: function () {
1859        }});
1860      toggle_student_nb_header(true);
1861      store.dispatch(toggleNavbar(b_maxed));
1862      $('#student_nb_toggle_header').toggle(b_maxed);
1863    }
1864    else {
1865      store.dispatch(toggleNavbar(b_maxed));
1866    }
1867  }
1868  return false;
1869}
1870
1871
1872function set_header_color(bk_color,text_color,b_dark_icons) {
1873
1874  //http://codepen.io/rss/pen/ftnDd
1875  $('#header').css('color',text_color).css('background-color',bk_color);
1876  $('#la_mainmenu').css('color',text_color).css('background-color',bk_color);
1877  if (!b_dark_icons) {
1878    // make then all white, and then fix the odd ball ones
1879    $('#header img').css('-webkit-filter','brightness(100)');
1880    $('#header [src*="info"]').css('-webkit-filter','invert(100%)');
1881    $('#header [src*="menu"]').css('-webkit-filter','invert(100%)');
1882  }
1883  else {
1884    $('#header img').css('-webkit-filter','');
1885  }
1886}
1887
1888
1889function laExists(variable) {
1890  return ((variable !== null) && (typeof variable !== 'undefined'))
1891}
1892
1893
1894function LAComponent() {
1895  this.event_names_used = [];
1896}
1897
1898LAComponent.prototype.on = function(event_name, event_handler) {
1899  this.event_names_used.push(event_name);
1900  this.$ele.on(event_name, event_handler);
1901};
1902
1903LAComponent.prototype.clearCollabEventListeners = function() {
1904  var event_name;
1905  for (var i = 0; event_name = this.event_names_used[i]; ++i) {
1906    this.$ele.off(event_name);
1907  }
1908
1909  this.event_names_used = [];
1910};
1911
1912LAComponent.prototype._jQueryEvents = function() {
1913  // NOTE(zmd): internal jQuery API, use for debugging only
1914  return $._data(this.$ele[0], 'events');
1915};
1916
1917
1918function LAPageComponent() {
1919  LAComponent.call(this);
1920
1921  this.$ele = $('#content');
1922  this.refresh_part_url = null;
1923  this.collab_page_auth_info_url = null;
1924  this.listen();
1925}
1926LAPageComponent.prototype = Object.create(LAComponent.prototype);
1927LAPageComponent.prototype.constructor = LAPageComponent;
1928
1929LAPageComponent.prototype.listen = function() {
1930  this.on('la:page:collab_auth_about_to_expire',
1931               this.renewCollabAuthInfo.bind(this));
1932};
1933
1934LAPageComponent.prototype.renewCollabAuthInfo = function() {
1935  if (laExists(current_la_page) && laExists(ep_current_entry_id)) {
1936    var action = current_la_page.component.collab_page_auth_info_url;
1937
1938    if (laExists(action)) {
1939      var data = {
1940        '_method': 'get',
1941        'entry_id': ep_current_entry_id
1942      };
1943
1944      $.get(action, data, function (auth_info) {
1945        if (auth_info !== null && typeof auth_info === "object") {
1946          withPage(function(la_page) {
1947            la_page.collabAuthRenewed('la:frontend', auth_info);
1948          });
1949        }
1950      }.bind(this));
1951    }
1952  }
1953};
1954
1955
1956function createLAEntryPartComponent(entry_part_type, entry_part_id) {
1957  var ComponentClass;
1958
1959  switch (entry_part_type) {
1960    case LAEntryPartComponent.RICH_TEXT:
1961      ComponentClass = LARichTextComponent;
1962      break;
1963    case LAEntryPartComponent.REFERENCES:
1964      ComponentClass = LAReferencesComponent;
1965      break;
1966    case LAEntryPartComponent.ATTACHMENT:
1967      ComponentClass = LAAttachmentComponent;
1968      break;
1969    case LAEntryPartComponent.ASSIGNMENT:
1970      ComponentClass = LAAssignmentComponent;
1971      break;
1972    default:
1973      ComponentClass = LAEntryPartComponent;
1974      break;
1975  }
1976
1977  return new ComponentClass(entry_part_id);
1978}
1979
1980function createAndRegisterLAEntryPartComponent(entry_part_type, entry_part_id) {
1981  var component = null;
1982  withPage(function (la_page) {
1983    component = createLAEntryPartComponent(entry_part_type, entry_part_id);
1984    la_page.registerEntryPartComponent(entry_part_id, component);
1985  });
1986  return component;
1987}
1988
1989function collabRemoveEntryPart(origin, entry_part_id) {
1990  withPage(function (la_page) {
1991    la_page.removeEntryPart(origin, entry_part_id, '');
1992  });
1993}
1994
1995
1996function LAEntryPartComponent(entry_part_id) {
1997  LAComponent.call(this);
1998
1999  this.$ele = $('div#epw_' + entry_part_id);
2000  this.entry_part_id = entry_part_id;
2001
2002  this.last_edit_activity = 0;
2003  this.edit_activity_rate_limit = 4 * 1000;
2004
2005  this.listen();
2006  this.listenForInteraction();
2007}
2008LAEntryPartComponent.prototype = Object.create(LAComponent.prototype);
2009LAEntryPartComponent.prototype.constructor = LAEntryPartComponent;
2010
2011LAEntryPartComponent.NAME = 0;
2012LAEntryPartComponent.RICH_TEXT = 1;
2013LAEntryPartComponent.ATTACHMENT = 2;
2014LAEntryPartComponent.SPREADSHEET = 3;
2015LAEntryPartComponent.HEADING = 4;
2016LAEntryPartComponent.PLAIN_TEXT = 5;
2017LAEntryPartComponent.REFERENCES = 6;
2018LAEntryPartComponent.EQUATION = 7;
2019LAEntryPartComponent.SKETCH = 8;
2020LAEntryPartComponent.FORM = 9;
2021LAEntryPartComponent.ASSIGNMENT = 10;
2022
2023LAEntryPartComponent.prototype.listen = function() {
2024  this.on('la:entry_part:locked', this.lock.bind(this));
2025  this.on('la:entry_part:unlocked', this.unlock.bind(this));
2026  this.on('la:entry_part:updated', this.update.bind(this));
2027  this.on('la:entry_part:edit', this.edit.bind(this));
2028  this.on('la:entry_part:editor_ready', this.editorReady.bind(this));
2029  this.on('la:entry_part:edit_active', this.editActive.bind(this));
2030  this.on('la:entry_part:edit_inactive', this.editInactive.bind(this));
2031  this.on('la:entry_part:edit_timeout', this.editLockTimeout.bind(this));
2032  this.on('la:entry_part:autosave', this.autosave.bind(this));
2033  this.on('la:entry_part:save', this.save.bind(this));
2034  this.on('la:entry_part:cancel', this.cancel.bind(this));
2035};
2036
2037LAEntryPartComponent.prototype.listenForInteraction = function() {
2038  var handler = function(event) {
2039    if (event.target.value != "save to page" &&
2040        event.target.value != "cancel") {
2041      this.signalEditActive();
2042    }
2043  }.bind(this);
2044
2045  this.$ele.on('click keydown focus',
2046               'input, button, textarea, select, option, a',
2047               handler);
2048};
2049
2050LAEntryPartComponent.prototype.lock = function(event, entry_part_model) {
2051  if (entry_part_model.theirLock()) {
2052    this.$ele.removeClass('ep_wrapper_active');
2053    window.store.dispatch(window.la_react.page.setEntryLock({ id: entry_part_model.entry_part_id, isLocked: true }));
2054
2055    this.$ele.attr('data-collab-ep-locked', 'locked');
2056    this.$ele.attr('data-collab-ep-locked-user', entry_part_model.locked_by);
2057    this.$ele.addClass('collab-ep-locked');
2058
2059    this.showLockInfo(entry_part_model.locked_by);
2060  }
2061};
2062
2063LAEntryPartComponent.prototype.unlock = function(event, entry_part_model) {
2064  if (entry_part_model.theirLock()) {
2065    this.$ele.attr('data-collab-ep-locked', null);
2066    this.$ele.attr('data-collab-ep-locked-user', null);
2067    this.$ele.removeClass('collab-ep-locked');
2068    this.$ele.children('.collab-ep-locked-info').remove();
2069    window.store.dispatch(window.la_react.page.setEntryLock({ id: entry_part_model.entry_part_id, isLocked: fal
2069se }));
2070  }
2071};
2072
2073LAEntryPartComponent.prototype.update = function(event, entry_part_model) {
2074  // NOTE(zmd): !entry_part_model.ourLock() is not equivalent to
2075  //     entry_part_model.theirLock() (updates might happen without the part
2076  //     being locked, such as from mobile and certain assignment grading
2077  //     paths)
2078  if (laExists(current_la_page) && !entry_part_model.ourLock()) {
2079    var action = current_la_page.component.refresh_part_url;
2080
2081    if (laExists(action)) {
2082      var data = {
2083        '_method': 'get',
2084        'entry_part[id]': entry_part_model.entry_part_id.toString()
2085      };
2086
2087      $.get(action, data, function () {
2088        if (entry_part_model.isLocked()) {
2089          this.showLockInfo(entry_part_model.locked_by);
2090          window.la_react.page.dispatchSetEntryLock(entry_part_model.entry_part_id.toString(), true);
2091        }
2092      }.bind(this));
2093    }
2094  }
2095};
2096
2097LAEntryPartComponent.prototype.edit = function(event, entry_part_model) {};
2098
2099LAEntryPartComponent.prototype.editorReady = function(event, entry_part_model) {};
2100
2101LAEntryPartComponent.prototype.editActive = function(event, entry_part_model) {
2102  if (entry_part_model.theirLock()) {
2103    this.$ele.attr('data-collab-ep-edit-active', 'active');
2104    this.$ele.addClass('collab-ep-edit-active');
2105    this.$ele.removeClass('collab-ep-edit-inactive');
2106  }
2107};
2108
2109LAEntryPartComponent.prototype.editInactive = function(event, entry_part_model) {
2110  if (entry_part_model.theirLock()) {
2111    this.$ele.attr('data-collab-ep-edit-active', null);
2112    this.$ele.removeClass('collab-ep-edit-active');
2113    this.$ele.addClass('collab-ep-edit-inactive');
2114  }
2115};
2116
2117LAEntryPartComponent.prototype.editLockTimeout = function(event, entry_part_model) {
2118  if (entry_part_model.ourLock()) {
2119    $('#ept_form #ep_part_timeout').attr('value', 'true');
2120    const $submitButton = $(`#ept_form input[type=submit], #epw_${entry_part_model.entry_part_id} form button[type=submit]`);
2121    if ($submitButton) {
2122      $submitButton.click();
2123    }
2124    this.showEditLockTimeoutDialog();
2125  }
2126};
2127
2128LAEntryPartComponent.prototype.showEditLockTimeoutDialog = function() {
2129  la_msg_dlg({
2130    title: 'Edit Session Timed Out',
2131    msg: ('Your edit session has timed out due to inactivity.'),
2132    buttons: 'ok'
2133  });
2134};
2135
2136LAEntryPartComponent.prototype.autosave = function(event, entry_part_model) {};
2137
2138LAEntryPartComponent.prototype.save = function(event, entry_part_model) {};
2139
2140LAEntryPartComponent.prototype.cancel = function(event, entry_part_model) {};
2141
2142LAEntryPartComponent.prototype.showLockInfo = function(user_display_name) {
2143  var $lock_info = this.$ele.find('.collab-ep-locked-info');
2144  if ($lock_info.length == 0) {
2145    this.$ele.prepend('<div class="collab-ep-locked-info">' +
2146                      '  <span class="sr_only">Entry locked.</span>' +
2147                      '  <i class="fa fa-lock fa-fw"></i>' +
2148                      '  Currently being edited by ' +
2149                      '  <span class="user_display_name"></span>' +
2150                      '  <span class="collab-ep-locked-typing"> ' +
2151                      '    <i class="fa fa-ellipsis-h"></i>' +
2152                      '  </span>' +
2153                      '</div>');
2154    $lock_info = this.$ele.find('.collab-ep-locked-info');
2155  }
2156
2157  this.updateLockUserDisplayName(
2158    $lock_info.find('.user_display_name'),
2159    user_display_name
2160  );
2161};
2162
2163LAEntryPartComponent.prototype.updateLockUserDisplayName = function($disp_name_ele, user_display_name) {
2164  if ($disp_name_ele.length > 0) {
2165    $disp_name_ele.text(user_display_name);
2166  }
2167};
2168
2169LAEntryPartComponent.prototype.signalEditActive = function() {
2170  if (this.okayToSignalEditActive()) {
2171    this.last_edit_activity = Date.now();
2172    var entry_part_model = current_la_page.findEntryPart(this.entry_part_id);
2173    entry_part_model.editActive('la:frontend');
2174  }
2175};
2176
2177LAEntryPartComponent.prototype.okayToSignalEditActive = function() {
2178  var result = false;
2179
2180  if (this.last_edit_activity == 0) {
2181    result = true;
2182  } else {
2183    var diff = Date.now() - this.last_edit_activity;
2184    if (diff >= this.edit_activity_rate_limit) {
2185      result = true;
2186    }
2187  }
2188
2189  return result;
2190};
2191
2192
2193function LARichTextComponent(entry_part_id) {
2194  LAEntryPartComponent.call(this, entry_part_id);
2195
2196  this.ckeDialog = null;
2197  this.$dialog = null;
2198}
2199LARichTextComponent.prototype = Object.create(LAEntryPartComponent.prototype);
2200LARichTextComponent.prototype.constructor = LAEntryPartComponent;
2201
2202LARichTextComponent.prototype.listenForInteraction = function() {
2203  var ckeEditorSelector = 'div.cke_editor_ept_' + this.entry_part_id + ' ';
2204  var ckeEditorChildrenSelector = (
2205    ckeEditorSelector + 'input, ' +
2206    ckeEditorSelector + 'button, ' +
2207    ckeEditorSelector + 'textarea, ' +
2208    ckeEditorSelector + 'select, ' +
2209    ckeEditorSelector + 'option, ' +
2210    ckeEditorSelector + 'a'
2211  );
2212
2213  this.$ele.on('click keydown',
2214               ckeEditorChildrenSelector,
2215               this.signalEditActive.bind(this));
2216};
2217
2218LARichTextComponent.prototype.editorReady = function(event, entry_part_model) {
2219  var signalEditActive = this.signalEditActive.bind(this);
2220  var ckeditor = LA_CKEDITOR().instances['ept_' + this.entry_part_id];
2221  ckeditor.on('change', signalEditActive);
2222  ckeditor.on('key', signalEditActive);
2223
2224  ckeditor.on('beforeDestroy', function(event) {
2225    this.unregisterDialog();
2226  }.bind(this));
2227
2228  ckeditor.on('dialogShow', function(event) {
2229    this.registerDialog(event.data);
2230  }.bind(this));
2231};
2232
2233LARichTextComponent.prototype.unregisterDialog = function() {
2234  if (this.ckeDialog) this.ckeDialog.hide();
2235  this.ckeDialog = null;
2236  this.$dialog = null;
2237};
2238
2239LARichTextComponent.prototype.registerDialog = function(ckeDialog) {
2240  if (!this.ckeDialog) {
2241    this.ckeDialog = ckeDialog;
2242    this.$dialog = $(ckeDialog.parts.dialog.$);
2243    this.$dialog.on('click keydown',
2244                    'input, button, textarea, select, option, a',
2245                    this.signalEditActive.bind(this));
2246  }
2247};
2248
2249
2250function LAReferencesComponent(entry_part_id) {
2251  LAEntryPartComponent.call(this, entry_part_id);
2252}
2253LAReferencesComponent.prototype = Object.create(LAEntryPartComponent.prototype);
2254LAReferencesComponent.prototype.constructor = LAEntryPartComponent;
2255
2256LAReferencesComponent.prototype.editorReady = function(event, entry_part_model) {
2257  var $dialog = $('#online_search_dialog');
2258  $dialog.on('click keydown',
2259             'input, button, textarea, select, option, a',
2260             this.signalEditActive.bind(this));
2261};
2262
2263
2264function LAAttachmentComponent(entry_part_id) {
2265  LAEntryPartComponent.call(this, entry_part_id);
2266}
2267LAAttachmentComponent.prototype = Object.create(LAEntryPartComponent.prototype);
2268LAAttachmentComponent.prototype.constructor = LAEntryPartComponent;
2269
2270
2271function LAAssignmentComponent(entry_part_id) {
2272  LAEntryPartComponent.call(this, entry_part_id);
2273  this.timeout_cancel = false;
2274}
2275LAAssignmentComponent.prototype = Object.create(LAEntryPartComponent.prototype);
2276LAAssignmentComponent.prototype.constructor = LAEntryPartComponent;
2277
2278LAAssignmentComponent.prototype.lock = function(event, entry_part_model) {
2279  LAEntryPartComponent.prototype.lock.call(this, event, entry_part_model);
2280
2281  if (entry_part_model.theirLock()) {
2282    this.$ele.find('.ui-button').button('disable');
2283    var $text_fields = this.$ele.find('input[type=text]');
2284    $text_fields.disable();
2285    $text_fields.attr('aria-disabled', 'true')
2286  }
2287};
2288
2289LAAssignmentComponent.prototype.unlock = function(event, entry_part_model) {
2290  LAEntryPartComponent.prototype.unlock.call(this, event, entry_part_model);
2291
2292  if (entry_part_model.theirLock()) {
2293    this.$ele.find('.ui-button').button('enable');
2294    var $text_fields = this.$ele.find('input[type=text]');
2295    $text_fields.enable();
2296    $text_fields.attr('aria-disabled', 'false')
2297  }
2298};
2299
2300LAAssignmentComponent.prototype.editLockTimeout = function(event, entry_part_model) {
2301  if (this.timeout_cancel) {
2302    if (entry_part_model.ourLock()) {
2303      var $cancel_button = $("#ept_form tr.as_status_btn_wrapper input[name=cancel]");
2304      $cancel_button.click();
2305      this.showEditLockTimeoutDialog();
2306    }
2307  } else {
2308    LAEntryPartComponent.prototype.editLockTimeout.call(this, event, entry_part_model);
2309  }
2310};
2311
2312
2313function LAObject() {
2314  this.component = null;
2315}
2316
2317LAObject.prototype.registerComponent = function(component) {
2318  // shared lifetime & lookup are primary goals here, not tight coupling
2319  this.component = component;
2320};
2321
2322LAObject.prototype.trigger = function(event, args) {
2323  this.component.$ele.trigger(event, args);
2324};
2325
2326LAObject.prototype.event = function(origin, event_name) {
2327  return $.Event(event_name, { origin: origin });
2328};
2329
2330LAObject.prototype.normalizePartId = function(id) {
2331    return Number(id);
2332};
2333
2334LAObject.prototype.localOrigin = function(origin) {
2335  return origin == 'la:frontend';
2336};
2337
2338function sameNotebookAndPage(notebookId, pageId) {
2339  let isSame = false;
2340  withPage(function (la_page) {
2341    const escapedNbId = la_page.notebook_id.replace(/\+/g, "-").replace(/\//g, "_").replace(/\=/g, "@");
2342    const escapedPageId = la_page.page_id.replace(/\+/g, "-").replace(/\//g, "_").replace(/\=/g, "@");
2343    if (la_page && escapedNbId === notebookId && escapedPageId === pageId) {
2344      isSame = true;
2345    }
2346  });
2347  return isSame;
2348};
2349
2350function collabEventOk(notebookId, pageId) {
2351  return sameNotebookAndPage(notebookId, pageId);
2352};
2353
2354function withCollab(fn) {
2355  if (laExists(window.collab)) {
2356    const LOCAL_ORIGIN = 'la:frontend';
2357    const COLLAB_ORIGIN = 'la:collab';
2358    const ENTRY_UPDATE_AUTOSAVE = 'autosave';
2359    const ENTRY_UPDATE_SAVE = 'save';
2360    const ENTRY_UPDATE_CANCEL = 'cancel';
2361
2362    const domEventOk = (event) => {
2363      return event.origin === LOCAL_ORIGIN ? true : false;
2364    };
2365
2366    const registerDomEvents = (la_page, page) => {
2367      let page_auth = page;
2368
2369      la_page.component.on('la:entry_part:removed', function(event, notebook_id, page_id, entry_part_id, why) {
2370        if (domEventOk(event)) {
2371          collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartRemoved,
2372            collabModule.MessageModel.create({ 
2373              ...page_auth,
2374              entryPartId: entry_part_id,
2375              why,
2376            }),
2377          );
2378        }
2379      }.bind(la_page));
2380
2381      la_page.component.on('la:entry_part:locked', function(event, entry_part_model, user_display_name) {
2382        if (domEventOk(event) && entry_part_model.user_editing) {
2383          try {
2384            collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartLocked,
2385              collabModule.MessageModel.create({ 
2386                ...page_auth,
2387                entryPartId: entry_part_model.entry_part_id,
2388                userDisplayName: user_display_name,
2389              }),
2390            );
2391            entry_part_model.lock_success(COLLAB_ORIGIN);
2392          } catch (err) {
2393            entry_part_model.lock_failure(COLLAB_ORIGIN, err);
2394          }
2395        }
2396      }.bind(la_page));
2397
2398      la_page.component.on('la:entry_part:unlocked', function(event, entry_part_model, why) {
2399        if (domEventOk(event)) {
2400          collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartUnlocked,
2401            collabModule.MessageModel.create({ 
2402              ...page_auth,
2403              entryPartId: entry_part_model.entry_part_id,
2404              why,
2405            }),
2406          );
2407        }
2408      }.bind(la_page));
2409
2410      la_page.component.on('la:entry_part:reiterate_lock', function(event, entry_part_model) {
2411        if (event.origin === COLLAB_ORIGIN) {
2412          collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartReiterateLock,
2413            collabModule.MessageModel.create({
2414              ...page_auth,
2415              entryPartId: entry_part_model.entry_part_id,
2416            }),
2417          );
2418        }
2419
2420      }.bind(la_page));
2421
2422      la_page.component.on('la:entry_part:edit_active', function(event, entry_part_model) {
2423        if (domEventOk(event) && entry_part_model.user_editing) {
2424          collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartEditActive,
2425            collabModule.MessageModel.create({ 
2426              ...page_auth,
2427              entryPartId: entry_part_model.entry_part_id,
2428            }),
2429          );
2430        }
2431      }.bind(la_page));
2432
2433      la_page.component.on('la:entry_part:edit_inactive', function(event, entry_part_model) {
2434        if (domEventOk(event) && entry_part_model.user_editing) {
2435          collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartEditInactive,
2436            collabModule.MessageModel.create({ 
2437              ...page_auth,
2438              entryPartId: entry_part_model.entry_part_id,
2439            }),
2440          );
2441        }
2442      }.bind(la_page));
2443
2444      la_page.component.on('la:entry_part:save', function(event, entry_part_model) {
2445        if (domEventOk(event) && entry_part_model.user_editing) {
2446          collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartUnlocked,
2447            collabModule.MessageModel.create({ 
2448              ...page_auth,
2449              entryPartId: entry_part_model.entry_part_id,
2450            }),
2451          );
2452
2453          collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartUpd
2453ated,
2454            collabModule.MessageModel.create({ 
2455              ...page_auth,
2456              entryPartId: entry_part_model.entry_part_id,
2457              why: ENTRY_UPDATE_SAVE,
2458            }),
2459          );
2460        }
2461      }.bind(la_page));
2462
2463      la_page.component.on('la:entry_part:autosave', function(event, entry_part_model) {
2464        if (domEventOk(event) && entry_part_model.user_editing) {
2465          collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartUpdated,
2466            collabModule.MessageModel.create({ 
2467              ...page_auth,
2468              entryPartId: entry_part_model.entry_part_id,
2469              why: ENTRY_UPDATE_AUTOSAVE,
2470            }),
2471          );
2472        }
2473      }.bind(la_page));
2474
2475      la_page.component.on('la:entry_part:cancel', function(event, entry_part_model) {
2476        if (domEventOk(event) && entry_part_model.user_editing) {
2477          collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartUnlocked,
2478            collabModule.MessageModel.create({ 
2479              ...page_auth,
2480              entryPartId: entry_part_model.entry_part_id,
2481            }),
2482          );
2483
2484          collabModule.collabClient.sendEvent(collabModule.SocketIOEvent.entryPartUpdated,
2485            collabModule.MessageModel.create({ 
2486              ...page_auth,
2487              entryPartId: entry_part_model.entry_part_id,
2488              why: ENTRY_UPDATE_CANCEL,
2489            }),
2490          );
2491        }
2492      }.bind(la_page));
2493      
2494      la_page.component.on('la:page:collab_auth_renewed', function(event, page_model, auth_info) {
2495        if (domEventOk(event)) {
2496          withPage(function (la_page) {
2497            if (la_page === page_model) {
2498              const updated_page_auth = {
2499                authToken: auth_info.auth_token,
2500                expiration: auth_info.expiration,
2501                notebookId: auth_info.notebook_api_id,
2502                pageId: page_auth.pageId,
2503                userId: auth_info.user_id,
2504              };
2505  
2506              page_auth = updated_page_auth;
2507            }
2508          });
2509        }
2510      }.bind(la_page));
2511
2512      la_page.component.on('la:page:disconnect_from_collab', function(event, entry_part_model, user_display_name) {
2513        if (domEventOk(event)) {
2514          collab.disconnectPage();
2515        }
2516      }.bind(la_page));
2517    };
2518
2519    const registerCollabListenerEvents = (la_page) => {
2520      const collabListenerEvents = [
2521        {
2522          eventName: collabModule.SocketIOEvent.entryPartAdded, eventHandler: (msg) => {
2523            if (collabEventOk(msg.notebookId, msg.pageId)) {
2524              la_page.addEntryPart(COLLAB_ORIGIN, msg.entryPartId, why);
2525            }
2526          },
2527        },
2528        {
2529          eventName: collabModule.SocketIOEvent.entryPartRemoved, eventHandler: (msg) => {
2530            if (collabEventOk(msg.notebookId, msg.pageId)) {
2531              collabRemoveEntryPart(COLLAB_ORIGIN, msg.entryPartId);
2532            }
2533          },
2534        },
2535        {
2536          eventName: collabModule.SocketIOEvent.entryPartLocked, eventHandler: (msg) => {
2537            if (collabEventOk(msg.notebookId, msg.pageId)) {
2538              const entry_part_model = la_page.findEntryPart(msg.entryPartId);
2539              entry_part_model.lock(COLLAB_ORIGIN, msg.userDisplayName);
2540            }
2541          },
2542        },
2543        {
2544          eventName: collabModule.SocketIOEvent.entryPartUnlocked, eventHandler: (msg) => {
2545            if (collabEventOk(msg.notebookId, msg.pageId)) {
2546              const entry_part_model = la_page.findEntryPart(msg.entryPartId);
2547              entry_part_model.unlock(COLLAB_ORIGIN, msg.why);
2548            }
2549          },
2550        },
2551        {
2552          eventName: collabModule.SocketIOEvent.entryPartChallengeLock, eventHandler: (msg) => {
2553            if (collabEventOk(msg.notebookId, msg.pageId)) {
2554              const entry_part_model = la_page.findEntryPart(msg.entryPartId);
2555              entry_part_model.challengeLock(COLLAB_ORIGIN);
2556            }
2557          },
2558        },
2559        {
2560          eventName: collabModule.SocketIOEvent.entryPartReiterateLock, eventHandler: (msg) => {
2561            if (collabEventOk(msg.notebookId, msg.pageId)) {
2562              const entry_part_model = la_page.findEntryPart(msg.entryPartId);
2563              entry_part_model.reiterateLock(COLLAB_ORIGIN);
2564            }
2565
2566          },
2567        },
2568        {
2569          eventName: collabModule.SocketIOEvent.entryPartUpdated, eventHandler: (msg) => {
2570            if (collabEventOk(msg.notebookId, msg.pageId)) {
2571              const entry_part_model = la_page.findEntryPart(msg.entryPartId);
2572              entry_part_model.update(COLLAB_ORIGIN, msg.why);
2573
2574              if (msg.why === ENTRY_UPDATE_AUTOSAVE) {
2575                entry_part_model.autosave(COLLAB_ORIGIN);
2576              } 
2577            }
2578          },
2579        },
2580        {
2581          eventName: collabModule.SocketIOEvent.entryPartEditActive, eventHandler: (msg) => {
2582            if (collabEventOk(msg.notebookId, msg.pageId)) {
2583              const entry_part_model = la_page.findEntryPart(msg.entryPartId);
2584              entry_part_model.editActive(COLLAB_ORIGIN);
2585            }
2586          },
2587        },
2588        {
2589          eventName: collabModule.SocketIOEvent.entryPartEditInactive, eventHandler: (msg) => {
2590            if (collabEventOk(msg.notebookId, msg.pageId)) {
2591              const entry_part_model = la_page.findEntryPart(msg.entryPartId);
2592              entry_part_model.editInactive(COLLAB_ORIGIN);
2593            }
2594          },
2595        },
2596      ];
2597
2598      return collabListenerEvents;
2599    }
2600
2601    fn(window.collab, registerDomEvents, registerCollabListenerEvents);
2602  }
2603}
2604
2605function withPage(fn) {
2606  if (laExists(window.current_la_page)) {
2607    fn(window.current_la_page);
2608  }
2609}
2610
2611function disconnectLACollabPage() {
2612  withPage(function (la_page) {
2613    la_page.disconnectFromCollab('la:frontend');
2614    la_page.component.clearCollabEventListeners();
2615  });
2616}
2617
2618function clearCurrentLAPage() {
2619  window.current_la_page = null;
2620}
2621
2622
2623function LAPage(notebook_id, page_id, user_display_name, signed) {
2624  LAObject.call(this);
2625
2626  this.notebook_id = notebook_id;
2627  this.page_id = page_id;
2628  this.user_display_name = user_display_name;
2629
2630  this._entry_parts = {};
2631
2632  this.signing_in_progress = false;
2633  this.signed = !!signed;
2634
2635  this.collab_auth_timeout_id = null;
2636  this.collab_auth_timeout_delay = 900000;  // 15 minutes
2637}
2638LAPage.prototype = Object.create(LAObject.prototype);
2639LAPage.prototype.constructor = LAPage;
2640
2641// class-level version of findEntryPart
2642LAPage.findEntryPart = function(entry_part_id) {
2643  var part = null;
2644  withPage(function (la_page) {
2645    part = la_page.findEntryPart(entry_part_id);
2646  });
2647  return part;
2648};
2649
2650// class-level version of entryPartLocked
2651LAPage.entryPartLocked = function(entry_part) {
2652  var part_locked = false;
2653  withPage(function (la_page) {
2654    part_locked = la_page.entryPartLocked(entry_part);
2655  });
2656  return part_locked;
2657};
2658
2659LAPage.prototype.show = function(origin) {
2660  this.trigger(this.event(origin, 'la:page:show'), [this]);
2661};
2662
2663LAPage.prototype.registerEntryPartComponent = function(entry_part_id, component) {
2664  var entry_part = this.findEntryPart(entry_part_id);
2665  if (!entry_part) {
2666    entry_part = this._addEntryPart(entry_part_id);
2667  }
2668  entry_part.registerComponent(component);
2669};
2670
2671LAPage.prototype.addEntryPart = function(origin, entry_part_id, why) {
2672  var entry_part = this.findEntryPart(entry_part_id);
2673  if (!entry_part) {
2674    entry_part = this._addEntryPart(entry_part_id);
2675    this.trigger(this.event(origin, 'la:entry_part:added'), [entry_part, why]);
2676  }
2677  return entry_part;
2678};
2679
2680LAPage.prototype._addEntryPart = function(entry_part_id) {
2681  entry_part_id = this.normalizePartId(entry_part_id);
2682  var entry_part = new LAEntryPart(
2683    this.notebook_id,
2684    this.page_id,
2685    this.user_display_name,
2686    entry_part_id
2687  );
2688
2689  this._entry_parts[entry_part_id] = entry_part;
2690
2691  return entry_part;
2692};
2693
2694LAPage.prototype.removeEntryPart = function(origin, entry_part_id, why) {
2695  var entry_part = this.findEntryPart(entry_part_id);
2696  if (entry_part) {
2697    delete this._entry_parts[entry_part.entry_part_id];
2698    if (this.localOrigin(origin)) {
2699      this.trigger(
2700        this.event(origin, 'la:entry_part:removed'),
2701        [this.notebook_id, this.page_id, entry_part_id, why]
2702      );
2703    } else {
2704      entry_part.component.update("", entry_part);
2705    }
2706  }
2707};
2708
2709LAPage.prototype.findEntryPart = function(entry_part_id) {
2710  return this._entry_parts[this.normalizePartId(entry_part_id)];
2711};
2712
2713LAPage.prototype.entryPartLocked = function(entry_part) {
2714  // NOTE(zmd): non-existent entry part (such as when new part is being
2715  //     authored) is considered not locked
2716  if ((typeof(entry_part) === "number") ||
2717      (typeof(entry_part) === "string")) {
2718    entry_part = this.findEntryPart(entry_part);
2719  }
2720  return (entry_part && entry_part.isLocked());
2721};
2722
2723// just unlocking (not cancelling, saving, etc.), for use on unload
2724LAPage.prototype.unlockOurLockedParts = function() {
2725  var our_locked_parts = this.findLockedParts('ours');
2726  var i;
2727  for (i = 0; i < our_locked_parts.length; ++i) {
2728    our_locked_parts[i].unlock();
2729  }
2730};
2731
2732LAPage.prototype.challengeLockedParts = function() {
2733  var their_parts = this.findLockedParts('theirs');
2734  var i, part;
2735  for (i = 0; part = their_parts[i]; ++i) {
2736    part.challengeLock('la:frontend');
2737  }
2738};
2739
2740LAPage.prototype.findLockedParts = function(whose) {
2741  // FIXME(zmd): LOW PRIORITY: OPTIMIZATION: store active locks in member array
2742  if (whose != 'ours' && whose != 'theirs') whose = 'all';
2743
2744  var entry_ids = Object.keys(this._entry_parts);
2745  var result = [];
2746  var i, e;
2747  for (i = 0; i < entry_ids.length; ++i) {
2748    e = this._entry_parts[entry_ids[i]];
2749    if (e.isLocked()) {
2750      if (whose == 'ours' && e.ourLock()) {
2751        result.push(e);
2752      } else if (whose == 'theirs' && e.theirLock()) {
2753        result.push(e);
2754      } else if (whose == 'all') {
2755        result.push(e);
2756      }
2757    }
2758  }
2759
2760  return result;
2761};
2762
2763LAPage.prototype.disconnectFromCollab = function(origin) {
2764  this.trigger(this.event(origin, 'la:page:disconnect_from_collab'), [this]);
2765}
2766
2767LAPage.prototype.collabAuthAboutToExpire = function(origin) {
2768  if (!origin) origin = 'la:frontend';
2769  this.trigger(this.event(origin, 'la:page:collab_auth_about_to_expire'),
2770               [this]);
2771};
2772
2773LAPage.prototype.collabAuthRenewed = function(origin, auth_info) {
2774  this.startAuthInfoRenewalTimeout();
2775  this.trigger(this.event(origin, 'la:page:collab_auth_renewed'),
2776               [this, auth_info]);
2777};
2778
2779LAPage.prototype.leavePage = function () {
2780  if (window.collab) {
2781    disconnectLACollabPage();
2782  }
2783  this.unlockOurLockedParts();
2784  this._cancelAuthInfoRenewalTimeout();
2785
2786  clearCurrentLAPage();
2787};
2788
2789LAPage.prototype.startAuthInfoRenewalTimeout = function() {
2790  this._cancelAuthInfoRenewalTimeout();
2791  this.collab_auth_timeout_id = setTimeout(
2792    this.collabAuthAboutToExpire.bind(this),
2793    this.collab_auth_timeout_delay
2794  );
2795};
2796
2797LAPage.prototype._cancelAuthInfoRenewalTimeout = function() {
2798  if (this.collab_auth_timeout_id) {
2799    clearTimeout(this.collab_auth_timeout_id);
2800    this.collab_auth_timeout_id = null;
2801  }
2802};
2803
2804
2805function LAEntryPart(notebook_id, page_id, user_display_name, entry_part_id) {
2806  LAObject.call(this);
2807
2808  this.notebook_id = notebook_id;
2809  this.page_id = page_id;
2810  this.user_display_name = user_display_name;
2811  this.entry_part_id = entry_part_id;
2812
2813  this.user_editing = false;
2814  this.autosaved = false;
2815
2816  this.locked = LAEntryPart.UNLOCKED;
2817  this.locked_by = null;
2818
2819  // NOTE(zmd): always keep this timeout delay in sync with the collab server's
2820  //     timeout delay
2821  this.edit_lock_timeout_id = null;
2822  this.edit_lock_timeout_preempt_id = null;
2823  this.edit_lock_grace_period  = 180000;  // 3 min
2824  this.edit_lock_timeout_delay = 600000;  // 10 min
2825  this.edit_lock_timeout_preempt_delay = 60000;  // 1 min (will be subtracted from the timeout delay)
2826
2827  this.edit_activity_timeout_id = null;
2828  this.edit_activity_timeout_delay = 5000;  // 5 sec
2829
2830  this.challenge_lock_timeout_id = null;
2831  this.challenge_lock_timeout_delay = 10000;  // 10 sec
2832}
2833LAEntryPart.prototype = Object.create(LAObject.prototype);
2834LAEntryPart.prototype.constructor = LAEntryPart;
2835
2836LAEntryPart.UNLOCKED = 1;
2837LAEntryPart.LOCKED_LOCAL = 2;
2838LAEntryPart.LOCKED_REMOTE = 3;
2839
2840LAEntryPart.prototype.isLocked = function() {
2841  return this.locked != LAEntryPart.UNLOCKED;
2842};
2843
2844LAEntryPart.prototype.isLockChallenged = function() {
2845  return !!this.challenge_lock_timeout_id;
2846};
2847
2848LAEntryPart.prototype.ourLock = function() {
2849  return this.locked == LAEntryPart.LOCKED_LOCAL;
2850};
2851
2852LAEntryPart.prototype.theirLock = function() {
2853  return this.locked == LAEntryPart.LOCKED_REMOTE;
2854};
2855
2856LAEntryPart.prototype.lock = function(origin, user_display_name) {
2857  if (!this.isLocked()) {
2858    if (this.localOrigin(origin)) {
2859      this.locked = LAEntryPart.LOCKED_LOCAL;
2860    } else {
2861      this.locked = LAEntryPart.LOCKED_REMOTE;
2862    }
2863
2864    this.locked_by = user_display_name;
2865    this.trigger(this.event(origin, 'la:entry_part:locked'),
2866                 [this, user_display_name]);
2867
2868    this._setOrRenewEditLockTimeout(this.edit_lock_timeout_delay);
2869  }
2870};
2871
2872LAEntryPart.prototype.lock_success = function(origin) {};
2873
2874LAEntryPart.prototype.lock_failure = function(origin, error) {};
2875
2876LAEntryPart.prototype.unlock = function(origin, why) {
2877  // unlock always implies running timers should stop, unconditionally
2878  this._cancelChallengeLockTimeout();
2879  this._cancelEditActivityTimeout();
2880  this._cancelEditLockTimeout();
2881
2882  if (this.isLocked()) {
2883    this.trigger(this.event(origin, 'la:entry_part:unlocked'), [this, why]);
2884
2885    // listeners to the lock need to read the lock information before we clear
2886    // it, so we reset the local lock state after issuing event
2887    this.locked = LAEntryPart.UNLOCKED;
2888    this.locked_by = null;
2889  }
2890};
2891
2892// If the lock is held by another client, then start a 'challenge lock timeout' that, 
2893// if reached, will execute a function to unlock the entry. If the lock is held by us,
2894// then start the process to emit a 'reiterate_lock' event--this will reach other 
2895// clients on the page and stop their 'challenge lock timeouts' 
2896LAEntryPart.prototype.challengeLock = function(origin) {
2897  if (!this.ourLock()) {
2898    this._setOrRenewChallengeLockTimeout(this.challenge_lock_timeout_delay);
2899  } else if (this.ourLock()) {
2900    this.trigger(this.event(origin, 'la:entry_part:reiterate_lock'), [this]);
2901  }
2902};
2903
2904LAEntryPart.prototype.reiterateLock = function(origin) {
2905  if (this.isLockChallenged() && !this.localOrigin(origin)) {
2906    // someone re-iterated their lock that we challenged, drop challenge
2907    this._cancelChallengeLockTimeout();
2908  }
2909};
2910
2911LAEntryPart.prototype.update = function(origin, why) {
2912  this.trigger(this.event(origin, 'la:entry_part:updated'), [this, why]);
2913};
2914
2915LAEntryPart.prototype.edit = function(origin) {
2916  // NOTE(zmd): lock enforcement hook point
2917  this._editBegin(origin);
2918  this.trigger(this.event(origin, 'la:entry_part:edit'), [this]);
2919  this._lockIfLocal(origin);
2920};
2921
2922LAEntryPart.prototype.editorReady = function(origin) {
2923  this.trigger(this.event(origin, 'la:entry_part:editor_ready'), [this]);
2924};
2925
2926LAEntryPart.prototype.editActive = function(origin) {
2927  this.trigger(this.event(origin, 'la:entry_part:edit_active'),
2928               [this]);
2929  this._cancelChallengeLockTimeout();
2930  this._cancelEditLockTimeout();
2931  this._setOrRenewEditActivityTimeout(this.edit_activity_timeout_delay);
2932};
2933
2934LAEntryPart.prototype.editInactive = function(origin) {
2935  this._setOrRenewEditLockTimeout(this.edit_lock_timeout_delay);
2936  this.trigger(this.event(origin, 'la:entry_part:edit_inactive'), [this]);
2937};
2938
2939LAEntryPart.prototype.autosave = function(origin) {
2940  this.autosaved = true;
2941  this.editActive(origin);
2942  this.trigger(this.event(origin, 'la:entry_part:autosave'), [this]);
2943};
2944
2945LAEntryPart.prototype.save = function(origin) {
2946  this.trigger(this.event(origin, 'la:entry_part:save'), [this]);
2947  // NOTE(zmd): unlock will be emitted by la backend upon save, need only
2948  //     cancel timers
2949  this._cancelChallengeLockTimeout();
2950  this._cancelEditActivityTimeout();
2951  this._cancelEditLockTimeout();
2952  this._editEnd();
2953};
2954
2955LAEntryPart.prototype.cancel = function(origin) {
2956  this.trigger(this.event(origin, 'la:entry_part:cancel'), [this]);
2957  this._cancelChallengeLockTimeout();
2958  this._cancelEditActivityTimeout();
2959  this._cancelEditLockTimeout();
2960  if (!this.autosaved) {
2961    // if autosaved, the server will issue the unlock, otherwise we must
2962    this._unlockIfLocal(origin);
2963  }
2964  this._editEnd();
2965};
2966
2967// FIXME(zmd): LOW PRIORITY: factor out common timer logic into general helpers
2968
2969LAEntryPart.prototype.extendDelay = function(timeout_delay) {
2970  var range = timeout_delay / 5;
2971  var extra_delay = (
2972    this.edit_lock_grace_period +
2973    Math.floor(Math.random() * range)
2974  );
2975  return timeout_delay + extra_delay;
2976};
2977
2978LAEntryPart.prototype._editLockTimeout = function(origin) {
2979  if (!origin) origin = 'la:frontend';
2980
2981  if (this.ourLock()) {
2982    // we hold lock, lock timed out, allow UI to save (backend will send
2983    // unlock)
2984    this.trigger(this.event(origin, 'la:entry_part:edit_timeout'), [this]);
2985  } else {
2986    // lock holder not active, lock stale, unlock from this client
2987    this.unlock(origin, 'timeout');
2988  }
2989};
2990
2991LAEntryPart.prototype._cancelEditLockTimeout = function() {
2992  if (this.edit_lock_timeout_id) {
2993    clearTimeout(this.edit_lock_timeout_id);
2994    this.edit_lock_timeout_id = null;
2995  }
2996  if (this.edit_lock_timeout_preempt_id) {
2997    clearTimeout(this.edit_lock_timeout_preempt_id);
2998    this.edit_lock_timeout_preempt_id = null;
2999  }
3000};
3001
3002LAEntryPart.prototype._setOrRenewEditLockTimeout = function(timeout_delay) {
3003  if (!this.ourLock()) {
3004    timeout_delay = this.extendDelay(timeout_delay)
3005  }
3006
3007  this._cancelEditLockTimeout();
3008  this.edit_lock_timeout_id = setTimeout(
3009    this._editLockTimeout.bind(this),
3010    timeout_delay
3011  );
3012  if (this.ourLock()) {
3013    this.edit_lock_timeout_preempt_id = setTimeout(() => {
3014        la_msg_dlg({
3015          title: 'Edit Session Time Out Warning',
3016          msg: ('Your edit session is about to time out due to inactivity.'),
3017          dialogClass: "no-close",
3018          buttons: 'ok',
3019          ok_text: 'Extend',
3020          ok_call: ()=> {
3021            this.component.signalEditActive();
3022          }
3023        });
3024      },
3025      timeout_delay - this.edit_lock_timeout_preempt_delay
3026    );
3027  }
3028};
3029
3030LAEntryPart.prototype._editActivityTimeout = function(origin) {
3031  if (!origin) origin = 'la:frontend';
3032  this.editInactive(origin);
3033};
3034
3035LAEntryPart.prototype._cancelEditActivityTimeout = function() {
3036  if (this.edit_activity_timeout_id) {
3037    clearTimeout(this.edit_activity_timeout_id);
3038    this.edit_activity_timeout_id = null;
3039  }
3040};
3041
3042LAEntryPart.prototype._setOrRenewEditActivityTimeout = function(timeout_delay) {
3043  this._cancelEditActivityTimeout();
3044  this.edit_activity_timeout_id = setTimeout(
3045    this._editActivityTimeout.bind(this),
3046    timeout_delay
3047  );
3048};
3049
3050LAEntryPart.prototype._challengeLockTimeout = function(origin) {
3051  if (!origin) origin = 'la:frontend';
3052  this.unlock(origin, 'lock_challenged');
3053};
3054
3055LAEntryPart.prototype._cancelChallengeLockTimeout = function() {
3056  if (this.challenge_lock_timeout_id) {
3057    clearTimeout(this.challenge_lock_timeout_id);
3058    this.challenge_lock_timeout_id = null;
3059  }
3060};
3061
3062LAEntryPart.prototype._setOrRenewChallengeLockTimeout = function(timeout_delay) {
3063  this._cancelChallengeLockTimeout();
3064  this.challenge_lock_timeout_id = setTimeout(
3065    this._challengeLockTimeout.bind(this),
3066    timeout_delay
3067  );
3068};
3069
3070LAEntryPart.prototype._lockIfLocal = function(origin) {
3071  if (this.localOrigin(origin)) {
3072    this.lock(origin, this.user_display_name);
3073    this.editActive(origin);
3074  }
3075};
3076
3077LAEntryPart.prototype._unlockIfLocal = function(origin) {
3078  if (this.localOrigin(origin)) {
3079    this.unlock(origin);
3080  }
3081};
3082
3083LAEntryPart.prototype._editBegin = function(origin) {
3084  if (this.localOrigin(origin)) {
3085    this.user_editing = true;
3086  }
3087  this.autosaved = false;
3088};
3089
3090LAEntryPart.prototype._editEnd = function() {
3091  this.user_editing = false;
3092  this.autosaved = false;
3093};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.