File modules/cms/ui/themes/default/script/openrat/workbench.js
Last commit: Tue Apr 26 22:58:31 2022 +0200 Jan Dankert Fix: reenabled Drag and drop to selector.
1 import $ from '../jquery-global.js'; 2 import Dialog from './dialog.js'; 3 import View from './view.js'; 4 import Callback from './callback.js'; 5 import WorkbenchNavigator from "./navigator.js"; 6 import Notice from "./notice.js"; 7 import Components from "./components.js"; 8 9 10 export default class Workbench { 11 'use strict'; // Strict mode 12 13 static state = { 14 action: '', 15 id: 0, 16 extra: {} 17 }; 18 19 static dialog; 20 21 static instance; 22 23 constructor() { 24 25 this.popupWindow = null; 26 27 Callback.dataChangedHandler.add( () => { 28 if ( Workbench.popupWindow ) 29 Workbench.popupWindow.location.reload(); 30 } ); 31 32 } 33 34 35 /** 36 * @return Workbench 37 */ 38 static getInstance() { 39 40 if ( ! Workbench.instance ) 41 Workbench.instance = new Workbench(); 42 43 return Workbench.instance; 44 } 45 46 47 /** 48 * Initializes the Workbench. 49 */ 50 initialize() { 51 52 this.checkBrowserRequirements(); 53 this.initializeState(); 54 55 $('html').removeClass('nojs'); 56 57 /* Fade in all elements. */ 58 $('.or--initial-hidden').removeClass('-initial-hidden'); 59 60 61 // Listening to the "popstate" event 62 // If the user navigates back or forward, the new state is set. 63 window.onpopstate = ev => { 64 console.debug("Event after navigating",ev); 65 this.closeDialog(); 66 this.closeMenu(); 67 this.loadNewActionState(ev.state); 68 }; 69 70 this.registerWorkbench(); 71 this.initializeTheme(); 72 this.initializeStartupNotices(); 73 this.initializeEvents(); 74 this.initializeKeystrokes(); 75 76 // Load all views 77 this.reloadAll().then( () => { 78 Callback.afterNewActionHandler.fire(); 79 } 80 ); 81 82 83 // Initialze Ping timer. 84 this.initializePingTimer(); 85 this.initializeDirtyWarning(); 86 87 //Workbench.registerOpenClose( $('.or-collapsible') ); 88 console.info('Application started'); 89 } 90 91 92 93 initializeTheme() { 94 if (window.localStorage) { 95 let style = window.localStorage.getItem('ui.style'); 96 if (style) 97 this.setUserStyle(style); 98 } 99 } 100 101 102 initializeDirtyWarning() { 103 104 // If the application should be closed, inform the user about unsaved changes. 105 window.addEventListener('beforeunload', function (e) { 106 107 // Are there views in the dirty state? 108 if ( $('.or-view--is-dirty').length > 0 ) { 109 110 e.preventDefault(); // Cancel the event 111 112 // This text is replaced by modern browsers with a common message. 113 return 'Unsaved content will be lost.'; 114 } 115 else { 116 // Let the browser quitting the page. 117 // Do NOT logout here, because there could be other windows/tabs with the same session. 118 return undefined; // nothing to do. 119 } 120 }); 121 } 122 123 124 /** 125 * Register this workbench in the window object 126 * Only for debugging in browser console. 127 */ 128 registerWorkbench() { 129 130 window.OpenRat = { workbench: this }; 131 } 132 133 134 /** 135 * Sets the workbench state with action/id. 136 * 137 * Example: #/name/1 is translated to the state {action:name,id:1} 138 */ 139 initializeState() { 140 141 let parts = window.location.hash.split('/'); 142 let state = { action:'index',id:0 }; 143 144 if ( parts.length >= 2 ) 145 state.action = parts[1].toLowerCase(); 146 147 if ( parts.length >= 3 ) 148 // Only numbers and '_' allowed in the id. 149 state.id = parts[2].replace(/[^0-9_]/gim,""); 150 151 Workbench.state = state; 152 153 WorkbenchNavigator.toActualHistory( state ); 154 } 155 156 /** 157 * Checks the browser requirements for this application. 158 */ 159 checkBrowserRequirements() { 160 161 if ( ! window.Promise ) { 162 console.error('This browser does not support Promises, which is required for this application.' ); 163 164 // Show a little Notice for the user that his shit browser is not supported. 165 let notice = new Notice(); 166 notice.msg = 'This browser is not supported'; 167 notice.msg = 'Promises are not available'; 168 notice.show(); 169 } 170 171 if ( ! window.fetch ) { 172 console.error('This browser does not support the fetch API, which is required for this application.' ); 173 174 // Show a little Notice for the user that his shit browser is not supported. 175 let notice = new Notice(); 176 notice.setStatus('error'); 177 notice.msg = 'This browser is not supported'; 178 notice.log = 'Fetch API is not available'; 179 notice.show(); 180 } 181 } 182 183 /** 184 * Registriert den Ping-Timer für den Sitzungserhalt. 185 */ 186 initializePingTimer() { 187 188 /** 189 * Ping den Server. Führt keine Aktion aus, aber sorgt dafür, dass die Sitzung erhalten bleibt. 190 * 191 * "Geben Sie mir ein Ping, Vasily. Und bitte nur ein einziges Ping!" (aus: Jagd auf Roter Oktober) 192 */ 193 let ping = async () => { 194 let url = View.createUrl('profile', 'ping' ); 195 console.debug('ping'); 196 197 try { 198 let response = await fetch( url,{ 199 method: 'GET', 200 headers: { 201 'Accept': 'application/json', 202 } 203 } ); 204 205 if ( !response.ok ) 206 throw "ping failed"; 207 } catch( cause ) { 208 // oO, what has happened? There is no session with a logged in user, or the server has gone. 209 console.warn( {message: 'The server ping has failed.',cause:cause }); 210 211 // Is there any user input? Ok, we should warn the user that the data could not be saved. 212 if ($('.or-view--is-dirty').length > 0) { 213 window.alert("The server session is lost, please save your data."); 214 } 215 else { 216 // no input data, so lets reload all views? 217 // no, maybe an anonymous user is looking around. 218 //Openrat.reloadAll(); 219 } 220 } 221 222 223 224 } 225 226 // Alle 5 Minuten pingen. 227 let timeoutMinutes = 5; 228 229 window.setInterval( ping, timeoutMinutes*60*1000 ); 230 } 231 232 233 /** 234 * Sets a new states and loads all views. 235 * 236 * @param state 237 */ 238 loadNewActionState(state) { 239 240 console.debug("New state",state); 241 Workbench.state = state; 242 243 this.reloadViews(); 244 this.filterMenus(); 245 246 Callback.afterNewActionHandler.fire(); 247 } 248 249 250 closeDialog() { 251 if ( Workbench.dialog ) { 252 Workbench.dialog.close(); 253 Workbench.dialog = null; 254 } 255 } 256 257 258 createDialog() { 259 260 this.closeDialog(); 261 262 Workbench.dialog = new Dialog(); 263 return Workbench.dialog; 264 } 265 266 /** 267 * 268 */ 269 reloadViews() { 270 271 this.startSpinner(); 272 let promise = this.loadViews( $('.or-workbench .or-act-view-loader') ); 273 274 promise.then( 275 () => this.stopSpinner() 276 ); 277 278 return promise; 279 } 280 281 282 startSpinner() { 283 $('.or-workbench-loader').addClass('loader').addClass('loader--is-active'); 284 } 285 stopSpinner() { 286 $('.or-workbench-loader').removeClass('loader').removeClass('loader--is-active'); 287 } 288 289 /** 290 * @return a promise for all views 291 */ 292 reloadAll() { 293 294 this.startSpinner(); 295 296 let promise = this.loadViews( $('.or-act-view-loader,.or-act-view-static') ); 297 console.debug('reloading all views'); 298 299 let stylePromise = this.loadUserStyle(); 300 let languagePromise = this.loadLanguage(); 301 let settingsPromise = this.loadUISettings(); 302 303 let all = Promise.all( [ promise,stylePromise,languagePromise,settingsPromise ] ); 304 305 all.then( 306 () => this.stopSpinner() 307 ); 308 309 return all; 310 } 311 312 313 async loadUserStyle() { 314 315 let url = View.createUrl('profile', 'userinfo' ); 316 317 let response = await fetch(url,{ 318 method: 'GET', 319 headers: { 320 'Accept': 'application/json', 321 } 322 }); 323 let json = await response.json(); 324 325 let style = json.output['style']; 326 this.setUserStyle(style); 327 328 let color = json.output['theme-color']; 329 this.setThemeColor(color); 330 } 331 332 333 334 static settings = {}; 335 static language = {}; 336 337 async loadLanguage() { 338 339 let url = View.createUrl('profile', 'language'); 340 341 let response = await fetch(url,{ 342 method: 'GET', 343 headers: { 344 'Accept': 'application/json', 345 } 346 } 347 ); 348 let data = await response.json(); 349 350 Workbench.language = data.output.language; 351 } 352 353 /** 354 * load UI settings from the server. 355 */ 356 async loadUISettings() { 357 358 let url = View.createUrl('profile', 'uisettings' ); 359 360 let response = await fetch(url,{ 361 method: 'GET', 362 headers: { 363 'Accept': 'application/json', 364 } 365 }); 366 let data = await response.json(); 367 368 Workbench.settings = data.output.settings.settings; 369 } 370 371 372 /** 373 * 374 * @param $views 375 * @returns Promise for all views 376 */ 377 loadViews( $views ) 378 { 379 let wb = this; 380 let promises = []; 381 $views.each(function (idx) { 382 383 let $targetDOMElement = $(this); 384 385 promises.push( wb.loadNewActionIntoElement( $targetDOMElement ) ); 386 }); 387 388 let all = Promise.all( promises ); 389 390 return all; 391 392 393 } 394 395 396 /** 397 * @param $viewElement 398 * @returns {Promise} 399 */ 400 loadNewActionIntoElement( $viewElement ) 401 { 402 let action; 403 if ( $viewElement.is('.or-act-view-static') ) 404 // Static views have always the same action. 405 action = $viewElement.attr('data-action'); 406 else 407 action = Workbench.state.action; 408 409 let id = Workbench.state.id; 410 let params = Workbench.state.extra; 411 412 let method = $viewElement.data('method'); 413 414 let view = new View( action,method,id,params ); 415 return view.start( $viewElement ); 416 } 417 418 419 420 421 /** 422 * Sets a new theme. 423 * @param styleName 424 */ 425 setUserStyle( styleName ) 426 { 427 if ( window.localStorage ) 428 window.localStorage.setItem('ui.style',styleName); 429 430 let styleUrl = View.createUrl('index', 'themestyle', 0, {'style': styleName}); 431 document.getElementById('user-style').setAttribute('href',styleUrl); 432 } 433 434 435 /** 436 * Sets a new theme color. 437 * @param color Theme-color 438 */ 439 setThemeColor( color ) 440 { 441 document.getElementById('theme-color').setAttribute('content',color); 442 } 443 444 445 446 /** 447 * Sets the application title. 448 */ 449 static setApplicationTitle( newTitle ) { 450 451 let title = document.querySelector('head > title'); 452 let defaultTitle = title.dataset.default; 453 454 title.textContent = (newTitle ? newTitle + ' - ' : '') + defaultTitle; 455 } 456 457 458 459 460 /** 461 * open and close groups. 462 * 463 * @param $el 464 */ 465 static registerOpenClose = function( $el ) 466 { 467 $($el).children('.or-collapsible-act-switch').click( function() { 468 let $group = $(this).closest('.or-collapsible'); 469 470 if ( $group.hasClass('collapsible--is-visible') ) { 471 // closing 472 $group.removeClass('collapsible--is-visible'); 473 setTimeout( () => { 474 $group.removeClass('collapsible--is-open'); 475 },300 ); 476 477 } 478 else { 479 // open 480 $group.addClass('collapsible--is-open'); 481 $group.addClass('collapsible--is-visible'); 482 } 483 }); 484 } 485 486 487 488 /** 489 * Setzt neue Action und aktualisiert alle Fenster. 490 * 491 * @param action Action 492 * @param id Id 493 */ 494 openNewAction( name,action,id ) 495 { 496 // Im Mobilmodus soll das Menü verschwinden, wenn eine neue Action geoeffnet wird. 497 $('.or-workbench-navigation').removeClass('workbench-navigation--is-open'); 498 499 Workbench.setApplicationTitle( name ); // Sets the title. 500 501 let newState = {'action':action, 'id':id }; 502 this.loadNewActionState( newState ); 503 504 WorkbenchNavigator.navigateToNew( newState ); 505 } 506 507 508 509 510 511 512 513 514 registerDraggable(viewEl) { 515 516 // Drag n Drop: Inhaltselemente (Dateien,Seiten,Ordner,Verknuepfungen) koennen auf Ordner gezogen werden. 517 518 /* 519 no jquery ui anymore. 520 $(viewEl).find('.or-draggable').draggable( 521 { 522 helper: 'clone', 523 opacity: 0.7, 524 zIndex: 3, 525 distance: 10, 526 cursor: 'move', 527 revert: 'false' 528 } 529 );*/ 530 531 // Enable HTML5-Drag n drop 532 $(viewEl).find('.or-draggable').attr('draggable','true') 533 .on('dragstart',(e)=>{ 534 $('.or-workbench').addClass('workbench--drag-active'); 535 let link = e.currentTarget; 536 e.dataTransfer.effectAllowed = 'link'; 537 e.dataTransfer.setData('id' , link.dataset.id ); 538 e.dataTransfer.setData('action', link.dataset.action); 539 e.dataTransfer.setData('name' , link.dataset.name || link.textContent.trim() ); 540 console.debug('drag started',e.dataTransfer); 541 }) 542 .on('drag',(e)=>{ 543 }) 544 .on('dragend',(e)=>{ 545 $('.or-workbench').removeClass('workbench--drag-active'); 546 }); 547 } 548 549 550 registerDroppable(viewEl) { 551 552 $(viewEl).find('.or-droppable-selector').on('dragover', (e) => { 553 e.preventDefault(); 554 }).on('drop', (event) => { 555 556 let id = event.dataTransfer.getData('id'); 557 if ( !id) { 558 console.debug("dropped object has no object id, ignoring"); 559 return; 560 } 561 let name = event.dataTransfer.getData('name'); 562 if (!name) 563 name = id; 564 565 console.debug("dropped",id,name,event.dataTransfer ); 566 $(event.currentTarget).find('.or-selector-link-value').val(id); 567 $(event.currentTarget).find('.or-selector-link-name').val(name).attr('placeholder', name); 568 event.preventDefault(); 569 }); 570 571 $(viewEl).find('.or-droppable') 572 } 573 574 registerAsDroppable( el, onDrop ) { 575 576 el.addEventListener('dragover', (e) => { 577 //e.stopPropagation(); 578 e.preventDefault(); 579 }); 580 581 el.addEventListener('dragenter', (e) => { 582 e.stopPropagation(); 583 e.preventDefault(); 584 e.currentTarget.classList.add('or-workbench--drop-active'); 585 }); 586 587 el.addEventListener('dragleave', (e) => { 588 e.stopPropagation(); 589 e.preventDefault(); 590 e.currentTarget.classList.remove('or-workbench--drop-active'); 591 }); 592 el.addEventListener('drop', onDrop); 593 } 594 595 596 static htmlDecode(input) { 597 let doc = new DOMParser().parseFromString(input, "text/html"); 598 return doc.documentElement.textContent; 599 } 600 601 602 603 604 async filterMenus() { 605 606 let action = Workbench.state.action; 607 let id = Workbench.state.id; 608 $('.or-workbench-title .or-dropdown-entry.or-act-clickable').addClass('dropdown-entry--active'); 609 $('.or-workbench-title .or-filtered').removeClass('dropdown-entry--active').addClass('dropdown-entry--inactive'); 610 // Jeder Menüeintrag bekommt die Id und Parameter. 611 $('.or-workbench-title .or-filtered .or-link').attr('data-id', id); 612 613 let url = View.createUrl('profile', 'available', id, {'queryaction': action}); 614 615 // Die Inhalte des Zweiges laden. 616 let response = await fetch(url, { 617 method: 'GET', 618 headers: { 619 'Accept': 'application/json', 620 } 621 }); 622 let data = await response.json(); 623 624 for (let method of Object.values(data.output.views)) 625 $('.or-workbench-title .or-filtered > .or-link[data-method=\'' + method + '\']') 626 .parent() 627 .addClass('dropdown-entry--active') 628 .removeClass('dropdown-entry--inactive'); 629 } 630 631 632 633 634 initializeStartupNotices() { 635 636 // Initial Notices 637 $('.or-act-initial-notice').each( function() { 638 639 let notice = new Notice(); 640 notice.setStatus('info'); 641 notice.msg = $(this).text(); 642 notice.show(); 643 644 //$(this).remove(); 645 }); 646 } 647 648 649 initializeKeystrokes() { 650 651 let keyPressedHandler = (event) => { 652 653 if (event.key === 'F4') { 654 // Open "properties" dialog. 655 656 let dialog = this.createDialog(); 657 dialog.start('', '', 'prop', 0, {}); 658 } 659 660 if (event.key === 'F2') { 661 662 // Toggle navigation bar 663 if ($('.or-workbench').hasClass('workbench--navigation-is-small')) 664 $('.or-act-nav-wide').click(); 665 else 666 $('.or-act-nav-small').click(); 667 } 668 669 if (event.code === 'Escape') { 670 // Close an existing dialog. 671 this.closeDialog(); 672 } 673 }; 674 675 676 677 document.addEventListener('keydown',keyPressedHandler); 678 } 679 680 681 closeMenu() { 682 $('.or-menu').removeClass('menu--is-open'); 683 } 684 685 686 /** 687 * Registriert alle Events, die in der Workbench laufen sollen. 688 */ 689 initializeEvents() { 690 691 new Components().registerComponents(); 692 693 // Mit der Maus irgendwo hin geklickt, das Menü muss schließen. 694 $('body').click( () => { 695 this.closeMenu(); 696 }); 697 698 // close dialog on click onto the blurred area. 699 $('.or-dialog-filler,.or-act-dialog-close').click( (e) => 700 { 701 e.preventDefault(); 702 this.closeDialog(); 703 } 704 ); 705 706 707 // Mobile navigation must close on a click on the workbench 708 $('.or-act-navigation-close').click( () => { 709 $('.or-workbench-navigation').removeClass('workbench-navigation--is-open'); 710 $('.or-workbench').removeClass('workbench--navigation-is-open'); 711 }); 712 713 // Handler for desktop navigation 714 $('.or-workbench-title .or-act-nav-small').click( () => { 715 $('.or-workbench').addClass('workbench--navigation-is-small'); 716 $('.or-workbench-navigation').addClass('workbench-navigation--is-small'); 717 }); 718 719 720 $('.or-search-input .or-input').orSearch( { 721 onSearchActive: function() { 722 $('.or-search').addClass('search--is-active'); 723 }, 724 onSearchInactive: function() { 725 $('.or-search').removeClass('search--is-active'); 726 }, 727 dropdown : '.or-act-search-result', 728 resultEntryClass: 'search-result-entry', 729 //openDropdown: true, // the dropdown is automatically opened by the menu. 730 select : function(obj) { 731 // open the search result 732 Workbench.getInstance().openNewAction( obj.name, obj.action, obj.id ); 733 }, 734 afterSelect: function() { 735 //$('.or-dropdown.or-act-selector-search-results').empty(); 736 } 737 } ); 738 $('.or-search .or-act-search-delete').click( () => { 739 $('.or-search .or-title-input').val('').input(); 740 } ); 741 742 743 Callback.afterNewActionHandler.add( function() { 744 745 $('.or-sidebar').find('.or-sidebar-button').orLinkify(); 746 } 747 ); 748 749 Callback.afterNewActionHandler.add( function() { 750 751 let url = View.createUrl('tree', 'path', Workbench.state.id, {'type': Workbench.state.action}); 752 753 // Die Inhalte des Zweiges laden. 754 let loadPromise = fetch( url,{ 755 method: 'GET', 756 headers: { 757 'Accept': 'text/html', 758 } 759 } ); 760 761 /** 762 * open a object in the navigation tree. 763 * @param action 764 * @param id 765 */ 766 function openNavTree(action, id) { 767 let $navControl = $('.or-link[data-action="'+action+'"][data-id="'+id+'"]').closest('.or-navtree-node'); 768 if ( $navControl.is( '.or-navtree-node--is-closed' ) ) 769 $navControl.find('.or-navtree-node-control').click(); 770 } 771 772 loadPromise 773 .then( response => response.text() ) 774 .then( data => { 775 776 $('.or-breadcrumb').empty().html( data ).find('.or-act-clickable').orLinkify(); 777 778 // Open the path in the navigator tree 779 $('.or-breadcrumb a').each( function () { 780 let action = $(this).data('action'); 781 let id = $(this).data('id' ); 782 783 openNavTree( action, id ); 784 }); 785 786 $('.or-link--is-active').removeClass('link--is-active'); 787 788 let action = Workbench.state.action; 789 let id = Workbench.state.id; 790 if (!id) id = '0'; 791 792 // Mark the links to the actual object 793 $('.or-link[data-action=\''+action+'\'][data-id=\''+id+'\']').addClass('link--is-active'); 794 // Open actual object 795 openNavTree( action,id ); 796 797 }).catch( cause => { 798 // Ups... aber was können wir hier schon tun, außer hässliche Meldungen anzeigen. 799 console.warn( { 800 message : 'Failed to load path', 801 url : url, 802 cause : cause } ); 803 }).finally(function () { 804 805 }); 806 } ); 807 808 809 Callback.afterViewLoadedHandler.add( function(element) { 810 $(element).find('.or-button').orButton(); 811 } ); 812 813 Callback.afterViewLoadedHandler.add( function(element) { 814 815 // Refresh already opened popup windows. 816 if ( Workbench.popupWindow ) 817 $(element).find("a[data-type='popup']").each( function() { 818 Workbench.popupWindow.location.href = $(this).attr('data-url'); 819 }); 820 821 }); 822 823 824 Callback.afterViewLoadedHandler.add( function(element) { 825 826 $(element).find(".or-input--password").dblclick( function() { 827 $(this).toggleAttr('type','text','password'); 828 }); 829 830 $(element).find(".or-act-make-visible").click( function() { 831 $(this).toggleClass('btn--is-active' ); 832 $(this).parent().children('input').toggleAttr('type','text','password'); 833 }); 834 }); 835 836 837 838 Callback.afterViewLoadedHandler.add( function($element) { 839 840 $element.find('.or-act-load-nav-tree').each( async function() { 841 842 let type = $(this).data('type') || 'root'; 843 let loadBranchUrl = View.createUrl('tree', 'branch', 0, {type: type}); 844 let $targetElement = $(this); 845 846 let response = await fetch( loadBranchUrl,{ 847 method: 'GET', 848 headers: { 849 'Accept': 'text/html', 850 } 851 } ); 852 let html = await response.text(); 853 854 // Den neuen Unter-Zweig erzeugen. 855 let $ul = $.create('ul' ).addClass('navtree-list'); 856 $ul.appendTo( $targetElement.empty() ).html( html ); 857 858 $ul.find('li').orTree( { 859 'openAction': function( name,action,id) { 860 Workbench.getInstance().openNewAction( name,action,id ); 861 } 862 863 } ); // All subnodes are getting event listener for open/close 864 865 // Die Navigationspunkte sind anklickbar, hier wird der Standardmechanismus benutzt. 866 $ul.find('.or-act-clickable').orLinkify(); 867 868 // Open the first node. 869 $ul.find('.or-navtree-node-control').first().click(); 870 871 } ); 872 873 } ); 874 875 876 877 878 /** 879 * Registriert alle Handler für den Inhalt einer View. 880 * 881 * @param viewEl DOM-Element der View 882 */ 883 Callback.afterViewLoadedHandler.add( function(viewEl ) { 884 885 // Handler for mobile navigation 886 $(viewEl).find('.or-act-nav-open-close').click( function() { 887 $('.or-workbench').toggleClass('workbench--navigation-is-open'); 888 $('.or-workbench-navigation').toggleClass('workbench-navigation--is-open'); 889 }); 890 891 // Handler for desktop navigation 892 $(viewEl).find('.or-act-nav-small').click( function() { 893 $('.or-workbench').addClass('workbench--navigation-is-small'); 894 $('.or-workbench-navigation').addClass('workbench-navigation--is-small'); 895 }); 896 $(viewEl).find('.or-act-nav-wide').click( function() { 897 $('.or-workbench').removeClass('workbench--navigation-is-small'); 898 $('.or-workbench-navigation').removeClass('workbench-navigation--is-small'); 899 }); 900 901 902 // Selectors (Einzel-Ausahl für Dateien) initialisieren 903 // Wurzel des Baums laden 904 $(viewEl).find('.or-act-selector-tree-button').click( function() { 905 906 let $selector = $(this).parent('.or-selector'); 907 let $targetElement = $selector.find('.or-act-load-selector-tree'); 908 909 if ( $selector.hasClass('selector--is-tree-active') ) { 910 $selector.removeClass('selector--is-tree-active'); 911 $targetElement.empty(); 912 } 913 else { 914 $selector.addClass('selector--is-tree-active'); 915 916 var selectorEl = this; 917 /* 918 $(this).orTree( { type:'project',selectable:$(selectorEl).attr('data-types').split(','),id:$(selectorEl).attr('data-init-folderid'),onSelect:function(name,type,id) { 919 920 var selector = $(selectorEl).parent(); 921 922 //console.log( 'Selected: '+name+" #"+id ); 923 $(selector).find('input[type=text]' ).attr( 'value',name ); 924 $(selector).find('input[type=hidden]').attr( 'value',id ); 925 } }); 926 */ 927 928 let id = $(this).data('init-folder-id'); 929 let type = id?'folder':'projects'; 930 let loadBranchUrl = './?action=tree&subaction=branch&id='+id+'&type='+type; 931 932 let load = fetch( loadBranchUrl,{ 933 method: 'GET', 934 headers: { 935 'Accept': 'text/html', 936 } 937 } ); 938 load 939 .then( response => response.text() ) 940 .then( html => { 941 942 // Den neuen Unter-Zweig erzeugen. 943 let $ul = $.create('ul' ).addClass('navtree-list'); 944 $ul.appendTo( $targetElement ).html( html ); 945 946 $ul.find('li').orTree( 947 { 948 'openAction' : function(name,action,id) { 949 $selector.find('.or-selector-link-value').val(id ); 950 $selector.find('.or-selector-link-name' ).val('').attr('placeholder',name); 951 952 $selector.removeClass('selector--is-tree-active'); 953 $targetElement.empty(); 954 } 955 } 956 ); // All subnodes are getting event listener for open/close 957 958 // Die Navigationspunkte sind anklickbar, hier wird der Standardmechanismus benutzt. 959 $ul.find('.or-act-clickable').orLinkify(); 960 961 // Open the first node. 962 $ul.find('.or-navtree-node-control').first().click(); 963 } ); 964 } 965 966 } ); 967 968 969 registerDragAndDrop(viewEl); 970 971 972 // Theme-Auswahl mit Preview 973 $(viewEl).find('.or-theme-chooser').change( function() { 974 Workbench.getInstance().setUserStyle( this.value ); 975 }); 976 977 978 979 980 function registerMenuEvents($element ) 981 { 982 // Mit der Maus geklicktes Menü aktivieren. 983 $($element).find('.or-menu-category').click( function(event) { 984 event.stopPropagation(); 985 $(this).closest('.or-menu').toggleClass('menu--is-open'); 986 }); 987 988 // Mit der Maus überstrichenes Menü aktivieren. 989 $($element).find('.or-menu-category').mouseover( function() { 990 991 // close other menus. 992 $(this).closest('.or-menu').find('.or-menu-category').removeClass('menu-category--is-open'); 993 // open the mouse-overed menu. 994 $(this).addClass('menu-category--is-open'); 995 }); 996 997 } 998 999 1000 function registerSelectorSearch( $element ) 1001 { 1002 $($element).find('.or-act-selector-search').orSearch( { 1003 onSearchActive: function() { 1004 $(this).parent('or-selector').addClass('selector-search--is-active'); 1005 }, 1006 onSearchInactive: function() { 1007 $(this).parent('or-selector').removeClass('selector-search--is-active' ); 1008 }, 1009 1010 dropdown: '.or-act-selector-search-results', 1011 resultEntryClass: 'search-result-entry', 1012 1013 select: function(obj) { 1014 $($element).find('.or-selector-link-value').val(obj.id ); 1015 $($element).find('.or-selector-link-name' ).val(obj.name).attr('placeholder',obj.name); 1016 }, 1017 1018 afterSelect: function() { 1019 $('.or-dropdown.or-act-selector-search-results').empty(); 1020 } 1021 } ); 1022 } 1023 1024 1025 1026 function registerTree(element) { 1027 1028 // Klick-Funktionen zum Öffnen/Schließen des Zweiges. 1029 //$(element).find('.or-navtree-node').orTree(); 1030 1031 } 1032 1033 1034 registerMenuEvents ( viewEl ); 1035 //registerGlobalSearch ( viewEl ); 1036 registerSelectorSearch( viewEl ); 1037 registerTree ( viewEl ); 1038 1039 function registerDragAndDrop(viewEl) 1040 { 1041 1042 Workbench.getInstance().registerDraggable(viewEl); 1043 Workbench.getInstance().registerDroppable(viewEl); 1044 } 1045 1046 registerDragAndDrop(viewEl); 1047 1048 1049 } ); 1050 }; 1051 1052 /** 1053 * Adds a new Stylesheet to the DOM. 1054 * 1055 * @param id ID of element (must be unique in the DOM) 1056 * @param href Link to the stylesheet 1057 */ 1058 static async addStyle( id, href ) { 1059 1060 return new Promise( (resolve,reject) => { 1061 let styleEl = document.getElementById(id); 1062 1063 if (!styleEl) { 1064 // Style is not present, so inserting it into the DOM 1065 styleEl = document.createElement('link'); 1066 styleEl.addEventListener('load',resolve); 1067 styleEl.setAttribute('rel', 'stylesheet'); 1068 styleEl.setAttribute('type', 'text/css'); 1069 styleEl.setAttribute('href', href); 1070 styleEl.setAttribute('id', id); 1071 1072 document.getElementsByTagName('head')[0].appendChild(styleEl); 1073 } else { 1074 resolve(); 1075 } 1076 } ); 1077 1078 } 1079 1080 1081 /** 1082 * Adds a new Script to the DOM. 1083 * 1084 * @param id ID of element (must be unique in the DOM) 1085 * @param href Link to the stylesheet 1086 */ 1087 static async addScript( id, href ) { 1088 1089 return new Promise( (resolve,reject) => { 1090 let scriptEl = document.getElementById( id ); 1091 1092 if ( ! scriptEl ) { 1093 // Script is not present, so inserting it into the DOM 1094 scriptEl = document.createElement( 'script' ); 1095 scriptEl.setAttribute('id' ,id ); 1096 scriptEl.setAttribute('type','text/javascript' ); 1097 scriptEl.addEventListener('load',resolve); 1098 scriptEl.setAttribute('src',href ); 1099 document.getElementsByTagName('head')[0].appendChild(scriptEl); 1100 } else { 1101 resolve(); // script is already there 1102 } 1103 } ); 1104 } 1105 1106 } 1107
Downloadmodules/cms/ui/themes/default/script/openrat/workbench.js
History Tue, 26 Apr 2022 22:58:31 +0200 Jan Dankert Fix: reenabled Drag and drop to selector. Mon, 25 Apr 2022 23:41:25 +0200 Jan Dankert Fix: Re-enabling selector-box. Thu, 10 Mar 2022 13:09:06 +0100 dankert New: Remember some user inputs in the browser local storage. Sun, 6 Feb 2022 22:06:09 +0100 dankert Refactoring: Ommit unnecessary parameters. Sun, 6 Feb 2022 21:34:42 +0100 dankert New: Use Accept-Header instead of "output" request parameter, this is the cleaner way. Sat, 18 Dec 2021 03:57:38 +0100 dankert New: Using localStorage for user style. Sat, 18 Dec 2021 03:47:23 +0100 dankert New: Every ES6-Module should have a minified version for performance reasons. Bad: The Minifier "Jsqueeze" is unable to minify ES6-modules, so we had to implement a simple JS-Minifier which strips out all comments. Fri, 17 Dec 2021 04:29:47 +0100 dankert New: Opening a dialog creates a new entry in history api. So, using the back button will close the dialog. Fri, 17 Dec 2021 01:27:54 +0100 dankert New: Check if browser has support for ES6-Promises and the fetch API. Mon, 6 Dec 2021 23:21:18 +0100 dankert Fixes: Click on search results will close the search results. Mon, 29 Nov 2021 23:54:33 +0100 Jan Dankert New: Spinner image with pure css. So we can colorize it now. Thu, 22 Apr 2021 00:30:08 +0200 Jan Dankert Fix: Re-enable drag and drop Sun, 18 Apr 2021 01:21:33 +0200 Jan Dankert Fix: Correct CSS3 syntax for transitions on multiple properties; New: Using a transition for open/close collapsibles. Tue, 13 Apr 2021 23:55:43 +0200 Jan Dankert New: Dynamic load of scripts and styles for the editors. Trumbowyg needs JQuery so , so JQuery is back again (but only for this case) :( Thu, 1 Apr 2021 23:51:12 +0200 Jan Dankert New: Toggle desktop navigation with F2 (on mobile devices there are no F-keys) Thu, 1 Apr 2021 23:31:49 +0200 Jan Dankert Fix: Open properties with F4 key. Thu, 1 Apr 2021 22:53:24 +0200 Jan Dankert Fix: Search Thu, 1 Apr 2021 01:01:54 +0200 Jan Dankert New: Some fixes for OQuery, our new selfmade light JQuery replacement. Now the UI is back again. Wed, 31 Mar 2021 01:52:57 +0200 Jan Dankert New: Replace JQuery with OQuery, a selfmade light JQuery replacement. Mon, 29 Mar 2021 02:40:58 +0200 Jan Dankert New: Preload for fonts and modules. Mon, 29 Mar 2021 01:06:08 +0200 Jan Dankert Removed common.js and moved the callbacks to the workbench module. Sat, 27 Mar 2021 10:40:59 +0100 Jan Dankert Only generate the actual necessary theme style. Sat, 27 Mar 2021 05:14:11 +0100 Jan Dankert Refactoring: Converting all script files to ES6 modules (work in progress); removed jquery-ui (drag and drop will be replaced by HTML5, sortable by a small lib) Wed, 17 Mar 2021 22:27:33 +0100 Jan Dankert Refactoring: Using ES6-Modules (experimental) Wed, 17 Mar 2021 02:18:50 +0100 Jan Dankert Refactoring: Using "Jquery slim" without ajax and effects. Tue, 16 Mar 2021 23:52:22 +0100 Jan Dankert Refactoring: Use ES6 classes. Tue, 23 Feb 2021 01:00:33 +0100 Jan Dankert New: Undo for closed dialogs with unsaved changes. Wed, 17 Feb 2021 02:34:51 +0100 Jan Dankert Refactoring: Extract Dialog into a separate js class Wed, 17 Feb 2021 00:37:45 +0100 Jan Dankert Refactoring: Extract Notices into a separate js class Mon, 15 Feb 2021 02:27:54 +0100 Jan Dankert Fix: Drag and drop from the navigation tree to a selector input. Sun, 14 Feb 2021 23:18:14 +0100 Jan Dankert New: Confirmation needed if a dialog is closed which has unsaved changes. Sat, 13 Feb 2021 00:22:42 +0100 Jan Dankert Fix: Deleting old code; Clear notices after a shorter time. Wed, 10 Feb 2021 00:00:21 +0100 Jan Dankert Using only fullscreen views; Navigate to parent instead of complete breadcrumb. Tue, 9 Feb 2021 19:25:59 +0100 Jan Dankert New: Adding console messages instead of weired dialog messages. Sat, 21 Nov 2020 12:13:03 +0100 Jan Dankert Fix: Dirty marker. Sat, 14 Nov 2020 22:02:21 +0100 Jan Dankert Fixed: Notices may display a message. Mon, 2 Nov 2020 23:59:36 +0100 Jan Dankert Refactoring: The promise of the ajax requests are returned to the caller. All view requests are collected into a singlel promise. Mon, 26 Oct 2020 21:36:14 +0100 Jan Dankert Rename css class 'clickable' to 'act-clickable'; enhanced view 'info' for projects. Sun, 25 Oct 2020 11:31:56 +0100 Jan Dankert Fix: close navigation on click. Sat, 24 Oct 2020 23:51:11 +0200 Jan Dankert Cleanup LESS files, introduce BEM. Wed, 21 Oct 2020 23:13:01 +0200 Jan Dankert Externalize constants. Sun, 18 Oct 2020 01:30:49 +0200 Jan Dankert New component "fieldset" for better form layout. Sat, 10 Oct 2020 01:29:41 +0200 Jan Dankert Refactoring: Only using CSS classes with the 'or-'-prefix. Tue, 6 Oct 2020 22:23:11 +0200 Jan Dankert Fix: Selecting a link with the selector (this was unusable since the last template refactoring). Searching is implemented, the selector tree must be fixed soon. Sun, 4 Oct 2020 23:53:25 +0200 Jan Dankert New: The tree is now hidable with a dedicated button. No more hover effect in the navigation. Fri, 2 Oct 2020 23:11:48 +0200 Jan Dankert Cleanup: No '.inputholder' any more, notices with links to objects. Thu, 1 Oct 2020 20:55:34 +0200 Jan Dankert Refactoring: Moving some global JS functions into the OR Namespace Sun, 27 Sep 2020 11:57:11 +0200 Jan Dankert Fix: Empty the navigaton tree before loading it. Sun, 27 Sep 2020 04:53:00 +0200 Jan Dankert Refactoring: Loading the initial tree with javascript. So we could remove the 'tree'-method from the TreeAction. Thu, 10 Sep 2020 23:49:27 +0200 Jan Dankert Warn the user, if the session is not available. Mon, 24 Aug 2020 22:58:32 +0200 Jan Dankert New: Browser should warn, if the application should be closed while there are unsaved changes. Fri, 10 Apr 2020 00:25:40 +0200 Jan Dankert Fix: The server ping must call the correct JQuery 'getJSON()'-method. Sun, 23 Feb 2020 04:01:30 +0100 Jan Dankert Refactoring with Namespaces for the cms modules, part 1: moving.