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, '"').replace(/'/g, '''); 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.