Refactor context menus using global service

pull/5117/head
Oliver Günther 8 years ago
parent f8a836671b
commit 74919789b6
No known key found for this signature in database
GPG Key ID: 88872239EB414F99
  1. 98
      frontend/app/components/context-menus/context-menu.service.ts
  2. 56
      frontend/app/components/context-menus/has-dropdown-menu/has-dropdown-menu-directive.js
  3. 39
      frontend/app/components/context-menus/wp-context-menu/wp-context-menu.controller.ts
  4. 48
      frontend/app/components/wp-fast-table/handlers/row/context-menu-handler.ts
  5. 6
      frontend/app/components/wp-fast-table/handlers/table-handler-registry.ts
  6. 19
      frontend/app/components/wp-fast-table/state/wp-table-selection.service.ts

@ -0,0 +1,98 @@
// -- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
interface ContextMenu {
active();
close();
open();
reposition();
}
export class ContextMenuService {
private _active_menu:ContextMenu|null;
constructor(public $window, public $injector, public $rootScope) {
"ngInject";
// Close context menus on state change
$rootScope.$on('$stateChangeStart', () => this.close());
// Listen to keyups on window to close context menus
Mousetrap.bind('escape', () => this.close());
// Listen to any click and close the active context menu
jQuery($window).click(() => this.close());
}
// Return the active context menu, if any
public get active():ContextMenu|null {
return this._active_menu;
}
public close() {
this.active && this.active.close();
}
public activate(contextMenuName, target:JQuery, locals) {
let contextMenu = this.$injector.get(contextMenuName);
// Close other context menu
this.close();
// Open the menu
contextMenu.open(target, locals).then((menuElement) => {
contextMenu.menuElement = menuElement;
this._active_menu = contextMenu;
this.reposition();
(menuElement as any).trap();
menuElement.on('click', function (e) {
// allow inputs to be clickable
// without closing the dropdown
if (angular.element(e.target).is(':input')) {
e.stopPropagation();
}
});
});
}
public reposition() {
if (!this.active) {
return;
}
let menuElement = this.active.menuElement;
let target = this.active.target;
menuElement.position({ my: 'center', at: 'bottom right', of: target });
}
}
angular
.module('openproject.services')
.service('contextMenu', ContextMenuService);

@ -27,63 +27,7 @@
// ++
function hasDropdownMenu($rootScope, $injector, $window, FocusHelper) {
function getCssPositionProperties(dropdown, trigger) {
var hOffset = 0,
vOffset = 0;
if (dropdown.hasClass('dropdown-anchor-top')) {
vOffset = -dropdown.outerHeight() - trigger.outerHeight() + parseInt(trigger.css('margin-top'), 10);
}
// Styling logic taken from jQuery-dropdown plugin: https://github.com/plapier/jquery-dropdown
// (dual MIT/GPL-Licensed)
// Position the dropdown relative-to-parent or relative-to-document
if (dropdown.hasClass('dropdown-relative')) {
return {
left: dropdown.hasClass('dropdown-anchor-right') ?
trigger.position().left -
(dropdown.outerWidth(true) - trigger.outerWidth(true)) -
parseInt(trigger.css('margin-right'), 10) + hOffset :
trigger.position().left + parseInt(trigger.css('margin-left'), 10) + hOffset,
top: trigger.position().top +
trigger.outerHeight(true) -
parseInt(trigger.css('margin-top'), 10) + vOffset
};
}
else {
return {
left: dropdown.hasClass('dropdown-anchor-right') ?
trigger.offset().left - (dropdown.outerWidth() - trigger.outerWidth()) + hOffset : trigger.offset().left + hOffset,
top: trigger.offset().top + trigger.outerHeight() + vOffset
};
}
}
function getPositionPropertiesOfEvent(event) {
var position = {};
if (event.pageX && event.pageY) {
position.top = Math.max(event.pageY, 0);
position.left = Math.max(event.pageX, 0);
}
else {
var bounding = angular.element(event.target)[0].getBoundingClientRect();
position.top = Math.max(bounding.bottom, 0);
position.left = Math.max(bounding.left, 0);
}
return position;
}
function getCssPositionPropertiesOfEvent(event) {
var position = getPositionPropertiesOfEvent(event);
position.top += 'px';
position.left += 'px';
return position;
}
return {
restrict: 'A',

@ -26,28 +26,25 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {WorkPackageTableSelection} from '../../wp-fast-table/state/wp-table-selection.service';
function wpContextMenuController($scope,
$rootScope,
$state,
WorkPackagesTableHelper,
WorkPackageContextMenuHelper,
WorkPackageService,
WorkPackagesTableService,
wpEditModeState,
I18n,
$window,
wpTableSelection:WorkPackageTableSelection,
PERMITTED_CONTEXT_MENU_ACTIONS) {
$scope.I18n = I18n;
$scope.$watch('row', function () {
if (!$scope.row.checked) {
WorkPackagesTableService.setCheckedStateForAllRows($scope.rows, false);
}
if (!wpTableSelection.isSelected($scope.row.object.id)) {
wpTableSelection.setSelection($scope.row);
}
$scope.row.checked = true;
$scope.permittedActions = WorkPackageContextMenuHelper.getPermittedActions(getSelectedWorkPackages(), PERMITTED_CONTEXT_MENU_ACTIONS);
});
$scope.permittedActions = WorkPackageContextMenuHelper.getPermittedActions(getSelectedWorkPackages(), PERMITTED_CONTEXT_MENU_ACTIONS);
$scope.isDetailsViewLinkPresent = function () {
return !!angular.element('#work-package-context-menu li.open').length;
@ -81,7 +78,7 @@ function wpContextMenuController($scope,
}
function deleteSelectedWorkPackages() {
var ids = getSelectedWorkPackages().map(wp => wp.id);
let ids = wpTableSelection.getSelectedWorkPackageIds();
WorkPackageService.performBulkDelete(ids, true);
}
@ -101,25 +98,19 @@ function wpContextMenuController($scope,
$state.transitionTo('work-packages.show.edit', params);
}
function getWorkPackagesFromSelectedRows() {
var selectedRows = WorkPackagesTableHelper.getSelectedRows($scope.rows);
return WorkPackagesTableHelper.getWorkPackagesFromRows(selectedRows);
}
function getSelectedWorkPackages() {
var workPackagefromContext = $scope.row.object;
var workPackagesfromSelectedRows = getWorkPackagesFromSelectedRows();
let workPackagefromContext = $scope.row.object;
let selectedWorkPackages = wpTableSelection.getSelectedWorkPackages();
if (workPackagesfromSelectedRows.length === 0) {
if (selectedWorkPackages.length === 0) {
return [workPackagefromContext];
}
else if (workPackagesfromSelectedRows.indexOf(workPackagefromContext) === -1) {
return [workPackagefromContext].concat(workPackagesfromSelectedRows);
}
else {
return workPackagesfromSelectedRows;
if (selectedWorkPackages.indexOf(workPackagefromContext) === -1) {
selectedWorkPackages.push(workPackagefromContext);
}
return selectedWorkPackages;
}
}

@ -0,0 +1,48 @@
import {injectorBridge} from '../../../angular/angular-injector-bridge.functions';
import {WorkPackageTable} from '../../wp-fast-table';
import {States} from '../../../states.service';
import {TableEventHandler} from '../table-handler-registry';
import {WorkPackageTableSelection} from '../../state/wp-table-selection.service';
import {rowClassName} from '../../builders/single-row-builder';
import {tdClassName} from '../../builders/cell-builder';
import {uiStateLinkClass} from '../../builders/ui-state-link-builder';
import {ContextMenuService} from '../../../context-menus/context-menu.service';
export class ContextMenuHandler implements TableEventHandler {
// Injections
public contextMenu:ContextMenuService;
constructor() {
injectorBridge(this);
}
public get EVENT() {
return 'contextmenu.table.rightclick';
}
public get SELECTOR() {
return `.${rowClassName}`;
}
public handleEvent(table: WorkPackageTable, evt:JQueryEventObject) {
let target = jQuery(evt.target);
// We want to keep the original context menu on hrefs
// (currently, this is only the id
if (target.closest(`.${uiStateLinkClass}`).length) {
console.log('Allowing original context menu on state link');
return;
}
evt.preventDefault();
evt.stopPropagation();
// Locate the row from event
let element = target.closest(this.SELECTOR);
let row = table.rowObject(element.data('workPackageId'));
this.contextMenu.activate('WorkPackageContextMenu', target, { row: row });
return false;
}
}
ContextMenuHandler.$inject = ['contextMenu'];

@ -6,6 +6,7 @@ import {SelectionTransformer} from './state/selection-transformer';
import {RowsTransformer} from './state/rows-transformer';
import {ColumnsTransformer} from './state/columns-transformer';
import {GroupRowHandler} from './row/group-row-handler';
import {ContextMenuHandler} from './row/context-menu-handler';
export interface TableEventHandler {
EVENT:string;
@ -21,7 +22,10 @@ export class TableHandlerRegistry {
WorkPackageStateLinksHandler,
// Clicking on the row (not within a cell)
RowClickHandler,
GroupRowHandler
// Clicking on group headers
GroupRowHandler,
// Right clicking on rows
ContextMenuHandler,
];
static stateTransformers = [

@ -33,6 +33,25 @@ export class WorkPackageTableSelection {
this.selectionState.put(state);
}
/**
* Get the current work package resource form the selection state.
*/
public getSelectedWorkPackages():WorkPackageResource[] {
let wpState = this.states.workPackages;
return this.getSelectedWorkPackageIds().map(id => wpState.get(id).getCurrentValue());
}
public getSelectedWorkPackageIds():string[] {
let selected:string[] = [];
_.each(this.currentState.selected, (isSelected:boolean, wpId:string) => {
if (isSelected) {
selected.push(wpId);
}
});
return selected;
}
/**
* Reset the selection state to an empty selection

Loading…
Cancel
Save