TimeTrex/interface/html5/main.js

525 lines
23 KiB
JavaScript

// JS
import * as StackTrace from 'stacktrace-js'
// import 'expose-loader?exposes=$,jQuery!jquery'; // Needs be done with expose otherwise other imports error "jQuery/$ is not defined." e.g. plugins like i18n.
// import _ from 'underscore'; // no longer needed here after importing differently in webpack config.
import 'backbone'; // does not seem to need expose loader.
import * as moment from 'moment';
import './framework/jquery.i18n.js';
import html2canvas from 'html2canvas';
// Vue imports
import { createApp, reactive } from 'vue'; // If there are any errors relating to 'vue' remember the alias in webpack.config points to the vue runtime
import main_ui_router from '@/components/main_ui_router';
import TTMainUI from '@/components/TTMainUI.vue';
import PrimeVue from 'primevue/config';
import Tooltip from 'primevue/tooltip';
import Ripple from 'primevue/ripple';
import Toast from 'primevue/toast';
import ToastService from 'primevue/toastservice';
import BadgeDirective from 'primevue/badgedirective';
import TTEventBus from '@/services/TTEventBus';
// import 'expose-loader?exposes=Global|Global!@/global/Global';
import { Global } from '@/global/Global';
import { LocalCacheData } from '@/global/LocalCacheData';
import { Base } from '@/model/Base';
import { BaseWindowController } from '@/views/BaseWindowController';
import { ServiceCaller } from '@/services/ServiceCaller';
import { BaseViewController } from '@/views/BaseViewController';
import { TTAPI } from '@/services/TimeTrexClientAPI';
import { NotificationConsumerObj } from '@/services/NotificationConsumer';
import { IndexViewController } from '@/IndexController';
// import { ReportBaseViewController } from '@/views/reports/ReportBaseViewController'; // Moved to post-login-app-dependancies
import { TTUUID } from '@/global/TTUUID';
import { TTPromise } from '@/global/TTPromise';// Potentially the same issue as ProgressBar (multiple instances, no single state), so lets just make it global to be safe.
import { ProgressBar } from '@/global/ProgressBarManager'; // Must only be imported in one place, otherwise multiple instances could be loaded and clash by leaving open Progressbars. e.g. Seen in TimeSheet
import { BaseWizardController } from '@/views/wizard/BaseWizardController';
import { PermissionManager } from '@/global/PermissionManager';
import { TAlertManager } from '@/global/TAlertManager';
//import { TopMenuManager } from 'exports-loader?exports=TopMenuManager!@/global/TopMenuManager';
import ContextMenuManager from '@/components/context_menu/ContextMenuManager';
import MenuManager from '@/components/main_menu/MenuManager';
import '@/global/widgets/text_input/TTextInput.js';
import '@/global/widgets/text_input/TPasswordInput.js';
import '@/global/widgets/combobox/TComboBox.js';
// Main execution for imported code
window.html2canvas = html2canvas;
window.StackTrace = StackTrace;
window.moment = moment;
// Function(jQueryBaResize)(); // Need this to execute in correct scope where this=window, otherwise the 'this' value in the jquery plugin results to {} instead of real window, as the 'this' param passed into the plugins IIFE is not window, but {}, which means `window` in plugin = {}. Results in error: "TypeError: window[str_setTimeout] is not a function"
// not needed if cookie script included in html <script>
// window.getCookie = getCookie;
// window.setCookie = setCookie;
// window.deleteCookie = deleteCookie;
// Main execution for imported code
// Define the globals for various plugins and views.
// window.TTBackboneView = TTBackboneView;
window.$ = window.jQuery = $; // #3028 This is needed because although webpack will handle any references to $/jQuery in our code, its done internally and not on the window. So 3rd party libraries will not find jQuery on the window.
window.Global = Global;
window.LocalCacheData = LocalCacheData;
window.Base = Base;
window.BaseWindowController = BaseWindowController;
window.ServiceCaller = ServiceCaller;
window.BaseViewController = BaseViewController;
window.TTAPI = TTAPI;
window.IndexViewController = IndexViewController;
// window.ReportBaseViewController = ReportBaseViewController;
window.TTUUID = TTUUID;
window.TTPromise = TTPromise;
window.ProgressBar = ProgressBar;
window.BaseWizardController = BaseWizardController;
window.PermissionManager = PermissionManager;
window.TAlertManager = TAlertManager;
//window.TopMenuManager = TopMenuManager;
window.NotificationConsumer = NotificationConsumerObj;
window.ContextMenuManager = ContextMenuManager;
window.MenuManager = MenuManager;
window.TTEventBus = TTEventBus; // Only used for globals. Make sure to instantiate in every view you want to use.
// TODO: At some point, the code in this file should be refactored into classes.
window.addEventListener('load', loadViewRequiredJS);
// Vue initialization
const tt_main_ui = createApp( TTMainUI );
// PrimeVue Apollo globals
tt_main_ui.directive('tooltip', Tooltip);
tt_main_ui.directive('ripple', Ripple);
// Vue.prototype.$appState = Vue.observable({inputStyle: 'outlined'});
// Vue.prototype.$primevue = Vue.observable({ripple: true});
// tt_main_ui.config.globalProperties.$appState = reactive({inputStyle: 'outlined'});
// tt_main_ui.config.globalProperties.$primevue = reactive({ripple: true});
tt_main_ui.config.globalProperties.$appState = reactive({ colorScheme: 'light' });
// tt_main_ui.config.globalProperties.$primevue = reactive({ripple: true, inputStyle: 'filled'}); // From: AppConfig.vue this.$primevue.config.inputStyle value is filled/outlined
tt_main_ui.use( PrimeVue, { ripple: true, inputStyle: 'filled' }); // From: AppConfig.vue this.$primevue.config.inputStyle value is filled/outlined as we dont use AppConfig in TT.
tt_main_ui.use( ToastService );
tt_main_ui.component( "Toast", Toast );
tt_main_ui.directive('badge', BadgeDirective); // Used mainly by notifications bell.
//main_ui_router.isReady().then(() => app.mount('#app'))
tt_main_ui.use( main_ui_router )
.mount( '#tt_main_ui' );
window.VueRouter = main_ui_router; // #VueContextMenu# Allows the TT UI to trigger route changes from within BaseViewController.loadView or elsewhere.
// End of Vue initialization
//Don't not show loading bar if refresh
if ( Global.isSet( LocalCacheData.getLoginUser() ) ) {
$( '.loading-view' ).hide();
} else {
setProgress();
}
function setProgress() {
window.loading_bar_time = setInterval( function() {
var progress_bar = $( '.progress-bar' );
var c_value = progress_bar.prop( 'value' );
if ( c_value < 90 ) {
progress_bar.prop( 'value', c_value + 10 );
}
}, 1000 );
}
function cleanProgress() {
if ( $( '.loading-view' ).is( ':visible' ) ) {
var progress_bar = $( '.progress-bar' );
progress_bar.prop( 'value', 100 );
clearInterval( loading_bar_time );
loading_bar_time = setInterval( function() {
$( '.progress-bar-div' ).hide();
clearInterval( loading_bar_time );
}, 50 );
}
}
window.is_browser_iOS = ( navigator.userAgent.match( /(iPad|iPhone|iPod)/g ) ? true : false );
$( function() {
Global.styleSandbox();
cleanProgress();
var api_authentication = TTAPI.APIAuthentication;
if ( Error ) {
Error.stackTraceLimit = 50; //Increase JS exception stack trace limit.
}
//BUG-2065 see also: require.onError()
window.onerror = function( error_msg, file, line, col, error_obj ) {
if ( !arguments || arguments.length < 1 ) {
Global.sendErrorReport( 'No error parameters when window.onerror', ServiceCaller.root_url, '', '', '' );
} else {
Global.sendErrorReport( error_msg, file, line, col, error_obj );
}
};
window.addEventListener( 'beforeunload', function( e ) {
// Note that Google recommends against the following, as it affects page caching, but we dont want caching anyway: https://developers.google.com/web/updates/2018/07/page-lifecycle-api#the-beforeunload-event
Global.sendAnalyticsEvent( 'browser', 'browser:beforeunload', 'browser:beforeunload' );
if ( ( LocalCacheData.current_open_primary_controller && LocalCacheData.current_open_primary_controller.edit_view && LocalCacheData.current_open_primary_controller.is_changed == true )
|| ( LocalCacheData.current_open_report_controller && LocalCacheData.current_open_report_controller.is_changed == true )
|| ( LocalCacheData.current_open_edit_only_controller && LocalCacheData.current_open_edit_only_controller.is_changed == true )
|| ( LocalCacheData.current_open_sub_controller && LocalCacheData.current_open_sub_controller.edit_view && LocalCacheData.current_open_sub_controller.is_changed == true )
) {
e.preventDefault(); // Cancel the unload event
e.returnValue = ''; // Chrome requires returnValue to be set
}
} );
$( 'body' ).addClass( 'login-bg' );
//Load need API class
$( document ).on( 'keydown', function( e ) {
if ( e.which === 8 && !$( e.target ).is( 'input, textarea' ) ) {
e.preventDefault();
}
} );
$( 'body' ).unbind( 'keydown' ).bind( 'keydown', function( e ) {
//Tab key must go to next search text field if a search text field is selected, otherwise, tab closes awesomebox.
//This allows consistent ui experience between awesomebox and default form input controls.
if ( e.keyCode === 27 || e.keyCode === 13 || ( e.keyCode === 9 && e.target.type != 'text' ) ) {
if ( LocalCacheData.openAwesomeBox ) {
LocalCacheData.openAwesomeBox.onClose();
}
if ( LocalCacheData.openAwesomeBoxColumnEditor ) {
LocalCacheData.openAwesomeBoxColumnEditor.onClose();
}
}
if ( LocalCacheData.openAwesomeBox ) {
if ( Global.isValidInputCodes( e.keyCode ) ) {
LocalCacheData.openAwesomeBox.selectNextItem( e );
}
} else if ( LocalCacheData.current_open_primary_controller &&
LocalCacheData.current_open_primary_controller.column_selector &&
LocalCacheData.current_open_primary_controller.column_selector.is( ':visible' ) &&
LocalCacheData.current_open_primary_controller.column_selector.has( $( ':focus' ) ).length > 0 ) {
if ( Global.isValidInputCodes( e.keyCode ) ) {
LocalCacheData.current_open_primary_controller.column_selector.selectNextItem( e );
}
}
if ( LocalCacheData.current_open_wizard_controllers.length > 0 && e.keyCode === 13 ) {
let password_wizard = LocalCacheData.current_open_wizard_controllers.find( wizard => wizard.wizard_id === 'ResetPasswordWizardController' );
if ( password_wizard && password_wizard.$el.hasClass( 'change-password-wizard' ) ) {
!password_wizard.done_btn.attr( 'disabled' ) &&
e.target &&
e.target.type !== 'textarea' && password_wizard.onDoneClick();
}
}
if ( ( e.keyCode === 65 && e.metaKey === true ) || ( e.keyCode === 65 && e.ctrlKey === true ) ) {
e.preventDefault();
selectAll();
}
if ( e.keyCode === 36 ) {
gridScrollTop();
}
if ( e.keyCode === 35 ) {
gridScrollDown();
}
// keyboard event to quick search permission adropdown
if ( LocalCacheData.current_open_primary_controller &&
LocalCacheData.current_open_primary_controller.viewId === 'PermissionControl' &&
LocalCacheData.current_open_primary_controller.edit_view ) {
LocalCacheData.current_open_primary_controller.onKeyDown( e );
}
} );
if ( window._addToDebugClickStack === undefined ) {
window._addToDebugClickStack = function( e ) {
// Must collect click data on 'event capture phase' vs bubbling phase, so the click is recorded as soon as possible, before any potential errors prevent the recording of last click.
// Function added to window, to prevent duplicate click listeners (JS wont add duplicate listeners referencing the same function). More context at https://stackoverflow.com/questions/38939937/when-are-duplicate-event-listeners-discarded-and-when-are-they-not
var ui_clicked_date = new Date();
var ui_stack = {
target_class: $( e.target ).attr( 'class' ) ? $( e.target ).attr( 'class' ) : '',
target_id: $( e.target ).attr( 'id' ) ? $( e.target ).attr( 'id' ) : '',
html: e.target.outerHTML,
ui_clicked_date: ui_clicked_date.toISOString()
};
if ( LocalCacheData.ui_click_stack.length === 16 ) {
LocalCacheData.ui_click_stack.pop();
}
LocalCacheData.ui_click_stack.unshift( ui_stack );
};
window.addEventListener( 'click', window._addToDebugClickStack, true ); // true is to set listener on 'event capture phase', so the click is recorded as soon as possible, before any potential errors prevent the recording of last click.
}
$( 'body' ).unbind( 'mousedown' ).bind( 'mousedown', function( e ) {
// MUST COLLECT DATA WHEN MOUSE down, otherwise when do save in edit view when awesomebox open, the data can't be saved.
// Mouse down to collect data so for some actions like search can read select data in its click event
if ( LocalCacheData.openAwesomeBox && LocalCacheData.openAwesomeBox.getADropDown() && LocalCacheData.openAwesomeBox.getADropDown().has( e.target ).length < 1 ) {
if ( $( e.target ).hasClass( 'a-combobox' ) ) {
var target = LocalCacheData.openAwesomeBox;
$( e.target ).unbind( 'mouseup' ).bind( 'mouseup', function( e ) {
target.find( '.focus-input' ).focus();
$( e.target ).unbind( 'mouseup' );
} );
}
LocalCacheData.openAwesomeBox.onClose();
}
//This closes pickers and dropdown boxes when clicking off them.
if ( LocalCacheData.openRangerPicker && !LocalCacheData.openRangerPicker.getIsMouseOver() ) {
LocalCacheData.openRangerPicker.close();
}
if ( LocalCacheData.openAwesomeBoxColumnEditor && !LocalCacheData.openAwesomeBoxColumnEditor.getIsMouseOver() ) {
LocalCacheData.openAwesomeBoxColumnEditor.onClose();
}
if ( LocalCacheData.openRibbonNaviMenu && !LocalCacheData.openRibbonNaviMenu.getIsMouseOver() ) {
LocalCacheData.openRibbonNaviMenu.close();
}
} );
ServiceCaller.base_url = Global.getBaseURL( '../../', false );
ServiceCaller.base_api_url = 'api/json/api.php';
ServiceCaller.root_url = Global.getRootURL();
var loginData = {};
//Set in APIGlobal.php
if ( !need_load_pre_login_data ) {
loginData = APIGlobal.pre_login_data;
} else {
need_load_pre_login_data = false;
}
if ( !loginData.hasOwnProperty( 'api_base_url' ) ) {
api_authentication.getPreLoginData( null, {
onResult: function( e ) {
var result = e.getResult();
LocalCacheData.setLoginData( result );
APIGlobal.pre_login_data = result;
loginData = LocalCacheData.getLoginData();
initApps();
}
} );
} else {
LocalCacheData.setLoginData( loginData ); //set here because the loginData is set from php
initApps();
}
initAnalytics();
function initAnalytics() {
/* jshint ignore:start */
if ( APIGlobal.pre_login_data.analytics_enabled === true && ServiceCaller && ServiceCaller.root_url && loginData && loginData.base_url ) {
try {
var gtag_script = document.createElement('script');
gtag_script.setAttribute('src', 'https://www.googletagmanager.com/gtag/js?id='+ APIGlobal.pre_login_data.analytics_tracking_code );
//gtag_script.setAttribute('src', ServiceCaller.root_url + loginData.base_url + 'html5/framework/google/analytics/gtag.js' ); //The JS isn't static, so load it remotely instead.
document.head.appendChild(gtag_script);
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag(){window.dataLayer.push(arguments);}
gtag( 'js', new Date() );
gtag( 'config', APIGlobal.pre_login_data.analytics_tracking_code, { 'debug_mode': !APIGlobal.pre_login_data.production, 'send_page_view': false, 'custom_map': { 'dimension1': 'application_version', 'dimension2': 'http_host', 'dimension3': 'product_edition_name', 'dimension4': 'registration_key', 'dimension5': 'primary_company_name', 'dimension6': 'user_name', 'dimension7': 'company_name', } } );
//Do not check exitstance of LocalCacheData with if(LocalCacheData) or JS will execute the unnamed function it uses as a constructor
if ( LocalCacheData.loginUser ) {
var current_company = LocalCacheData.getCurrentCompany();
Global.setAnalyticDimensions( LocalCacheData.getLoginUser().first_name + ' (' + LocalCacheData.getLoginUser().id + ')', current_company.name );
} else {
Global.setAnalyticDimensions();
}
} catch ( e ) {
throw e; //Attempt to catch any errors thrown by Google Analytics.
}
}
/* jshint ignore:end */
}
function initApps() {
TAlertManager.showBrowserTopBanner(); //Checks for supported browser versions and display banner if not.
// loadViewRequiredJS(); // Moved to top of document, waiting on 'load' completion, to load after the 'red line'
//Optimization: Only change locale if its *not* en_US or enable_default_language_translation = TRUE
if ( loginData.locale !== 'en_US' || loginData.enable_default_language_translation == true ) {
Global.loadLanguage( loginData.locale );
Debug.Text( 'Using Locale: ' + loginData.locale, 'main.js', '', 'initApps', 1 );
} else {
LocalCacheData.setI18nDic( {} );
}
$.i18n.load( LocalCacheData.getI18nDic() );
Global.initStaticStrings();
LocalCacheData.deployment_on_demand = loginData.deployment_on_demand;
LocalCacheData.productEditionId = loginData.product_edition;
var controller = new IndexViewController(); //Even though controller variable is not used, this must be called.
var alternate_session_data = getCookie( 'AlternateSessionData' );
if ( alternate_session_data ) {
try { //Prevent JS exception if we can't parse alternate_session_data for some reason.
alternate_session_data = JSON.parse( alternate_session_data );
if ( alternate_session_data && alternate_session_data.previous_session_id ) {
TAlertManager.showPreSessionAlert();
}
} catch ( e ) {
Debug.Text( e.message, 'main.js', 'require', 'initApps', 10 );
}
}
//When the user switched away from, then back again to our tab, make sure the session is still the same so they don't get two different login sessions confused.
function handleVisibilityChange() {
//No need to handle anything if installer is enabled.
if ( APIGlobal.pre_login_data && APIGlobal.pre_login_data.installer_enabled && APIGlobal.pre_login_data.installer_enabled == true ) {
return null;
}
var is_hidden = document.hidden;
var cookie_session_id = getCookie( Global.getSessionIDKey() );
Debug.Text( 'Tab Visibility Change: ' + is_hidden + ' Session ID: ' + LocalCacheData.getSessionID(), 'main.js', '', 'handleVisibilityChange', 10 );
if ( is_hidden == false ) {
//Check to make sure our session_id matches what the server returns.
// When duplicating a tab or middle clicking a menu item, then switching to the new tab immediately, LocalCacheData.getSessionID() could be blank.
// So since its doing a browser refresh anyways, prevent that from triggering this message and another reload.
if ( LocalCacheData.getSessionID() != '' && cookie_session_id != LocalCacheData.getSessionID() ) {
Debug.Text( 'Session ID has changed out from out underneath us! Session ID: Memory: ' + LocalCacheData.getSessionID() + ' Cookie: ' + cookie_session_id, 'main.js', '', 'handleVisibilityChange', 1 );
var api = TTAPI.APIAuthentication;
api.isLoggedIn( false, {
onResult: function( result ) {
var result_data = result.getResult();
if ( result_data === true && cookie_session_id != LocalCacheData.getSessionID() ) { //Recheck the cookie/session ID as they could have changed by this point due to the API call taking some time to finish.
TAlertManager.showAlert( $.i18n._( 'It appears that you have logged in from another web browser window or tab.<br><br>Please be patient while the session is resumed here...' ), 'Session Changed', function() {
Global.sendAnalyticsEvent( 'session', 'session:changed', 'session:changed' );
window.location.reload( true );
} );
} else {
//Don't do Logout here, as we need to display a "Session Expired" message to the user, which is triggered from the ServiceCaller.
// In order to trigger that though, we need to make an *Authenticated* API call to APIMisc.Ping(), rather than UnAuthenticated call to APIAuthentication.Ping()
var api = TTAPI.APIMisc;
api.ping( {
onResult: function() {
}
} );
}
}
} );
}
}
}
window.addEventListener( 'visibilitychange', handleVisibilityChange );
}
function gridScrollDown() {
if ( LocalCacheData.openAwesomeBox &&
_.isFunction( LocalCacheData.openAwesomeBox.gridScrollDown ) ) {
LocalCacheData.openAwesomeBox.gridScrollDown();
return;
}
if ( LocalCacheData.current_open_sub_controller ) {
if ( !LocalCacheData.current_open_sub_controller.edit_view &&
_.isFunction( LocalCacheData.current_open_sub_controller.gridScrollDown ) ) {
LocalCacheData.current_open_sub_controller.gridScrollDown();
}
return;
}
if ( LocalCacheData.current_open_primary_controller ) {
if ( !LocalCacheData.current_open_primary_controller.edit_view &&
_.isFunction( LocalCacheData.current_open_primary_controller.gridScrollDown ) ) {
LocalCacheData.current_open_primary_controller.gridScrollDown();
}
return;
}
}
function gridScrollTop() {
if ( LocalCacheData.openAwesomeBox &&
_.isFunction( LocalCacheData.openAwesomeBox.gridScrollTop ) ) {
LocalCacheData.openAwesomeBox.gridScrollTop();
return;
}
if ( LocalCacheData.current_open_sub_controller ) {
if ( !LocalCacheData.current_open_sub_controller.edit_view &&
_.isFunction( LocalCacheData.current_open_sub_controller.gridScrollTop ) ) {
LocalCacheData.current_open_sub_controller.gridScrollTop();
}
return;
}
//Error: Uncaught TypeError: LocalCacheData.current_open_primary_controller.gridScrollTop is not a function in interface/html5/main.js?v=9.0.2-20151106-092147 line 434
if ( LocalCacheData.current_open_primary_controller ) {
if ( !LocalCacheData.current_open_primary_controller.edit_view &&
_.isFunction( LocalCacheData.current_open_primary_controller.gridScrollTop ) ) {
LocalCacheData.current_open_primary_controller.gridScrollTop();
}
return;
}
}
function selectAll() {
//Error: Uncaught TypeError: LocalCacheData.current_open_primary_controller.selectAll is not a function in interface/html5/main.js?v=9.0.4-20151123-121757 line 457
if ( LocalCacheData.openAwesomeBox &&
_.isFunction( LocalCacheData.openAwesomeBox.selectAll ) ) {
LocalCacheData.openAwesomeBox.selectAll();
return;
}
if ( LocalCacheData.current_open_sub_controller ) {
if ( !LocalCacheData.current_open_sub_controller.edit_view &&
_.isFunction( LocalCacheData.current_open_sub_controller.selectAll ) ) {
LocalCacheData.current_open_sub_controller.selectAll();
}
return;
}
if ( LocalCacheData.current_open_primary_controller ) {
if ( !LocalCacheData.current_open_primary_controller.edit_view &&
_.isFunction( LocalCacheData.current_open_primary_controller.selectAll ) ) {
LocalCacheData.current_open_primary_controller.selectAll();
}
return;
}
};
} );
function loadViewRequiredJS() {
LocalCacheData.loadViewRequiredJSReady = false; // #2848 Set to true was moved to post-login-main_ui-dependancies.js to ensure all dependancies fully loaded.
//Revert jQuery preFilter behavior in v3.5.0 back to pre-v3.5.0 so it doesn't break jqGrid getGridParam( 'colModel' ).
// Attendance -> TimeSheet, expand employee dropdown, click icon to customize columns, triggers JS exception: Uncaught TypeError: Cannot read property 'width' of undefined
var rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([a-z][^\/\0>\x20\t\r\n\f]*)[^>]*)\/>/gi;
jQuery.htmlPrefilter = function( html ) {
return html.replace( rxhtmlTag, "<$1></$2>" );
};
import(
/* webpackChunkName: "post-login-main_ui-vendor-dependancies" */
'@/post-login-main_ui-vendor-dependancies'
).catch( Global.importErrorHandler );
}