WIP - New context menu impl

This commit is contained in:
Matthew Ross 2018-06-30 08:33:22 -04:00
parent c58e54b176
commit 044b72c820
20 changed files with 1363 additions and 463 deletions

View File

@ -20,5 +20,5 @@ after_success:
- touch fakeconfig
- echo "OAUTH_ACCESS_TOKEN=$OAUTH_ACCESS_TOKEN" > fakeconfig
- ./dropbox_uploader.sh -f fakeconfig upload coverage/api coverage-$(php version.php)/
- ./dropbox_uploader.sh -f fakeconfig upload coverage/app coverage-$(php version.php)/
- ./dropbox_uploader.sh -f fakeconfig upload coverage/app/lcov-report coverage-$(php version.php)/

View File

@ -155,11 +155,11 @@ Because I like seeing the numbers.
Language | Files | Blank | Comment | Code
-------------|--------:|---------:|--------:|---------:
TypeScript | 64 | 925 | 86 | 4091
TypeScript | 63 | 904 | 78 | 3932
PHP | 19 | 624 | 27 | 1997
HTML | 19 | 152 | 1 | 1422
SASS | 14 | 269 | 12 | 1215
__SUM:__ | __116__ | __1970__ | __126__ | __8725__
HTML | 20 | 159 | 0 | 1479
SASS | 14 | 269 | 12 | 1211
__SUM:__ | __116__ | __1956__ | __117__ | __8619__
Command: `cloc --exclude-dir=vendor --exclude-ext=json src/`

1269
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -40,15 +40,15 @@
"postinstall": "cd src/api/ && composer update && composer install --optimize-autoloader && cd ../../"
},
"dependencies": {
"@angular/animations": "^6.0.4",
"@angular/common": "^6.0.4",
"@angular/compiler": "^6.0.4",
"@angular/core": "^6.0.4",
"@angular/forms": "^6.0.4",
"@angular/http": "^6.0.4",
"@angular/platform-browser": "^6.0.4",
"@angular/platform-browser-dynamic": "^6.0.4",
"@angular/router": "^6.0.4",
"@angular/animations": "^6.0.7",
"@angular/common": "^6.0.7",
"@angular/compiler": "^6.0.7",
"@angular/core": "^6.0.7",
"@angular/forms": "^6.0.7",
"@angular/http": "^6.0.7",
"@angular/platform-browser": "^6.0.7",
"@angular/platform-browser-dynamic": "^6.0.7",
"@angular/router": "^6.0.7",
"chartist": "^0.11.0",
"chartist-plugin-tooltips": "^0.0.17",
"classlist.js": "^1.1.20150312",
@ -58,25 +58,25 @@
"marked": "^0.4.0",
"ng2-dragula": "^1.5.0",
"node-sass": "^4.9.0",
"rxjs": "^6.2.0",
"rxjs": "^6.2.1",
"scss-base": "^1.4.0",
"zone.js": "^0.8.26"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.6.5",
"@angular-devkit/build-angular": "~0.6.8",
"@angular/cli": "^6.0.8",
"@angular/compiler-cli": "^6.0.4",
"@angular/language-service": "^6.0.4",
"@types/jasmine": "~2.8.7",
"@angular/compiler-cli": "^6.0.7",
"@angular/language-service": "^6.0.7",
"@types/jasmine": "~2.8.8",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~10.1.4",
"bourbon": "5.0.0",
"@types/node": "~10.5.0",
"bourbon": "5.0.1",
"bourbon-neat": "1.9.0",
"codelyzer": "^4.3.0",
"codelyzer": "^4.4.2",
"jasmine": "^3.1.0",
"jasmine-core": "^3.1.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~2.0.2",
"karma": "~2.0.4",
"karma-chrome-launcher": "~2.2.0",
"karma-coverage-istanbul-reporter": "^2.0.1",
"karma-jasmine": "~1.1.2",
@ -85,7 +85,7 @@
"nodemon": "^1.17.5",
"npm-run-all": "^4.1.3",
"protractor": "~5.3.2",
"ts-node": "~6.0.5",
"ts-node": "~7.0.0",
"tslint": "~5.10.0",
"typescript": "^2.7.2"
}

10
src/api/composer.lock generated
View File

@ -258,16 +258,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.8.0",
"version": "1.8.1",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "478465659fd987669df0bd8a9bf22a8710e5f1b6"
"reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/478465659fd987669df0bd8a9bf22a8710e5f1b6",
"reference": "478465659fd987669df0bd8a9bf22a8710e5f1b6",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
"reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
"shasum": ""
},
"require": {
@ -302,7 +302,7 @@
"object",
"object graph"
],
"time": "2018-05-29T17:25:09+00:00"
"time": "2018-06-11T23:09:50+00:00"
},
{
"name": "myclabs/php-enum",

View File

@ -26,7 +26,7 @@ export class ApiInterceptor implements HttpInterceptor {
const headers = {
'Content-Type': 'application/json'
};
const token = localStorage.getItem(this.JWT_KEY);
const token = sessionStorage.getItem(this.JWT_KEY);
if (token !== null) {
headers['Authorization'] = token;
@ -45,14 +45,14 @@ export class ApiInterceptor implements HttpInterceptor {
if ((evt.status === 401 || evt.status === 400) &&
(evt.url + '').indexOf('login') === -1) {
localStorage.removeItem(this.JWT_KEY);
sessionStorage.removeItem(this.JWT_KEY);
this.router.navigate(['']);
return;
}
const response: ApiResponse = evt.body;
if (response.data) {
localStorage.setItem(this.JWT_KEY, response.data[0]);
sessionStorage.setItem(this.JWT_KEY, response.data[0]);
}
})
);

View File

@ -76,12 +76,7 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
this.pageName = this.strings.boards;
sub = this.boardService.getBoards().subscribe((response: ApiResponse) => {
this.boards = [];
this.updateBoardsList(response.data[1]);
this.loading = false;
});
this.subs.push(sub);
this.updateBoards();
sub = boardService.activeBoardChanged.subscribe((board: Board) => {
if (!board) {
@ -137,7 +132,14 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
}
});
this.dragula.dropModel.subscribe((value: any) => {
let makeCall = false;
this.dragula.dropModel.subscribe(async (value: any) => {
makeCall = !makeCall;
if (!makeCall) {
return;
}
let taskId = +value[1].id,
toColumnId = +value[2].parentNode.id,
fromColumnId = +value[3].parentNode.id;
@ -146,6 +148,8 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
fromColumnId = -1;
}
let updateList = [];
this.activeBoard.columns.forEach(column => {
if (column.id === toColumnId || column.id === fromColumnId) {
let position = 1,
@ -158,10 +162,17 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
position++;
});
let oneOff = this.boardService.updateColumn(column).subscribe();
oneOff.unsubscribe();
updateList.push(column);
}
});
const update = async (column) => {
this.boardService.updateColumn(column).subscribe();
}
for (let i = 0, len = updateList.length; i < len; ++i) {
await update(updateList[i]);
}
});
}
@ -239,6 +250,13 @@ export class BoardDisplay implements OnInit, OnDestroy, AfterContentInit {
return false;
}
private updateBoards(): void {
this.boardService.getBoards().subscribe((response: ApiResponse) => {
this.boards = [];
this.updateBoardsList(response.data[1]);
});
}
private updateBoardsList(boards: Array<any>): void {
let activeBoards: Array<Board> = [];

View File

@ -1,6 +1,8 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import * as marked from 'marked';
import * as hljs from 'highlight.js';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { map, catchError } from 'rxjs/operators';
@ -13,13 +15,42 @@ import {
User
} from '../shared/models';
interface MarkedReturn {
html: string;
counts: any;
}
@Injectable()
export class BoardService {
private checkCounts = {
total: 0,
complete: 0
}
private activeBoard = new BehaviorSubject<Board>(null);
private defaultCallback = (err: any, text: string) => {
console.log('default', err, text);
return text;
};
public activeBoardChanged = this.activeBoard.asObservable();
constructor(private http: HttpClient) {
this.initMarked();
}
convertMarkdown(markdown: string, callback = this.defaultCallback, doCount = false): MarkedReturn {
this.checkCounts.total = 0;
this.checkCounts.complete = 0;
let retVal: MarkedReturn = { html: '', counts: {} };
retVal.html = marked(markdown, callback);
if (doCount) {
retVal.counts = this.checkCounts;
}
return retVal;
}
updateActiveBoard(board: Board): void {
@ -124,5 +155,42 @@ export class BoardService {
boardData.ownIssuetracker,
boardData.sharedUser);
}
private initMarked(): void {
let renderer = new marked.Renderer();
renderer.checkbox = isChecked => {
let text = '<i class="icon icon-check' + (isChecked ? '' : '-empty') + '"></i>';
this.checkCounts.total += 1;
if (isChecked) {
this.checkCounts.complete += 1;
}
return text;
};
renderer.link = (href, title, text) => {
let out = '<a href="' + href + '"';
if (title) {
out += ' title="' + title + '"';
}
out += ' target="tb_external" rel="noreferrer">' + text + '</a>';
return out;
};
marked.setOptions({
renderer,
smartypants: true,
highlight: code => {
return hljs.highlightAuto(code).value;
}
});
}
}

View File

@ -87,7 +87,9 @@
</div>
<tb-context-menu>
<tb-context-menu-item>Column Test</tb-context-menu-item>
<tb-context-menu-item (click)="showModal()">
{{ strings['boards_addTask'] }}
</tb-context-menu-item>
</tb-context-menu>
<tb-modal *ngIf="activeBoard && columnData"
@ -108,7 +110,7 @@
</div>
<div class="description" *ngIf="viewModalProps.description.length"
[innerHtml]="getTaskDescription()">
[innerHtml]="viewModalProps.html">
</div>
<div class="stats">

View File

@ -9,9 +9,6 @@ import {
} from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import * as marked from 'marked';
import * as hljs from 'highlight.js';
import {
ApiResponse,
ActivitySimple,
@ -74,7 +71,8 @@ export class ColumnDisplay implements OnInit, OnDestroy {
@Input('column') columnData: Column;
@Input('boards') boards: Array<Board>;
@Output('on-update-boards') onUpdateBoards: EventEmitter<any> = new EventEmitter<any>();
@Output('on-update-boards')
onUpdateBoards: EventEmitter<any> = new EventEmitter<any>();
constructor(private elRef: ElementRef,
private auth: AuthService,
@ -99,11 +97,6 @@ export class ColumnDisplay implements OnInit, OnDestroy {
let sub = stringsService.stringsChanged.subscribe(newStrings => {
this.strings = newStrings;
// this.contextMenuItems = [
// new ContextMenuItem(this.strings.boards_addTask,
// this.getShowModalFunction())
// ];
});
this.subs.push(sub);
@ -477,6 +470,11 @@ export class ColumnDisplay implements OnInit, OnDestroy {
updatedTask.ownAttachment,
updatedTask.sharedUser,
updatedTask.sharedCategory);
const data = this.boardService.convertMarkdown(task.description,
(_, text) => { return text; }, true);
task.html = data.html;
return task;
}
@ -536,14 +534,9 @@ export class ColumnDisplay implements OnInit, OnDestroy {
return text;
}
private getTaskDescription() {
let html = marked(this.viewModalProps.description, this.markedCallback);
return html.replace(/(\{)([^}]+)(\})/g, '{{ "{" }}$2{{ "}" }}');
}
private getComment(text: string) {
let html = marked(text, this.markedCallback);
return this.sanitizer.bypassSecurityTrustHtml(html);
let data = this.boardService.convertMarkdown(text, this.markedCallback);
return this.sanitizer.bypassSecurityTrustHtml(data.html);
}
private getUserName(userId: number) {
@ -579,12 +572,7 @@ export class ColumnDisplay implements OnInit, OnDestroy {
});
this.newComment = '';
this.viewModalProps = new Task(viewTask.id, viewTask.title,
viewTask.description, viewTask.color,
viewTask.due_date, viewTask.points,
viewTask.position, viewTask.column_id,
viewTask.comments, viewTask.attachments,
viewTask.assignees, viewTask.categories);
this.viewModalProps = this.convertToTask(viewTask);
this.checkDueDate();
if (this.showActivity) {

View File

@ -19,7 +19,7 @@
</h4>
<div class="description" *ngIf="!isCollapsed"
[innerHtml]="getTaskDescription()">
[innerHtml]="taskData.html">
</div>
<div class="stats">
@ -56,9 +56,71 @@
</div>
<tb-context-menu>
<tb-context-menu-item>
Task Test
<tb-context-menu-item (click)="viewTask()">
{{ strings['boards_viewTask'] }}
</tb-context-menu-item>
<tb-context-menu-item (click)="editTask()">
{{ strings['boards_editTask'] }}
</tb-context-menu-item>
<tb-context-menu-item (click)="removeTask()">
{{ strings['boards_removeTask'] }}
</tb-context-menu-item>
<div *ngIf="boardsList && boardsList.length > 1">
<tb-context-menu-item isSeparator="true"></tb-context-menu-item>
<tb-context-menu-item isCustomEvent="true">
{{ strings['boards_copyTaskTo'] }}:
<i class="icon icon-help-circled"
attr.data-help="{{ strings['boards_copyMoveHelp'] }}"></i>
<select id="boardsList{{ taskData.id }}{{ strings['boards_copyTaskTo'].split(' ')[0] }}"
(change)="copyTaskToBoard($event)">
<option value="0">{{ strings['boards_selectBoard'] }}</option>
<ng-container *ngFor="let board of boardsList">
<option *ngIf="board.id !== activeBoard.id" [value]="board.id">
{{ board.name }}
</option>
</ng-container>
</select>
</tb-context-menu-item>
<tb-context-menu-item isCustomEvent="true">
{{ strings['boards_moveTaskTo'] }}:
<i class="icon icon-help-circled"
attr.data-help="{{ strings['boards_copyMoveHelp'] }}"></i>
<select id="boardsList{{ taskData.id }}{{
strings['boards_moveTaskTo'].split(' ')[0] }}"
(change)="moveTaskToBoard($event)">
<option value="0">{{ strings['boards_selectBoard'] }}</option>
<ng-container *ngFor="let board of boardsList">
<option *ngIf="board.id !== activeBoard.id" [value]="board.id">
{{ board.name }}
</option>
</ng-container>
</select>
</tb-context-menu-item>
</div>
<div *ngIf="activeBoard.columns.length > 1">
<tb-context-menu-item isSeparator="true"></tb-context-menu-item>
<tb-context-menu-item isCustomEvent="true">
{{ strings['boards_moveTask'] }}:
<select id="columnsList{{ taskData.id }}" (change)="changeTaskColumn($event)">
<option value="0">{{ strings['boards_selectColumn'] }}</option>
<option *ngFor="let col of activeBoard.columns" [value]="col.id">
{{ col.name }}
</option>
</select>
</tb-context-menu-item>
</div>
<tb-context-menu-item isSeparator="true"></tb-context-menu-item>
<tb-context-menu-item (click)="addTask()">
{{ strings['boards_addTask'] }}
</tb-context-menu-item>
</tb-context-menu>
</div>

View File

@ -7,9 +7,6 @@ import {
} from '@angular/core';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import * as marked from 'marked';
import * as hljs from 'highlight.js';
import {
ApiResponse,
Board,
@ -31,8 +28,6 @@ import { BoardService } from '../board.service';
templateUrl: './task.component.html'
})
export class TaskDisplay implements OnInit {
private totalTasks: number;
private completeTasks: number;
private isOverdue: boolean;
private isNearlyDue: boolean;
@ -54,7 +49,6 @@ export class TaskDisplay implements OnInit {
@Input('boards')
set boards(boards: Array<Board>) {
this.boardsList = boards;
// this.generateContextMenuItems();
}
constructor(private auth: AuthService,
@ -64,16 +58,10 @@ export class TaskDisplay implements OnInit {
private notes: NotificationsService,
private stringsService: StringsService) {
this.onUpdateBoards = new EventEmitter<any>();
this.totalTasks = 0;
this.completeTasks = 0;
this.percentComplete = 0;
stringsService.stringsChanged.subscribe(newStrings => {
this.strings = newStrings;
if (this.taskData) {
// this.generateContextMenuItems();
}
});
auth.userChanged.subscribe(() => {
@ -86,27 +74,21 @@ export class TaskDisplay implements OnInit {
}
ngOnInit() {
// Since marked is global, the counts need to be stored uniquely per task.
// String literal access needed because augmenting the type doesn't work.
marked['taskCounts'] = []; // tslint:disable-line
if (!this.taskData) {
return;
}
marked['taskCounts'][this.taskData.id] = { // tslint:disable-line
total: 0,
complete: 0
};
// this.generateContextMenuItems();
this.initMarked();
this.calcPercentComplete();
this.checkDueDate();
this.convertTaskDescription();
}
getTaskDescription(): string {
let html = marked(this.taskData.description, this.markedCallback);
return html.replace(/(\{)([^}]+)(\})/g, '{{ "{" }}$2{{ "}" }}');
private convertTaskDescription() {
let data = this.boardService.convertMarkdown(
this.taskData.description, this.markedCallback, true
);
data.html.replace(/(\{)([^}]+)(\})/g, '{{ "{" }}$2{{ "}" }}');
if (data.counts.total) {
this.percentComplete = data.counts.complete / data.counts.total;
}
this.taskData.html = this.sanitizer.bypassSecurityTrustHtml(data.html);
}
getPercentStyle() {
@ -131,7 +113,12 @@ export class TaskDisplay implements OnInit {
return yiq >= 140 ? '#333333' : '#efefef';
}
changeTaskColumn() {
changeTaskColumn(event: any) {
if (event.target.tagName !== 'SELECT') {
return;
}
event.target.parentElement.parentElement.click();
let select = document.getElementById('columnsList' + this.taskData.id) as HTMLSelectElement,
id = +select[select.selectedIndex].value;
@ -151,7 +138,12 @@ export class TaskDisplay implements OnInit {
});
}
copyTaskToBoard() {
copyTaskToBoard(event: any) {
if (event.target.tagName !== 'SELECT') {
return;
}
event.target.parentElement.parentElement.click();
let select = document.getElementById('boardsList' + this.taskData.id +
this.strings.boards_copyTaskTo.split(' ')[0]) as HTMLSelectElement;
@ -184,7 +176,12 @@ export class TaskDisplay implements OnInit {
});
}
moveTaskToBoard() {
moveTaskToBoard(event: any) {
if (event.target.tagName !== 'SELECT') {
return;
}
event.target.parentElement.parentElement.click();
let select = document.getElementById('boardsList' + this.taskData.id +
this.strings.boards_moveTaskTo.split(' ')[0]) as HTMLSelectElement;
@ -217,7 +214,7 @@ export class TaskDisplay implements OnInit {
}
private checkDueDate() {
if (this.taskData.due_date === '') {
if (!this.taskData || this.taskData.due_date === '') {
return;
}
@ -270,126 +267,5 @@ export class TaskDisplay implements OnInit {
return text;
}
// private getMoveMenuItem() {
// let menuText = this.strings.boards_moveTask +
// ': <select id="columnsList' + this.taskData.id + '" ' +
// '(click)="action($event)">' +
// '<option value="0">' + this.strings.boards_selectColumn + '</option>';
//
// this.activeBoard.columns.forEach((column: Column) => {
// menuText += '<option value="' + column.id + '">' + column.name + '</option>';
// });
//
// menuText += '</select>';
//
// let action = (event: any) => {
// if (event.target.tagName !== 'SELECT') {
// return;
// }
//
// this.changeTaskColumn();
// };
//
// return new ContextMenuItem(menuText, action, false, false, true);
// }
private calcPercentComplete() {
this.percentComplete = 0;
// String literal access needed because augmenting the type doesn't work.
marked['taskCounts'][this.taskData.id] = { // tslint:disable-line
total: 0,
complete: 0
};
marked(this.taskData.description);
if (marked['taskCounts'][this.taskData.id].total) { // tslint:disable-line
this.percentComplete = marked['taskCounts'][this.taskData.id].complete / // tslint:disable-line
marked['taskCounts'][this.taskData.id].total; // tslint:disable-line
}
}
// private generateContextMenuItems() {
// this.contextMenuItems = [
// new ContextMenuItem(this.strings.boards_viewTask, this.viewTask),
// new ContextMenuItem(this.strings.boards_editTask, this.editTask),
// new ContextMenuItem(this.strings.boards_removeTask, this.removeTask),
// new ContextMenuItem('', null, true),
// this.getMoveMenuItem(),
// new ContextMenuItem('', null, true),
// new ContextMenuItem(this.strings.boards_addTask, this.addTask)
// ];
//
// if (this.boardsList && this.boardsList.length > 1) {
// this.contextMenuItems
// .splice(3, 0,
// new ContextMenuItem('', null, true),
// this.getMenuItem(this.strings.boards_copyTaskTo),
// this.getMenuItem(this.strings.boards_moveTaskTo));
// }
// }
// private getMenuItem(text: string): ContextMenuItem {
// let menuText = text + ': ' +
// '<i class="icon icon-help-circled" ' +
// 'data-help="' + this.strings.boards_copyMoveHelp + '"></i> ' +
// '<select id="boardsList' + this.taskData.id + text.split(' ')[0] + '" ' +
// '(click)="action($event)">' +
// '<option value="0">' + this.strings.boards_selectBoard + '</option>';
//
// this.boardsList.forEach((board: Board) => {
// if (board.name !== this.activeBoard.name) {
// menuText += '<option value="' + board.id + '">' + board.name + '</option>';
// }
// });
//
// menuText += '</select>';
//
// let action = (event: any) => {
// if (event.target.tagName !== 'SELECT') {
// return;
// }
//
// if (text === this.strings.boards_copyTaskTo) {
// this.copyTaskToBoard();
// return;
// }
//
// this.moveTaskToBoard();
// };
//
// return new ContextMenuItem(menuText, action, false, false, true);
// }
private initMarked() {
let renderer = new marked.Renderer();
renderer.checkbox = isChecked => {
let text = '<i class="icon icon-check' + (isChecked ? '' : '-empty') + '"></i>';
return text;
};
renderer.link = (href, title, text) => {
let out = '<a href="' + href + '"';
if (title) {
out += ' title="' + title + '"';
}
out += ' target="tb_external" rel="noreferrer">' + text + '</a>';
return out;
};
marked.setOptions({
renderer,
smartypants: true,
highlight: code => {
return hljs.highlightAuto(code).value;
}
});
}
}

View File

@ -1,2 +1,5 @@
<ng-content *ngIf="!isSeparator; else separator"></ng-content>
<ng-template #separator><hr></ng-template>
<ng-content *ngIf="!isSeparator; else separator"
class="context-menu-item"></ng-content>
<ng-template #separator class="context-menu-separator">
<hr>
</ng-template>

View File

@ -1,10 +1,4 @@
import {
Component,
ElementRef,
EventEmitter,
Input,
Output
} from '@angular/core';
import { Component, ElementRef, EventEmitter, Input } from '@angular/core';
@Component({
selector: 'tb-context-menu-item',
@ -24,19 +18,17 @@ export class ContextMenuItem {
@Input()
isSeparator: boolean;
@Output()
clickEvent: EventEmitter<MouseEvent> = new EventEmitter();
@Input()
isCustomEvent: boolean;
constructor(private el: ElementRef) {
const elem = el.nativeElement;
elem.onclick = (event) => {
if (this.isSeparator) {
if (this.isSeparator || this.isCustomEvent) {
this.killEvent(event);
return;
}
this.clickEvent.next(event);
};
elem.oncontextmenu = (event) => {

View File

@ -1,4 +1,4 @@
<div class="context-menu-container" *ngIf="isOpen">
<div class="context-menu-container" style="top: -99999em;" *ngIf="isOpen">
<ng-content></ng-content>
</div>

View File

@ -15,33 +15,43 @@ export class ContextMenu {
const parentEl = el.nativeElement.parentElement;
if (!parentEl) {
return;
}
parentEl.oncontextmenu = (event: MouseEvent) => {
this.parentEventHandler(event);
setTimeout(() => {
this.updatePosition(event);
}, 0);
};
}
private updatePosition(event: MouseEvent) {
const edgeBuffer = 10;
// Adjust position if near an edge
const target = this.el.nativeElement.firstElementChild;
if (!target) {
return;
}
const rect = target.getBoundingClientRect();
const offsetX = (event.pageX + rect.width + edgeBuffer) > window.innerWidth;
const offsetY = (event.pageY + rect.height + edgeBuffer) > window.innerHeight;
target.style.left = event.pageX - (offsetX ? rect.width : 0) + 'px';
target.style.top = event.pageY - (offsetY ? rect.height : 0) + 'px';
}
private parentEventHandler(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.menuService.closeAllMenus();
this.el.nativeElement.style.top = '-10000px';
this.isOpen = true;
const edgeBuffer = 10;
// Adjust position if near an edge
const adjustPosition = () => {
const target = this.el.nativeElement.firstElementChild;
const rect = target.getBoundingClientRect();
const offsetX = (event.pageX + rect.width + edgeBuffer) > window.innerWidth;
const offsetY = (event.pageY + rect.height + edgeBuffer) > window.innerHeight;
target.style.left = event.pageX - (offsetX ? rect.width : 0) + 'px';
target.style.top = event.pageY - (offsetY ? rect.height : 0) + 'px';
};
setTimeout(adjustPosition, 0);
}
}

View File

@ -1,3 +1,5 @@
import { SafeHtml } from '@angular/platform-browser';
import { Attachment } from './attachment.model';
import { Category } from './category.model';
import { Comment } from './comment.model';
@ -8,6 +10,7 @@ export class Task {
public attachments: Array<Attachment>;
public assignees: Array<User>;
public categories: Array<Category>;
public html: SafeHtml;
public filtered: boolean;
public hideFiltered: boolean;

View File

@ -16,3 +16,20 @@
z-index: 100;
}
tb-context-menu-item {
cursor: pointer;
}
tb-context-menu-item:hover {
background-color: darken($white, 5%);
}
tb-context-menu-item[isseparator] {
cursor: default;
height: 1em;
}
tb-context-menu-item[isseparator] {
background-color: $white;
}

View File

@ -63,7 +63,7 @@ describe('ApiInterceptor', () => {
it('adds Authorization header when JWT is present',
inject([HttpClient, HttpTestingController],
(http: HttpClient, httpMock: HttpTestingController) => {
localStorage.setItem('taskboard.jwt', 'fake');
sessionStorage.setItem('taskboard.jwt', 'fake');
http.post('', {}).subscribe(response => {
expect(response).toBeTruthy();
@ -76,7 +76,7 @@ describe('ApiInterceptor', () => {
expect(req.request.method).toEqual('POST');
req.flush({ data: ['newToken'] });
expect(localStorage.getItem('taskboard.jwt')).toEqual('newToken');
expect(sessionStorage.getItem('taskboard.jwt')).toEqual('newToken');
}
)
)
@ -84,7 +84,7 @@ describe('ApiInterceptor', () => {
it('handles errors and clears the JWT',
inject([HttpClient, HttpTestingController],
(http: HttpClient, httpMock: HttpTestingController) => {
localStorage.setItem('taskboard.jwt', 'fake');
sessionStorage.setItem('taskboard.jwt', 'fake');
http.get('').subscribe(response => {
expect(response).toBeTruthy();
@ -100,7 +100,7 @@ describe('ApiInterceptor', () => {
const error = new HttpErrorResponse({ status: 401 });
req.flush(error);
expect(localStorage.getItem('Authorization')).toEqual(null);
expect(sessionStorage.getItem('Authorization')).toEqual(null);
}
)
)

View File

@ -20,8 +20,6 @@ describe('ContextMenu', () => {
fixture: ComponentFixture<ContextMenu>,
elementRef: ElementRefMock;
const getPrivateFunction = name => component[name].bind(component);
beforeEach(() => {
elementRef = new ElementRefMock();