-
-
Notifications
You must be signed in to change notification settings - Fork 15
/
ngx-editorjs.directive.ts
312 lines (280 loc) · 7.99 KB
/
ngx-editorjs.directive.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
import {
AfterContentInit,
ChangeDetectorRef,
Directive,
ElementRef,
EventEmitter,
HostListener,
Input,
OnChanges,
OnDestroy,
Output,
SimpleChanges
} from '@angular/core';
import EditorJS, { EditorConfig, OutputData, SanitizerConfig } from '@editorjs/editorjs';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { createEditorJSConfig } from '../config/editor-config';
import { NgxEditorJSService } from '../services/editorjs.service';
import { Block } from '../types/blocks';
/**
* The main directive of `ngx-editorjs` provides a way to attach
* an EditorJS instance to any element and control it via
* Angular services and components
*
* To use attach to any element with an `id` property
*
* @example
* <div id="my-editor" ngxEditorJS></div>
*/
@Directive({
selector: '[ngxEditorJS]',
})
export class NgxEditorJSDirective implements OnDestroy, OnChanges, AfterContentInit {
/**
* On Destroyed subject
*/
private readonly onDestroy$ = new Subject<boolean>();
/**
* Form touched state
*/
private touched$ = new BehaviorSubject<boolean>(false);
/**
* The DOM element ID, it will use the Directive DOM element ID or falls back to the provided `holder` property
*/
private id: string;
/**
* Boolean, If set to true the EditorJS instance gets autofocus when initialized
*/
@Input()
public autofocus: boolean;
/**
* Boolean, If set to true the toolbar will not show in the EditorJS instance
*/
@Input()
public hideToolbar: boolean;
/**
* String, the ID property of the element that the EditorJS instance will be attached to
*/
@Input()
public holder: string;
/**
* String, The type of block to set as the initial block type. This needs to match a name in the `UserPlugins` object.
* The default value is "paragraph"
*/
@Input()
public initialBlock?: string;
/**
* Number, The minimum height of the EditorJS instance bottom after the last block
*/
@Input()
public minHeight: number;
/**
* String, The text to display as the placeholder text in the first block before content is added
*/
@Input()
public blockPlaceholder: string;
/**
* SanitizerConfig, The configuration for the HTML sanitizer
*/
@Input()
public sanitizer: SanitizerConfig;
/**
* String Array, If empty all tools available via the plugin service will be added. If a string
* array is set only the tools with the provided keys will be added
*/
@Input()
public excludeTools: string[] = [];
/**
* Number, Used with Angular Forms this sets an autosave timer active that calls the EditorJS save
* method. This patches the `FormControl` value with every block change and focus and blur, it also autosaves after
* a set time
* Set to 0 to disable or pass a value in `ms` of the autosave time
*/
@Input()
public autosave: number;
/**
* Blocks, An initial set of block data to render inside the component
*/
@Input()
public blocks: Block[];
/**
* Emits if the content from the EditorJS instance has been saved to the component value
*/
@Output()
public hasSaved = new EventEmitter<boolean>();
/**
* Emits if the component has been touched
*/
@Output()
public isTouched = new EventEmitter<boolean>();
/**
* Emits if the component is focused
*/
@Output()
public isFocused = new EventEmitter<boolean>();
/**
* Emits if the EditorJS content has changed when `save` is called
*/
@Output()
public hasChanged = new EventEmitter<OutputData>();
/**
* Emits if the EditorJS component is ready
*/
@Output()
public isReady = new EventEmitter<boolean>();
/**
* Host click listener
*/
@HostListener('click')
onclick() {
this.touched$.next(true);
this.isTouched.emit(true);
this.cd.markForCheck();
}
/**
* Creates the directive instance
* @param el The element the directive is attached to
* @param editorService The editor service
*/
constructor(
protected readonly el: ElementRef,
protected readonly editorService: NgxEditorJSService,
protected readonly cd: ChangeDetectorRef
) {}
/**
* Get the EditorJS instance for this directive
*/
public get editor(): Observable<EditorJS> {
return this.service.getEditor({ holder: this.id });
}
/**
* Get the element for the directive
*/
public get element() {
return this.el.nativeElement;
}
/**
* Get the `NgxEditorJSService` for this directive
*/
public get service(): NgxEditorJSService {
return this.editorService;
}
/**
* Get the touched state
*/
public get touched() {
return this.touched$.asObservable();
}
/**
* Creates an EditorJS instance for this directive
* @param config Configuration for this instance
* @param excludeTools
*/
public async createEditor(config?: EditorConfig, excludeTools = []): Promise<void> {
await this.service.createInstance({
config,
excludeTools: this.excludeTools || excludeTools,
autoSave: this.autosave || 0
});
this.cd.markForCheck();
}
/**
* When ngOnChanges are called, there are two paths
* If it's just blocks, then the service is updated with the blocks
* If it's any other property it means we create a new editor, as this
* is a destructive process we also need to wait for an existing editor
* to be ready
* @param changes Changes on the component
*/
async ngOnChanges(changes: SimpleChanges): Promise<void> {
if (
changes.blocks &&
changes.blocks.previousValue !== null &&
!changes.blocks.firstChange &&
JSON.stringify(changes.blocks.previousValue) !== JSON.stringify(changes.blocks.currentValue)
) {
this.service.update({ holder: this.holder }).subscribe();
this.cd.markForCheck();
} else {
const changesKeys = Object.keys(changes);
if (
this.id &&
// Ignore changes for values not related to EditorJS
[
'autofocus',
'holder',
'hideToolbar',
'initialBlock',
'minHeight',
'blockPlaceholder',
'sanitizer',
'includeTools'
].find(key => {
return changesKeys.includes(key);
})
) {
await this.createEditor(this.createConfig());
this.cd.markForCheck();
}
}
}
/**
* After content is created, we create the editor instance and set up listners
*/
async ngAfterContentInit() {
this.id = this.el.nativeElement.id || this.holder;
if (!this.id) {
throw new Error('Error in NgxEditorJSDirective::ngAfterContentInit - Directive element has no ID');
}
await this.createEditor(this.createConfig());
this.service
.isReady({ holder: this.holder })
.pipe(takeUntil(this.onDestroy$))
.subscribe(isReady => {
this.isReady.emit(isReady);
});
this.service
.lastChange({ holder: this.holder })
.pipe(takeUntil(this.onDestroy$))
.subscribe(change => {
this.hasChanged.emit(change);
});
this.service
.hasSaved({ holder: this.holder })
.pipe(takeUntil(this.onDestroy$))
.subscribe(saved => {
this.hasSaved.next(saved);
});
}
/**
* Destroy the editor and subjects for this service
*/
ngOnDestroy() {
this.onDestroy$.next(true);
this.onDestroy$.complete();
this.service.destroyInstance({ holder: this.id });
}
/**
* Create a configuration for EditorJS
*/
private createConfig(): EditorConfig {
const config: EditorConfig = createEditorJSConfig({
holder: this.id,
autofocus: this.autofocus,
hideToolbar: this.hideToolbar,
initialBlock: this.initialBlock,
placeholder: this.blockPlaceholder,
minHeight: this.minHeight,
sanitizer: this.sanitizer
});
if (this.blocks && this.blocks.length > 0) {
config.data = {
time: Date.now(),
version: typeof EditorJS !== 'undefined' && EditorJS.version || '',
blocks: this.blocks
};
}
return config;
}
}