@talrace/ngx-noder 0.0.47 → 19.0.24

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/README.md CHANGED
@@ -1,649 +1,650 @@
1
- # NgxNoder
2
-
3
- ## Description
4
-
5
- Rich Text Editor for Angular with basic WORD features and ability to create and use your own custom components. The package also provides customizable toolbar, header, custom search in editor and basic interfaces of custom components which you can overwrite to suit your needs in your application.
6
-
7
- #### You can get information about the API and more NgxNoder examples from the [documentation].
8
-
9
- ## Installation
10
-
11
- Run this command to add ngx-noder package to your project
12
-
13
- ```console
14
- npm i @talrace/ngx-noder
15
- ```
16
-
17
- ### Translation
18
-
19
- NgxNoder uses @ngx-translate for its translations. Its should be included to list of static application assets and application should import TranslateModule. NgxNoder translates are loaded by NoderTranslateLoader.
20
-
21
- angular.json
22
-
23
- ```typescript
24
- "assets": [
25
- ...
26
- { "glob": "**/*", "input": "node_modules/@talrace/ngx-noder/assets", "output": "assets" }
27
- ],
28
- ```
29
-
30
- app.module.ts
31
-
32
- ```typescript
33
- import { TranslateModule } from '@ngx-translate/core';
34
-
35
- @NgModule({
36
- imports: [
37
- ...
38
- TranslateModule.forRoot()
39
- ],
40
- ...
41
- })
42
- ```
43
-
44
- NgxNoder translates loading
45
-
46
- app.module.ts (if application do not have own translations, preferred language can be set by the 'use' method of TranslateService)
47
-
48
- ```typescript
49
- import { NoderTranslateLoader } from '@talrace/ngx-noder';
50
- import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
51
-
52
- @NgModule({
53
- imports: [
54
- ...
55
- TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: NoderTranslateLoader } })
56
- ],
57
- ...
58
- })
59
- ```
60
-
61
- service or state where translation is controlled (if application have own translations)
62
-
63
- ```typescript
64
- import { NoderTranslateLoader } from '@talrace/ngx-noder';
65
- import { TranslateService } from '@ngx-translate/core';
66
- ...
67
- constructor(private translateService: TranslateService, private noderTranslateLoader: NoderTranslateLoader) {}
68
- ...
69
- this.noderTranslateLoader
70
- .getTranslation(language)
71
- .pipe(take(1))
72
- .subscribe(x => this.translateService.setTranslation(language, x, true))
73
- ```
74
-
75
- ### Editor
76
-
77
- Import editor module. This is basic module and provides creating, editing and viewing document functionality.
78
-
79
- ```typescript
80
- import { EditorModule } from '@talrace/ngx-noder';
81
-
82
- @NgModule({
83
- ...
84
- imports: [
85
- EditorModule.forRoot({
86
-             elementService: ElementApiService,
87
-             imageApiService: ImageUploadService,
88
-             sidenav: { autoFocus: true }
89
-         }),
90
- ],
91
- ...
92
- })
93
- export class SomeModule{}
94
- ```
95
-
96
- ### Editor Toolbar
97
-
98
- Import editor toolbar module, if you need toolbar for your editor. This module provides ready-made toolbar with buttons that covers all basic logic of the document content editing functionality.
99
-
100
- ```typescript
101
- import { EditorToolbarModule } from '@talrace/ngx-noder';
102
-
103
- @NgModule({
104
- // ...
105
- imports: [EditorToolbarModule]
106
- // ...
107
- })
108
- export class SomeModule {}
109
- ```
110
-
111
- ### Editor Title
112
-
113
- Import standalone editor title components to display and edit the document title line and to change the document editing mode (editing, filling, viewing).
114
-
115
- ```typescript
116
- import { EditorTitleComponent, EditorTitleMobileComponent } from '@talrace/ngx-noder';
117
- @NgModule({
118
- // ...
119
- imports: [EditorTitleComponent, EditorTitleMobileComponent]
120
- // ...
121
- })
122
- export class SomeModule {}
123
- ```
124
-
125
- ### Editor Search
126
-
127
- Import standalone editor search component, if you need custom search in your editor. This component provides custom search component and search and replace functionality in editor.
128
-
129
- ```typescript
130
- import { EditorSearchDialogComponent } from '@talrace/ngx-noder';
131
-
132
- @NgModule({
133
- // ...
134
- imports: [EditorSearchDialogComponent]
135
- // ...
136
- })
137
- export class SomeModule {}
138
- ```
139
-
140
- ### Editor Service
141
-
142
- Provide Editor Service in your component with editor if you need to send commands to editor or gets some document status data. This service provides functionality of send data or commands to the editor and receive document state data or document interaction mode
143
-
144
- ```typescript
145
- import { EditorService } from '@talrace/ngx-noder';
146
-
147
- @NgModule({
148
- // ...
149
- provides: [EditorService]
150
- // ...
151
- })
152
- export class SomeModule {}
153
- ```
154
-
155
- ## Usage
156
-
157
- Example of full-featured NgxNoder with toolbar, header, custom search and receiving commands from editor.
158
-
159
- angular.json
160
-
161
- ```typescript
162
- "assets": [
163
- ...
164
- { "glob": "**/*", "input": "node_modules/@talrace/ngx-noder/assets", "output": "assets" }
165
- ],
166
- ```
167
-
168
- app.module.ts
169
-
170
- ```typescript
171
- import { AppComponent } from './app.component';
172
- import {
173
- EditorModule,
174
- EditorSearchDialogComponent,
175
- EditorSearchDialogComponent,
176
- EditorTitleComponent,
177
- EditorTitleMobileComponent,
178
- EditorToolbarModule
179
- } from '@talrace/ngx-noder';
180
- import { NgModule } from '@angular/core';
181
- import { NoderTranslateLoader } from '@talrace/ngx-noder';
182
- import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
183
-
184
- @NgModule({
185
- declarations: [AppComponent],
186
- imports: [
187
-
188
- EditorModule.forRoot({
189
-             elementService: ElementApiService,
190
-             imageApiService: ImageUploadService,
191
-             sidenav: { autoFocus: true }
192
-         }),
193
- EditorSearchDialogComponent,
194
- EditorTitleComponent,
195
- EditorTitleMobileComponent,
196
- EditorToolbarModule,
197
- TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: NoderTranslateLoader } })
198
-
199
- ],
200
- providers: [EditorService, ElementApiService, ImageUploadService],
201
- bootstrap: [AppComponent]
202
- })
203
-
204
- export class AppModule{}
205
- ```
206
-
207
- app.component.html
208
-
209
- ```html
210
- <div class="”header”">
211
- <app-nod-editor-mobile-toolbar
212
- *ngIf="isMobile; else desktopToolbar"
213
- (openFileFromDisk)="fileInput.click()"
214
- (addCustomElement)="addCustomElement($event)"
215
- (insertPageBreak)="onInsertPageBreak()"
216
- (createDocument)="createDocument()"
217
- (saveAs)="onSave()"
218
- (print)="onPrint()"
219
- (delete)="onDelete()"
220
- (rename)="rename$.next()"
221
- (undo)="onUndo()"
222
- (redo)="onRedo()"
223
- (insertImage)="imageFileInput.click()"
224
- (insertLink)="onInsertLink()"
225
- (insertTable)="onInsertTableMobile()"
226
- (createElement)="createElement($event)"
227
- (changeParagraphStyle)="onSetParagraphStyle($event)"
228
- (changeTextStyle)="onSetTextStyle($event)"
229
- (textFormat)="onTextFormatMobile()" />
230
- <ng-template #desktopToolbar>
231
- <app-nod-editor-toolbar
232
- (openFileFromDisk)="fileInput.click()"
233
- (addCustomElement)="addCustomElement($event)"
234
- (insertPageBreak)="onInsertPageBreak()"
235
- (createDocument)="createDocument()"
236
- (saveAs)="onSave()"
237
- (print)="onPrint()"
238
- (delete)="onDelete()"
239
- (rename)="rename$.next()"
240
- (undo)="onUndo()"
241
- (redo)="onRedo()"
242
- (openEditMenu)="onOpenEditMenu()"
243
- (cutSelected)="onCutSelected()"
244
- (copySelected)="onCopySelected()"
245
- (pasteClipboardData)="onPasteClipboardData()"
246
- (selectAll)="onSelectAll()"
247
- (removeSelected)="onRemoveSelected()"
248
- (insertImage)="imageFileInput.click()"
249
- (insertLink)="onInsertLink()"
250
- (insertTable)="onInsertTable($event)"
251
- (createElement)="createElement($event)"
252
- (changeParagraphStyle)="onSetParagraphStyle($event)"
253
- (changeTextStyle)="onSetTextStyle($event)"
254
- (setNumberingTemplateType)="onSetNumberingTemplateType($event)"
255
- (removeNumberings)="onRemoveNumberings()" />
256
- </ng-template>
257
- </div>
258
- <app-nod-editor
259
- #editor
260
- [isMobile]="isMobile"
261
- [customPageWidth]="customPageWidth"
262
- [content]="content$ | async" />
263
- <ng-container *ngIf="{ mode: mode$ | async } as modeData">
264
- <ng-container *ngIf="isMobile; else desktopTitle">
265
- <app-nod-editor-title-mobile
266
- *ngIf="showTitleModes.includes(editorToolbarService.mode$ | async)"
267
- [selectedMode]="modeData.mode"
268
- [title]="title"
269
- [rename$]="rename$.asObservable()"
270
- (changeMode)="onModeChanged($event)"
271
- (renameDocumentTitle)="onRenameDocumentTitle($event)" />
272
- </ng-container>
273
- <ng-template #desktopTitle>
274
- <app-nod-editor-title
275
- [selectedMode]="modeData.mode"
276
- [title]="title"
277
- [rename$]="rename$.asObservable()"
278
- (changeMode)="onModeChanged($event)"
279
- (renameDocumentTitle)="onRenameDocumentTitle($event)" />
280
- </ng-template>
281
- </ng-container>
282
- <input
283
- #fileInput
284
- class="upload-input"
285
- accept=".docx"
286
- type="file"
287
- id="file-input"
288
- (change)="onConvertFile($event)" />
289
- <input
290
- #imageFileInput
291
- class="upload-input"
292
- accept="image/gif, image/jpeg, image/png, image/bmp, image/webp"
293
- type="file"
294
- id="image-file-input"
295
- (change)="onInsertImage($event)" />
296
- ```
297
-
298
- app.component.ts
299
-
300
- ```typescript
301
- import { Actions, ofActionDispatched, Store } from '@ngxs/store';
302
- import { ActivatedRoute } from '@angular/router';
303
- import { BehaviorSubject, filter, Observable, Subject, take, takeUntil, tap } from 'rxjs';
304
- import {
305
- AddLinkDialogComponent,
306
- AddLinkMobileComponent,
307
- BreakTypes,
308
- CommandsService,
309
- DestroyComponent,
310
- DocxModel,
311
- EditorComponent,
312
- EditorSearchDialogComponent,
313
- EditorService,
314
- EditorToolbarMode,
315
- EditorToolbarService,
316
- ElementDataModel,
317
- Mode,
318
- NumberingLevelModel,
319
- ParagraphStyleModel,
320
- RevisionHelper,
321
- TextFormatMobileComponent,
322
- TextStyleModel
323
- } from '@talrace/ngx-noder';
324
- import { Component, ElementRef, ViewChild } from '@angular/core';
325
- import { MatDialog } from '@angular/material/dialog';
326
- import { Navigate } from '@ngxs/router-plugin';
327
-
328
- import { ClearRevision, ConvertFile, OpenDocx, RenameDocument, SaveDocument, SetMode } from '../../store/noder/noder.actions';
329
- import { CreateOperation } from '../../store/operations/operations.actions';
330
- import { DeleteDocuments } from '../../../documents/store/documents/documents.actions';
331
- import { FileSaveDialogComponent } from '../file-save/file-save-dialog.component';
332
- import { ImageUploadService } from '../../services/image-upload.service';
333
- import { LayoutState } from '../../../+shared/layout/store/layout.state';
334
- import { NoderState } from '../../store/noder/noder.state';
335
-
336
- @Component({
337
- selector: 'app-root',
338
- templateUrl: './app.component.html',
339
- styleUrls: ['./app.component.scss']
340
- })
341
- export class AppComponent extends DestroyComponent {
342
- mode$ = this.store.select(NoderState.mode);
343
- rename$ = new Subject<void>();
344
- title: string;
345
- documentId: number;
346
- isMobile = false;
347
- get content$(): Observable<DocxModel> {
348
- return this._content$.asObservable();
349
- }
350
- private _content$ = new BehaviorSubject<DocxModel>(RevisionHelper.getEmptyDocxModel());
351
- @ViewChild('editor', { static: true }) editor: EditorComponent;
352
- @ViewChild('fileInput', { static: true }) fileInput: ElementRef<HTMLInputElement>;
353
- readonly customPageWidth = this.store.selectSnapshot(LayoutState.isMobile) ? window.innerWidth : null;
354
- readonly showTitleModes = [
355
- EditorToolbarMode.Base,
356
- EditorToolbarMode.TextFormat,
357
- EditorToolbarMode.StyleFormat,
358
- EditorToolbarMode.AlignFormat
359
- ];
360
-
361
- constructor(
362
- private actions$: Actions,
363
- private commandService: CommandsService,
364
- private dialog: MatDialog,
365
- private editorService: EditorService,
366
- private imageUploadService: ImageUploadService,
367
- private route: ActivatedRoute,
368
- private store: Store,
369
- public editorToolbarService: EditorToolbarService
370
- ) {
371
- super();
372
- const mode: Mode = this.store.selectSnapshot(NoderState.mode);
373
- this.editorService.setIsViewOnly(mode !== Mode.Edit);
374
- }
375
-
376
- ngOnInit() {
377
- this.isMobileSubscription();
378
- this.openCloseSearchSubscription();
379
- this.routeDataSubscription();
380
- this.documentTitleSubscription();
381
- this.documentIdSubscription();
382
- this.createCommandSubscription();
383
- this.editorHotKeyDownSubscription();
384
- }
385
-
386
- createDocument(): void {
387
- const documentId = +this.route.snapshot.params['id'];
388
- if (documentId) {
389
- this.store.dispatch([new ClearRevision(), new Navigate([''])]);
390
- } else {
391
- this.store.dispatch(new ClearRevision());
392
- this._content$.next(RevisionHelper.getEmptyDocxModel());
393
- }
394
- }
395
-
396
- onConvertFile(event: any): void {
397
- if (!event?.target?.files?.length) {
398
- return;
399
- }
400
- let file: File = event.target.files[0];
401
- if (!file.type.match('application/vnd.openxmlformats-officedocument.wordprocessingml.document')) {
402
- return;
403
- }
404
- this.store.dispatch(new ConvertFile(file as Blob));
405
- this.actions$
406
- .pipe(ofActionDispatched(OpenDocx), take(1))
407
- .subscribe(payload => this.store.dispatch(new Navigate([`noder/${payload.documentId}`])));
408
- event.target.value = null;
409
- }
410
-
411
- onSave(): void {
412
- const dialogRef = this.dialog.open(FileSaveDialogComponent, {
413
- panelClass: 'file-save-dialog',
414
- height: '230px',
415
- width: '350px',
416
- data: { documentName: '' }
417
- });
418
- dialogRef.afterClosed().subscribe((name: string) => {
419
- if (!name?.length) {
420
- return;
421
- }
422
- this.store.dispatch(new SaveDocument(name, this.editor.content));
423
- });
424
- }
425
-
426
- onPrint(): void {
427
- this.editorService.print();
428
- }
429
-
430
- onDelete(): void {
431
- const documentId = this.store.selectSnapshot(NoderState.documentId);
432
- this.dialog
433
- .open<ConfirmDialogComponent>(ConfirmDialogComponent, {
434
- autoFocus: false,
435
- restoreFocus: false,
436
- data: <IDialogData>{ message: 'Are you sure you want to delete?' }
437
- })
438
- .afterClosed()
439
- .pipe(filter(x => !!x))
440
- .subscribe(() => {
441
- const actions: any = [new Navigate(['noder', 'new'])];
442
- if (documentId) {
443
- actions.unshift(new DeleteDocuments([documentId]));
444
- }
445
- this.store.dispatch(actions);
446
- });
447
- }
448
-
449
- onInsertPageBreak(): void {
450
- this.editorService.insertBreak(BreakTypes.Page);
451
- }
452
-
453
- async onInsertImage(event: any): Promise<void> {
454
- if (!event?.target?.files?.length) {
455
- return;
456
- }
457
- const file = event.target.files[0];
458
- if (!'image/gif, image/jpeg, image/png, image/bmp, image/webp'.match(file.type as string)) {
459
- return;
460
- }
461
- const model = await this.imageUploadService.uploadImage(file as Blob);
462
- this.editorService.insertImage(model);
463
- event.target.value = null;
464
- }
465
-
466
- onInsertTable(model: { rows: number; columns: number }): void {
467
- this.editorService.insertTable(model);
468
- }
469
-
470
- onInsertTableMobile(): void {
471
- this.editorService.openSidenav(InsertTableMobileComponent);
472
- }
473
-
474
- onSetTextStyle(value: TextStyleModel): void {
475
- this.editorService.setTextStyles(value);
476
- }
477
-
478
- onSetNumberingTemplateType(value: NumberingLevelModel[]): void {
479
- this.editorService.setNumberingTemplateType(value);
480
- }
481
-
482
- onRemoveNumberings(): void {
483
- this.editorService.removeNumberings();
484
- }
485
-
486
- onInsertLink(): void {
487
- if (this.isMobile) {
488
- this.editorService.openSidenav(AddLinkMobileComponent);
489
- } else {
490
- this.dialog
491
- .open(AddLinkDialogComponent, { panelClass: 'add-link-dialog' })
492
- .afterClosed()
493
- .subscribe((result: any) => {
494
- if (result) {
495
- this.editorService.insertLink(result.text as string, result.link as string);
496
- }
497
- });
498
- }
499
- }
500
-
501
- addCustomElement(model: ElementDataModel): void {
502
- this.editorService.createCustomComponent(model);
503
- }
504
-
505
- onSetParagraphStyle(value: ParagraphStyleModel): void {
506
- this.editorService.setParagraphStyles(value);
507
- }
508
-
509
- onModeChanged(value: Mode): void {
510
- this.store.dispatch(new SetMode(value));
511
- this.editorService.setIsViewOnly(value !== Mode.Edit);
512
- }
513
-
514
- onUndo(): void {
515
- this.editorService.undo();
516
- }
517
-
518
- onRedo(): void {
519
- this.editorService.redo();
520
- }
521
-
522
- async onOpenEditMenu(): Promise<void> {
523
- const read = await navigator.permissions.query({ name: 'clipboard-read' as PermissionName });
524
- if (read.state != 'denied' && navigator.clipboard) {
525
- navigator.clipboard
526
- .readText()
527
- .then(value => {
528
- return this.editorService.setClipboardData(value);
529
- })
530
- .catch(() => {
531
- // continue regardless error
532
- });
533
- }
534
- }
535
-
536
- onCutSelected(): void {
537
- this.editorService.cutSelected();
538
- }
539
-
540
- onCopySelected(): void {
541
- this.editorService.copySelected();
542
- }
543
-
544
- onPasteClipboardData(): void {
545
- this.editorService.pasteFromClipboard();
546
- }
547
-
548
- onSelectAll(): void {
549
- this.editorService.selectAll();
550
- }
551
-
552
- onRemoveSelected(): void {
553
- this.editorService.removeSelected();
554
- }
555
-
556
- onRenameDocumentTitle(title: string): void {
557
- this.store.dispatch(new RenameDocument(new DocumentNameModel({ documentId: this.documentId, name: title })));
558
- }
559
-
560
- createElement(model: ElementDataModel) {
561
- this.editor.editor.createCustomElement(model);
562
- this.editor.editor.focus();
563
- }
564
-
565
- onTextFormatMobile(): void {
566
- this.editorService.openSidenav(TextFormatMobileComponent);
567
- }
568
-
569
- onEditorHotKeyDown(event: KeyboardEvent): void {
570
- if (event.ctrlKey && event.code === 'KeyO') {
571
- this.fileInput.nativeElement.click();
572
- } else if (event.ctrlKey && event.code === 'KeyK') {
573
- this.onInsertLink();
574
- } else {
575
- return;
576
- }
577
- event.preventDefault();
578
- }
579
-
580
- private onOpenSearch(): void {
581
- this.dialog.open(EditorSearchDialogComponent, {
582
- position: { top: '0px', right: '0px' },
583
- hasBackdrop: false
584
- });
585
- }
586
-
587
- private isMobileSubscription(): void {
588
- this.store
589
- .select(LayoutState.isMobile)
590
- .pipe(takeUntil(this.destroy$))
591
- .subscribe(x => (this.isMobile = x));
592
- }
593
-
594
- private openCloseSearchSubscription(): void {
595
- this.editorService.openSearchDialog$.pipe(takeUntil(this.destroy$)).subscribe(value => {
596
- value ? this.onOpenSearch() : this.dialog.closeAll();
597
- });
598
- }
599
-
600
- private routeDataSubscription(): void {
601
- this.route.data.pipe(takeUntil(this.destroy$)).subscribe(({ content }) => this._content$.next(content as DocxModel));
602
- }
603
-
604
- private documentTitleSubscription(): void {
605
- this.store
606
- .select(NoderState.title)
607
- .pipe(takeUntil(this.destroy$))
608
- .subscribe(x => (this.title = x ?? 'Untitled document'));
609
- }
610
-
611
- private documentIdSubscription(): void {
612
- this.store
613
- .select(NoderState.documentId)
614
- .pipe(takeUntil(this.destroy$))
615
- .subscribe(x => (this.documentId = x));
616
- }
617
-
618
- private createCommandSubscription(): void {
619
- this.commandService.createCommand$
620
- .pipe(
621
- filter(x => !!x),
622
- tap(command => {
623
- this.store.dispatch(new CreateOperation(command));
624
- }),
625
- takeUntil(this.destroy$)
626
- )
627
- .subscribe();
628
- }
629
-
630
- private editorHotKeyDownSubscription(): void {
631
- this.editorService.keyDown$.pipe(takeUntil(this.destroy$)).subscribe(event => this.onEditorHotKeyDown(event));
632
- }
633
- }
634
- ```
635
-
636
- app.component.scss
637
-
638
- ```scss
639
- // styles of your app component
640
- :host {
641
- height: inherit;
642
- width: 100%;
643
- display: flex;
644
- flex-direction: column;
645
- overflow: hidden;
646
- }
647
- ```
648
-
649
- [documentation]: https://npmjs.talrace.com/text/ngx-noder
1
+ # NgxNoder
2
+
3
+ ## Description
4
+
5
+ Rich Text Editor for Angular with basic WORD features and ability to create and use your own custom components. The package also provides customizable toolbar, header, custom search in editor and basic interfaces of custom components which you can overwrite to suit your needs in your application.
6
+ Current version fist 2 element verion angular
7
+
8
+ #### You can get information about the API and more NgxNoder examples from the [documentation].
9
+
10
+ ## Installation
11
+
12
+ Run this command to add ngx-noder package to your project
13
+
14
+ ```console
15
+ npm i @talrace/ngx-noder
16
+ ```
17
+
18
+ ### Translation
19
+
20
+ NgxNoder uses @ngx-translate for its translations. Its should be included to list of static application assets and application should import TranslateModule. NgxNoder translates are loaded by NoderTranslateLoader.
21
+
22
+ angular.json
23
+
24
+ ```typescript
25
+ "assets": [
26
+ ...
27
+ { "glob": "**/*", "input": "node_modules/@talrace/ngx-noder/assets", "output": "assets" }
28
+ ],
29
+ ```
30
+
31
+ app.module.ts
32
+
33
+ ```typescript
34
+ import { TranslateModule } from '@ngx-translate/core';
35
+
36
+ @NgModule({
37
+ imports: [
38
+ ...
39
+ TranslateModule.forRoot()
40
+ ],
41
+ ...
42
+ })
43
+ ```
44
+
45
+ NgxNoder translates loading
46
+
47
+ app.module.ts (if application do not have own translations, preferred language can be set by the 'use' method of TranslateService)
48
+
49
+ ```typescript
50
+ import { NoderTranslateLoader } from '@talrace/ngx-noder';
51
+ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
52
+
53
+ @NgModule({
54
+ imports: [
55
+ ...
56
+ TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: NoderTranslateLoader } })
57
+ ],
58
+ ...
59
+ })
60
+ ```
61
+
62
+ service or state where translation is controlled (if application have own translations)
63
+
64
+ ```typescript
65
+ import { NoderTranslateLoader } from '@talrace/ngx-noder';
66
+ import { TranslateService } from '@ngx-translate/core';
67
+ ...
68
+ constructor(private translateService: TranslateService, private noderTranslateLoader: NoderTranslateLoader) {}
69
+ ...
70
+ this.noderTranslateLoader
71
+ .getTranslation(language)
72
+ .pipe(take(1))
73
+ .subscribe(x => this.translateService.setTranslation(language, x, true))
74
+ ```
75
+
76
+ ### Editor
77
+
78
+ Import editor module. This is basic module and provides creating, editing and viewing document functionality.
79
+
80
+ ```typescript
81
+ import { EditorModule } from '@talrace/ngx-noder';
82
+
83
+ @NgModule({
84
+ ...
85
+ imports: [
86
+ EditorModule.forRoot({
87
+             elementService: ElementApiService,
88
+             imageApiService: ImageUploadService,
89
+             sidenav: { autoFocus: true }
90
+         }),
91
+ ],
92
+ ...
93
+ })
94
+ export class SomeModule{}
95
+ ```
96
+
97
+ ### Editor Toolbar
98
+
99
+ Import editor toolbar module, if you need toolbar for your editor. This module provides ready-made toolbar with buttons that covers all basic logic of the document content editing functionality.
100
+
101
+ ```typescript
102
+ import { EditorToolbarModule } from '@talrace/ngx-noder';
103
+
104
+ @NgModule({
105
+ // ...
106
+ imports: [EditorToolbarModule]
107
+ // ...
108
+ })
109
+ export class SomeModule {}
110
+ ```
111
+
112
+ ### Editor Title
113
+
114
+ Import standalone editor title components to display and edit the document title line and to change the document editing mode (editing, filling, viewing).
115
+
116
+ ```typescript
117
+ import { EditorTitleComponent, EditorTitleMobileComponent } from '@talrace/ngx-noder';
118
+ @NgModule({
119
+ // ...
120
+ imports: [EditorTitleComponent, EditorTitleMobileComponent]
121
+ // ...
122
+ })
123
+ export class SomeModule {}
124
+ ```
125
+
126
+ ### Editor Search
127
+
128
+ Import standalone editor search component, if you need custom search in your editor. This component provides custom search component and search and replace functionality in editor.
129
+
130
+ ```typescript
131
+ import { EditorSearchDialogComponent } from '@talrace/ngx-noder';
132
+
133
+ @NgModule({
134
+ // ...
135
+ imports: [EditorSearchDialogComponent]
136
+ // ...
137
+ })
138
+ export class SomeModule {}
139
+ ```
140
+
141
+ ### Editor Service
142
+
143
+ Provide Editor Service in your component with editor if you need to send commands to editor or gets some document status data. This service provides functionality of send data or commands to the editor and receive document state data or document interaction mode
144
+
145
+ ```typescript
146
+ import { EditorService } from '@talrace/ngx-noder';
147
+
148
+ @NgModule({
149
+ // ...
150
+ provides: [EditorService]
151
+ // ...
152
+ })
153
+ export class SomeModule {}
154
+ ```
155
+
156
+ ## Usage
157
+
158
+ Example of full-featured NgxNoder with toolbar, header, custom search and receiving commands from editor.
159
+
160
+ angular.json
161
+
162
+ ```typescript
163
+ "assets": [
164
+ ...
165
+ { "glob": "**/*", "input": "node_modules/@talrace/ngx-noder/assets", "output": "assets" }
166
+ ],
167
+ ```
168
+
169
+ app.module.ts
170
+
171
+ ```typescript
172
+ import { AppComponent } from './app.component';
173
+ import {
174
+ EditorModule,
175
+ EditorSearchDialogComponent,
176
+ EditorSearchDialogComponent,
177
+ EditorTitleComponent,
178
+ EditorTitleMobileComponent,
179
+ EditorToolbarModule
180
+ } from '@talrace/ngx-noder';
181
+ import { NgModule } from '@angular/core';
182
+ import { NoderTranslateLoader } from '@talrace/ngx-noder';
183
+ import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
184
+
185
+ @NgModule({
186
+ declarations: [AppComponent],
187
+ imports: [
188
+
189
+ EditorModule.forRoot({
190
+             elementService: ElementApiService,
191
+             imageApiService: ImageUploadService,
192
+             sidenav: { autoFocus: true }
193
+         }),
194
+ EditorSearchDialogComponent,
195
+ EditorTitleComponent,
196
+ EditorTitleMobileComponent,
197
+ EditorToolbarModule,
198
+ TranslateModule.forRoot({ loader: { provide: TranslateLoader, useClass: NoderTranslateLoader } })
199
+
200
+ ],
201
+ providers: [EditorService, ElementApiService, ImageUploadService],
202
+ bootstrap: [AppComponent]
203
+ })
204
+
205
+ export class AppModule{}
206
+ ```
207
+
208
+ app.component.html
209
+
210
+ ```html
211
+ <div class="”header”">
212
+ <app-nod-editor-mobile-toolbar
213
+ *ngIf="isMobile; else desktopToolbar"
214
+ (openFileFromDisk)="fileInput.click()"
215
+ (addCustomElement)="addCustomElement($event)"
216
+ (insertPageBreak)="onInsertPageBreak()"
217
+ (createDocument)="createDocument()"
218
+ (saveAs)="onSave()"
219
+ (print)="onPrint()"
220
+ (delete)="onDelete()"
221
+ (rename)="rename$.next()"
222
+ (undo)="onUndo()"
223
+ (redo)="onRedo()"
224
+ (insertImage)="imageFileInput.click()"
225
+ (insertLink)="onInsertLink()"
226
+ (insertTable)="onInsertTableMobile()"
227
+ (createElement)="createElement($event)"
228
+ (changeParagraphStyle)="onSetParagraphStyle($event)"
229
+ (changeTextStyle)="onSetTextStyle($event)"
230
+ (textFormat)="onTextFormatMobile()" />
231
+ <ng-template #desktopToolbar>
232
+ <app-nod-editor-toolbar
233
+ (openFileFromDisk)="fileInput.click()"
234
+ (addCustomElement)="addCustomElement($event)"
235
+ (insertPageBreak)="onInsertPageBreak()"
236
+ (createDocument)="createDocument()"
237
+ (saveAs)="onSave()"
238
+ (print)="onPrint()"
239
+ (delete)="onDelete()"
240
+ (rename)="rename$.next()"
241
+ (undo)="onUndo()"
242
+ (redo)="onRedo()"
243
+ (openEditMenu)="onOpenEditMenu()"
244
+ (cutSelected)="onCutSelected()"
245
+ (copySelected)="onCopySelected()"
246
+ (pasteClipboardData)="onPasteClipboardData()"
247
+ (selectAll)="onSelectAll()"
248
+ (removeSelected)="onRemoveSelected()"
249
+ (insertImage)="imageFileInput.click()"
250
+ (insertLink)="onInsertLink()"
251
+ (insertTable)="onInsertTable($event)"
252
+ (createElement)="createElement($event)"
253
+ (changeParagraphStyle)="onSetParagraphStyle($event)"
254
+ (changeTextStyle)="onSetTextStyle($event)"
255
+ (setNumberingTemplateType)="onSetNumberingTemplateType($event)"
256
+ (removeNumberings)="onRemoveNumberings()" />
257
+ </ng-template>
258
+ </div>
259
+ <app-nod-editor
260
+ #editor
261
+ [isMobile]="isMobile"
262
+ [customPageWidth]="customPageWidth"
263
+ [content]="content$ | async" />
264
+ <ng-container *ngIf="{ mode: mode$ | async } as modeData">
265
+ <ng-container *ngIf="isMobile; else desktopTitle">
266
+ <app-nod-editor-title-mobile
267
+ *ngIf="showTitleModes.includes(editorToolbarService.mode$ | async)"
268
+ [selectedMode]="modeData.mode"
269
+ [title]="title"
270
+ [rename$]="rename$.asObservable()"
271
+ (changeMode)="onModeChanged($event)"
272
+ (renameDocumentTitle)="onRenameDocumentTitle($event)" />
273
+ </ng-container>
274
+ <ng-template #desktopTitle>
275
+ <app-nod-editor-title
276
+ [selectedMode]="modeData.mode"
277
+ [title]="title"
278
+ [rename$]="rename$.asObservable()"
279
+ (changeMode)="onModeChanged($event)"
280
+ (renameDocumentTitle)="onRenameDocumentTitle($event)" />
281
+ </ng-template>
282
+ </ng-container>
283
+ <input
284
+ #fileInput
285
+ class="upload-input"
286
+ accept=".docx"
287
+ type="file"
288
+ id="file-input"
289
+ (change)="onConvertFile($event)" />
290
+ <input
291
+ #imageFileInput
292
+ class="upload-input"
293
+ accept="image/gif, image/jpeg, image/png, image/bmp, image/webp"
294
+ type="file"
295
+ id="image-file-input"
296
+ (change)="onInsertImage($event)" />
297
+ ```
298
+
299
+ app.component.ts
300
+
301
+ ```typescript
302
+ import { Actions, ofActionDispatched, Store } from '@ngxs/store';
303
+ import { ActivatedRoute } from '@angular/router';
304
+ import { BehaviorSubject, filter, Observable, Subject, take, takeUntil, tap } from 'rxjs';
305
+ import {
306
+ AddLinkDialogComponent,
307
+ AddLinkMobileComponent,
308
+ BreakTypes,
309
+ CommandsService,
310
+ DestroyComponent,
311
+ DocxModel,
312
+ EditorComponent,
313
+ EditorSearchDialogComponent,
314
+ EditorService,
315
+ EditorToolbarMode,
316
+ EditorToolbarService,
317
+ ElementDataModel,
318
+ Mode,
319
+ NumberingLevelModel,
320
+ ParagraphStyleModel,
321
+ RevisionHelper,
322
+ TextFormatMobileComponent,
323
+ TextStyleModel
324
+ } from '@talrace/ngx-noder';
325
+ import { Component, ElementRef, ViewChild } from '@angular/core';
326
+ import { MatDialog } from '@angular/material/dialog';
327
+ import { Navigate } from '@ngxs/router-plugin';
328
+
329
+ import { ClearRevision, ConvertFile, OpenDocx, RenameDocument, SaveDocument, SetMode } from '../../store/noder/noder.actions';
330
+ import { CreateOperation } from '../../store/operations/operations.actions';
331
+ import { DeleteDocuments } from '../../../documents/store/documents/documents.actions';
332
+ import { FileSaveDialogComponent } from '../file-save/file-save-dialog.component';
333
+ import { ImageUploadService } from '../../services/image-upload.service';
334
+ import { LayoutState } from '../../../+shared/layout/store/layout.state';
335
+ import { NoderState } from '../../store/noder/noder.state';
336
+
337
+ @Component({
338
+ selector: 'app-root',
339
+ templateUrl: './app.component.html',
340
+ styleUrls: ['./app.component.scss']
341
+ })
342
+ export class AppComponent extends DestroyComponent {
343
+ mode$ = this.store.select(NoderState.mode);
344
+ rename$ = new Subject<void>();
345
+ title: string;
346
+ documentId: number;
347
+ isMobile = false;
348
+ get content$(): Observable<DocxModel> {
349
+ return this._content$.asObservable();
350
+ }
351
+ private _content$ = new BehaviorSubject<DocxModel>(RevisionHelper.getEmptyDocxModel());
352
+ @ViewChild('editor', { static: true }) editor: EditorComponent;
353
+ @ViewChild('fileInput', { static: true }) fileInput: ElementRef<HTMLInputElement>;
354
+ readonly customPageWidth = this.store.selectSnapshot(LayoutState.isMobile) ? window.innerWidth : null;
355
+ readonly showTitleModes = [
356
+ EditorToolbarMode.Base,
357
+ EditorToolbarMode.TextFormat,
358
+ EditorToolbarMode.StyleFormat,
359
+ EditorToolbarMode.AlignFormat
360
+ ];
361
+
362
+ constructor(
363
+ private actions$: Actions,
364
+ private commandService: CommandsService,
365
+ private dialog: MatDialog,
366
+ private editorService: EditorService,
367
+ private imageUploadService: ImageUploadService,
368
+ private route: ActivatedRoute,
369
+ private store: Store,
370
+ public editorToolbarService: EditorToolbarService
371
+ ) {
372
+ super();
373
+ const mode: Mode = this.store.selectSnapshot(NoderState.mode);
374
+ this.editorService.setIsViewOnly(mode !== Mode.Edit);
375
+ }
376
+
377
+ ngOnInit() {
378
+ this.isMobileSubscription();
379
+ this.openCloseSearchSubscription();
380
+ this.routeDataSubscription();
381
+ this.documentTitleSubscription();
382
+ this.documentIdSubscription();
383
+ this.createCommandSubscription();
384
+ this.editorHotKeyDownSubscription();
385
+ }
386
+
387
+ createDocument(): void {
388
+ const documentId = +this.route.snapshot.params['id'];
389
+ if (documentId) {
390
+ this.store.dispatch([new ClearRevision(), new Navigate([''])]);
391
+ } else {
392
+ this.store.dispatch(new ClearRevision());
393
+ this._content$.next(RevisionHelper.getEmptyDocxModel());
394
+ }
395
+ }
396
+
397
+ onConvertFile(event: any): void {
398
+ if (!event?.target?.files?.length) {
399
+ return;
400
+ }
401
+ let file: File = event.target.files[0];
402
+ if (!file.type.match('application/vnd.openxmlformats-officedocument.wordprocessingml.document')) {
403
+ return;
404
+ }
405
+ this.store.dispatch(new ConvertFile(file as Blob));
406
+ this.actions$
407
+ .pipe(ofActionDispatched(OpenDocx), take(1))
408
+ .subscribe(payload => this.store.dispatch(new Navigate([`noder/${payload.documentId}`])));
409
+ event.target.value = null;
410
+ }
411
+
412
+ onSave(): void {
413
+ const dialogRef = this.dialog.open(FileSaveDialogComponent, {
414
+ panelClass: 'file-save-dialog',
415
+ height: '230px',
416
+ width: '350px',
417
+ data: { documentName: '' }
418
+ });
419
+ dialogRef.afterClosed().subscribe((name: string) => {
420
+ if (!name?.length) {
421
+ return;
422
+ }
423
+ this.store.dispatch(new SaveDocument(name, this.editor.content));
424
+ });
425
+ }
426
+
427
+ onPrint(): void {
428
+ this.editorService.print();
429
+ }
430
+
431
+ onDelete(): void {
432
+ const documentId = this.store.selectSnapshot(NoderState.documentId);
433
+ this.dialog
434
+ .open<ConfirmDialogComponent>(ConfirmDialogComponent, {
435
+ autoFocus: false,
436
+ restoreFocus: false,
437
+ data: <IDialogData>{ message: 'Are you sure you want to delete?' }
438
+ })
439
+ .afterClosed()
440
+ .pipe(filter(x => !!x))
441
+ .subscribe(() => {
442
+ const actions: any = [new Navigate(['noder', 'new'])];
443
+ if (documentId) {
444
+ actions.unshift(new DeleteDocuments([documentId]));
445
+ }
446
+ this.store.dispatch(actions);
447
+ });
448
+ }
449
+
450
+ onInsertPageBreak(): void {
451
+ this.editorService.insertBreak(BreakTypes.Page);
452
+ }
453
+
454
+ async onInsertImage(event: any): Promise<void> {
455
+ if (!event?.target?.files?.length) {
456
+ return;
457
+ }
458
+ const file = event.target.files[0];
459
+ if (!'image/gif, image/jpeg, image/png, image/bmp, image/webp'.match(file.type as string)) {
460
+ return;
461
+ }
462
+ const model = await this.imageUploadService.uploadImage(file as Blob);
463
+ this.editorService.insertImage(model);
464
+ event.target.value = null;
465
+ }
466
+
467
+ onInsertTable(model: { rows: number; columns: number }): void {
468
+ this.editorService.insertTable(model);
469
+ }
470
+
471
+ onInsertTableMobile(): void {
472
+ this.editorService.openSidenav(InsertTableMobileComponent);
473
+ }
474
+
475
+ onSetTextStyle(value: TextStyleModel): void {
476
+ this.editorService.setTextStyles(value);
477
+ }
478
+
479
+ onSetNumberingTemplateType(value: NumberingLevelModel[]): void {
480
+ this.editorService.setNumberingTemplateType(value);
481
+ }
482
+
483
+ onRemoveNumberings(): void {
484
+ this.editorService.removeNumberings();
485
+ }
486
+
487
+ onInsertLink(): void {
488
+ if (this.isMobile) {
489
+ this.editorService.openSidenav(AddLinkMobileComponent);
490
+ } else {
491
+ this.dialog
492
+ .open(AddLinkDialogComponent, { panelClass: 'add-link-dialog' })
493
+ .afterClosed()
494
+ .subscribe((result: any) => {
495
+ if (result) {
496
+ this.editorService.insertLink(result.text as string, result.link as string);
497
+ }
498
+ });
499
+ }
500
+ }
501
+
502
+ addCustomElement(model: ElementDataModel): void {
503
+ this.editorService.createCustomComponent(model);
504
+ }
505
+
506
+ onSetParagraphStyle(value: ParagraphStyleModel): void {
507
+ this.editorService.setParagraphStyles(value);
508
+ }
509
+
510
+ onModeChanged(value: Mode): void {
511
+ this.store.dispatch(new SetMode(value));
512
+ this.editorService.setIsViewOnly(value !== Mode.Edit);
513
+ }
514
+
515
+ onUndo(): void {
516
+ this.editorService.undo();
517
+ }
518
+
519
+ onRedo(): void {
520
+ this.editorService.redo();
521
+ }
522
+
523
+ async onOpenEditMenu(): Promise<void> {
524
+ const read = await navigator.permissions.query({ name: 'clipboard-read' as PermissionName });
525
+ if (read.state != 'denied' && navigator.clipboard) {
526
+ navigator.clipboard
527
+ .readText()
528
+ .then(value => {
529
+ return this.editorService.setClipboardData(value);
530
+ })
531
+ .catch(() => {
532
+ // continue regardless error
533
+ });
534
+ }
535
+ }
536
+
537
+ onCutSelected(): void {
538
+ this.editorService.cutSelected();
539
+ }
540
+
541
+ onCopySelected(): void {
542
+ this.editorService.copySelected();
543
+ }
544
+
545
+ onPasteClipboardData(): void {
546
+ this.editorService.pasteFromClipboard();
547
+ }
548
+
549
+ onSelectAll(): void {
550
+ this.editorService.selectAll();
551
+ }
552
+
553
+ onRemoveSelected(): void {
554
+ this.editorService.removeSelected();
555
+ }
556
+
557
+ onRenameDocumentTitle(title: string): void {
558
+ this.store.dispatch(new RenameDocument(new DocumentNameModel({ documentId: this.documentId, name: title })));
559
+ }
560
+
561
+ createElement(model: ElementDataModel) {
562
+ this.editor.editor.createCustomElement(model);
563
+ this.editor.editor.focus();
564
+ }
565
+
566
+ onTextFormatMobile(): void {
567
+ this.editorService.openSidenav(TextFormatMobileComponent);
568
+ }
569
+
570
+ onEditorHotKeyDown(event: KeyboardEvent): void {
571
+ if (event.ctrlKey && event.code === 'KeyO') {
572
+ this.fileInput.nativeElement.click();
573
+ } else if (event.ctrlKey && event.code === 'KeyK') {
574
+ this.onInsertLink();
575
+ } else {
576
+ return;
577
+ }
578
+ event.preventDefault();
579
+ }
580
+
581
+ private onOpenSearch(): void {
582
+ this.dialog.open(EditorSearchDialogComponent, {
583
+ position: { top: '0px', right: '0px' },
584
+ hasBackdrop: false
585
+ });
586
+ }
587
+
588
+ private isMobileSubscription(): void {
589
+ this.store
590
+ .select(LayoutState.isMobile)
591
+ .pipe(takeUntil(this.destroy$))
592
+ .subscribe(x => (this.isMobile = x));
593
+ }
594
+
595
+ private openCloseSearchSubscription(): void {
596
+ this.editorService.openSearchDialog$.pipe(takeUntil(this.destroy$)).subscribe(value => {
597
+ value ? this.onOpenSearch() : this.dialog.closeAll();
598
+ });
599
+ }
600
+
601
+ private routeDataSubscription(): void {
602
+ this.route.data.pipe(takeUntil(this.destroy$)).subscribe(({ content }) => this._content$.next(content as DocxModel));
603
+ }
604
+
605
+ private documentTitleSubscription(): void {
606
+ this.store
607
+ .select(NoderState.title)
608
+ .pipe(takeUntil(this.destroy$))
609
+ .subscribe(x => (this.title = x ?? 'Untitled document'));
610
+ }
611
+
612
+ private documentIdSubscription(): void {
613
+ this.store
614
+ .select(NoderState.documentId)
615
+ .pipe(takeUntil(this.destroy$))
616
+ .subscribe(x => (this.documentId = x));
617
+ }
618
+
619
+ private createCommandSubscription(): void {
620
+ this.commandService.createCommand$
621
+ .pipe(
622
+ filter(x => !!x),
623
+ tap(command => {
624
+ this.store.dispatch(new CreateOperation(command));
625
+ }),
626
+ takeUntil(this.destroy$)
627
+ )
628
+ .subscribe();
629
+ }
630
+
631
+ private editorHotKeyDownSubscription(): void {
632
+ this.editorService.keyDown$.pipe(takeUntil(this.destroy$)).subscribe(event => this.onEditorHotKeyDown(event));
633
+ }
634
+ }
635
+ ```
636
+
637
+ app.component.scss
638
+
639
+ ```scss
640
+ // styles of your app component
641
+ :host {
642
+ height: inherit;
643
+ width: 100%;
644
+ display: flex;
645
+ flex-direction: column;
646
+ overflow: hidden;
647
+ }
648
+ ```
649
+
650
+ [documentation]: https://npmjs.talrace.com/text/ngx-noder