openrat-cms

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

workbench.js (27070B)


      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