openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

workbench.min.js (18540B)


      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 }