282 lines
17 KiB
Vue
282 lines
17 KiB
Vue
|
<template>
|
||
|
<div class="context-menu-bar">
|
||
|
<div class="left-container">
|
||
|
<TTContextButton :id="'context-group-'+item.items[0].id" :class="[dynamicClasses.nowrap, item.action_group_id]" v-for="item in alignItems.left" :items="item.items" />
|
||
|
</div>
|
||
|
<div class="center-container">
|
||
|
<TTContextButton :id="'context-group-'+item.items[0].id" :class="[dynamicClasses.nowrap, item.action_group_id]" v-for="item in alignItems.center" :items="item.items" />
|
||
|
</div>
|
||
|
<div class="right-container">
|
||
|
<TTContextButton :id="'context-group-'+item.items[0].id" :class="[dynamicClasses.nowrap, item.action_group_id]" v-for="item in alignItems.right" :items="item.items" />
|
||
|
</div>
|
||
|
</div>
|
||
|
</template>
|
||
|
|
||
|
<script>
|
||
|
import TTContextButton from '@/components/context_menu/TTContextButton';
|
||
|
// Note: No longer need to import the context menu manager, as we only respond to events generated from it.
|
||
|
|
||
|
export default {
|
||
|
name: "TTContextMenu",
|
||
|
created() {
|
||
|
this.event_bus = new TTEventBus( {
|
||
|
component_id: this.component_id + this.menu_id, //Context menu is unique to each view and is removed when the view is closed.
|
||
|
} );
|
||
|
// Debug.Text( 'Context menu created.', 'TTContextMenu.vue', 'TTContextMenu', 'created', 10 );
|
||
|
|
||
|
/*
|
||
|
* Init event listeners with EventBus to handle data coming from outside the component/Vue.
|
||
|
*/
|
||
|
|
||
|
/* -------- Event Listener for MENU updates -------- */
|
||
|
let onUpdateMenu = function ( event_data ) {
|
||
|
// If the view id changes when the menu is re-built, update current view id. Only menu update should change active view id.
|
||
|
// if( this.active_view_id !== event_data.view_id ) {
|
||
|
// // Debug.Highlight('Active view for context menu updated to: '+event_data.view_id+', previous was: '+ this.active_view_id);
|
||
|
// this.active_view_id = event_data.view_id;
|
||
|
// }
|
||
|
|
||
|
if( this.menu_id === event_data.menu_id ) {
|
||
|
// Debug.Highlight('Context menu update received for: '+ event_data.menu_id, event_data );
|
||
|
Debug.Text( 'Contextmenu\n MENU update RECEIVED for: '+ event_data.menu_id, 'TTContextMenu.vue', 'TTContextMenu', 'created:EventBus:update_context_menu', 11 );
|
||
|
this.rebuildMenu( event_data.menu_model );
|
||
|
} else {
|
||
|
Debug.Warn( 'Error: Context Menu update does not match menu id.\nThis menu: '+ this.menu_id +'\nEvent menu: '+event_data.menu_id, 'TTContextMenu.vue', 'TTContextMenu', 'created:EventBus:update_context_menu', 11 );
|
||
|
}
|
||
|
}.bind(this); // Must bind to this at variable definition, not in the EventBus on/off as each .bind creates a new function reference, so it wont be able to match up on delete.
|
||
|
|
||
|
// EventBus.on( this.viewId + '.updateContextMenu', ( event_data ) => {
|
||
|
this.event_bus.on( this.component_id, 'update_context_menu', onUpdateMenu, TTEventBusStatics.AUTO_CLEAR_ON_EXIT );
|
||
|
|
||
|
/* -------- Event Listener for ITEM updates -------- */
|
||
|
let onUpdateItem = function( event_data ) {
|
||
|
if( this.menu_id === event_data.menu_id ) {
|
||
|
Debug.Text( 'Contextmenu\n ITEM update RECEIVED for: '+ event_data.menu_id + ':' + event_data.item_id, 'TTContextMenu.vue', 'TTContextMenu', 'created:EventBus:update_item', 11 );
|
||
|
this.updateMenuItem( event_data.item_id, event_data.item_attributes );
|
||
|
} else {
|
||
|
// If the view id does not match active view, then ignore the update, as its probably the previous view.
|
||
|
// Must ignore update if no match, otherwise item updates from old and new view will conflict and context menu items may then not display as expected. (BugFix: TimeSheet->JumpTo->AddRequest->Cancel. Visible icons not as expected. Cancel icon not disabled).
|
||
|
Debug.Warn( 'Error: Context Menu Item update does not match menu id.\nThis menu: '+ this.menu_id +'\nEvent menu: '+event_data.menu_id, 'TTContextMenu.vue', 'TTContextMenu', 'created:EventBus:updateItem', 11 );
|
||
|
}
|
||
|
}.bind(this); // Must bind to this at variable definition, not in the EventBus on/off as each .bind creates a new function reference, so it wont be able to match up on delete.
|
||
|
|
||
|
|
||
|
// EventBus.on( this.viewId + '.updateItem', ( event_data ) => {
|
||
|
this.event_bus.on( this.component_id, 'update_item', onUpdateItem, TTEventBusStatics.AUTO_CLEAR_ON_EXIT );
|
||
|
|
||
|
/* -------- Event Listener for MULTISELECTITEM updates -------- */
|
||
|
let onActivateMultiSelectItem = function( event_data ) {
|
||
|
if( this.menu_id === event_data.menu_id ) {
|
||
|
Debug.Text( 'Contextmenu\n MULTI_ITEM update RECEIVED for: '+ event_data.menu_id + ':' + event_data.item_id, 'TTContextMenu.vue', 'TTContextMenu', 'created:EventBus:activate_multi_select_item', 11 );
|
||
|
this.activateMultiSelectItem( event_data.item_id );
|
||
|
} else {
|
||
|
// If the view id does not match active view, then ignore the update, as its probably the previous view.
|
||
|
// Must ignore update if no match, otherwise item updates from old and new view will conflict and context menu items may then not display as expected. (BugFix: TimeSheet->JumpTo->AddRequest->Cancel. Visible icons not as expected. Cancel icon not disabled).
|
||
|
Debug.Warn( 'Error: Context Menu Multi-select Item activation does not match menu id.\nThis menu: '+ this.menu_id +'\n Event menu: '+event_data.menu_id+' )', 'TTContextMenu.vue', 'TTContextMenu', 'created:EventBus:activate_multi_select_item', 11 );
|
||
|
}
|
||
|
}.bind(this); // Must bind to this at variable definition, not in the EventBus on/off as each .bind creates a new function reference, so it wont be able to match up on delete.
|
||
|
|
||
|
this.event_bus.on( this.component_id, 'activate_multi_select_item', onActivateMultiSelectItem, TTEventBusStatics.AUTO_CLEAR_ON_EXIT );
|
||
|
|
||
|
/* -------- Event Listener for SPLITBUTTON updates -------- */
|
||
|
let onActivateSplitButtonItem = function( event_data ) {
|
||
|
if ( this.menu_id === event_data.menu_id ) {
|
||
|
Debug.Text( 'Contextmenu\n SPLIT BUTTON update RECEIVED for: ' + event_data.menu_id + ':' + event_data.item_id, 'TTContextMenu.vue', 'TTContextMenu', 'created:EventBus:activate_split_button_item', 11 );
|
||
|
this.activateSplitButtonItem( event_data.item_id );
|
||
|
} else {
|
||
|
// If the view id does not match active view, then ignore the update, as its probably the previous view.
|
||
|
Debug.Warn( 'Error: Context Menu Split Button Item activation does not match menu id.\nThis menu: ' + this.menu_id + '\n Event menu: ' + event_data.menu_id + ' )', 'TTContextMenu.vue', 'TTContextMenu', 'created:EventBus:activate_split_button_item', 11 );
|
||
|
}
|
||
|
}.bind( this ); // Must bind to this at variable definition, not in the EventBus on/off as each .bind creates a new function reference, so it wont be able to match up on delete.
|
||
|
|
||
|
this.event_bus.on( this.component_id, 'activate_split_button_item', onActivateSplitButtonItem,TTEventBusStatics.AUTO_CLEAR_ON_EXIT );
|
||
|
|
||
|
let onFreezeSplitButtonActiveItem = function( event_data ) {
|
||
|
if ( this.menu_id === event_data.menu_id ) {
|
||
|
Debug.Text( 'Contextmenu\n SPLIT BUTTON update RECEIVED for: ' + event_data.menu_id + ':' + event_data.item_id, 'TTContextMenu.vue', 'TTContextMenu', 'created:EventBus:activate_split_button_item', 11 );
|
||
|
this.freezeSplitButtonActiveItem( event_data.item_id );
|
||
|
} else {
|
||
|
// If the view id does not match active view, then ignore the update, as its probably the previous view.
|
||
|
Debug.Warn( 'Error: Context Menu Split Button Item activation does not match menu id.\nThis menu: ' + this.menu_id + '\n Event menu: ' + event_data.menu_id + ' )', 'TTContextMenu.vue', 'TTContextMenu', 'created:EventBus:activate_split_button_item', 11 );
|
||
|
}
|
||
|
}.bind( this ); // Must bind to this at variable definition, not in the EventBus on/off as each .bind creates a new function reference, so it wont be able to match up on delete.
|
||
|
|
||
|
this.event_bus.on( this.component_id, 'freeze_split_button_active_item', onFreezeSplitButtonActiveItem, TTEventBusStatics.AUTO_CLEAR_ON_EXIT );
|
||
|
},
|
||
|
unmounted() {
|
||
|
Debug.Text( 'Vue context menu component unmounted ('+ this.menu_id +').', 'TTContextMenu.vue', 'TTContextMenu', 'unmounted', 10 );
|
||
|
this.event_bus.autoClear();
|
||
|
},
|
||
|
props: {
|
||
|
menu_id: String
|
||
|
},
|
||
|
data() {
|
||
|
return {
|
||
|
component_id: 'context_menu',
|
||
|
built_menu: [],
|
||
|
removeEventsOnUnmount: [],
|
||
|
nowrap_views: [ 'TimeSheet' ], // TimeSheet is to handle the New Punch label wrapping. This is here to test if this will work globally, nowrap might cause issues.
|
||
|
}
|
||
|
},
|
||
|
computed: {
|
||
|
alignItems() {
|
||
|
let filter_left = ( element ) => element.menu_align === 'left';
|
||
|
let filter_center = ( element ) => element.menu_align === 'center';
|
||
|
let filter_right = ( element ) => element.menu_align === 'right';
|
||
|
|
||
|
return {
|
||
|
left: this.built_menu.filter( filter_left ),
|
||
|
center: this.built_menu.filter( filter_center ),
|
||
|
right: this.built_menu.filter( filter_right )
|
||
|
};
|
||
|
},
|
||
|
dynamicClasses() {
|
||
|
return {
|
||
|
// 'nowrap': this.nowrap_views.includes( this.viewId ) ? 'no-wrap' : '', // If this returns true, this prevents the text label of a nav button wrapping to a new-line, affecting the button heights for the whole menu. Care needs to be taken if not all buttons will fit with no-wrap.
|
||
|
nowrap: 'no-wrap', // Simplifing this while we no longer pass in viewId. If this feature is needed, then we need to get viewId another way.
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
methods: {
|
||
|
rebuildMenu( menu_model ) {
|
||
|
this.built_menu.length = 0; // TODO: Should not need to do this once TTContextMenu has an independant instance for each view. At the moment, all views are one Vue View (LegacyView).
|
||
|
this.built_menu.push(...menu_model ); // TODO: Need to handle the eventuality where icons array needs to be cleared first without losing the JS reference to the array.
|
||
|
},
|
||
|
/**
|
||
|
* Update several item params in one go.
|
||
|
* This will also be useful for event driven updates, reduces the need to have an event function for each action.
|
||
|
* @param {string} item_id - ID of the item to update
|
||
|
* @param {Object} new_item_attributes - The new data as attributes in an object.
|
||
|
* @returns {Object}
|
||
|
*/
|
||
|
updateMenuItem( item_id, new_item_attributes ) {
|
||
|
var item = this.getMenuItemById( item_id );
|
||
|
if( item === undefined ) {
|
||
|
// If this happens, it might be that the Vue menu has cleared already, and the menu is trying to set states on old icons no longer present. Might be a legacy->Vue issue on a view change. But the vue menu items should always match the legacy menu on the view controller. Trace the icons and try and fix the disconnect.
|
||
|
Debug.Warn( 'Menu item not found ('+ item_id +') unable to update with: ' + JSON.stringify( new_item_attributes ), 'TTContextMenu.vue', 'TTContextMenu', 'updateMenuItem', 1 );
|
||
|
return false;
|
||
|
}
|
||
|
return Object.assign( item, new_item_attributes);
|
||
|
},
|
||
|
activateMultiSelectItem( item_id ) {
|
||
|
var item = this.getMenuItemById( item_id );
|
||
|
if( item === undefined ) {
|
||
|
// If this happens, it might be that the Vue menu has cleared already, and the menu is trying to set states on old icons no longer present. Might be a legacy->Vue issue on a view change. But the vue menu items should always match the legacy menu on the view controller. Trace the icons and try and fix the disconnect.
|
||
|
Debug.Warn( 'Menu item not found ('+ item_id +') unable to update with: ' + JSON.stringify( new_item_attributes ), 'TTContextMenu.vue', 'TTContextMenu', 'updateMenuItem', 1 );
|
||
|
return false;
|
||
|
}
|
||
|
if( item.setOnlySelfActive === undefined ) {
|
||
|
item.default_active_item = true;
|
||
|
return 1;
|
||
|
} else {
|
||
|
item.setOnlySelfActive();
|
||
|
return 2;
|
||
|
}
|
||
|
},
|
||
|
activateSplitButtonItem( item_id ) {
|
||
|
for ( let i = 0; i < this.built_menu.length; i++ ) {
|
||
|
//Only modify the action group that contains the item_id we want. This is to avoid affecting any other action groups active item.
|
||
|
if ( this.built_menu[i].items.some(item => item.id === item_id ) ) {
|
||
|
for ( let j = 0; j < this.built_menu[i].items.length; j++ ) {
|
||
|
if ( this.built_menu[i].items[j].id === item_id ) {
|
||
|
this.built_menu[i].items[j].menu_force_active = true;
|
||
|
} else {
|
||
|
this.built_menu[i].items[j].menu_force_active = false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
freezeSplitButtonActiveItem( item_id ) {
|
||
|
var item = this.getMenuItemById( item_id );
|
||
|
if ( item === undefined ) {
|
||
|
// If this happens, it might be that the Vue menu has cleared already, and the menu is trying to set states on old icons no longer present. Might be a legacy->Vue issue on a view change. But the vue menu items should always match the legacy menu on the view controller. Trace the icons and try and fix the disconnect.
|
||
|
Debug.Warn( 'Menu item not found (' + item_id + ') unable to update', 'TTContextMenu.vue', 'TTContextMenu', 'setSplitButtonTemporarilyIgnoreDisabled', 1 );
|
||
|
return false;
|
||
|
}
|
||
|
item.freeze_active_item = true;
|
||
|
},
|
||
|
enableMenuItem( item_id ) {
|
||
|
var item = this.getMenuItemById( item_id );
|
||
|
item.disabled = false;
|
||
|
},
|
||
|
disableMenuItem( item_id ) {
|
||
|
var item = this.getMenuItemById( item_id );
|
||
|
item.disabled = true;
|
||
|
},
|
||
|
hideMenuItem( item_id ) {
|
||
|
var item = this.getMenuItemById( item_id );
|
||
|
item.visible = false;
|
||
|
|
||
|
},
|
||
|
showMenuItem( item_id ) {
|
||
|
var item = this.getMenuItemById( item_id );
|
||
|
item.visible = true;
|
||
|
|
||
|
},
|
||
|
getMenuItemById( item_id ) {
|
||
|
// TODO: Have I already done a similar function elsewhere? It seems familiar, but with ES6 array functions instead of for loops?
|
||
|
// Check the context menu class as well as the left menu class. Put in Global if found.
|
||
|
|
||
|
var result;
|
||
|
function recursiveFind( haystack_array, needle_id ) {
|
||
|
return haystack_array.find( element => {
|
||
|
if ( Array.isArray( element ) ) {
|
||
|
return recursiveFind( element, needle_id );
|
||
|
} else {
|
||
|
if( element.id === undefined && Array.isArray( element.items ) ) {
|
||
|
return recursiveFind( element.items, needle_id );
|
||
|
} else if( element.id === needle_id ) {
|
||
|
result = element;
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
} );
|
||
|
}
|
||
|
recursiveFind( this.built_menu, item_id );
|
||
|
|
||
|
return result;
|
||
|
}
|
||
|
},
|
||
|
components: { TTContextButton }
|
||
|
};
|
||
|
</script>
|
||
|
|
||
|
<style>
|
||
|
/* Hide icons from dropdown menus of context buttons and not the buttons themselves. */
|
||
|
.p-menuitem-link .p-menuitem-icon {
|
||
|
display: none;
|
||
|
}
|
||
|
/* Icons can be displayed in dropdowns by using the class "tticon-show-in-dropdown"
|
||
|
for example: Multi-select (radio and check boxes) still need to be displayed.
|
||
|
*/
|
||
|
.p-menuitem-link .tticon-show-in-dropdown {
|
||
|
display: inline;
|
||
|
}
|
||
|
</style>
|
||
|
|
||
|
|
||
|
<style scoped>
|
||
|
.context-menu-bar {
|
||
|
display: flex;
|
||
|
justify-content: space-between;
|
||
|
/*background: #e9ecef; !* Chose #e9ecef, previously #ced4da but looked too dark once the rest of the page background is white/off-white. *!*/
|
||
|
margin-bottom: 10px;
|
||
|
padding-bottom: 5px; /* To balance out the top padding applied from .context-border padding. Trying 5px instead of 10px to balance out closeness of the context-border heading to the menu, but now top and bottom not equal but might be ok. */
|
||
|
border-bottom: 1px solid #dbdee1;
|
||
|
}
|
||
|
.left-container,
|
||
|
.center-container,
|
||
|
.right-container {
|
||
|
display: flex;
|
||
|
justify-content: flex-start;
|
||
|
padding: 4px; /* To match figma design */
|
||
|
}
|
||
|
.no-wrap {
|
||
|
white-space: nowrap;
|
||
|
}
|
||
|
.p-menuitem-icon {
|
||
|
display: none;
|
||
|
}
|
||
|
</style>
|