parent
6f01a18be2
commit
5b8bdac753
@ -0,0 +1,61 @@ |
||||
<div class="ng-modal-window columns-modal loading-indicator--location" |
||||
data-indicator-name="modal"> |
||||
<div class="ng-modal-inner" tabindex="0"> |
||||
<div class="modal-header"> |
||||
<a> |
||||
<i |
||||
class="icon-close" |
||||
(click)="closeMe()" |
||||
[attr.title]="text.closePopup"> |
||||
</i> |
||||
</a> |
||||
</div> |
||||
|
||||
<h3 [textContent]="text.columnsLabel"></h3> |
||||
|
||||
<div class="columns-modal-content select2-modal-content" |
||||
*ngIf="!impaired"> |
||||
|
||||
</div> |
||||
|
||||
<div |
||||
class="columns-modal-content select2-modal-content" |
||||
*ngIf="impaired"> |
||||
<label |
||||
for="selected_columns" |
||||
[textContent]="text.selectedColumns" |
||||
class="hidden-for-sighted"> |
||||
</label> |
||||
|
||||
<div *ngFor="let column of availableColumns; let first = first;"> |
||||
<label class="form--label-with-check-box" for="column-{{column.id}}"> |
||||
<div class="form--check-box-container"> |
||||
<input id="column-{{column.id}}" |
||||
type="checkbox" |
||||
title="{{ column.name }}" |
||||
[(ngModel)]="selectedColumnMap[column.id]" |
||||
(ngModelChange)="setSelectedColumn(column)" |
||||
focus="first" /> |
||||
</div> |
||||
{{column.name}} |
||||
</label> |
||||
</div> |
||||
</div> |
||||
<div *ngIf="eeShowBanners" class="ee-relation-columns-upsale"> |
||||
{{text.upsaleRelationColumns}} |
||||
<a href="https://www.openproject.org/enterprise-edition/?op_edtion=community-edition&op_referrer=wp-list-columns#relations" |
||||
target='blank' |
||||
[textContent]="text.upsaleRelationColumnsLink"></a> |
||||
</div> |
||||
<div> |
||||
<button class="button -highlight" |
||||
[textContent]="text.applyButton" |
||||
(click)="updateSelectedColumns()"> |
||||
</button> |
||||
<button class="button" |
||||
[textContent]="text.cancelButton" |
||||
(click)="closeMe()"> |
||||
</button> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -0,0 +1,120 @@ |
||||
import {Component, ElementRef, Inject, OnInit} from '@angular/core'; |
||||
import {I18nToken, OpModalLocalsToken} from 'core-app/angular4-transition-utils'; |
||||
import {OpModalLocalsMap} from 'core-components/op-modals/op-modal.types'; |
||||
import {QueryColumn} from 'core-components/wp-query/query-column'; |
||||
import {ConfigurationService} from 'core-components/common/config/configuration.service'; |
||||
import {WorkPackageTableColumnsService} from 'core-components/wp-fast-table/state/wp-table-columns.service'; |
||||
import {OpModalComponent} from 'core-components/op-modals/op-modal.component'; |
||||
|
||||
@Component({ |
||||
template: require('!!raw-loader!./columns-modal.component.html') |
||||
}) |
||||
export class ColumnsModalComponent extends OpModalComponent { |
||||
|
||||
/* Close on escape? */ |
||||
public closeOnEscape = false; |
||||
|
||||
/* Close on outside click */ |
||||
public closeOnOutsideClick = false; |
||||
|
||||
public $element:JQuery; |
||||
|
||||
public text = { |
||||
closePopup: this.I18n.t('js.close_popup_title'), |
||||
columnsLabel: this.I18n.t('js.label_columns'), |
||||
selectedColumns: this.I18n.t('js.description_selected_columns'), |
||||
multiSelectLabel: this.I18n.t('js.work_packages.label_column_multiselect'), |
||||
applyButton: this.I18n.t('js.modals.button_apply'), |
||||
cancelButton: this.I18n.t('js.modals.button_cancel'), |
||||
upsaleRelationColumns: this.I18n.t('js.modals.upsale_relation_columns'), |
||||
upsaleRelationColumnsLink: this.I18n.t('js.modals.upsale_relation_columns_link') |
||||
}; |
||||
|
||||
|
||||
public availableColumns = this.wpTableColumns.all; |
||||
public unusedColumns = this.wpTableColumns.unused; |
||||
public selectedColumns = angular.copy(this.wpTableColumns.getColumns()); |
||||
|
||||
public impaired = this.ConfigurationService.accessibilityModeEnabled(); |
||||
public selectedColumnMap:{ [id:string]: boolean } = {}; |
||||
|
||||
public eeShowBanners:boolean; |
||||
|
||||
|
||||
|
||||
// //hack to prevent dragging of close icons
|
||||
// $timeout(() => {
|
||||
// angular.element('.columns-modal-content .ui-select-match-close').on('dragstart', event => {
|
||||
// event.preventDefault();
|
||||
// });
|
||||
// });
|
||||
//
|
||||
// $scope.$on('uiSelectSort:change', (event:any, args:any) => {
|
||||
// vm.selectedColumns = args.array;
|
||||
// });
|
||||
|
||||
|
||||
constructor(@Inject(OpModalLocalsToken) public locals:OpModalLocalsMap, |
||||
@Inject(I18nToken) readonly I18n:op.I18n, |
||||
readonly wpTableColumns:WorkPackageTableColumnsService, |
||||
readonly ConfigurationService:ConfigurationService, |
||||
readonly elementRef:ElementRef) { |
||||
super(locals, elementRef); |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.$element = jQuery(this.elementRef.nativeElement); |
||||
this.impaired = true; // TODO
|
||||
this.eeShowBanners = angular.element('body').hasClass('ee-banners-visible'); |
||||
|
||||
if (this.impaired) { |
||||
this.selectedColumns.forEach((column:QueryColumn) => { |
||||
this.selectedColumnMap[column.id] = true; |
||||
}); |
||||
} |
||||
} |
||||
|
||||
public updateSelectedColumns() { |
||||
this.wpTableColumns.setColumns(this.selectedColumns); |
||||
this.service.close(); |
||||
} |
||||
|
||||
/** |
||||
* When a column is removed from the selection it becomes unused and hence available for |
||||
* selection again. When a column is added to the selection it becomes used and is |
||||
* therefore unavailable for selection. |
||||
* |
||||
* This function updates the unused columns according to the currently selected columns. |
||||
* |
||||
* @param selectedColumns Columns currently selected through the multi select box. |
||||
*/ |
||||
public updateUnusedColumns(selectedColumns:QueryColumn[]) { |
||||
this.unusedColumns = _.differenceBy(this.availableColumns, selectedColumns, '$href'); |
||||
} |
||||
|
||||
public setSelectedColumn(column:QueryColumn) { |
||||
if (this.selectedColumnMap[column.id]) { |
||||
this.selectedColumns.push(column); |
||||
} |
||||
else { |
||||
_.remove(this.selectedColumns, (c:QueryColumn) => c.id === column.id); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Called when the user attempts to close the modal window. |
||||
* The service will close this modal if this method returns true |
||||
* @returns {boolean} |
||||
*/ |
||||
public onClose():boolean { |
||||
this.afterFocusOn.focus(); |
||||
return true; |
||||
} |
||||
|
||||
public onOpen(modalElement:JQuery) { |
||||
} |
||||
|
||||
protected get afterFocusOn():JQuery { |
||||
return this.$element; |
||||
} |
||||
} |
@ -0,0 +1,45 @@ |
||||
import {ElementRef, OnInit} from '@angular/core'; |
||||
import {OpModalLocalsMap} from 'core-components/op-modals/op-modal.types'; |
||||
import {OpModalService} from 'core-components/op-modals/op-modal.service'; |
||||
|
||||
export abstract class OpModalComponent implements OnInit { |
||||
|
||||
/* Close on escape? */ |
||||
public closeOnEscape:boolean = true; |
||||
|
||||
/* Close on outside click */ |
||||
public closeOnOutsideClick:boolean = true; |
||||
|
||||
/* Reference to service */ |
||||
protected service:OpModalService = this.locals.service; |
||||
|
||||
public $element:JQuery; |
||||
|
||||
constructor(public locals:OpModalLocalsMap, readonly elementRef:ElementRef) { |
||||
} |
||||
|
||||
ngOnInit() { |
||||
this.$element = jQuery(this.elementRef.nativeElement); |
||||
} |
||||
|
||||
/** |
||||
* Called when the user attempts to close the modal window. |
||||
* The service will close this modal if this method returns true |
||||
* @returns {boolean} |
||||
*/ |
||||
public onClose():boolean { |
||||
this.afterFocusOn.focus(); |
||||
return true; |
||||
} |
||||
|
||||
public closeMe() { |
||||
this.service.close(); |
||||
} |
||||
|
||||
public onOpen(modalElement:JQuery) { |
||||
} |
||||
|
||||
protected get afterFocusOn():JQuery { |
||||
return this.$element; |
||||
} |
||||
} |
@ -0,0 +1,110 @@ |
||||
import { |
||||
ApplicationRef, |
||||
ComponentFactoryResolver, ComponentRef, |
||||
Inject, |
||||
Injectable, |
||||
Injector |
||||
} from '@angular/core'; |
||||
import {ComponentPortal, DomPortalOutlet, PortalInjector} from '@angular/cdk/portal'; |
||||
import {TransitionService} from '@uirouter/core'; |
||||
import {FocusHelperToken, OpModalLocalsToken} from 'core-app/angular4-transition-utils'; |
||||
import {OpModalComponent} from 'core-components/op-modals/op-modal.component'; |
||||
|
||||
@Injectable() |
||||
export class OpModalService { |
||||
public active:OpModalComponent|null = null; |
||||
|
||||
// Hold a reference to the DOM node we're using as a host
|
||||
private portalHostElement:HTMLElement; |
||||
// And a reference to the actual portal host interface on top of the element
|
||||
private bodyPortalHost:DomPortalOutlet; |
||||
|
||||
constructor(private componentFactoryResolver:ComponentFactoryResolver, |
||||
@Inject(FocusHelperToken) readonly FocusHelper:any, |
||||
private appRef:ApplicationRef, |
||||
private $transitions:TransitionService, |
||||
private injector:Injector) { |
||||
|
||||
const hostElement = this.portalHostElement = document.createElement('div'); |
||||
hostElement.classList.add('op-modals--overlay'); |
||||
document.body.appendChild(hostElement); |
||||
|
||||
// Listen to keyups on window to close context menus
|
||||
Mousetrap.bind('escape', () => { |
||||
if (this.active && this.active.closeOnEscape) { |
||||
this.close(); |
||||
} |
||||
}); |
||||
|
||||
// Listen to any click when should close outside modal
|
||||
jQuery(window).click((evt) => { |
||||
if (this.active && |
||||
this.active.closeOnOutsideClick && |
||||
!this.portalHostElement.contains(evt.target)) { |
||||
this.close(); |
||||
} |
||||
}); |
||||
|
||||
this.bodyPortalHost = new DomPortalOutlet( |
||||
hostElement, |
||||
this.componentFactoryResolver, |
||||
this.appRef, |
||||
this.injector |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Open a Modal reference and append it to the portal |
||||
*/ |
||||
public show(modal:any, locals:any = {}) { |
||||
this.close(); |
||||
|
||||
// Create a portal for the given component class and render it
|
||||
const portal = new ComponentPortal(modal, null, this.injectorFor(locals)); |
||||
const ref:ComponentRef<OpModalComponent> = this.bodyPortalHost.attach(portal) as ComponentRef<OpModalComponent>; |
||||
this.active = ref.instance; |
||||
this.portalHostElement.style.display = 'block'; |
||||
|
||||
setTimeout(() => { |
||||
// Focus on the first element
|
||||
this.active && this.active.onOpen(this.activeModal); |
||||
}); |
||||
} |
||||
|
||||
public isActive(modal:OpModalComponent) { |
||||
return this.active && this.active === modal; |
||||
} |
||||
|
||||
/** |
||||
* Closes currently open modal window |
||||
*/ |
||||
public close() { |
||||
// Detach any component currently in the portal
|
||||
if (this.active && this.active.onClose()) { |
||||
this.bodyPortalHost.detach(); |
||||
this.portalHostElement.style.display = 'none'; |
||||
this.active = null; |
||||
} |
||||
} |
||||
|
||||
public get activeModal():JQuery { |
||||
return jQuery(this.portalHostElement).find('.op-modal--container'); |
||||
} |
||||
|
||||
/** |
||||
* Create an augmented injector that is equal to this service's injector + the additional data |
||||
* passed into +show+. |
||||
* This allows callers to pass data into the newly created modal. |
||||
* |
||||
*/ |
||||
private injectorFor(data:any) { |
||||
const injectorTokens = new WeakMap(); |
||||
// Pass the service because otherwise we're getting a cyclic dependency between the portal
|
||||
// host service and the bound portal
|
||||
data.service = this; |
||||
|
||||
injectorTokens.set(OpModalLocalsToken, data); |
||||
|
||||
return new PortalInjector(this.injector, injectorTokens); |
||||
} |
||||
} |
@ -0,0 +1,7 @@ |
||||
import {OpModalService} from 'core-components/op-modals/op-modal.service'; |
||||
|
||||
export interface OpModalLocalsMap { |
||||
service:OpModalService; |
||||
[key:string]:any; |
||||
}; |
||||
|
Loading…
Reference in new issue