-
Notifications
You must be signed in to change notification settings - Fork 4.5k
/
editor.ts
executable file
·352 lines (316 loc) · 12 KB
/
editor.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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
import { CommonModule, isPlatformServer } from '@angular/common';
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ContentChild,
ContentChildren,
ElementRef,
EventEmitter,
Inject,
Input,
NgModule,
Output,
PLATFORM_ID,
QueryList,
TemplateRef,
ViewEncapsulation,
afterNextRender,
forwardRef
} from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { Header, PrimeTemplate, SharedModule } from 'primeng/api';
import { DomHandler } from 'primeng/dom';
import { Nullable } from 'primeng/ts-helpers';
import { EditorInitEvent, EditorSelectionChangeEvent, EditorTextChangeEvent } from './editor.interface';
export const EDITOR_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => Editor),
multi: true
};
/**
* Editor groups a collection of contents in tabs.
* @group Components
*/
@Component({
selector: 'p-editor',
template: `
<div [ngClass]="'p-editor-container'" [class]="styleClass">
<div class="p-editor-toolbar" *ngIf="toolbar || headerTemplate">
<ng-content select="p-header"></ng-content>
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
</div>
<div class="p-editor-toolbar" *ngIf="!toolbar && !headerTemplate">
<span class="ql-formats">
<select class="ql-header">
<option value="1">Heading</option>
<option value="2">Subheading</option>
<option selected>Normal</option>
</select>
<select class="ql-font">
<option selected>Sans Serif</option>
<option value="serif">Serif</option>
<option value="monospace">Monospace</option>
</select>
</span>
<span class="ql-formats">
<button class="ql-bold" aria-label="Bold" type="button"></button>
<button class="ql-italic" aria-label="Italic" type="button"></button>
<button class="ql-underline" aria-label="Underline" type="button"></button>
</span>
<span class="ql-formats">
<select class="ql-color"></select>
<select class="ql-background"></select>
</span>
<span class="ql-formats">
<button class="ql-list" value="ordered" aria-label="Ordered List" type="button"></button>
<button class="ql-list" value="bullet" aria-label="Unordered List" type="button"></button>
<select class="ql-align">
<option selected></option>
<option value="center">center</option>
<option value="right">right</option>
<option value="justify">justify</option>
</select>
</span>
<span class="ql-formats">
<button class="ql-link" aria-label="Insert Link" type="button"></button>
<button class="ql-image" aria-label="Insert Image" type="button"></button>
<button class="ql-code-block" aria-label="Insert Code Block" type="button"></button>
</span>
<span class="ql-formats">
<button class="ql-clean" aria-label="Remove Styles" type="button"></button>
</span>
</div>
<div class="p-editor-content" [ngStyle]="style"></div>
</div>
`,
providers: [EDITOR_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./editor.css'],
encapsulation: ViewEncapsulation.None,
host: {
class: 'p-element'
}
})
export class Editor implements AfterContentInit, ControlValueAccessor {
/**
* Inline style of the container.
* @group Props
*/
@Input() style: { [klass: string]: any } | null | undefined;
/**
* Style class of the container.
* @group Props
*/
@Input() styleClass: string | undefined;
/**
* Placeholder text to show when editor is empty.
* @group Props
*/
@Input() placeholder: string | undefined;
/**
* Whitelist of formats to display, see here for available options.
* @group Props
*/
@Input() formats: string[] | undefined;
/**
* Modules configuration of Editor, see here for available options.
* @group Props
*/
@Input() modules: object | undefined;
/**
* DOM Element or a CSS selector for a DOM Element, within which the editor’s p elements (i.e. tooltips, etc.) should be confined. Currently, it only considers left and right boundaries.
* @group Props
*/
@Input() bounds: HTMLElement | string | undefined;
/**
* DOM Element or a CSS selector for a DOM Element, specifying which container has the scrollbars (i.e. overflow-y: auto), if is has been changed from the default ql-editor with custom CSS. Necessary to fix scroll jumping bugs when Quill is set to auto grow its height, and another ancestor container is responsible from the scrolling..
* @group Props
*/
@Input() scrollingContainer: HTMLElement | string | undefined;
/**
* Shortcut for debug. Note debug is a static method and will affect other instances of Quill editors on the page. Only warning and error messages are enabled by default.
* @group Props
*/
@Input() debug: string | undefined;
/**
* Whether to instantiate the editor to read-only mode.
* @group Props
*/
@Input() get readonly(): boolean {
return this._readonly;
}
set readonly(val: boolean) {
this._readonly = val;
if (this.quill) {
if (this._readonly) this.quill.disable();
else this.quill.enable();
}
}
/**
* Callback to invoke when the quill modules are loaded.
* @param {EditorInitEvent} event - custom event.
* @group Emits
*/
@Output() onInit: EventEmitter<EditorInitEvent> = new EventEmitter<EditorInitEvent>();
/**
* Callback to invoke when text of editor changes.
* @param {EditorTextChangeEvent} event - custom event.
* @group Emits
*/
@Output() onTextChange: EventEmitter<EditorTextChangeEvent> = new EventEmitter<EditorTextChangeEvent>();
/**
* Callback to invoke when selection of the text changes.
* @param {EditorSelectionChangeEvent} event - custom event.
* @group Emits
*/
@Output() onSelectionChange: EventEmitter<EditorSelectionChangeEvent> = new EventEmitter<EditorSelectionChangeEvent>();
@ContentChildren(PrimeTemplate) templates!: QueryList<PrimeTemplate>;
@ContentChild(Header) toolbar: any;
value: Nullable<string>;
delayedCommand: Function | null = null;
_readonly: boolean = false;
onModelChange: Function = () => {};
onModelTouched: Function = () => {};
quill: any;
dynamicQuill: any;
headerTemplate: Nullable<TemplateRef<any>>;
private get isAttachedQuillEditorToDOM(): boolean | undefined {
return this.quillElements?.editorElement?.isConnected;
}
private quillElements!: { editorElement: HTMLElement; toolbarElement: HTMLElement };
constructor(
public el: ElementRef,
@Inject(PLATFORM_ID) private platformId: object
) {
/**
* Read or write the DOM once, when initializing non-Angular (Quill) library.
*/
afterNextRender(() => {
this.initQuillElements();
this.initQuillEditor();
});
}
ngAfterContentInit() {
this.templates.forEach((item) => {
switch (item.getType()) {
case 'header':
this.headerTemplate = item.template;
break;
}
});
}
writeValue(value: any): void {
this.value = value;
if (this.quill) {
if (value) {
const command = (): void => {
this.quill.setContents(this.quill.clipboard.convert(this.dynamicQuill.version.startsWith('2') ? { html: this.value } : this.value));
};
if (this.isAttachedQuillEditorToDOM) {
command();
} else {
this.delayedCommand = command;
}
} else {
const command = (): void => {
this.quill.setText('');
};
if (this.isAttachedQuillEditorToDOM) {
command();
} else {
this.delayedCommand = command;
}
}
}
}
registerOnChange(fn: Function): void {
this.onModelChange = fn;
}
registerOnTouched(fn: Function): void {
this.onModelTouched = fn;
}
getQuill() {
return this.quill;
}
private initQuillEditor(): void {
if (isPlatformServer(this.platformId)) {
return;
}
/**
* Importing Quill at top level, throws `document is undefined` error during when
* building for SSR, so this dynamically loads quill when it's in browser module.
*/
if (!this.dynamicQuill) {
import('quill')
.then((quillModule: any) => {
this.dynamicQuill = quillModule.default;
this.createQuillEditor();
})
.catch((e) => console.error(e.message));
} else {
this.createQuillEditor();
}
}
private createQuillEditor(): void {
this.initQuillElements();
const { toolbarElement, editorElement } = this.quillElements;
let defaultModule = { toolbar: toolbarElement };
let modules = this.modules ? { ...defaultModule, ...this.modules } : defaultModule;
this.quill = new this.dynamicQuill(editorElement, {
modules: modules,
placeholder: this.placeholder,
readOnly: this.readonly,
theme: 'snow',
formats: this.formats,
bounds: this.bounds,
debug: this.debug,
scrollingContainer: this.scrollingContainer
});
const isQuill2 = this.dynamicQuill.version.startsWith('2');
if (this.value) {
this.quill.setContents(this.quill.clipboard.convert(isQuill2 ? { html: this.value } : this.value));
}
this.quill.on('text-change', (delta: any, oldContents: any, source: any) => {
if (source === 'user') {
let html = isQuill2 ? this.quill.getSemanticHTML() : DomHandler.findSingle(editorElement, '.ql-editor').innerHTML;
let text = this.quill.getText().trim();
if (html === '<p><br></p>') {
html = null;
}
this.onTextChange.emit({
htmlValue: html,
textValue: text,
delta: delta,
source: source
});
this.onModelChange(html);
this.onModelTouched();
}
});
this.quill.on('selection-change', (range: string, oldRange: string, source: string) => {
this.onSelectionChange.emit({
range: range,
oldRange: oldRange,
source: source
});
});
this.onInit.emit({
editor: this.quill
});
}
private initQuillElements(): void {
if (!this.quillElements) {
this.quillElements = {
editorElement: DomHandler.findSingle(this.el.nativeElement, 'div.p-editor-content'),
toolbarElement: DomHandler.findSingle(this.el.nativeElement, 'div.p-editor-toolbar')
};
}
}
}
@NgModule({
imports: [CommonModule],
exports: [Editor, SharedModule],
declarations: [Editor]
})
export class EditorModule {}