ngx-edu-sharing-ui 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.browserslistrc +16 -0
- package/.eslintrc.json +44 -0
- package/README.md +40 -0
- package/assets/scss/mixins.scss +95 -0
- package/assets/scss/variables.scss +33 -0
- package/karma.conf.js +42 -0
- package/ng-package.json +10 -0
- package/package.json +19 -0
- package/src/lib/actionbar/actionbar.component.html +59 -0
- package/src/lib/actionbar/actionbar.component.scss +123 -0
- package/src/lib/actionbar/actionbar.component.ts +174 -0
- package/src/lib/common/edu-sharing-ui-common.module.ts +80 -0
- package/src/lib/directives/border-box-observer.directive.ts +75 -0
- package/src/lib/directives/check-text-overflow.directive.ts +61 -0
- package/src/lib/directives/drag-nodes/drag-nodes.ts +32 -0
- package/src/lib/directives/drag-nodes/nodes-drag-source.directive.ts +79 -0
- package/src/lib/directives/drag-nodes/nodes-drag.directive.ts +43 -0
- package/src/lib/directives/drag-nodes/nodes-drop-target.directive.ts +116 -0
- package/src/lib/directives/focus-state.directive.ts +34 -0
- package/src/lib/directives/icon.directive.ts +142 -0
- package/src/lib/directives/nodes-drop-target-legacy.directive.ts +155 -0
- package/src/lib/dropdown/dropdown.component.html +32 -0
- package/src/lib/dropdown/dropdown.component.scss +67 -0
- package/src/lib/dropdown/dropdown.component.ts +71 -0
- package/src/lib/edu-sharing-ui-configuration.ts +47 -0
- package/src/lib/edu-sharing-ui.module.ts +49 -0
- package/src/lib/list-items/available-widgets.ts +30 -0
- package/src/lib/list-items/format-duration.pipe.ts +17 -0
- package/src/lib/list-items/list-base/list-base.component.html +52 -0
- package/src/lib/list-items/list-base/list-base.component.ts +44 -0
- package/src/lib/list-items/list-collection-info/list-collection-info.component.html +48 -0
- package/src/lib/list-items/list-collection-info/list-collection-info.component.scss +8 -0
- package/src/lib/list-items/list-collection-info/list-collection-info.component.ts +24 -0
- package/src/lib/list-items/list-counts/list-counts.component.html +1 -0
- package/src/lib/list-items/list-counts/list-counts.component.scss +3 -0
- package/src/lib/list-items/list-counts/list-counts.component.ts +59 -0
- package/src/lib/list-items/list-items.module.ts +33 -0
- package/src/lib/list-items/list-node-license/list-node-license.component.html +8 -0
- package/src/lib/list-items/list-node-license/list-node-license.component.ts +47 -0
- package/src/lib/list-items/list-node-replication-source/list-node-replication-source.component.html +11 -0
- package/src/lib/list-items/list-node-replication-source/list-node-replication-source.component.ts +60 -0
- package/src/lib/list-items/list-node-workflow/list-node-workflow.component.html +3 -0
- package/src/lib/list-items/list-node-workflow/list-node-workflow.component.ts +21 -0
- package/src/lib/list-items/list-text/list-text.component.html +176 -0
- package/src/lib/list-items/list-text/list-text.component.scss +3 -0
- package/src/lib/list-items/list-text/list-text.component.ts +107 -0
- package/src/lib/list-items/list-widget.ts +52 -0
- package/src/lib/list-items/node-row/node-row.component.html +31 -0
- package/src/lib/list-items/node-row/node-row.component.scss +50 -0
- package/src/lib/list-items/node-row/node-row.component.ts +16 -0
- package/src/lib/list-items/node-source.pipe.ts +48 -0
- package/src/lib/node-entries/combined-data-source.ts +51 -0
- package/src/lib/node-entries/custom-templates-data-source.ts +6 -0
- package/src/lib/node-entries/drag-preview/drag-preview.component.html +6 -0
- package/src/lib/node-entries/drag-preview/drag-preview.component.scss +35 -0
- package/src/lib/node-entries/drag-preview/drag-preview.component.ts +15 -0
- package/src/lib/node-entries/entries-model.ts +120 -0
- package/src/lib/node-entries/items-cap.ts +54 -0
- package/src/lib/node-entries/list-item-label.pipe.ts +28 -0
- package/src/lib/node-entries/mixins.scss +23 -0
- package/src/lib/node-entries/node-cache.spec.ts +199 -0
- package/src/lib/node-entries/node-cache.ts +81 -0
- package/src/lib/node-entries/node-data-source-remote.ts +33 -0
- package/src/lib/node-entries/node-data-source.ts +148 -0
- package/src/lib/node-entries/node-entries-card/node-entries-card.component.html +167 -0
- package/src/lib/node-entries/node-entries-card/node-entries-card.component.scss +28 -0
- package/src/lib/node-entries/node-entries-card/node-entries-card.component.ts +132 -0
- package/src/lib/node-entries/node-entries-card/node-entries-card.main.scss +261 -0
- package/src/lib/node-entries/node-entries-card-grid/node-entries-card-grid.component.html +205 -0
- package/src/lib/node-entries/node-entries-card-grid/node-entries-card-grid.component.scss +181 -0
- package/src/lib/node-entries/node-entries-card-grid/node-entries-card-grid.component.ts +361 -0
- package/src/lib/node-entries/node-entries-card-small/node-entries-card-small.component.html +100 -0
- package/src/lib/node-entries/node-entries-card-small/node-entries-card-small.component.scss +46 -0
- package/src/lib/node-entries/node-entries-card-small/node-entries-card-small.component.ts +40 -0
- package/src/lib/node-entries/node-entries-global-options/node-entries-global-options.component.html +23 -0
- package/src/lib/node-entries/node-entries-global-options/node-entries-global-options.component.scss +58 -0
- package/src/lib/node-entries/node-entries-global-options/node-entries-global-options.component.ts +16 -0
- package/src/lib/node-entries/node-entries-global.service.ts +79 -0
- package/src/lib/node-entries/node-entries-table/column-chooser/column-chooser.component.html +25 -0
- package/src/lib/node-entries/node-entries-table/column-chooser/column-chooser.component.scss +32 -0
- package/src/lib/node-entries/node-entries-table/column-chooser/column-chooser.component.ts +31 -0
- package/src/lib/node-entries/node-entries-table/node-entries-table.component.html +270 -0
- package/src/lib/node-entries/node-entries-table/node-entries-table.component.scss +169 -0
- package/src/lib/node-entries/node-entries-table/node-entries-table.component.ts +333 -0
- package/src/lib/node-entries/node-entries-templates.service.ts +31 -0
- package/src/lib/node-entries/node-entries-wrapper.component.ts +363 -0
- package/src/lib/node-entries/node-entries.component.html +33 -0
- package/src/lib/node-entries/node-entries.component.scss +13 -0
- package/src/lib/node-entries/node-entries.component.ts +151 -0
- package/src/lib/node-entries/node-entries.module.ts +93 -0
- package/src/lib/node-entries/node-rating/node-rating.component.html +53 -0
- package/src/lib/node-entries/node-rating/node-rating.component.scss +31 -0
- package/src/lib/node-entries/node-rating/node-rating.component.ts +105 -0
- package/src/lib/node-entries/node-stats-badges/node-stats-badges.component.html +39 -0
- package/src/lib/node-entries/node-stats-badges/node-stats-badges.component.scss +44 -0
- package/src/lib/node-entries/node-stats-badges/node-stats-badges.component.ts +43 -0
- package/src/lib/node-entries/node-type-badge/node-type-badge.component.html +31 -0
- package/src/lib/node-entries/node-type-badge/node-type-badge.component.scss +5 -0
- package/src/lib/node-entries/node-type-badge/node-type-badge.component.ts +36 -0
- package/src/lib/node-entries/option-button/option-button.component.ts +42 -0
- package/src/lib/node-entries/preview-image/preview-image.component.html +19 -0
- package/src/lib/node-entries/preview-image/preview-image.component.scss +31 -0
- package/src/lib/node-entries/preview-image/preview-image.component.ts +47 -0
- package/src/lib/node-entries/sort-select-panel/sort-select-panel.component.html +27 -0
- package/src/lib/node-entries/sort-select-panel/sort-select-panel.component.scss +9 -0
- package/src/lib/node-entries/sort-select-panel/sort-select-panel.component.ts +26 -0
- package/src/lib/node-url/node-url.component.html +66 -0
- package/src/lib/node-url/node-url.component.scss +32 -0
- package/src/lib/node-url/node-url.component.ts +136 -0
- package/src/lib/pipes/file-size.pipe.ts +24 -0
- package/src/lib/pipes/format-date.pipe.ts +39 -0
- package/src/lib/pipes/node-icon.pipe.ts +11 -0
- package/src/lib/pipes/node-image-size.pipe.ts +18 -0
- package/src/lib/pipes/node-image.pipe.ts +71 -0
- package/src/lib/pipes/node-person-name.pipe.ts +41 -0
- package/src/lib/pipes/node-title.pipe.ts +12 -0
- package/src/lib/pipes/option-tooltip.pipe.ts +32 -0
- package/src/lib/pipes/replace-chars.pipe.ts +21 -0
- package/src/lib/pipes/vcard-name.pipe.ts +11 -0
- package/src/lib/services/abstract/app.service.ts +4 -0
- package/src/lib/services/abstract/keyboard-shortcuts.service.ts +10 -0
- package/src/lib/services/abstract/options-helper.service.ts +29 -0
- package/src/lib/services/abstract/toast.service.ts +5 -0
- package/src/lib/services/accessibility.service.ts +101 -0
- package/src/lib/services/local-events.service.ts +29 -0
- package/src/lib/services/node-entries.service.ts +172 -0
- package/src/lib/services/node-helper.service.ts +239 -0
- package/src/lib/services/nodes-drag-drop.service.ts +165 -0
- package/src/lib/services/options-helper-data.service.ts +186 -0
- package/src/lib/services/repo-url.service.ts +46 -0
- package/src/lib/services/temporary-storage.service.ts +58 -0
- package/src/lib/services/ui.service.ts +182 -0
- package/src/lib/sort-dropdown/sort-dropdown.component.html +22 -0
- package/src/lib/sort-dropdown/sort-dropdown.component.scss +47 -0
- package/src/lib/sort-dropdown/sort-dropdown.component.ts +42 -0
- package/src/lib/spinner/spinner.component.html +14 -0
- package/src/lib/spinner/spinner.component.scss +141 -0
- package/src/lib/spinner/spinner.component.ts +12 -0
- package/src/lib/translations/README.md +44 -0
- package/src/lib/translations/fallback-translation-handler.ts +7 -0
- package/src/lib/translations/languages.ts +6 -0
- package/src/lib/translations/translation-loader.spec.ts +352 -0
- package/src/lib/translations/translation-loader.ts +189 -0
- package/src/lib/translations/translation-source.ts +9 -0
- package/src/lib/translations/translations.module.ts +49 -0
- package/src/lib/translations/translations.service.spec.ts +152 -0
- package/src/lib/translations/translations.service.ts +188 -0
- package/src/lib/types/accessibillity.ts +15 -0
- package/src/lib/types/api-models.ts +4 -0
- package/src/lib/types/drag-drop.ts +22 -0
- package/src/lib/types/keyboard-shortcuts.ts +29 -0
- package/src/lib/types/list-item.ts +67 -0
- package/src/lib/types/option-item.ts +247 -0
- package/src/lib/types/workflow.ts +35 -0
- package/src/lib/util/DateHelper.spec.ts +112 -0
- package/src/lib/util/DateHelper.ts +197 -0
- package/src/lib/util/VCard.ts +277 -0
- package/src/lib/util/color-helper.ts +125 -0
- package/src/lib/util/duration-helper.spec.ts +35 -0
- package/src/lib/util/duration-helper.ts +98 -0
- package/src/lib/util/functions.ts +15 -0
- package/src/lib/util/helper.ts +60 -0
- package/src/lib/util/isNumeric.ts +13 -0
- package/src/lib/util/rest-helper.ts +28 -0
- package/src/lib/util/ui-animation.ts +154 -0
- package/src/lib/util/ui-constants.ts +20 -0
- package/src/module.ts +76 -0
- package/src/test.ts +28 -0
- package/tsconfig.lib.json +15 -0
- package/tsconfig.lib.prod.json +10 -0
- package/tsconfig.spec.json +17 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { NodeIconPipe } from '../pipes/node-icon.pipe';
|
|
3
|
+
import { VCardNamePipe } from '../pipes/vcard-name.pipe';
|
|
4
|
+
import { NodeImagePipe } from '../pipes/node-image.pipe';
|
|
5
|
+
import { IconDirective } from '../directives/icon.directive';
|
|
6
|
+
import { FormatSizePipe } from '../pipes/file-size.pipe';
|
|
7
|
+
import { CommonModule } from '@angular/common';
|
|
8
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
9
|
+
import { NodeImageSizePipe } from '../pipes/node-image-size.pipe';
|
|
10
|
+
import { NodePersonNamePipe } from '../pipes/node-person-name.pipe';
|
|
11
|
+
import { FormatDatePipe } from '../pipes/format-date.pipe';
|
|
12
|
+
import { SortDropdownComponent } from '../sort-dropdown/sort-dropdown.component';
|
|
13
|
+
import { CheckTextOverflowDirective } from '../directives/check-text-overflow.directive';
|
|
14
|
+
import { MatMenuModule } from '@angular/material/menu';
|
|
15
|
+
import { NodeTitlePipe } from '../pipes/node-title.pipe';
|
|
16
|
+
import { SpinnerComponent } from '../spinner/spinner.component';
|
|
17
|
+
import { NodeUrlComponent } from '../node-url/node-url.component';
|
|
18
|
+
import { DropdownComponent } from '../dropdown/dropdown.component';
|
|
19
|
+
import { RouterModule } from '@angular/router';
|
|
20
|
+
import { MatRippleModule } from '@angular/material/core';
|
|
21
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
22
|
+
import { OptionTooltipPipe } from '../pipes/option-tooltip.pipe';
|
|
23
|
+
import { ReplaceCharsPipe } from '../pipes/replace-chars.pipe';
|
|
24
|
+
import { ActionbarComponent } from '../actionbar/actionbar.component';
|
|
25
|
+
import { BorderBoxObserverDirective } from '../directives/border-box-observer.directive';
|
|
26
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
27
|
+
import { FocusStateDirective } from '../directives/focus-state.directive';
|
|
28
|
+
|
|
29
|
+
@NgModule({
|
|
30
|
+
declarations: [
|
|
31
|
+
IconDirective,
|
|
32
|
+
CheckTextOverflowDirective,
|
|
33
|
+
NodeIconPipe,
|
|
34
|
+
NodeImagePipe,
|
|
35
|
+
VCardNamePipe,
|
|
36
|
+
FocusStateDirective,
|
|
37
|
+
FormatSizePipe,
|
|
38
|
+
NodeImageSizePipe,
|
|
39
|
+
NodePersonNamePipe,
|
|
40
|
+
NodeTitlePipe,
|
|
41
|
+
FormatDatePipe,
|
|
42
|
+
ReplaceCharsPipe,
|
|
43
|
+
SortDropdownComponent,
|
|
44
|
+
SpinnerComponent,
|
|
45
|
+
NodeUrlComponent,
|
|
46
|
+
DropdownComponent,
|
|
47
|
+
OptionTooltipPipe,
|
|
48
|
+
ActionbarComponent,
|
|
49
|
+
BorderBoxObserverDirective,
|
|
50
|
+
],
|
|
51
|
+
imports: [
|
|
52
|
+
CommonModule,
|
|
53
|
+
MatMenuModule,
|
|
54
|
+
MatButtonModule,
|
|
55
|
+
MatTooltipModule,
|
|
56
|
+
MatRippleModule,
|
|
57
|
+
TranslateModule,
|
|
58
|
+
RouterModule,
|
|
59
|
+
],
|
|
60
|
+
exports: [
|
|
61
|
+
IconDirective,
|
|
62
|
+
CheckTextOverflowDirective,
|
|
63
|
+
NodeIconPipe,
|
|
64
|
+
NodeImagePipe,
|
|
65
|
+
VCardNamePipe,
|
|
66
|
+
SortDropdownComponent,
|
|
67
|
+
FocusStateDirective,
|
|
68
|
+
FormatSizePipe,
|
|
69
|
+
NodeImageSizePipe,
|
|
70
|
+
NodePersonNamePipe,
|
|
71
|
+
NodeTitlePipe,
|
|
72
|
+
FormatDatePipe,
|
|
73
|
+
SpinnerComponent,
|
|
74
|
+
NodeUrlComponent,
|
|
75
|
+
DropdownComponent,
|
|
76
|
+
ActionbarComponent,
|
|
77
|
+
BorderBoxObserverDirective,
|
|
78
|
+
],
|
|
79
|
+
})
|
|
80
|
+
export class EduSharingUiCommonModule {}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Directive, ElementRef, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
|
2
|
+
import { ReplaySubject, Observable } from 'rxjs';
|
|
3
|
+
import { map } from 'rxjs/operators';
|
|
4
|
+
|
|
5
|
+
interface BorderBox {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Gets the border-box dimensions of an element in an asynchronous manner.
|
|
12
|
+
*
|
|
13
|
+
* This allows subscribing to changes and using values as input to other Angular components without
|
|
14
|
+
* causing changed-after-checked errors.
|
|
15
|
+
*/
|
|
16
|
+
@Directive({
|
|
17
|
+
selector: '[esBorderBoxObserver]',
|
|
18
|
+
exportAs: 'borderBoxObserver',
|
|
19
|
+
})
|
|
20
|
+
export class BorderBoxObserverDirective implements OnInit, OnDestroy {
|
|
21
|
+
static observeElement(elementRef: ElementRef<HTMLElement>): Observable<BorderBox> {
|
|
22
|
+
return new Observable((subscriber) => {
|
|
23
|
+
const borderBoxObserver = new BorderBoxObserverDirective(elementRef);
|
|
24
|
+
borderBoxObserver.ngOnInit();
|
|
25
|
+
borderBoxObserver.borderBoxSubject.subscribe(subscriber);
|
|
26
|
+
return () => borderBoxObserver.ngOnDestroy();
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@Output('esBorderBoxObserver') borderBoxEmitter = new EventEmitter<BorderBox>();
|
|
31
|
+
|
|
32
|
+
private observer: ResizeObserver;
|
|
33
|
+
private readonly borderBoxSubject = new ReplaySubject<BorderBox>(1);
|
|
34
|
+
|
|
35
|
+
width$ = this.borderBoxSubject.pipe(map(({ width }) => width));
|
|
36
|
+
height$ = this.borderBoxSubject.pipe(map(({ height }) => height));
|
|
37
|
+
|
|
38
|
+
constructor(private elementRef: ElementRef<HTMLElement>) {}
|
|
39
|
+
|
|
40
|
+
ngOnInit(): void {
|
|
41
|
+
this.registerEventEmitter();
|
|
42
|
+
this.registerObserver();
|
|
43
|
+
// Can cause changed-after-checked errors if done synchronously.
|
|
44
|
+
Promise.resolve().then(() => this.setInitialValue());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ngOnDestroy(): void {
|
|
48
|
+
this.observer.disconnect();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private registerEventEmitter(): void {
|
|
52
|
+
this.borderBoxSubject.subscribe((borderBox) => this.borderBoxEmitter.emit(borderBox));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private registerObserver(): void {
|
|
56
|
+
this.observer = new ResizeObserver((entries) => {
|
|
57
|
+
entries.forEach((entry) => {
|
|
58
|
+
const borderBoxSize: ResizeObserverSize = entry.borderBoxSize[0];
|
|
59
|
+
this.borderBoxSubject.next({
|
|
60
|
+
width: borderBoxSize.inlineSize,
|
|
61
|
+
height: borderBoxSize.blockSize,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
this.observer.observe(this.elementRef.nativeElement);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private setInitialValue(): void {
|
|
69
|
+
const boundingClientRect = this.elementRef.nativeElement.getBoundingClientRect();
|
|
70
|
+
this.borderBoxSubject.next({
|
|
71
|
+
width: boundingClientRect.width,
|
|
72
|
+
height: boundingClientRect.height,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Directive, ElementRef, Input, OnInit } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks whether text of the annotated element or a descendent (give a selector) was cut of, e.g.,
|
|
5
|
+
* with an ellipsis.
|
|
6
|
+
*
|
|
7
|
+
* Call `hasTextOverflow()` for the result.
|
|
8
|
+
*/
|
|
9
|
+
@Directive({
|
|
10
|
+
selector: '[esCheckTextOverflow]',
|
|
11
|
+
exportAs: 'esCheckTextOverflow',
|
|
12
|
+
})
|
|
13
|
+
export class CheckTextOverflowDirective implements OnInit {
|
|
14
|
+
@Input('esCheckTextOverflow') selector?: string;
|
|
15
|
+
|
|
16
|
+
private textElement: HTMLElement;
|
|
17
|
+
|
|
18
|
+
hasTextOverflow = delay(this.hasTextOverflow_);
|
|
19
|
+
|
|
20
|
+
constructor(private readonly elementRef: ElementRef<HTMLElement>) {}
|
|
21
|
+
|
|
22
|
+
ngOnInit(): void {
|
|
23
|
+
this.textElement = this.getTextElement();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private hasTextOverflow_(): boolean {
|
|
27
|
+
const element = this.textElement;
|
|
28
|
+
if (element) {
|
|
29
|
+
return element.offsetWidth < element.scrollWidth;
|
|
30
|
+
} else {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private getTextElement(): HTMLElement {
|
|
36
|
+
if (this.selector) {
|
|
37
|
+
return this.elementRef.nativeElement.querySelector(this.selector);
|
|
38
|
+
} else {
|
|
39
|
+
return this.elementRef.nativeElement;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Delay the result of a function one tick to avoid changed-after-checked errors. */
|
|
45
|
+
function delay<T>(f: () => T): () => T {
|
|
46
|
+
let previousValue: any = null;
|
|
47
|
+
let updating = false;
|
|
48
|
+
return function () {
|
|
49
|
+
if (!updating) {
|
|
50
|
+
const newValue = f.apply(this);
|
|
51
|
+
if (newValue !== previousValue) {
|
|
52
|
+
updating = true;
|
|
53
|
+
Promise.resolve().then(() => {
|
|
54
|
+
previousValue = newValue;
|
|
55
|
+
updating = false;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return previousValue;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Node } from 'ngx-edu-sharing-api';
|
|
2
|
+
import { DropAction } from '../../types/drag-drop';
|
|
3
|
+
|
|
4
|
+
export interface DragDataLegacy {
|
|
5
|
+
event?: DragEvent;
|
|
6
|
+
nodes?: Node[];
|
|
7
|
+
dropAction?: DropAction;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DropDataLegacy extends DragDataLegacy {
|
|
11
|
+
target: DragNodeTarget;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type DragNodeTarget = Node | 'HOME';
|
|
15
|
+
|
|
16
|
+
export const dragNodesTransferType = 'application/nodes';
|
|
17
|
+
|
|
18
|
+
const storageKey = 'app-drag-nodes';
|
|
19
|
+
|
|
20
|
+
export function readDraggedNodes(): Node[] {
|
|
21
|
+
const json = window.localStorage.getItem(storageKey);
|
|
22
|
+
return JSON.parse(json);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function saveDraggedNodes(nodes: Node[]): void {
|
|
26
|
+
const json = JSON.stringify(nodes);
|
|
27
|
+
window.localStorage.setItem(storageKey, json);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function clearDraggedNodes(): void {
|
|
31
|
+
window.localStorage.removeItem(storageKey);
|
|
32
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Directive,
|
|
3
|
+
EventEmitter,
|
|
4
|
+
HostListener,
|
|
5
|
+
Input,
|
|
6
|
+
Output,
|
|
7
|
+
ElementRef,
|
|
8
|
+
OnChanges,
|
|
9
|
+
SimpleChanges,
|
|
10
|
+
} from '@angular/core';
|
|
11
|
+
import { Node } from 'ngx-edu-sharing-api';
|
|
12
|
+
import { clearDraggedNodes, dragNodesTransferType, saveDraggedNodes } from './drag-nodes';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Handle dragging and dropping of node elements.
|
|
16
|
+
*
|
|
17
|
+
* Use in combination with `NodesDropTargetDirective`.
|
|
18
|
+
*
|
|
19
|
+
* When nodes that set this directive are dragged, we save information about
|
|
20
|
+
* these nodes that can be used by `NodesDropTargetDirective`.
|
|
21
|
+
*/
|
|
22
|
+
@Directive({
|
|
23
|
+
selector: '[esNodesDragSource]',
|
|
24
|
+
})
|
|
25
|
+
export class NodesDragSourceDirective implements OnChanges {
|
|
26
|
+
/**
|
|
27
|
+
* The nodes to be dragged.
|
|
28
|
+
*
|
|
29
|
+
* When not set, this directive is effectively disabled.
|
|
30
|
+
*/
|
|
31
|
+
@Input('esNodesDragSource') nodes?: Node[];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Triggered when processing the dragstart event.
|
|
35
|
+
*
|
|
36
|
+
* Changes to `nodes` performed by event handlers of `nodesDragStart` will
|
|
37
|
+
* be taken into account for the drag operation.
|
|
38
|
+
*/
|
|
39
|
+
@Output() nodesDragStart = new EventEmitter<DragEvent>(false);
|
|
40
|
+
/**
|
|
41
|
+
* Triggered when processing the dragend event.
|
|
42
|
+
*/
|
|
43
|
+
@Output() nodesDragEnd = new EventEmitter<DragEvent>();
|
|
44
|
+
|
|
45
|
+
constructor(private elementRef: ElementRef<Element>) {}
|
|
46
|
+
|
|
47
|
+
ngOnChanges(changes: SimpleChanges) {
|
|
48
|
+
// Set the `draggable` attribute when this directive is active.
|
|
49
|
+
if (changes.nodes) {
|
|
50
|
+
if (!!changes.nodes.currentValue !== !!changes.nodes.previousValue) {
|
|
51
|
+
this.elementRef.nativeElement.setAttribute('draggable', (!!this.nodes).toString());
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@HostListener('dragstart', ['$event']) onDragStart(event: DragEvent) {
|
|
57
|
+
if (!this.nodes) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.nodesDragStart.emit(event);
|
|
61
|
+
// Use the transfer-data type to identify a node-drag operation.
|
|
62
|
+
event.dataTransfer.setData(dragNodesTransferType, '');
|
|
63
|
+
// Chrome doesn't provide drag transfer data to dragover event listeners
|
|
64
|
+
// for security reasons, so we provide the data via localStorage.
|
|
65
|
+
//
|
|
66
|
+
// Wait for updates by event handlers of `nodesDragStart` to propagate.
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
saveDraggedNodes(this.nodes);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@HostListener('dragend', ['$event']) onDragEnd(event: DragEvent) {
|
|
73
|
+
if (!this.nodes) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
this.nodesDragEnd.emit(event);
|
|
77
|
+
clearDraggedNodes();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CdkDrag } from '@angular/cdk/drag-drop';
|
|
2
|
+
import { Directive } from '@angular/core';
|
|
3
|
+
import { Node } from 'ngx-edu-sharing-api';
|
|
4
|
+
import { NodesDragDropService } from '../../services/nodes-drag-drop.service';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A draggable node.
|
|
8
|
+
*
|
|
9
|
+
* Use in combination with `cdkDrag`, setting `cdkDragData` to the array of nodes to be dragged.
|
|
10
|
+
*/
|
|
11
|
+
@Directive({
|
|
12
|
+
selector: '[esNodesDrag]',
|
|
13
|
+
})
|
|
14
|
+
export class NodesDragDirective {
|
|
15
|
+
constructor(private cdkDrag: CdkDrag<Node[]>, private nodesDragDrop: NodesDragDropService) {
|
|
16
|
+
this.cdkDrag.started.subscribe((event) => {
|
|
17
|
+
this.nodesDragDrop.draggedNodes = event.source.data;
|
|
18
|
+
// Position the preview element (the one being dragged around) next to the cursor to
|
|
19
|
+
// avoid covering possible drop targets with the preview.
|
|
20
|
+
event.source._dragRef['_pickupPositionInElement'] = { x: 0, y: 0 };
|
|
21
|
+
});
|
|
22
|
+
this.cdkDrag.released.subscribe(() => {
|
|
23
|
+
if (this.nodesDragDrop.canDrop?.accept) {
|
|
24
|
+
this.inhibitPreviewAnimation();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
this.cdkDrag.dropped.subscribe(() => {
|
|
28
|
+
this.nodesDragDrop.onDropped(this.cdkDrag.data);
|
|
29
|
+
this.nodesDragDrop.draggedNodes = null;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private inhibitPreviewAnimation() {
|
|
34
|
+
const style = document.createElement('style');
|
|
35
|
+
document.body.appendChild(style);
|
|
36
|
+
style.innerHTML = `.cdk-drag-preview { transition: none !important; }`;
|
|
37
|
+
// Don't need to go outside ng zone because `cdkDrag.released` already runs outside the
|
|
38
|
+
// zone.
|
|
39
|
+
setTimeout(() => {
|
|
40
|
+
document.body.removeChild(style);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Directive,
|
|
3
|
+
ElementRef,
|
|
4
|
+
EventEmitter,
|
|
5
|
+
Input,
|
|
6
|
+
NgZone,
|
|
7
|
+
OnDestroy,
|
|
8
|
+
Output,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
|
11
|
+
import { takeUntil } from 'rxjs/operators';
|
|
12
|
+
import { CanDrop, DragData, DropTargetState } from '../../types/drag-drop';
|
|
13
|
+
import { NodesDragDropService } from '../../services/nodes-drag-drop.service';
|
|
14
|
+
|
|
15
|
+
const ACTIVE_DROP_TARGET_ACCEPT_CLASS = 'es-nodes-active-drop-target-accept';
|
|
16
|
+
const ACTIVE_DROP_TARGET_DENY_CLASS = 'es-nodes-active-drop-target-deny';
|
|
17
|
+
|
|
18
|
+
@Directive({
|
|
19
|
+
selector: '[esNodesDropTarget]',
|
|
20
|
+
exportAs: 'esNodesDropTarget',
|
|
21
|
+
})
|
|
22
|
+
export class NodesDropTargetDirective<T = unknown> implements OnDestroy {
|
|
23
|
+
@Input('esNodesDropTarget') target: T;
|
|
24
|
+
@Input() canDropNodes: (dragData: DragData<T>) => CanDrop;
|
|
25
|
+
@Output() nodeDropped = new EventEmitter<DragData<T>>();
|
|
26
|
+
|
|
27
|
+
get active() {
|
|
28
|
+
return this.activeDropTargetSubject.value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private activeDropTargetSubject = new BehaviorSubject<DropTargetState | null>(null);
|
|
32
|
+
private destroyed = new Subject<void>();
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
private ngZone: NgZone,
|
|
36
|
+
private elementRef: ElementRef<HTMLElement>,
|
|
37
|
+
private nodesDragDrop: NodesDragDropService,
|
|
38
|
+
) {
|
|
39
|
+
this.registerMouseEnterLeave();
|
|
40
|
+
this.registerActiveDropTarget();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ngOnDestroy(): void {
|
|
44
|
+
this.destroyed.next();
|
|
45
|
+
this.destroyed.complete();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_setActiveDropTarget(value: DropTargetState | null) {
|
|
49
|
+
this.activeDropTargetSubject.next(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private registerMouseEnterLeave() {
|
|
53
|
+
this.ngZone.runOutsideAngular(() => {
|
|
54
|
+
this.elementRef.nativeElement.addEventListener('mouseenter', () =>
|
|
55
|
+
this.nodesDragDrop.onMouseEnter(this),
|
|
56
|
+
);
|
|
57
|
+
this.elementRef.nativeElement.addEventListener('mouseleave', () =>
|
|
58
|
+
this.nodesDragDrop.onMouseLeave(this),
|
|
59
|
+
);
|
|
60
|
+
// Firefox does not fire a mouseleave event when the element is removed from the DOM
|
|
61
|
+
// while being hovered. When an element is dragged, it will be replaced with a
|
|
62
|
+
// placeholder. If the element was also a drop target, we would think that we are still
|
|
63
|
+
// hovering the element.
|
|
64
|
+
observeRemovedFromParent(this.elementRef.nativeElement)
|
|
65
|
+
.pipe(takeUntil(this.destroyed))
|
|
66
|
+
.subscribe(() => this.nodesDragDrop.onMouseLeave(this));
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private registerActiveDropTarget() {
|
|
71
|
+
this.activeDropTargetSubject
|
|
72
|
+
.pipe(takeUntil(this.destroyed))
|
|
73
|
+
.subscribe((dropTargetState) => {
|
|
74
|
+
const canDrop = dropTargetState?.canDrop;
|
|
75
|
+
const classList = this.elementRef.nativeElement.classList;
|
|
76
|
+
classList.remove(ACTIVE_DROP_TARGET_ACCEPT_CLASS, ACTIVE_DROP_TARGET_DENY_CLASS);
|
|
77
|
+
if (canDrop?.accept) {
|
|
78
|
+
classList.add(ACTIVE_DROP_TARGET_ACCEPT_CLASS);
|
|
79
|
+
} else if (canDrop?.denyExplicit) {
|
|
80
|
+
classList.add(ACTIVE_DROP_TARGET_DENY_CLASS);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function observeRemovedFromParent(element: HTMLElement): Observable<void> {
|
|
87
|
+
return new Observable((subscriber) => {
|
|
88
|
+
const observer = new MutationObserver((event) => {
|
|
89
|
+
for (const mutation of event) {
|
|
90
|
+
// @ts-ignore
|
|
91
|
+
if ([...mutation.removedNodes].includes(element)) {
|
|
92
|
+
subscriber.next();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
// Wait for `element` to be attached to the DOM.
|
|
97
|
+
let timeout = setTimeout(() => {
|
|
98
|
+
timeout = null;
|
|
99
|
+
observer.observe(element.parentNode, { childList: true, subtree: false });
|
|
100
|
+
});
|
|
101
|
+
return () => {
|
|
102
|
+
// In case the element was destroyed before we attached the mutation observer, we cancel
|
|
103
|
+
// the observable returned by this function and don't attach the mutation observer.
|
|
104
|
+
if (timeout !== null) {
|
|
105
|
+
// TODO: Investigate elements that trigger the following warning.
|
|
106
|
+
//
|
|
107
|
+
// console.warn(
|
|
108
|
+
// 'Possible performance leak: the element got destroyed before it could be added to the DOM.',
|
|
109
|
+
// element,
|
|
110
|
+
// );
|
|
111
|
+
clearTimeout(timeout);
|
|
112
|
+
}
|
|
113
|
+
observer.disconnect();
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Directive, HostListener } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Directive({
|
|
4
|
+
selector: '[esFocusState]',
|
|
5
|
+
exportAs: 'esFocusState',
|
|
6
|
+
})
|
|
7
|
+
export class FocusStateDirective {
|
|
8
|
+
/** Either the element or one of its descendants has focus. */
|
|
9
|
+
hasFocus = false;
|
|
10
|
+
/** The element is being hovered with the cursor. */
|
|
11
|
+
hovering = false;
|
|
12
|
+
|
|
13
|
+
constructor() {}
|
|
14
|
+
|
|
15
|
+
@HostListener('focusin')
|
|
16
|
+
onFocusIn() {
|
|
17
|
+
this.hasFocus = true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@HostListener('focusout')
|
|
21
|
+
onFocusOut() {
|
|
22
|
+
this.hasFocus = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@HostListener('mouseenter')
|
|
26
|
+
onMouseOver() {
|
|
27
|
+
this.hovering = true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@HostListener('mouseleave')
|
|
31
|
+
onMouseOut() {
|
|
32
|
+
this.hovering = false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Created by Torsten on 13.01.2017.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
|
6
|
+
import { Directive, ElementRef, Input, OnInit, OnDestroy } from '@angular/core';
|
|
7
|
+
import { TranslateService } from '@ngx-translate/core';
|
|
8
|
+
import { ConfigService } from 'ngx-edu-sharing-api';
|
|
9
|
+
import { take } from 'rxjs/operators';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Replaces the element's content with an icon.
|
|
13
|
+
*
|
|
14
|
+
* Example: `<i esIcon="save"></i>`
|
|
15
|
+
*
|
|
16
|
+
* Optionally, a translated `aria-label` can be attached by setting `aria` to a truthy value: `<i
|
|
17
|
+
* esIcon="save" aria="true"></i>`. Otherwise, `aria-hidden` will be set.
|
|
18
|
+
*
|
|
19
|
+
* For backwards compatibility, the directive is also activated on elements that set
|
|
20
|
+
* `class="material-icons"`. This is mainly to set the `aria-hidden` attribute. Occurrences should
|
|
21
|
+
* be updated to the syntax above.
|
|
22
|
+
*/
|
|
23
|
+
@Directive({
|
|
24
|
+
selector: 'i[esIcon], i.material-icons',
|
|
25
|
+
})
|
|
26
|
+
export class IconDirective implements OnInit, OnDestroy {
|
|
27
|
+
private _id: string;
|
|
28
|
+
private _aria: boolean;
|
|
29
|
+
private iconsConfig: Array<{ original: string; replace?: string; cssClass?: string }>;
|
|
30
|
+
private altTextSpan: HTMLElement;
|
|
31
|
+
private isReady = false;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* An alt text to show to screen readers.
|
|
35
|
+
*
|
|
36
|
+
* If omitted, the icon will be invisible to screen readers.
|
|
37
|
+
*
|
|
38
|
+
* @see https://material.angular.io/components/icon/overview#indicator-icons
|
|
39
|
+
*/
|
|
40
|
+
@Input() set altText(altText: string) {
|
|
41
|
+
this.setAltText(altText);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** If true, an alt text (see above) will be set based on the icon. */
|
|
45
|
+
@Input() set aria(aria: boolean) {
|
|
46
|
+
aria = coerceBooleanProperty(aria);
|
|
47
|
+
if (aria !== this._aria) {
|
|
48
|
+
this._aria = aria;
|
|
49
|
+
if (this.isReady) {
|
|
50
|
+
this.updateAria();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Input() set esIcon(id: string) {
|
|
56
|
+
this.setIcon(id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
private element: ElementRef<HTMLElement>,
|
|
61
|
+
private translate: TranslateService,
|
|
62
|
+
private config: ConfigService,
|
|
63
|
+
) {}
|
|
64
|
+
|
|
65
|
+
async ngOnInit() {
|
|
66
|
+
this.isReady = true;
|
|
67
|
+
this.element.nativeElement.setAttribute('aria-hidden', 'true');
|
|
68
|
+
this.updateAria();
|
|
69
|
+
|
|
70
|
+
// FIXME: This might resolve after `setIcon` was called and mappings might be ignored.
|
|
71
|
+
await this.config.observeConfig().pipe(take(1)).toPromise();
|
|
72
|
+
this.iconsConfig = this.config.instant('icons', null);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ngOnDestroy(): void {
|
|
76
|
+
if (this.altTextSpan) {
|
|
77
|
+
this.altTextSpan.remove();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private setIcon(id: string) {
|
|
82
|
+
if (this._id) {
|
|
83
|
+
this.element.nativeElement.classList.remove(
|
|
84
|
+
'edu-icons',
|
|
85
|
+
'custom-icons',
|
|
86
|
+
'material-icons',
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
let customClass: string = null;
|
|
90
|
+
const mapping = this.iconsConfig?.filter((i) => i.original === id);
|
|
91
|
+
if (mapping?.length === 1) {
|
|
92
|
+
id = mapping[0].replace || '';
|
|
93
|
+
customClass = mapping[0].cssClass;
|
|
94
|
+
}
|
|
95
|
+
this._id = id;
|
|
96
|
+
if (this._aria) {
|
|
97
|
+
this.updateAria();
|
|
98
|
+
}
|
|
99
|
+
let cssClass: string;
|
|
100
|
+
if (id?.startsWith('edu-') && !customClass) {
|
|
101
|
+
cssClass = 'edu-icons';
|
|
102
|
+
id = id.substr(4);
|
|
103
|
+
} else if (id?.startsWith('custom-') || customClass) {
|
|
104
|
+
cssClass = 'custom-icons';
|
|
105
|
+
id = id.substr(7);
|
|
106
|
+
} else {
|
|
107
|
+
cssClass = 'material-icons';
|
|
108
|
+
}
|
|
109
|
+
this.element.nativeElement.classList.add(cssClass);
|
|
110
|
+
if (customClass) {
|
|
111
|
+
this.element.nativeElement.classList.add(customClass);
|
|
112
|
+
}
|
|
113
|
+
this.element.nativeElement.innerText = id;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private updateAria() {
|
|
117
|
+
if (this._aria !== undefined) {
|
|
118
|
+
if (this._aria && this._id) {
|
|
119
|
+
this.translate.get('ICON_LABELS.' + this._id).subscribe((lang) => {
|
|
120
|
+
this.setAltText(lang);
|
|
121
|
+
});
|
|
122
|
+
} else {
|
|
123
|
+
this.setAltText(null);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private setAltText(altText: string): void {
|
|
129
|
+
if (altText && !this.altTextSpan) {
|
|
130
|
+
this.insertAltTextSpan();
|
|
131
|
+
}
|
|
132
|
+
if (this.altTextSpan) {
|
|
133
|
+
this.altTextSpan.innerText = altText;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private insertAltTextSpan(): void {
|
|
138
|
+
this.altTextSpan = document.createElement('span');
|
|
139
|
+
this.altTextSpan.classList.add('cdk-visually-hidden');
|
|
140
|
+
this.element.nativeElement.insertAdjacentElement('afterend', this.altTextSpan);
|
|
141
|
+
}
|
|
142
|
+
}
|