File modules/cms/ui/themes/default/script/openrat/workbench.min.js
Last commit: Thu Jun 2 01:50:05 2022 +0200 Jan Dankert Refactoring: DSL Interpreter is now using a write buffer
1 import $ from '../jquery-global.min.js'; 2 import Dialog from './dialog.min.js'; 3 import View from './view.min.js'; 4 import Callback from './callback.min.js'; 5 import WorkbenchNavigator from "./navigator.min.js"; 6 import Notice from "./notice.min.js"; 7 import Components from "./components.min.js"; 8 export default class Workbench { 9 'use strict'; 10 static state = { 11 action: '', 12 id: 0, 13 extra: {} 14 }; 15 static dialog; 16 static instance; 17 constructor() { 18 this.popupWindow = null; 19 Callback.dataChangedHandler.add( () => { 20 if ( Workbench.popupWindow ) 21 Workbench.popupWindow.location.reload(); 22 } ); 23 } 24 static getInstance() { 25 if ( ! Workbench.instance ) 26 Workbench.instance = new Workbench(); 27 return Workbench.instance; 28 } 29 initialize() { 30 this.checkBrowserRequirements(); 31 this.initializeState(); 32 $('html').removeClass('nojs'); 33 $('.or--initial-hidden').removeClass('-initial-hidden'); 34 window.onpopstate = ev => { 35 console.debug("Event after navigating",ev); 36 this.closeDialog(); 37 this.closeMenu(); 38 this.loadNewActionState(ev.state); 39 }; 40 this.registerWorkbench(); 41 this.initializeTheme(); 42 this.initializeStartupNotices(); 43 this.initializeEvents(); 44 this.initializeKeystrokes(); 45 this.reloadAll().then( () => { 46 Callback.afterNewActionHandler.fire(); 47 } 48 ); 49 this.initializePingTimer(); 50 this.initializeDirtyWarning(); 51 console.info('Application started'); 52 } 53 initializeTheme() { 54 if (window.localStorage) { 55 let style = window.localStorage.getItem('ui.style'); 56 if (style) 57 this.setUserStyle(style); 58 } 59 } 60 initializeDirtyWarning() { 61 window.addEventListener('beforeunload', function (e) { 62 if ( $('.or-view--is-dirty').length > 0 ) { 63 e.preventDefault(); 64 return 'Unsaved content will be lost.'; 65 } 66 else { 67 return undefined; 68 } 69 }); 70 } 71 registerWorkbench() { 72 window.OpenRat = { workbench: this }; 73 } 74 initializeState() { 75 let parts = window.location.hash.split('/'); 76 let state = { action:'index',id:0 }; 77 if ( parts.length >= 2 ) 78 state.action = parts[1].toLowerCase(); 79 if ( parts.length >= 3 ) 80 state.id = parts[2].replace(/[^0-9_]/gim,""); 81 Workbench.state = state; 82 WorkbenchNavigator.toActualHistory( state ); 83 } 84 checkBrowserRequirements() { 85 if ( ! window.Promise ) { 86 console.error('This browser does not support Promises, which is required for this application.' ); 87 let notice = new Notice(); 88 notice.msg = 'This browser is not supported'; 89 notice.msg = 'Promises are not available'; 90 notice.show(); 91 } 92 if ( ! window.fetch ) { 93 console.error('This browser does not support the fetch API, which is required for this application.' ); 94 let notice = new Notice(); 95 notice.setStatus('error'); 96 notice.msg = 'This browser is not supported'; 97 notice.log = 'Fetch API is not available'; 98 notice.show(); 99 } 100 } 101 initializePingTimer() { 102 let ping = async () => { 103 let url = View.createUrl('profile', 'ping' ); 104 console.debug('ping'); 105 try { 106 let response = await fetch( url,{ 107 method: 'GET', 108 headers: { 109 'Accept': 'application/json', 110 } 111 } ); 112 if ( !response.ok ) 113 throw "ping failed"; 114 } catch( cause ) { 115 console.warn( {message: 'The server ping has failed.',cause:cause }); 116 if ($('.or-view--is-dirty').length > 0) { 117 window.alert("The server session is lost, please save your data."); 118 } 119 else { 120 } 121 } 122 } 123 let timeoutMinutes = 5; 124 window.setInterval( ping, timeoutMinutes*60*1000 ); 125 } 126 loadNewActionState(state) { 127 console.debug("New state",state); 128 Workbench.state = state; 129 this.reloadViews(); 130 this.filterMenus(); 131 Callback.afterNewActionHandler.fire(); 132 } 133 closeDialog() { 134 if ( Workbench.dialog ) { 135 Workbench.dialog.close(); 136 Workbench.dialog = null; 137 } 138 } 139 createDialog() { 140 this.closeDialog(); 141 Workbench.dialog = new Dialog(); 142 return Workbench.dialog; 143 } 144 reloadViews() { 145 this.startSpinner(); 146 let promise = this.loadViews( $('.or-workbench .or-act-view-loader') ); 147 promise.then( 148 () => this.stopSpinner() 149 ); 150 return promise; 151 } 152 startSpinner() { 153 $('.or-workbench-loader').addClass('loader').addClass('loader--is-active'); 154 } 155 stopSpinner() { 156 $('.or-workbench-loader').removeClass('loader').removeClass('loader--is-active'); 157 } 158 reloadAll() { 159 this.startSpinner(); 160 let promise = this.loadViews( $('.or-act-view-loader,.or-act-view-static') ); 161 console.debug('reloading all views'); 162 let stylePromise = this.loadUserStyle(); 163 let languagePromise = this.loadLanguage(); 164 let settingsPromise = this.loadUISettings(); 165 let all = Promise.all( [ promise,stylePromise,languagePromise,settingsPromise ] ); 166 all.then( 167 () => this.stopSpinner() 168 ); 169 return all; 170 } 171 async loadUserStyle() { 172 let url = View.createUrl('profile', 'userinfo' ); 173 let response = await fetch(url,{ 174 method: 'GET', 175 headers: { 176 'Accept': 'application/json', 177 } 178 }); 179 let json = await response.json(); 180 let style = json.output['style']; 181 this.setUserStyle(style); 182 let color = json.output['theme-color']; 183 this.setThemeColor(color); 184 } 185 static settings = {}; 186 static language = {}; 187 async loadLanguage() { 188 let url = View.createUrl('profile', 'language'); 189 let response = await fetch(url,{ 190 method: 'GET', 191 headers: { 192 'Accept': 'application/json', 193 } 194 } 195 ); 196 let data = await response.json(); 197 Workbench.language = data.output.language; 198 } 199 async loadUISettings() { 200 let url = View.createUrl('profile', 'uisettings' ); 201 let response = await fetch(url,{ 202 method: 'GET', 203 headers: { 204 'Accept': 'application/json', 205 } 206 }); 207 let data = await response.json(); 208 Workbench.settings = data.output.settings.settings; 209 } 210 loadViews( $views ) 211 { 212 let wb = this; 213 let promises = []; 214 $views.each(function (idx) { 215 let $targetDOMElement = $(this); 216 promises.push( wb.loadNewActionIntoElement( $targetDOMElement ) ); 217 }); 218 let all = Promise.all( promises ); 219 return all; 220 } 221 loadNewActionIntoElement( $viewElement ) 222 { 223 let action; 224 if ( $viewElement.is('.or-act-view-static') ) 225 action = $viewElement.attr('data-action'); 226 else 227 action = Workbench.state.action; 228 let id = Workbench.state.id; 229 let params = Workbench.state.extra; 230 let method = $viewElement.data('method'); 231 let view = new View( action,method,id,params ); 232 return view.start( $viewElement ); 233 } 234 setUserStyle( styleName ) 235 { 236 if ( window.localStorage ) 237 window.localStorage.setItem('ui.style',styleName); 238 let styleUrl = View.createUrl('index', 'themestyle', 0, {'style': styleName}); 239 document.getElementById('user-style').setAttribute('href',styleUrl); 240 } 241 setThemeColor( color ) 242 { 243 document.getElementById('theme-color').setAttribute('content',color); 244 } 245 static setApplicationTitle( newTitle ) { 246 let title = document.querySelector('head > title'); 247 let defaultTitle = title.dataset.default; 248 title.textContent = (newTitle ? newTitle + ' - ' : '') + defaultTitle; 249 } 250 static registerOpenClose = function( $el ) 251 { 252 $($el).children('.or-collapsible-act-switch').click( function() { 253 let $group = $(this).closest('.or-collapsible'); 254 if ( $group.hasClass('collapsible--is-visible') ) { 255 $group.removeClass('collapsible--is-visible'); 256 setTimeout( () => { 257 $group.removeClass('collapsible--is-open'); 258 },300 ); 259 } 260 else { 261 $group.addClass('collapsible--is-open'); 262 $group.addClass('collapsible--is-visible'); 263 } 264 }); 265 } 266 openNewAction( name,action,id ) 267 { 268 $('.or-workbench-navigation').removeClass('workbench-navigation--is-open'); 269 Workbench.setApplicationTitle( name ); 270 let newState = {'action':action, 'id':id }; 271 this.loadNewActionState( newState ); 272 WorkbenchNavigator.navigateToNew( newState ); 273 } 274 registerDraggable(viewEl) { 275 $(viewEl).find('.or-draggable').attr('draggable','true') 276 .on('dragstart',(e)=>{ 277 $('.or-workbench').addClass('workbench--drag-active'); 278 let link = e.currentTarget; 279 e.dataTransfer.effectAllowed = 'link'; 280 e.dataTransfer.setData('id' , link.dataset.id ); 281 e.dataTransfer.setData('action', link.dataset.action); 282 e.dataTransfer.setData('name' , link.dataset.name || link.textContent.trim() ); 283 console.debug('drag started',e.dataTransfer); 284 }) 285 .on('drag',(e)=>{ 286 }) 287 .on('dragend',(e)=>{ 288 $('.or-workbench').removeClass('workbench--drag-active'); 289 }); 290 } 291 registerDroppable(viewEl) { 292 $(viewEl).find('.or-droppable-selector').on('dragover', (e) => { 293 e.preventDefault(); 294 }).on('drop', (event) => { 295 let id = event.dataTransfer.getData('id'); 296 if ( !id) { 297 console.debug("dropped object has no object id, ignoring"); 298 return; 299 } 300 let name = event.dataTransfer.getData('name'); 301 if (!name) 302 name = id; 303 console.debug("dropped",id,name,event.dataTransfer ); 304 $(event.currentTarget).find('.or-selector-link-value').val(id); 305 $(event.currentTarget).find('.or-selector-link-name').val(name).attr('placeholder', name); 306 event.preventDefault(); 307 }); 308 $(viewEl).find('.or-droppable') 309 } 310 registerAsDroppable( el, onDrop ) { 311 el.addEventListener('dragover', (e) => { 312 e.preventDefault(); 313 }); 314 el.addEventListener('dragenter', (e) => { 315 e.stopPropagation(); 316 e.preventDefault(); 317 e.currentTarget.classList.add('or-workbench--drop-active'); 318 }); 319 el.addEventListener('dragleave', (e) => { 320 e.stopPropagation(); 321 e.preventDefault(); 322 e.currentTarget.classList.remove('or-workbench--drop-active'); 323 }); 324 el.addEventListener('drop', onDrop); 325 } 326 static htmlDecode(input) { 327 let doc = new DOMParser().parseFromString(input, "text/html"); 328 return doc.documentElement.textContent; 329 } 330 async filterMenus() { 331 let action = Workbench.state.action; 332 let id = Workbench.state.id; 333 $('.or-workbench-title .or-dropdown-entry.or-act-clickable').addClass('dropdown-entry--active'); 334 $('.or-workbench-title .or-filtered').removeClass('dropdown-entry--active').addClass('dropdown-entry--inactive'); 335 $('.or-workbench-title .or-filtered .or-link').attr('data-id', id); 336 let url = View.createUrl('profile', 'available', id, {'queryaction': action}); 337 let response = await fetch(url, { 338 method: 'GET', 339 headers: { 340 'Accept': 'application/json', 341 } 342 }); 343 let data = await response.json(); 344 for (let method of Object.values(data.output.views)) 345 $('.or-workbench-title .or-filtered > .or-link[data-method=\'' + method + '\']') 346 .parent() 347 .addClass('dropdown-entry--active') 348 .removeClass('dropdown-entry--inactive'); 349 } 350 initializeStartupNotices() { 351 $('.or-act-initial-notice').each( function() { 352 let notice = new Notice(); 353 notice.setStatus('info'); 354 notice.msg = $(this).text(); 355 notice.show(); 356 }); 357 } 358 initializeKeystrokes() { 359 let keyPressedHandler = (event) => { 360 if (event.key === 'F4') { 361 let dialog = this.createDialog(); 362 dialog.start('', '', 'prop', 0, {}); 363 } 364 if (event.key === 'F2') { 365 if ($('.or-workbench').hasClass('workbench--navigation-is-small')) 366 $('.or-act-nav-wide').click(); 367 else 368 $('.or-act-nav-small').click(); 369 } 370 if (event.code === 'Escape') { 371 this.closeDialog(); 372 } 373 }; 374 document.addEventListener('keydown',keyPressedHandler); 375 } 376 closeMenu() { 377 $('.or-menu').removeClass('menu--is-open'); 378 } 379 initializeEvents() { 380 new Components().registerComponents(); 381 $('body').click( () => { 382 this.closeMenu(); 383 }); 384 $('.or-dialog-filler,.or-act-dialog-close').click( (e) => 385 { 386 e.preventDefault(); 387 this.closeDialog(); 388 } 389 ); 390 $('.or-act-navigation-close').click( () => { 391 $('.or-workbench-navigation').removeClass('workbench-navigation--is-open'); 392 $('.or-workbench').removeClass('workbench--navigation-is-open'); 393 }); 394 $('.or-workbench-title .or-act-nav-small').click( () => { 395 $('.or-workbench').addClass('workbench--navigation-is-small'); 396 $('.or-workbench-navigation').addClass('workbench-navigation--is-small'); 397 }); 398 $('.or-search-input .or-input').orSearch( { 399 onSearchActive: function() { 400 $('.or-search').addClass('search--is-active'); 401 }, 402 onSearchInactive: function() { 403 $('.or-search').removeClass('search--is-active'); 404 }, 405 dropdown : '.or-act-search-result', 406 resultEntryClass: 'search-result-entry', 407 //openDropdown: true, 408 select : function(obj) { 409 Workbench.getInstance().openNewAction( obj.name, obj.action, obj.id ); 410 }, 411 afterSelect: function() { 412 } 413 } ); 414 $('.or-search .or-act-search-delete').click( () => { 415 $('.or-search .or-title-input').val('').input(); 416 } ); 417 Callback.afterNewActionHandler.add( function() { 418 $('.or-sidebar').find('.or-sidebar-button').orLinkify(); 419 } 420 ); 421 Callback.afterNewActionHandler.add( function() { 422 let url = View.createUrl('tree', 'path', Workbench.state.id, {'type': Workbench.state.action}); 423 let loadPromise = fetch( url,{ 424 method: 'GET', 425 headers: { 426 'Accept': 'text/html', 427 } 428 } ); 429 function openNavTree(action, id) { 430 let $navControl = $('.or-link[data-action="'+action+'"][data-id="'+id+'"]').closest('.or-navtree-node'); 431 if ( $navControl.is( '.or-navtree-node--is-closed' ) ) 432 $navControl.find('.or-navtree-node-control').click(); 433 } 434 loadPromise 435 .then( response => response.text() ) 436 .then( data => { 437 $('.or-breadcrumb').empty().html( data ).find('.or-act-clickable').orLinkify(); 438 $('.or-breadcrumb a').each( function () { 439 let action = $(this).data('action'); 440 let id = $(this).data('id' ); 441 openNavTree( action, id ); 442 }); 443 $('.or-link--is-active').removeClass('link--is-active'); 444 let action = Workbench.state.action; 445 let id = Workbench.state.id; 446 if (!id) id = '0'; 447 $('.or-link[data-action=\''+action+'\'][data-id=\''+id+'\']').addClass('link--is-active'); 448 openNavTree( action,id ); 449 }).catch( cause => { 450 console.warn( { 451 message : 'Failed to load path', 452 url : url, 453 cause : cause } ); 454 }).finally(function () { 455 }); 456 } ); 457 Callback.afterViewLoadedHandler.add( function(element) { 458 $(element).find('.or-button').orButton(); 459 } ); 460 Callback.afterViewLoadedHandler.add( function(element) { 461 if ( Workbench.popupWindow ) 462 $(element).find("a[data-type='popup']").each( function() { 463 Workbench.popupWindow.location.href = $(this).attr('data-url'); 464 }); 465 }); 466 Callback.afterViewLoadedHandler.add( function(element) { 467 $(element).find(".or-input--password").dblclick( function() { 468 $(this).toggleAttr('type','text','password'); 469 }); 470 $(element).find(".or-act-make-visible").click( function() { 471 $(this).toggleClass('btn--is-active' ); 472 $(this).parent().children('input').toggleAttr('type','text','password'); 473 }); 474 }); 475 Callback.afterViewLoadedHandler.add( function($element) { 476 $element.find('.or-act-load-nav-tree').each( async function() { 477 let type = $(this).data('type') || 'root'; 478 let loadBranchUrl = View.createUrl('tree', 'branch', 0, {type: type}); 479 let $targetElement = $(this); 480 let response = await fetch( loadBranchUrl,{ 481 method: 'GET', 482 headers: { 483 'Accept': 'text/html', 484 } 485 } ); 486 let html = await response.text(); 487 let $ul = $.create('ul' ).addClass('navtree-list'); 488 $ul.appendTo( $targetElement.empty() ).html( html ); 489 $ul.find('li').orTree( { 490 'openAction': function( name,action,id) { 491 Workbench.getInstance().openNewAction( name,action,id ); 492 } 493 } ); 494 $ul.find('.or-act-clickable').orLinkify(); 495 $ul.find('.or-navtree-node-control').first().click(); 496 } ); 497 } ); 498 Callback.afterViewLoadedHandler.add( function(viewEl ) { 499 $(viewEl).find('.or-act-nav-open-close').click( function() { 500 $('.or-workbench').toggleClass('workbench--navigation-is-open'); 501 $('.or-workbench-navigation').toggleClass('workbench-navigation--is-open'); 502 }); 503 $(viewEl).find('.or-act-nav-small').click( function() { 504 $('.or-workbench').addClass('workbench--navigation-is-small'); 505 $('.or-workbench-navigation').addClass('workbench-navigation--is-small'); 506 }); 507 $(viewEl).find('.or-act-nav-wide').click( function() { 508 $('.or-workbench').removeClass('workbench--navigation-is-small'); 509 $('.or-workbench-navigation').removeClass('workbench-navigation--is-small'); 510 }); 511 $(viewEl).find('.or-act-selector-tree-button').click( function() { 512 let $selector = $(this).parent('.or-selector'); 513 let $targetElement = $selector.find('.or-act-load-selector-tree'); 514 if ( $selector.hasClass('selector--is-tree-active') ) { 515 $selector.removeClass('selector--is-tree-active'); 516 $targetElement.empty(); 517 } 518 else { 519 $selector.addClass('selector--is-tree-active'); 520 var selectorEl = this; 521 let id = $(this).data('init-folder-id'); 522 let type = id?'folder':'projects'; 523 let loadBranchUrl = './?action=tree&subaction=branch&id='+id+'&type='+type; 524 let load = fetch( loadBranchUrl,{ 525 method: 'GET', 526 headers: { 527 'Accept': 'text/html', 528 } 529 } ); 530 load 531 .then( response => response.text() ) 532 .then( html => { 533 let $ul = $.create('ul' ).addClass('navtree-list'); 534 $ul.appendTo( $targetElement ).html( html ); 535 $ul.find('li').orTree( 536 { 537 'openAction' : function(name,action,id) { 538 $selector.find('.or-selector-link-value').val(id ); 539 $selector.find('.or-selector-link-name' ).val('').attr('placeholder',name); 540 $selector.removeClass('selector--is-tree-active'); 541 $targetElement.empty(); 542 } 543 } 544 ); 545 $ul.find('.or-act-clickable').orLinkify(); 546 $ul.find('.or-navtree-node-control').first().click(); 547 } ); 548 } 549 } ); 550 registerDragAndDrop(viewEl); 551 $(viewEl).find('.or-theme-chooser').change( function() { 552 Workbench.getInstance().setUserStyle( this.value ); 553 }); 554 function registerMenuEvents($element ) 555 { 556 $($element).find('.or-menu-category').click( function(event) { 557 event.stopPropagation(); 558 $(this).closest('.or-menu').toggleClass('menu--is-open'); 559 }); 560 $($element).find('.or-menu-category').mouseover( function() { 561 $(this).closest('.or-menu').find('.or-menu-category').removeClass('menu-category--is-open'); 562 $(this).addClass('menu-category--is-open'); 563 }); 564 } 565 function registerSelectorSearch( $element ) 566 { 567 $($element).find('.or-act-selector-search').orSearch( { 568 onSearchActive: function() { 569 $(this).parent('or-selector').addClass('selector-search--is-active'); 570 }, 571 onSearchInactive: function() { 572 $(this).parent('or-selector').removeClass('selector-search--is-active' ); 573 }, 574 dropdown: '.or-act-selector-search-results', 575 resultEntryClass: 'search-result-entry', 576 select: function(obj) { 577 $($element).find('.or-selector-link-value').val(obj.id ); 578 $($element).find('.or-selector-link-name' ).val(obj.name).attr('placeholder',obj.name); 579 }, 580 afterSelect: function() { 581 $('.or-dropdown.or-act-selector-search-results').empty(); 582 } 583 } ); 584 } 585 function registerTree(element) { 586 } 587 registerMenuEvents ( viewEl ); 588 registerSelectorSearch( viewEl ); 589 registerTree ( viewEl ); 590 function registerDragAndDrop(viewEl) 591 { 592 Workbench.getInstance().registerDraggable(viewEl); 593 Workbench.getInstance().registerDroppable(viewEl); 594 } 595 registerDragAndDrop(viewEl); 596 } ); 597 }; 598 static async addStyle( id, href ) { 599 return new Promise( (resolve,reject) => { 600 let styleEl = document.getElementById(id); 601 if (!styleEl) { 602 styleEl = document.createElement('link'); 603 styleEl.addEventListener('load',resolve); 604 styleEl.setAttribute('rel', 'stylesheet'); 605 styleEl.setAttribute('type', 'text/css'); 606 styleEl.setAttribute('href', href); 607 styleEl.setAttribute('id', id); 608 document.getElementsByTagName('head')[0].appendChild(styleEl); 609 } else { 610 resolve(); 611 } 612 } ); 613 } 614 static async addScript( id, href ) { 615 return new Promise( (resolve,reject) => { 616 let scriptEl = document.getElementById( id ); 617 if ( ! scriptEl ) { 618 scriptEl = document.createElement( 'script' ); 619 scriptEl.setAttribute('id' ,id ); 620 scriptEl.setAttribute('type','text/javascript' ); 621 scriptEl.addEventListener('load',resolve); 622 scriptEl.setAttribute('src',href ); 623 document.getElementsByTagName('head')[0].appendChild(scriptEl); 624 } else { 625 resolve(); 626 } 627 } ); 628 } 629 }
Downloadmodules/cms/ui/themes/default/script/openrat/workbench.min.js
History Thu, 2 Jun 2022 01:50:05 +0200 Jan Dankert Refactoring: DSL Interpreter is now using a write buffer 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: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, 21 Aug 2020 00:22:13 +0200 Jan Dankert Refactoring: Collect all frontend compiler scripts in update.php. Compiling of CSS and JS was extracted to a new TemplateCompiler. JS and CSS is now collected in a new openrat.[min.][js|css]. 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.