@sd-angular/core 19.0.0-beta.1 → 19.0.0-beta.11
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/assets/scss/core/bootstrap.scss +25 -0
- package/assets/scss/core/form.scss +4 -10
- package/components/avatar/index.d.ts +1 -0
- package/components/avatar/src/avatar.component.d.ts +14 -0
- package/components/document-builder/src/document-builder.component.d.ts +23 -6
- package/components/document-builder/src/document-builder.config.d.ts +21 -0
- package/components/document-builder/src/document-builder.model.d.ts +1 -0
- package/components/document-builder/src/document-builder.utils.d.ts +10 -0
- package/components/document-builder/src/plugins/heading/heading.plugin.d.ts +4 -0
- package/components/document-builder/src/plugins/{image-upload.plugin.d.ts → image-upload/image-upload.plugin.d.ts} +0 -4
- package/components/document-builder/src/plugins/index.d.ts +6 -5
- package/components/index.d.ts +1 -0
- package/components/table/src/models/table-item.model.d.ts +2 -1
- package/components/table/src/models/table-option.model.d.ts +2 -1
- package/components/workflow/src/models/index.d.ts +1 -0
- package/fesm2022/sd-angular-core-components-avatar.mjs +88 -0
- package/fesm2022/sd-angular-core-components-avatar.mjs.map +1 -0
- package/fesm2022/sd-angular-core-components-badge.mjs +2 -2
- package/fesm2022/sd-angular-core-components-badge.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-document-builder.mjs +721 -513
- package/fesm2022/sd-angular-core-components-document-builder.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-table.mjs +366 -77
- package/fesm2022/sd-angular-core-components-table.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components-workflow.mjs +23 -23
- package/fesm2022/sd-angular-core-components-workflow.mjs.map +1 -1
- package/fesm2022/sd-angular-core-components.mjs +1 -0
- package/fesm2022/sd-angular-core-components.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-autocomplete.mjs +24 -2
- package/fesm2022/sd-angular-core-forms-autocomplete.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-date.mjs +15 -3
- package/fesm2022/sd-angular-core-forms-date.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-datetime.mjs +17 -3
- package/fesm2022/sd-angular-core-forms-datetime.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-input-number.mjs +18 -3
- package/fesm2022/sd-angular-core-forms-input-number.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-input.mjs +20 -6
- package/fesm2022/sd-angular-core-forms-input.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-radio.mjs +17 -2
- package/fesm2022/sd-angular-core-forms-radio.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-select.mjs +15 -2
- package/fesm2022/sd-angular-core-forms-select.mjs.map +1 -1
- package/fesm2022/sd-angular-core-forms-textarea.mjs +21 -2
- package/fesm2022/sd-angular-core-forms-textarea.mjs.map +1 -1
- package/fesm2022/sd-angular-core-modules-auth.mjs +137 -0
- package/fesm2022/sd-angular-core-modules-auth.mjs.map +1 -0
- package/fesm2022/sd-angular-core-modules-layout.mjs +1 -1
- package/fesm2022/sd-angular-core-modules-layout.mjs.map +1 -1
- package/fesm2022/sd-angular-core-modules.mjs +1 -0
- package/fesm2022/sd-angular-core-modules.mjs.map +1 -1
- package/fesm2022/sd-angular-core-pipes.mjs +21 -1
- package/fesm2022/sd-angular-core-pipes.mjs.map +1 -1
- package/fesm2022/sd-angular-core-services-confirm.mjs +60 -25
- package/fesm2022/sd-angular-core-services-confirm.mjs.map +1 -1
- package/fesm2022/sd-angular-core-utilities-extensions.mjs +66 -1
- package/fesm2022/sd-angular-core-utilities-extensions.mjs.map +1 -1
- package/fesm2022/sd-angular-core-utilities-models.mjs +12 -3
- package/fesm2022/sd-angular-core-utilities-models.mjs.map +1 -1
- package/forms/autocomplete/src/autocomplete.component.d.ts +5 -1
- package/forms/date/src/date.component.d.ts +4 -1
- package/forms/datetime/src/datetime.component.d.ts +4 -1
- package/forms/input/src/input.component.d.ts +6 -4
- package/forms/input-number/src/input-number.component.d.ts +4 -1
- package/forms/radio/src/radio.component.d.ts +5 -1
- package/forms/select/src/select.component.d.ts +5 -1
- package/forms/textarea/src/textarea.component.d.ts +3 -1
- package/modules/auth/configurations/auth.configuration.d.ts +19 -0
- package/modules/auth/configurations/index.d.ts +1 -0
- package/modules/auth/guards/auth.guard.d.ts +11 -0
- package/modules/auth/guards/index.d.ts +2 -0
- package/modules/auth/guards/portal.guard.d.ts +11 -0
- package/modules/auth/index.d.ts +3 -0
- package/modules/auth/services/auth.model.d.ts +8 -0
- package/modules/auth/services/auth.service.d.ts +17 -0
- package/modules/auth/services/index.d.ts +2 -0
- package/modules/index.d.ts +1 -0
- package/package.json +57 -49
- package/pipes/index.d.ts +1 -0
- package/pipes/src/empty.pipe.d.ts +7 -0
- package/sd-angular-core-19.0.0-beta.11.tgz +0 -0
- package/services/confirm/src/lib/components/dialog-confirm/dialog-confirm.component.d.ts +8 -0
- package/services/confirm/src/lib/confirm.service.d.ts +14 -0
- package/utilities/extensions/index.d.ts +1 -0
- package/utilities/extensions/src/color.extension.d.ts +20 -0
- package/utilities/models/src/maybe-async.model.d.ts +1 -0
- package/utilities/models/src/pattern.model.d.ts +2 -2
- /package/components/document-builder/src/plugins/{comment.plugin.d.ts → comment/comment.plugin.d.ts} +0 -0
- /package/components/document-builder/src/plugins/{page-orientation.plugin.d.ts → page-orientation/page-orientation.plugin.d.ts} +0 -0
- /package/components/document-builder/src/plugins/{table-fit.plugin.d.ts → table-fit/table-fit.plugin.d.ts} +0 -0
- /package/components/document-builder/src/plugins/{variable.plugin.d.ts → variable/variable.plugin.d.ts} +0 -0
|
@@ -4,9 +4,9 @@ import { CommonModule } from '@angular/common';
|
|
|
4
4
|
import * as i1 from '@ckeditor/ckeditor5-angular';
|
|
5
5
|
import { CKEditorModule } from '@ckeditor/ckeditor5-angular';
|
|
6
6
|
import { Plugin, ButtonView, ClassicEditor, Essentials, Paragraph, Bold, Italic, Underline, FontSize, FontColor, FontBackgroundColor, Alignment, Widget, toWidget, GeneralHtmlSupport, FontFamily, Heading, List, Table, TableToolbar, TableProperties, TableCellProperties, TableColumnResize, PasteFromOffice, PageBreak, Undo, Subscript, Superscript, Image, ImageUpload, ImageToolbar, ImageCaption, ImageResize, ImageStyle } from 'ckeditor5';
|
|
7
|
-
import { SdResolveMaybeAsync } from '@sd-angular/core/utilities';
|
|
8
|
-
import { SdUtilities } from '@sd-angular/core/utilities/extensions';
|
|
9
7
|
import { Subscription, Subject, throttleTime } from 'rxjs';
|
|
8
|
+
import { SdResolveMaybeAsync, hslToHex, rgbToHex, SdUtilities } from '@sd-angular/core/utilities';
|
|
9
|
+
import { v4 } from 'uuid';
|
|
10
10
|
|
|
11
11
|
class PageNumberPlugin extends Plugin {
|
|
12
12
|
init() {
|
|
@@ -130,100 +130,42 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImpo
|
|
|
130
130
|
type: Output
|
|
131
131
|
}] } });
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
const ICON_PORTRAIT = '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M14 2H6c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6V4h8v12z"/></svg>';
|
|
135
|
-
// Icon khổ ngang (Mới)
|
|
136
|
-
const ICON_LANDSCAPE = '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M18 4H2C.9 4 0 4.9 0 6v8c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 10H2V6h16v8z"/></svg>';
|
|
137
|
-
class PageOrientationPlugin extends Plugin {
|
|
138
|
-
static pluginName = 'PageOrientationPlugin';
|
|
139
|
-
_currentOrientation = 'PORTRAIT';
|
|
140
|
-
orientationChangeEmitter;
|
|
141
|
-
buttonView;
|
|
133
|
+
class HeadingPlugin extends Plugin {
|
|
142
134
|
init() {
|
|
143
135
|
const editor = this.editor;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
view.set({
|
|
150
|
-
// label: 'Xoay giấy (A4)',
|
|
151
|
-
icon: ICON_PORTRAIT,
|
|
152
|
-
// tooltip: true,
|
|
153
|
-
// withText: true,
|
|
154
|
-
class: 'btn-orientation', // Class để style nếu cần
|
|
155
|
-
});
|
|
156
|
-
// Xử lý khi bấm nút
|
|
157
|
-
view.on('execute', () => {
|
|
158
|
-
this.toggleOrientation();
|
|
159
|
-
});
|
|
160
|
-
return view;
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Toggle between portrait and landscape orientation
|
|
165
|
-
*/
|
|
166
|
-
toggleOrientation() {
|
|
167
|
-
const newOrientation = this._currentOrientation === 'PORTRAIT' ? 'LANDSCAPE' : 'PORTRAIT';
|
|
168
|
-
this.setOrientation(newOrientation);
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* Set orientation programmatically
|
|
172
|
-
*/
|
|
173
|
-
setOrientation(orientation) {
|
|
174
|
-
const editor = this.editor;
|
|
175
|
-
const editingView = editor.editing.view;
|
|
176
|
-
const rootElement = editingView.document.getRoot();
|
|
177
|
-
editor.editing.view.change(writer => {
|
|
178
|
-
if (orientation === 'LANDSCAPE') {
|
|
179
|
-
writer.addClass('landscape', rootElement);
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
writer.removeClass('landscape', rootElement);
|
|
183
|
-
}
|
|
136
|
+
editor.conversion.for('editingDowncast').markerToHighlight({
|
|
137
|
+
model: 'highlightMarker',
|
|
138
|
+
view: {
|
|
139
|
+
classes: 'ck-heading-highlight',
|
|
140
|
+
},
|
|
184
141
|
});
|
|
185
|
-
// Update button icon
|
|
186
|
-
if (this.buttonView) {
|
|
187
|
-
this.buttonView.icon = orientation === 'LANDSCAPE' ? ICON_LANDSCAPE : ICON_PORTRAIT;
|
|
188
|
-
}
|
|
189
|
-
this._currentOrientation = orientation;
|
|
190
|
-
this.orientationChangeEmitter?.(orientation);
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* Get current orientation
|
|
194
|
-
*/
|
|
195
|
-
getOrientation() {
|
|
196
|
-
return this._currentOrientation;
|
|
197
|
-
}
|
|
198
|
-
/**
|
|
199
|
-
* Register callback for orientation changes
|
|
200
|
-
*/
|
|
201
|
-
onOrientationChange(callback) {
|
|
202
|
-
this.orientationChangeEmitter = callback;
|
|
203
142
|
}
|
|
204
143
|
}
|
|
205
144
|
|
|
206
145
|
class CommentPlugin extends Plugin {
|
|
207
146
|
init() {
|
|
208
147
|
const editor = this.editor;
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
148
|
+
// 1. MODEL MARKER -> VIEW CSS
|
|
149
|
+
editor.conversion.for('downcast').markerToHighlight({
|
|
150
|
+
model: 'comment',
|
|
151
|
+
view: data => {
|
|
152
|
+
return {
|
|
153
|
+
classes: 'ck-comment-marker',
|
|
154
|
+
attributes: {
|
|
155
|
+
'data-comment-id': data.markerName,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
215
158
|
},
|
|
216
159
|
});
|
|
217
|
-
//
|
|
160
|
+
// 2. ĐĂNG KÝ UI COMPONENT: 'addCommentBtn'
|
|
218
161
|
editor.ui.componentFactory.add('addCommentBtn', locale => {
|
|
219
162
|
const view = new ButtonView(locale);
|
|
220
163
|
// Lấy config từ Angular
|
|
221
164
|
const config = editor.config;
|
|
222
165
|
const getOption = config.get('getOption');
|
|
223
166
|
const option = getOption?.();
|
|
224
|
-
//
|
|
167
|
+
// Ẩn button nếu không có onAddComment
|
|
225
168
|
if (!option?.onAddComment) {
|
|
226
|
-
// Trả về button rỗng hoặc null để không hiển thị
|
|
227
169
|
view.set({
|
|
228
170
|
label: '',
|
|
229
171
|
isVisible: false,
|
|
@@ -232,19 +174,18 @@ class CommentPlugin extends Plugin {
|
|
|
232
174
|
}
|
|
233
175
|
view.set({
|
|
234
176
|
label: 'Thêm bình luận',
|
|
235
|
-
// Icon SVG comment
|
|
236
177
|
icon: '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M18 13v6l-4-4H4a2 2 0 01-2-2V4a2 2 0 012-2h14a2 2 0 012 2v9zM5 7h10v2H5V7zm0 4h10v2H5v-2z"/></svg>',
|
|
237
178
|
tooltip: true,
|
|
238
|
-
isEnabled: false,
|
|
179
|
+
isEnabled: false,
|
|
239
180
|
});
|
|
240
|
-
//
|
|
181
|
+
// Logic Enable/Disable: Dựa theo Selection
|
|
241
182
|
const selection = editor.model.document.selection;
|
|
242
183
|
// Lắng nghe sự kiện change selection để bật/tắt nút
|
|
243
184
|
this.listenTo(selection, 'change', () => {
|
|
244
185
|
// Enable khi có bôi đen text (không phải collapsed)
|
|
245
186
|
view.isEnabled = !selection.isCollapsed;
|
|
246
187
|
});
|
|
247
|
-
//
|
|
188
|
+
// Logic Execute: Khi bấm nút
|
|
248
189
|
this.listenTo(view, 'execute', () => {
|
|
249
190
|
const range = selection.getFirstRange();
|
|
250
191
|
if (!range || !option?.onAddComment)
|
|
@@ -256,13 +197,41 @@ class CommentPlugin extends Plugin {
|
|
|
256
197
|
selectedText += item.data;
|
|
257
198
|
}
|
|
258
199
|
}
|
|
259
|
-
//
|
|
200
|
+
// BẮN EVENT RA NGOÀI - KHÔNG TỰ ADD MARKER
|
|
260
201
|
// Angular component sẽ xử lý logic (mở modal, validation, etc.)
|
|
261
202
|
// và gọi lại hàm addComment() nếu cần
|
|
262
203
|
option.onAddComment(range);
|
|
263
204
|
});
|
|
264
205
|
return view;
|
|
265
206
|
});
|
|
207
|
+
// 3. Xử lý sự kiện Copy (Clipboard Output)
|
|
208
|
+
this.listenTo(editor.editing.view.document, 'clipboardOutput', (_, data) => {
|
|
209
|
+
const isCopyOrCut = data.method === 'copy' || data.method === 'cut';
|
|
210
|
+
// Nếu không phải hành động copy hoặc cut thì thoát hàm
|
|
211
|
+
if (!isCopyOrCut) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const content = data.content;
|
|
215
|
+
editor.editing.view.change(writer => {
|
|
216
|
+
// Tạo range bao quanh toàn bộ nội dung clipboard
|
|
217
|
+
const range = writer.createRangeIn(content);
|
|
218
|
+
// Mảng chứa các item cần xử lý
|
|
219
|
+
const itemsToClean = [];
|
|
220
|
+
// 1. Duyệt qua để tìm các thẻ có class ck-comment-marker
|
|
221
|
+
for (const item of range.getItems()) {
|
|
222
|
+
if (item.is('element') && item.hasClass('ck-comment-marker')) {
|
|
223
|
+
itemsToClean.push(item);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// 2. Thực hiện xóa Class và Attribute
|
|
227
|
+
for (const item of itemsToClean) {
|
|
228
|
+
// Xóa class 'ck-comment-marker'
|
|
229
|
+
writer.removeClass('ck-comment-marker', item);
|
|
230
|
+
// Xóa thuộc tính 'data-comment-id'
|
|
231
|
+
writer.removeAttribute('data-comment-id', item);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
});
|
|
266
235
|
}
|
|
267
236
|
}
|
|
268
237
|
|
|
@@ -279,56 +248,63 @@ class VariablePlugin extends Plugin {
|
|
|
279
248
|
// 1. Định nghĩa Schema (Model)
|
|
280
249
|
schema.register('variable', {
|
|
281
250
|
inheritAllFrom: '$inlineObject',
|
|
282
|
-
|
|
251
|
+
allowWhere: '$text',
|
|
252
|
+
isInline: true,
|
|
253
|
+
isObject: true,
|
|
254
|
+
allowAttributes: ['id', 'uuid', 'value', 'display'],
|
|
283
255
|
});
|
|
284
|
-
//
|
|
285
|
-
conversion.for('
|
|
256
|
+
// 1. Model -> HTML trên giao diện
|
|
257
|
+
conversion.for('editingDowncast').elementToElement({
|
|
286
258
|
model: 'variable',
|
|
287
259
|
view: (modelItem, { writer: viewWriter }) => {
|
|
288
260
|
const id = modelItem.getAttribute('id');
|
|
261
|
+
const uuid = modelItem.getAttribute('uuid');
|
|
289
262
|
const display = modelItem.getAttribute('display');
|
|
290
263
|
const value = modelItem.getAttribute('value');
|
|
291
|
-
// Xử lý data (Object -> String)
|
|
292
|
-
const rawData = modelItem.getAttribute('data');
|
|
293
|
-
const dataJson = rawData ? JSON.stringify(rawData) : '';
|
|
294
264
|
const widgetElement = viewWriter.createContainerElement('span', {
|
|
295
265
|
class: 'variable-widget',
|
|
296
266
|
'data-id': id,
|
|
267
|
+
'data-uuid': uuid,
|
|
297
268
|
'data-value': value,
|
|
298
269
|
'data-display': display,
|
|
299
|
-
'data-json': dataJson,
|
|
300
270
|
});
|
|
301
271
|
const innerText = viewWriter.createText(`{{${display}}}`);
|
|
302
272
|
viewWriter.insert(viewWriter.createPositionAt(widgetElement, 0), innerText);
|
|
303
273
|
return toWidget(widgetElement, viewWriter);
|
|
304
274
|
},
|
|
305
275
|
});
|
|
306
|
-
//
|
|
276
|
+
// 2. Model -> HTML gửi lên BE
|
|
277
|
+
conversion.for('dataDowncast').elementToElement({
|
|
278
|
+
model: 'variable',
|
|
279
|
+
view: (modelItem, { writer: viewWriter }) => {
|
|
280
|
+
const id = modelItem.getAttribute('id');
|
|
281
|
+
const uuid = modelItem.getAttribute('uuid');
|
|
282
|
+
const display = modelItem.getAttribute('display');
|
|
283
|
+
const value = modelItem.getAttribute('value');
|
|
284
|
+
const widgetElement = viewWriter.createContainerElement('span', {
|
|
285
|
+
class: 'variable-widget',
|
|
286
|
+
'data-id': id,
|
|
287
|
+
'data-uuid': uuid,
|
|
288
|
+
'data-value': value,
|
|
289
|
+
'data-display': display,
|
|
290
|
+
});
|
|
291
|
+
const innerText = viewWriter.createText(`{{${display}}}`);
|
|
292
|
+
viewWriter.insert(viewWriter.createPositionAt(widgetElement, 0), innerText);
|
|
293
|
+
return widgetElement;
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
// 3. HTML -> model của CDK
|
|
307
297
|
conversion.for('upcast').elementToElement({
|
|
308
298
|
view: {
|
|
309
299
|
name: 'span',
|
|
310
300
|
classes: 'variable-widget',
|
|
311
301
|
},
|
|
312
302
|
model: (viewElement, { writer: modelWriter }) => {
|
|
313
|
-
// Lấy text bên trong làm label, bỏ dấu {{ }}
|
|
314
|
-
// const textNode = viewElement.getChild(0);
|
|
315
|
-
// let label = '';
|
|
316
|
-
// if (textNode && textNode.is('$text')) {
|
|
317
|
-
// label = textNode.data.replace(/{{|}}/g, '');
|
|
318
|
-
// }
|
|
319
|
-
const dataJson = viewElement.getAttribute('data-json');
|
|
320
|
-
let parsedData = null;
|
|
321
|
-
try {
|
|
322
|
-
parsedData = dataJson ? JSON.parse(dataJson) : null;
|
|
323
|
-
}
|
|
324
|
-
catch (e) {
|
|
325
|
-
console.error('Lỗi parse variable data', e);
|
|
326
|
-
}
|
|
327
303
|
return modelWriter.createElement('variable', {
|
|
328
304
|
id: viewElement.getAttribute('data-id'),
|
|
305
|
+
uuid: viewElement.getAttribute('data-uuid'),
|
|
329
306
|
value: viewElement.getAttribute('data-value'),
|
|
330
307
|
display: viewElement.getAttribute('data-display'),
|
|
331
|
-
data: parsedData, // Lưu vào model dưới dạng Object gốc
|
|
332
308
|
});
|
|
333
309
|
},
|
|
334
310
|
});
|
|
@@ -338,91 +314,187 @@ class VariablePlugin extends Plugin {
|
|
|
338
314
|
const jsonData = dataTransfer.getData('ck-variable');
|
|
339
315
|
if (!jsonData)
|
|
340
316
|
return;
|
|
341
|
-
//
|
|
317
|
+
// data.dropRange là vị trí con chuột trên View khi thả
|
|
318
|
+
const viewRange = data.dropRange;
|
|
319
|
+
const modelRange = editor.editing.mapper.toModelRange(viewRange);
|
|
342
320
|
evt.stop();
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
//* Hỗ trợ dữ liệu lấy từ API (Kiểm tra xem result có đúng định dạng interface SdDocumentBuilderVariable hay không?)
|
|
356
|
-
if (this.#isSdDocumentBuilderVariableResult(result)) {
|
|
357
|
-
variable = result;
|
|
321
|
+
try {
|
|
322
|
+
let variable = JSON.parse(jsonData);
|
|
323
|
+
const config = editor.config;
|
|
324
|
+
const getOption = config.get('getOption');
|
|
325
|
+
const option = getOption?.();
|
|
326
|
+
if (option?.onDropVariable) {
|
|
327
|
+
const result = await SdResolveMaybeAsync(option.onDropVariable(variable, 0));
|
|
328
|
+
// * Hỗ trợ dữ liệu có sẵn sẽ chỉ cần nhận vào boolean có cho phép thả hay không?
|
|
329
|
+
if (typeof result === 'boolean') {
|
|
330
|
+
if (!result) {
|
|
331
|
+
throw new Error('Không cho phép thêm variable vào văn bản');
|
|
332
|
+
}
|
|
358
333
|
}
|
|
359
334
|
else {
|
|
360
|
-
|
|
335
|
+
// * Hỗ trợ dữ liệu lấy từ API (Kiểm tra xem result có đúng định dạng interface SdDocumentBuilderVariable hay không?)
|
|
336
|
+
if (this.#isSdDocumentBuilderVariableResult(result)) {
|
|
337
|
+
variable = result;
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
throw new Error('Dữ liệu variable không hợp lệ');
|
|
341
|
+
}
|
|
361
342
|
}
|
|
362
343
|
}
|
|
344
|
+
editor.model.change(writer => {
|
|
345
|
+
// 4.1. Chèn biến
|
|
346
|
+
const variableElem = writer.createElement('variable', {
|
|
347
|
+
id: variable.id,
|
|
348
|
+
uuid: v4(),
|
|
349
|
+
value: variable.value,
|
|
350
|
+
display: variable.display,
|
|
351
|
+
});
|
|
352
|
+
editor.model.insertContent(variableElem, modelRange);
|
|
353
|
+
// 4.2. Đặt con trỏ ra sau biến
|
|
354
|
+
writer.setSelection(variableElem, 'after');
|
|
355
|
+
});
|
|
363
356
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
357
|
+
catch (e) {
|
|
358
|
+
// Đặt con trỏ ngay tại vị trí lỗi
|
|
359
|
+
if (modelRange) {
|
|
360
|
+
editor.model.change(writer => {
|
|
361
|
+
writer.setSelection(modelRange);
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
console.error(e);
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
// 5. Dọn dẹp drop-target dù thành công hay lỗi
|
|
368
|
+
editor.model.change(writer => {
|
|
369
|
+
for (const marker of editor.model.markers) {
|
|
370
|
+
if (marker.name.startsWith('drop-target')) {
|
|
371
|
+
writer.removeMarker(marker);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
375
374
|
});
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
// 5. Lắng nghe sự kiện bàn phím
|
|
378
|
+
let isNavigating = false;
|
|
379
|
+
this.editor.editing.view.document.on('keydown', (evt, data) => {
|
|
380
|
+
// Mã phím mũi tên: 37 (Left), 38 (Up), 39 (Right), 40 (Down)
|
|
381
|
+
const isArrowKey = data.keyCode >= 37 && data.keyCode <= 40;
|
|
382
|
+
if (isArrowKey) {
|
|
383
|
+
isNavigating = true;
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
isNavigating = false;
|
|
387
|
+
}
|
|
388
|
+
}, { priority: 'high' });
|
|
389
|
+
// 6. Lắng nghe sự kiện Click chuột
|
|
390
|
+
this.editor.editing.view.document.on('mousedown', () => {
|
|
391
|
+
isNavigating = true;
|
|
392
|
+
});
|
|
393
|
+
this.listenTo(editor.model.document.selection, 'change:range', () => {
|
|
394
|
+
// Nếu không phải là hành động click hoặc mũi tên thì thoát hàm.
|
|
395
|
+
if (!isNavigating) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const model = editor.model;
|
|
399
|
+
const selection = model.document.selection;
|
|
400
|
+
if (!selection.isCollapsed)
|
|
401
|
+
return;
|
|
402
|
+
const position = selection.getFirstPosition();
|
|
403
|
+
const nodeBefore = position?.nodeBefore;
|
|
404
|
+
if (!position)
|
|
405
|
+
return;
|
|
406
|
+
// Kiểm tra: Node đứng trước con trỏ là variable
|
|
407
|
+
if (nodeBefore && nodeBefore.is('element', 'variable')) {
|
|
408
|
+
// Lấy node ngay sau variable để kiểm tra
|
|
409
|
+
const nextNode = nodeBefore.nextSibling;
|
|
410
|
+
// Logic: Nếu phía sau KHÔNG CÓ GÌ hoặc KHÔNG PHẢI LÀ TEXT
|
|
411
|
+
if (!nextNode || !nextNode.is('$text')) {
|
|
412
|
+
model.change(writer => {
|
|
413
|
+
// Chèn thêm con trỏ variable
|
|
414
|
+
writer.insertText('\u00A0', nodeBefore, 'after');
|
|
415
|
+
// Lấy vị trí ngay sau variable (lúc này đang là đầu của text node mới)
|
|
416
|
+
const posAfterVariable = writer.createPositionAfter(nodeBefore);
|
|
417
|
+
// Dịch chuyển vị trí đó sang phải 1 đơn vị (bỏ qua ký tự vừa thêm)
|
|
418
|
+
const targetPos = posAfterVariable.getShiftedBy(1);
|
|
419
|
+
// 3. Đặt con trỏ vào vị trí đã tính toán
|
|
420
|
+
writer.setSelection(targetPos);
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
// 7. Handle xóa variable
|
|
426
|
+
this.editor.editing.view.document.on('keydown', (evt, data) => {
|
|
427
|
+
// Mã phím 8 là Backspace, 46 là Delete
|
|
428
|
+
const btnBackspace = data.keyCode === 8;
|
|
429
|
+
const btnDelete = data.keyCode === 46;
|
|
430
|
+
if (btnBackspace || btnDelete) {
|
|
431
|
+
const selection = editor.model.document.selection;
|
|
432
|
+
const model = editor.model;
|
|
433
|
+
// CASE 1: Nếu con trỏ đang nhấp nháy (Collapsed)
|
|
434
|
+
if (selection.isCollapsed) {
|
|
435
|
+
const position = selection.getFirstPosition();
|
|
436
|
+
// Với Backspace ta kiểm tra nodeBefore, với Delete ta kiểm tra nodeAfter
|
|
437
|
+
const targetNode = data.keyCode === 8 ? position?.nodeBefore : position?.nodeAfter;
|
|
438
|
+
if (targetNode && targetNode.is('element', 'variable')) {
|
|
439
|
+
data.preventDefault();
|
|
440
|
+
evt.stop();
|
|
441
|
+
model.change(writer => {
|
|
442
|
+
// Chọn bao quanh Variable đó
|
|
443
|
+
writer.setSelection(targetNode, 'on');
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
385
446
|
}
|
|
386
447
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
448
|
+
// CASE 2: Nếu đang có một vùng chọn (đã được highlight từ lần bấm trước)
|
|
449
|
+
else {
|
|
450
|
+
const selectedElement = selection.getSelectedElement();
|
|
451
|
+
// Nếu phần tử đang được chọn chính là variable
|
|
452
|
+
if (selectedElement && selectedElement.is('element', 'variable')) {
|
|
453
|
+
// Cho phép hành động mặc định diễn ra (CKEditor sẽ tự xóa phần tử đang được chọn)
|
|
454
|
+
// Hoặc chủ động xóa để chắc chắn:
|
|
455
|
+
data.preventDefault();
|
|
456
|
+
evt.stop();
|
|
457
|
+
model.change(writer => {
|
|
458
|
+
writer.remove(selectedElement);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}, { priority: 'highest' });
|
|
464
|
+
// 8. Xử lý sự kiện Copy (Clipboard Output)
|
|
465
|
+
// Khi copy, thay thế variable bằng text
|
|
466
|
+
this.listenTo(editor.editing.view.document, 'clipboardOutput', (_, data) => {
|
|
467
|
+
const isCopyOrCut = data.method === 'copy' || data.method === 'cut';
|
|
468
|
+
// Nếu không phải hành động copy hoặc cut thì thoát hàm
|
|
469
|
+
if (!isCopyOrCut) {
|
|
470
|
+
return;
|
|
400
471
|
}
|
|
472
|
+
const content = data.content;
|
|
473
|
+
editor.editing.view.change(writer => {
|
|
474
|
+
// Tạo range bao quanh toàn bộ nội dung clipboard
|
|
475
|
+
const range = writer.createRangeIn(content);
|
|
476
|
+
const itemsToReplace = [];
|
|
477
|
+
// Duyệt qua tất cả các phần tử trong clipboard để tìm variable
|
|
478
|
+
for (const item of range.getItems()) {
|
|
479
|
+
// Kiểm tra đúng là thẻ span và có class variable-widget
|
|
480
|
+
if (item.is('element', 'span') && item.hasClass('variable-widget')) {
|
|
481
|
+
itemsToReplace.push(item);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
// Thay thế variable bằng text
|
|
485
|
+
for (const item of itemsToReplace) {
|
|
486
|
+
const displayText = item.getAttribute('data-display');
|
|
487
|
+
if (displayText) {
|
|
488
|
+
// Tạo một node text thuần túy
|
|
489
|
+
const textNode = writer.createText(`{{${displayText}}}`);
|
|
490
|
+
// Chèn text node vào ngay trước widget cũ
|
|
491
|
+
writer.insert(writer.createPositionBefore(item), textNode);
|
|
492
|
+
// Xóa widget cũ đi
|
|
493
|
+
writer.remove(item);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
});
|
|
401
497
|
});
|
|
402
|
-
// this.listenTo(editingView.document, 'drop', (evt, data) => {
|
|
403
|
-
// const config = editor.config as Config<SdEditorConfig>;
|
|
404
|
-
// const getOption = config.get('getOption') as SdEditorConfig['getOption'];
|
|
405
|
-
// const option = getOption?.();
|
|
406
|
-
// // Dữ liệu kéo thả
|
|
407
|
-
// const dataTransfer = (data as any).dataTransfer;
|
|
408
|
-
// const jsonData = dataTransfer.getData('ck-variable');
|
|
409
|
-
// if (!jsonData) return;
|
|
410
|
-
// evt.stop();
|
|
411
|
-
// const variable: SdDocumentBuilderVariable = JSON.parse(jsonData);
|
|
412
|
-
// // Gọi callback ra ngoài Angular (nếu có)
|
|
413
|
-
// if (option && option.onDropVariable) {
|
|
414
|
-
// const allow = option.onDropVariable(variable, 0);
|
|
415
|
-
// if (allow === false) return; // Angular chặn drop
|
|
416
|
-
// }
|
|
417
|
-
// // Insert vào Model
|
|
418
|
-
// editor.model.change(writer => {
|
|
419
|
-
// const variableElem = writer.createElement('variable', {
|
|
420
|
-
// id: variable.id,
|
|
421
|
-
// label: variable.label
|
|
422
|
-
// });
|
|
423
|
-
// editor.model.insertContent(variableElem);
|
|
424
|
-
// });
|
|
425
|
-
// });
|
|
426
498
|
}
|
|
427
499
|
#isSdDocumentBuilderVariableResult = (obj) => {
|
|
428
500
|
return (obj !== null &&
|
|
@@ -485,25 +557,17 @@ class TableFitPlugin extends Plugin {
|
|
|
485
557
|
}
|
|
486
558
|
}
|
|
487
559
|
|
|
488
|
-
/**
|
|
489
|
-
* Custom base64 upload adapter plugin for CKEditor 5.
|
|
490
|
-
* Converts uploaded images to base64 data URLs instead of uploading to a server.
|
|
491
|
-
*/
|
|
492
560
|
class ImageUploadPlugin extends Plugin {
|
|
493
561
|
static get pluginName() {
|
|
494
562
|
return 'ImageUploadPlugin';
|
|
495
563
|
}
|
|
496
564
|
init() {
|
|
497
565
|
const editor = this.editor;
|
|
498
|
-
// Register the custom upload adapter
|
|
499
566
|
editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
|
|
500
567
|
return new Base64UploadAdapter(loader);
|
|
501
568
|
};
|
|
502
569
|
}
|
|
503
570
|
}
|
|
504
|
-
/**
|
|
505
|
-
* Custom upload adapter that converts images to base64 data URLs.
|
|
506
|
-
*/
|
|
507
571
|
class Base64UploadAdapter {
|
|
508
572
|
loader;
|
|
509
573
|
constructor(loader) {
|
|
@@ -537,16 +601,288 @@ class Base64UploadAdapter {
|
|
|
537
601
|
}
|
|
538
602
|
}
|
|
539
603
|
|
|
604
|
+
// Icon khổ dọc (Mặc định cũ)
|
|
605
|
+
const ICON_PORTRAIT = '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M14 2H6c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6V4h8v12z"/></svg>';
|
|
606
|
+
// Icon khổ ngang (Mới)
|
|
607
|
+
const ICON_LANDSCAPE = '<svg viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M18 4H2C.9 4 0 4.9 0 6v8c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 10H2V6h16v8z"/></svg>';
|
|
608
|
+
class PageOrientationPlugin extends Plugin {
|
|
609
|
+
static pluginName = 'PageOrientationPlugin';
|
|
610
|
+
_currentOrientation = 'PORTRAIT';
|
|
611
|
+
orientationChangeEmitter;
|
|
612
|
+
buttonView;
|
|
613
|
+
init() {
|
|
614
|
+
const editor = this.editor;
|
|
615
|
+
const componentFactory = editor.ui.componentFactory;
|
|
616
|
+
// Đăng ký nút tên là 'pageOrientation'
|
|
617
|
+
componentFactory.add('pageOrientation', locale => {
|
|
618
|
+
const view = new ButtonView(locale);
|
|
619
|
+
this.buttonView = view;
|
|
620
|
+
view.set({
|
|
621
|
+
// label: 'Xoay giấy (A4)',
|
|
622
|
+
icon: ICON_PORTRAIT,
|
|
623
|
+
// tooltip: true,
|
|
624
|
+
// withText: true,
|
|
625
|
+
class: 'btn-orientation', // Class để style nếu cần
|
|
626
|
+
});
|
|
627
|
+
// Xử lý khi bấm nút
|
|
628
|
+
view.on('execute', () => {
|
|
629
|
+
this.toggleOrientation();
|
|
630
|
+
});
|
|
631
|
+
return view;
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Toggle between portrait and landscape orientation
|
|
636
|
+
*/
|
|
637
|
+
toggleOrientation() {
|
|
638
|
+
const newOrientation = this._currentOrientation === 'PORTRAIT' ? 'LANDSCAPE' : 'PORTRAIT';
|
|
639
|
+
this.setOrientation(newOrientation);
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Set orientation programmatically
|
|
643
|
+
*/
|
|
644
|
+
setOrientation(orientation) {
|
|
645
|
+
const editor = this.editor;
|
|
646
|
+
const editingView = editor.editing.view;
|
|
647
|
+
const rootElement = editingView.document.getRoot();
|
|
648
|
+
editor.editing.view.change(writer => {
|
|
649
|
+
if (orientation === 'LANDSCAPE') {
|
|
650
|
+
writer.addClass('landscape', rootElement);
|
|
651
|
+
}
|
|
652
|
+
else {
|
|
653
|
+
writer.removeClass('landscape', rootElement);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
// Update button icon
|
|
657
|
+
if (this.buttonView) {
|
|
658
|
+
this.buttonView.icon = orientation === 'LANDSCAPE' ? ICON_LANDSCAPE : ICON_PORTRAIT;
|
|
659
|
+
}
|
|
660
|
+
this._currentOrientation = orientation;
|
|
661
|
+
this.orientationChangeEmitter?.(orientation);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Get current orientation
|
|
665
|
+
*/
|
|
666
|
+
getOrientation() {
|
|
667
|
+
return this._currentOrientation;
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Register callback for orientation changes
|
|
671
|
+
*/
|
|
672
|
+
onOrientationChange(callback) {
|
|
673
|
+
this.orientationChangeEmitter = callback;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Cấu hình màu cho Document Builder
|
|
679
|
+
* Bảng màu tập trung và cấu hình cho việc lựa chọn màu nhất quán
|
|
680
|
+
*/
|
|
681
|
+
/**
|
|
682
|
+
* Trả về bảng màu chung được sử dụng trong tất cả tính năng của document builder
|
|
683
|
+
* @returns Mảng các tùy chọn màu được định sẵn với giá trị hex và label
|
|
684
|
+
*/
|
|
685
|
+
function getPresetColors() {
|
|
686
|
+
return [
|
|
687
|
+
{ color: '#000000', label: 'Black' },
|
|
688
|
+
{ color: '#4D4D4D', label: 'Dim grey' },
|
|
689
|
+
{ color: '#999999', label: 'Grey' },
|
|
690
|
+
{ color: '#E6E6E6', label: 'Light grey' },
|
|
691
|
+
{ color: '#FFFFFF', label: 'White' },
|
|
692
|
+
{ color: '#E64D4D', label: 'Red' },
|
|
693
|
+
{ color: '#E6994D', label: 'Orange' },
|
|
694
|
+
{ color: '#E6E64D', label: 'Yellow' },
|
|
695
|
+
{ color: '#99E64D', label: 'Light green' },
|
|
696
|
+
{ color: '#4DE64D', label: 'Green' },
|
|
697
|
+
{ color: '#4DE699', label: 'Aquamarine' },
|
|
698
|
+
{ color: '#4DE6E6', label: 'Turquoise' },
|
|
699
|
+
{ color: '#4D99E6', label: 'Light blue' },
|
|
700
|
+
{ color: '#4D4DE6', label: 'Blue' },
|
|
701
|
+
{ color: '#994DE6', label: 'Purple' },
|
|
702
|
+
];
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Trả về cấu hình bộ chọn màu với định dạng hex
|
|
706
|
+
* @returns Đối tượng cấu hình bộ chọn màu
|
|
707
|
+
*/
|
|
708
|
+
function getColorPickerConfig() {
|
|
709
|
+
return {
|
|
710
|
+
format: 'hex',
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Trả về cấu hình kích thước font cho document builder
|
|
715
|
+
* @returns Mảng các tùy chọn kích thước font được định sẵn
|
|
716
|
+
*/
|
|
717
|
+
function getFontSizeOptions() {
|
|
718
|
+
return [
|
|
719
|
+
{
|
|
720
|
+
title: '9',
|
|
721
|
+
model: '9pt',
|
|
722
|
+
view: {
|
|
723
|
+
name: 'span',
|
|
724
|
+
styles: { 'font-size': '9pt' },
|
|
725
|
+
priority: 7,
|
|
726
|
+
},
|
|
727
|
+
},
|
|
728
|
+
{
|
|
729
|
+
title: '10',
|
|
730
|
+
model: '10pt',
|
|
731
|
+
view: {
|
|
732
|
+
name: 'span',
|
|
733
|
+
styles: { 'font-size': '10pt' },
|
|
734
|
+
priority: 7,
|
|
735
|
+
},
|
|
736
|
+
},
|
|
737
|
+
{
|
|
738
|
+
title: '11',
|
|
739
|
+
model: '11pt',
|
|
740
|
+
view: {
|
|
741
|
+
name: 'span',
|
|
742
|
+
styles: { 'font-size': '11pt' },
|
|
743
|
+
priority: 7,
|
|
744
|
+
},
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
title: '12',
|
|
748
|
+
model: '12pt',
|
|
749
|
+
view: {
|
|
750
|
+
name: 'span',
|
|
751
|
+
styles: { 'font-size': '12pt' },
|
|
752
|
+
priority: 7,
|
|
753
|
+
},
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
title: '13',
|
|
757
|
+
model: '13pt',
|
|
758
|
+
view: {
|
|
759
|
+
name: 'span',
|
|
760
|
+
styles: { 'font-size': '13pt' },
|
|
761
|
+
priority: 7,
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
title: '14',
|
|
766
|
+
model: '14pt',
|
|
767
|
+
view: {
|
|
768
|
+
name: 'span',
|
|
769
|
+
styles: { 'font-size': '14pt' },
|
|
770
|
+
priority: 7,
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
title: '16',
|
|
775
|
+
model: '16pt',
|
|
776
|
+
view: {
|
|
777
|
+
name: 'span',
|
|
778
|
+
styles: { 'font-size': '16pt' },
|
|
779
|
+
priority: 7,
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
title: '18',
|
|
784
|
+
model: '18pt',
|
|
785
|
+
view: {
|
|
786
|
+
name: 'span',
|
|
787
|
+
styles: { 'font-size': '18pt' },
|
|
788
|
+
priority: 7,
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
{
|
|
792
|
+
title: '20',
|
|
793
|
+
model: '20pt',
|
|
794
|
+
view: {
|
|
795
|
+
name: 'span',
|
|
796
|
+
styles: { 'font-size': '20pt' },
|
|
797
|
+
priority: 7,
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
title: '24',
|
|
802
|
+
model: '24pt',
|
|
803
|
+
view: {
|
|
804
|
+
name: 'span',
|
|
805
|
+
styles: { 'font-size': '24pt' },
|
|
806
|
+
priority: 7,
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
];
|
|
810
|
+
}
|
|
811
|
+
function getHeadingOptions() {
|
|
812
|
+
return [
|
|
813
|
+
{ model: 'paragraph', title: 'Paragraph', class: 'ck-heading_paragraph' },
|
|
814
|
+
{ model: 'heading1', view: 'h1', title: 'Heading 1', class: 'ck-heading_heading1' },
|
|
815
|
+
{ model: 'heading2', view: 'h2', title: 'Heading 2', class: 'ck-heading_heading2' },
|
|
816
|
+
{ model: 'heading3', view: 'h3', title: 'Heading 3', class: 'ck-heading_heading3' },
|
|
817
|
+
];
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Document Builder Utilities
|
|
822
|
+
* Các hàm tiện ích cho document builder
|
|
823
|
+
*/
|
|
824
|
+
/**
|
|
825
|
+
* Chuẩn hóa nội dung bằng cách chuyển đổi tất cả màu HSL và RGB sang hex
|
|
826
|
+
* @param content - Nội dung HTML cần chuẩn hóa
|
|
827
|
+
* @returns Nội dung đã được chuẩn hóa với màu hex
|
|
828
|
+
*/
|
|
829
|
+
function normalize(content) {
|
|
830
|
+
let normalized = content;
|
|
831
|
+
// Chuyển đổi HSL sang hex
|
|
832
|
+
const hslRegex = /hsl\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*\)/gi;
|
|
833
|
+
normalized = normalized.replace(hslRegex, (match, h, s, l) => {
|
|
834
|
+
try {
|
|
835
|
+
const hue = parseInt(h, 10);
|
|
836
|
+
const saturation = parseInt(s, 10);
|
|
837
|
+
const lightness = parseInt(l, 10);
|
|
838
|
+
// Kiểm tra giá trị hợp lệ
|
|
839
|
+
if (hue >= 0 && hue <= 360 && saturation >= 0 && saturation <= 100 && lightness >= 0 && lightness <= 100) {
|
|
840
|
+
return hslToHex(hue, saturation, lightness);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
catch (error) {
|
|
844
|
+
console.warn('Failed to convert HSL to hex:', error, match);
|
|
845
|
+
}
|
|
846
|
+
return match; // Giữ nguyên nếu không thể chuyển đổi
|
|
847
|
+
});
|
|
848
|
+
// Chuyển đổi RGB sang hex
|
|
849
|
+
const rgbRegex = /rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/gi;
|
|
850
|
+
normalized = normalized.replace(rgbRegex, (match, r, g, b) => {
|
|
851
|
+
try {
|
|
852
|
+
const red = parseInt(r, 10);
|
|
853
|
+
const green = parseInt(g, 10);
|
|
854
|
+
const blue = parseInt(b, 10);
|
|
855
|
+
if (red >= 0 && red <= 255 && green >= 0 && green <= 255 && blue >= 0 && blue <= 255) {
|
|
856
|
+
return rgbToHex(red, green, blue);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
console.warn('Failed to convert RGB to hex:', error, match);
|
|
861
|
+
}
|
|
862
|
+
return match;
|
|
863
|
+
});
|
|
864
|
+
return normalized;
|
|
865
|
+
}
|
|
866
|
+
|
|
540
867
|
class SdDocumentBuilder {
|
|
541
|
-
#id = '1212';
|
|
542
868
|
option;
|
|
543
869
|
disabled = false;
|
|
544
870
|
set _disabled(val) {
|
|
545
871
|
this.disabled = val === '' || !!val;
|
|
546
872
|
this.#updateState();
|
|
547
873
|
}
|
|
874
|
+
contentChange = new EventEmitter(); // Emit HTML content
|
|
548
875
|
Editor = ClassicEditor;
|
|
549
876
|
#editor;
|
|
877
|
+
#id = '55b0afb0-288d-423c-98b3-5f9db286e16d';
|
|
878
|
+
#subscription = new Subscription();
|
|
879
|
+
#sharedColors = getPresetColors();
|
|
880
|
+
#headingOptions = getHeadingOptions();
|
|
881
|
+
#fontSizeOptions = getFontSizeOptions();
|
|
882
|
+
#colorPickerConfig = getColorPickerConfig();
|
|
883
|
+
#contentChangeSubject = new Subject();
|
|
884
|
+
#idTimeOutScrollHeading = null;
|
|
885
|
+
#headingElementsMap = new Map(); // Hash lưu trữ các heading
|
|
550
886
|
// Config
|
|
551
887
|
config = {
|
|
552
888
|
getOption: () => this.option,
|
|
@@ -583,11 +919,12 @@ class SdDocumentBuilder {
|
|
|
583
919
|
ImageResize,
|
|
584
920
|
ImageStyle,
|
|
585
921
|
// Custom Plugin
|
|
586
|
-
|
|
922
|
+
HeadingPlugin,
|
|
587
923
|
CommentPlugin,
|
|
588
924
|
VariablePlugin,
|
|
589
925
|
TableFitPlugin,
|
|
590
926
|
ImageUploadPlugin,
|
|
927
|
+
PageOrientationPlugin,
|
|
591
928
|
],
|
|
592
929
|
toolbar: {
|
|
593
930
|
items: [
|
|
@@ -625,119 +962,35 @@ class SdDocumentBuilder {
|
|
|
625
962
|
toolbar: ['toggleImageCaption', '|', 'imageStyle:inline', 'imageStyle:block', 'imageStyle:side'],
|
|
626
963
|
},
|
|
627
964
|
fontSize: {
|
|
628
|
-
options:
|
|
629
|
-
// Định nghĩa từng size một cách tường minh
|
|
630
|
-
{
|
|
631
|
-
title: '9',
|
|
632
|
-
model: '9pt',
|
|
633
|
-
view: {
|
|
634
|
-
name: 'span',
|
|
635
|
-
styles: { 'font-size': '9pt' },
|
|
636
|
-
priority: 7,
|
|
637
|
-
},
|
|
638
|
-
},
|
|
639
|
-
{
|
|
640
|
-
title: '10',
|
|
641
|
-
model: '10pt',
|
|
642
|
-
view: {
|
|
643
|
-
name: 'span',
|
|
644
|
-
styles: { 'font-size': '10pt' },
|
|
645
|
-
priority: 7,
|
|
646
|
-
},
|
|
647
|
-
},
|
|
648
|
-
{
|
|
649
|
-
title: '11',
|
|
650
|
-
model: '11pt',
|
|
651
|
-
view: {
|
|
652
|
-
name: 'span',
|
|
653
|
-
styles: { 'font-size': '11pt' },
|
|
654
|
-
priority: 7,
|
|
655
|
-
},
|
|
656
|
-
},
|
|
657
|
-
{
|
|
658
|
-
title: '12',
|
|
659
|
-
model: '12pt',
|
|
660
|
-
view: {
|
|
661
|
-
name: 'span',
|
|
662
|
-
styles: { 'font-size': '12pt' },
|
|
663
|
-
priority: 7,
|
|
664
|
-
},
|
|
665
|
-
},
|
|
666
|
-
{
|
|
667
|
-
title: '13',
|
|
668
|
-
model: '13pt',
|
|
669
|
-
view: {
|
|
670
|
-
name: 'span',
|
|
671
|
-
styles: { 'font-size': '13pt' },
|
|
672
|
-
priority: 7,
|
|
673
|
-
},
|
|
674
|
-
},
|
|
675
|
-
{
|
|
676
|
-
title: '14',
|
|
677
|
-
model: '14pt',
|
|
678
|
-
view: {
|
|
679
|
-
name: 'span',
|
|
680
|
-
styles: { 'font-size': '14pt' },
|
|
681
|
-
priority: 7,
|
|
682
|
-
},
|
|
683
|
-
},
|
|
684
|
-
{
|
|
685
|
-
title: '16',
|
|
686
|
-
model: '16pt',
|
|
687
|
-
view: {
|
|
688
|
-
name: 'span',
|
|
689
|
-
styles: { 'font-size': '16pt' },
|
|
690
|
-
priority: 7,
|
|
691
|
-
},
|
|
692
|
-
},
|
|
693
|
-
{
|
|
694
|
-
title: '18',
|
|
695
|
-
model: '18pt',
|
|
696
|
-
view: {
|
|
697
|
-
name: 'span',
|
|
698
|
-
styles: { 'font-size': '18pt' },
|
|
699
|
-
priority: 7,
|
|
700
|
-
},
|
|
701
|
-
},
|
|
702
|
-
{
|
|
703
|
-
title: '20',
|
|
704
|
-
model: '20pt',
|
|
705
|
-
view: {
|
|
706
|
-
name: 'span',
|
|
707
|
-
styles: { 'font-size': '20pt' },
|
|
708
|
-
priority: 7,
|
|
709
|
-
},
|
|
710
|
-
},
|
|
711
|
-
{
|
|
712
|
-
title: '24',
|
|
713
|
-
model: '24pt',
|
|
714
|
-
view: {
|
|
715
|
-
name: 'span',
|
|
716
|
-
styles: { 'font-size': '24pt' },
|
|
717
|
-
priority: 7,
|
|
718
|
-
},
|
|
719
|
-
},
|
|
720
|
-
],
|
|
965
|
+
options: this.#fontSizeOptions,
|
|
721
966
|
supportAllValues: false, // Khuyên dùng false để ép user chọn đúng size chuẩn
|
|
722
967
|
},
|
|
968
|
+
heading: {
|
|
969
|
+
options: this.#headingOptions,
|
|
970
|
+
},
|
|
723
971
|
// 4. Cấu hình bảng màu (Tùy chọn)
|
|
724
972
|
fontColor: {
|
|
725
|
-
columns: 5,
|
|
973
|
+
// columns: 5,
|
|
726
974
|
documentColors: 10,
|
|
975
|
+
colorPicker: this.#colorPickerConfig,
|
|
976
|
+
colors: this.#sharedColors,
|
|
727
977
|
},
|
|
728
978
|
fontBackgroundColor: {
|
|
729
|
-
columns: 5,
|
|
979
|
+
// columns: 5,
|
|
730
980
|
documentColors: 10,
|
|
981
|
+
colorPicker: this.#colorPickerConfig,
|
|
982
|
+
colors: this.#sharedColors,
|
|
731
983
|
},
|
|
732
984
|
table: {
|
|
733
|
-
contentToolbar: [
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
985
|
+
contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', '|', 'tableProperties', 'tableCellProperties'],
|
|
986
|
+
tableProperties: {
|
|
987
|
+
borderColors: this.#sharedColors,
|
|
988
|
+
colorPicker: this.#colorPickerConfig,
|
|
989
|
+
},
|
|
990
|
+
tableCellProperties: {
|
|
991
|
+
borderColors: this.#sharedColors,
|
|
992
|
+
colorPicker: this.#colorPickerConfig,
|
|
993
|
+
},
|
|
741
994
|
},
|
|
742
995
|
// Quan trọng: Cho phép paste style từ Word nhưng bỏ qua margin/padding
|
|
743
996
|
htmlSupport: {
|
|
@@ -754,15 +1007,10 @@ class SdDocumentBuilder {
|
|
|
754
1007
|
],
|
|
755
1008
|
},
|
|
756
1009
|
};
|
|
757
|
-
contentChange = new EventEmitter(); // Emit HTML content
|
|
758
|
-
#subscription = new Subscription();
|
|
759
|
-
#contentChangeSubject = new Subject();
|
|
760
|
-
#editorChangeRxjs = new Subject();
|
|
761
1010
|
ngOnInit() {
|
|
762
|
-
//
|
|
763
|
-
// Debounce trong rxjs không hỗ trợ leading -->
|
|
1011
|
+
// Debounce trong rxjs không hỗ trợ leading --> throttleTime
|
|
764
1012
|
this.#subscription.add(this.#contentChangeSubject.pipe(throttleTime(500, undefined, { leading: true, trailing: true })).subscribe(content => {
|
|
765
|
-
this.contentChange.emit(content);
|
|
1013
|
+
this.contentChange.emit(normalize(content));
|
|
766
1014
|
}));
|
|
767
1015
|
}
|
|
768
1016
|
ngOnDestroy() {
|
|
@@ -786,49 +1034,17 @@ class SdDocumentBuilder {
|
|
|
786
1034
|
catch (error) {
|
|
787
1035
|
console.warn('PageOrientationPlugin not available:', error);
|
|
788
1036
|
}
|
|
789
|
-
//
|
|
1037
|
+
// Lắng nghe selection
|
|
790
1038
|
editor.model.document.selection.on('change', $event => {
|
|
791
1039
|
this.option.onSelection?.(this.#editor.model.document.selection, $event);
|
|
792
1040
|
});
|
|
793
|
-
//
|
|
1041
|
+
// Lắng nghe sự kiện thay đổi nội dung
|
|
794
1042
|
editor.model.document.on('change:data', () => {
|
|
795
1043
|
const content = editor.getData();
|
|
796
1044
|
this.#contentChangeSubject.next(content);
|
|
797
1045
|
});
|
|
798
1046
|
this.#updateState();
|
|
799
1047
|
}
|
|
800
|
-
#updateState() {
|
|
801
|
-
if (!this.#editor)
|
|
802
|
-
return;
|
|
803
|
-
if (this.disabled) {
|
|
804
|
-
// Bật chế độ chỉ đọc với ID khóa
|
|
805
|
-
this.#editor.enableReadOnlyMode(this.#id);
|
|
806
|
-
}
|
|
807
|
-
else {
|
|
808
|
-
// Tắt chế độ chỉ đọc với ID khóa tương ứng
|
|
809
|
-
this.#editor.disableReadOnlyMode(this.#id);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
scrollToComment = (markerId) => {
|
|
813
|
-
if (!this.#editor)
|
|
814
|
-
return;
|
|
815
|
-
const editor = this.#editor;
|
|
816
|
-
const marker = editor.model.markers.get(markerId);
|
|
817
|
-
if (marker) {
|
|
818
|
-
// 1. Set Selection vào Marker đó trước
|
|
819
|
-
this.#editor.model.change(writer => {
|
|
820
|
-
writer.setSelection(marker.getRange());
|
|
821
|
-
});
|
|
822
|
-
// 2. Sau đó gọi scrollToTheSelection
|
|
823
|
-
this.#editor.editing.view.scrollToTheSelection({
|
|
824
|
-
alignToTop: true,
|
|
825
|
-
});
|
|
826
|
-
editor.editing.view.focus();
|
|
827
|
-
}
|
|
828
|
-
else {
|
|
829
|
-
console.warn(`Marker with id ${markerId} not found.`);
|
|
830
|
-
}
|
|
831
|
-
};
|
|
832
1048
|
setContent = (html) => {
|
|
833
1049
|
this.#editor?.setData?.(html);
|
|
834
1050
|
};
|
|
@@ -865,55 +1081,6 @@ class SdDocumentBuilder {
|
|
|
865
1081
|
}
|
|
866
1082
|
return 'PORTRAIT';
|
|
867
1083
|
};
|
|
868
|
-
getVariables = () => {
|
|
869
|
-
if (!this.#editor)
|
|
870
|
-
return [];
|
|
871
|
-
const model = this.#editor.model;
|
|
872
|
-
const root = model.document.getRoot();
|
|
873
|
-
if (!root)
|
|
874
|
-
return [];
|
|
875
|
-
const variables = [];
|
|
876
|
-
// Duyệt qua tất cả các phần tử trong range
|
|
877
|
-
// range.getItems() sẽ trả về từng node (text, element...)
|
|
878
|
-
try {
|
|
879
|
-
const range = model.createRangeIn(root);
|
|
880
|
-
for (const item of range.getItems()) {
|
|
881
|
-
// Sử dụng item.is('element', 'variable') là chính xác
|
|
882
|
-
if (item.is('element', 'variable')) {
|
|
883
|
-
variables.push({
|
|
884
|
-
id: item.getAttribute('id'),
|
|
885
|
-
value: item.getAttribute('value'),
|
|
886
|
-
display: item.getAttribute('display'),
|
|
887
|
-
data: item.getAttribute('data'),
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
catch (e) {
|
|
893
|
-
console.error(e);
|
|
894
|
-
return [];
|
|
895
|
-
}
|
|
896
|
-
return variables;
|
|
897
|
-
};
|
|
898
|
-
getComments() {
|
|
899
|
-
if (!this.#editor)
|
|
900
|
-
return [];
|
|
901
|
-
const markers = this.#editor.model.markers;
|
|
902
|
-
const comments = [];
|
|
903
|
-
// Duyệt qua tất cả markers trong Model
|
|
904
|
-
for (const marker of markers) {
|
|
905
|
-
// Chỉ lấy marker do plugin comment tạo ra (prefix 'comment:')
|
|
906
|
-
if (marker.name.startsWith('comment:')) {
|
|
907
|
-
// Lấy text nằm trong vùng marker đó
|
|
908
|
-
const currentText = this.#getTextFromRange(marker.getRange());
|
|
909
|
-
comments.push({
|
|
910
|
-
markerId: marker.name,
|
|
911
|
-
selectedText: currentText,
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
return comments;
|
|
916
|
-
}
|
|
917
1084
|
scrollToTop() {
|
|
918
1085
|
setTimeout(() => {
|
|
919
1086
|
if (this.#editor) {
|
|
@@ -933,13 +1100,67 @@ class SdDocumentBuilder {
|
|
|
933
1100
|
}
|
|
934
1101
|
}, 100);
|
|
935
1102
|
}
|
|
936
|
-
|
|
937
|
-
|
|
1103
|
+
#updateState() {
|
|
1104
|
+
if (!this.#editor)
|
|
1105
|
+
return;
|
|
1106
|
+
if (this.disabled) {
|
|
1107
|
+
// Bật chế độ chỉ đọc với ID khóa
|
|
1108
|
+
this.#editor.enableReadOnlyMode(this.#id);
|
|
1109
|
+
// Disable page orientation button
|
|
1110
|
+
try {
|
|
1111
|
+
const orientationPlugin = this.#editor.plugins.get('PageOrientationPlugin');
|
|
1112
|
+
if (orientationPlugin && orientationPlugin.buttonView) {
|
|
1113
|
+
orientationPlugin.buttonView.isEnabled = false;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
catch (error) {
|
|
1117
|
+
console.warn('Failed to disable orientation button:', error);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
else {
|
|
1121
|
+
// Tắt chế độ chỉ đọc với ID khóa tương ứng
|
|
1122
|
+
this.#editor.disableReadOnlyMode(this.#id);
|
|
1123
|
+
// Enable page orientation button
|
|
1124
|
+
try {
|
|
1125
|
+
const orientationPlugin = this.#editor.plugins.get('PageOrientationPlugin');
|
|
1126
|
+
if (orientationPlugin && orientationPlugin.buttonView) {
|
|
1127
|
+
orientationPlugin.buttonView.isEnabled = true;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
catch (error) {
|
|
1131
|
+
console.warn('Failed to enable orientation button:', error);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
#getTextFromElement = (element) => {
|
|
1136
|
+
let text = '';
|
|
1137
|
+
// Heading trong Model chứa các text node con
|
|
1138
|
+
for (const child of element.getChildren()) {
|
|
1139
|
+
if (child.is('$text') || child.is('$textProxy')) {
|
|
1140
|
+
text += child.data;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return text;
|
|
1144
|
+
};
|
|
1145
|
+
#getTextFromRange = (range) => {
|
|
1146
|
+
let text = '';
|
|
1147
|
+
for (const item of range.getItems()) {
|
|
1148
|
+
// TextProxy là một phần của Text Node nằm trong Range
|
|
1149
|
+
if (item.is('$textProxy') || item.is('$text')) {
|
|
1150
|
+
text += item.data;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
return text;
|
|
1154
|
+
};
|
|
1155
|
+
// ========================================================================
|
|
1156
|
+
// 1. QUẢN LÝ HEADING
|
|
1157
|
+
// ========================================================================
|
|
938
1158
|
heading = {
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
1159
|
+
/**
|
|
1160
|
+
* Lấy tất cả headings trong document
|
|
1161
|
+
* @returns Danh sách tất cả headings
|
|
1162
|
+
*/
|
|
1163
|
+
all: () => {
|
|
943
1164
|
if (!this.#editor)
|
|
944
1165
|
return [];
|
|
945
1166
|
const root = this.#editor.model.document.getRoot();
|
|
@@ -975,37 +1196,50 @@ class SdDocumentBuilder {
|
|
|
975
1196
|
}
|
|
976
1197
|
return headings;
|
|
977
1198
|
},
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1199
|
+
/**
|
|
1200
|
+
* Scroll tới vị trí của heading
|
|
1201
|
+
* @param id - ID của heading cần scroll tới
|
|
1202
|
+
*/
|
|
1203
|
+
scroll: (id) => {
|
|
982
1204
|
if (!this.#editor)
|
|
983
1205
|
return;
|
|
984
|
-
// 1. Lấy Model Element từ kho lưu trữ
|
|
985
1206
|
const modelElement = this.#headingElementsMap.get(id);
|
|
986
1207
|
if (modelElement) {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1208
|
+
this.#editor.model.change(writer => {
|
|
1209
|
+
// Xóa marker cũ
|
|
1210
|
+
if (this.#idTimeOutScrollHeading) {
|
|
1211
|
+
clearTimeout(this.#idTimeOutScrollHeading);
|
|
1212
|
+
}
|
|
1213
|
+
const currentMarker = this.#editor.model.markers.get('highlightMarker');
|
|
1214
|
+
if (currentMarker) {
|
|
1215
|
+
writer.removeMarker(currentMarker);
|
|
1216
|
+
}
|
|
1217
|
+
// Tạo Range bao trùm highlight
|
|
1218
|
+
const range = writer.createRangeOn(modelElement);
|
|
1219
|
+
// Thêm Marker mới
|
|
1220
|
+
writer.addMarker('highlightMarker', {
|
|
1221
|
+
range: range,
|
|
1222
|
+
usingOperation: false,
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
// Scroll tới vị trí tìm được
|
|
1226
|
+
const viewElement = this.#editor.editing.mapper.toViewElement(modelElement);
|
|
992
1227
|
if (viewElement) {
|
|
993
|
-
|
|
994
|
-
const domElement = view.domConverter.mapViewToDom(viewElement);
|
|
1228
|
+
const domElement = this.#editor.editing.view.domConverter.viewToDom(viewElement);
|
|
995
1229
|
if (domElement) {
|
|
996
|
-
|
|
997
|
-
domElement.scrollIntoView({
|
|
998
|
-
behavior: 'smooth',
|
|
999
|
-
block: 'start',
|
|
1000
|
-
inline: 'nearest',
|
|
1001
|
-
});
|
|
1002
|
-
// 5. (Tùy chọn) Focus và đặt con trỏ vào đó
|
|
1003
|
-
view.focus();
|
|
1004
|
-
editor.model.change(writer => {
|
|
1005
|
-
writer.setSelection(modelElement, 'on');
|
|
1006
|
-
});
|
|
1230
|
+
domElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1007
1231
|
}
|
|
1008
1232
|
}
|
|
1233
|
+
// Tự động tắt marker sau 5 giây
|
|
1234
|
+
this.#idTimeOutScrollHeading = setTimeout(() => {
|
|
1235
|
+
if (this.#editor) {
|
|
1236
|
+
this.#editor.model.change(writer => {
|
|
1237
|
+
const marker = this.#editor.model.markers.get('highlightMarker');
|
|
1238
|
+
if (marker)
|
|
1239
|
+
writer.removeMarker(marker);
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}, 5000);
|
|
1009
1243
|
}
|
|
1010
1244
|
else {
|
|
1011
1245
|
console.warn(`Heading with id ${id} not found.`);
|
|
@@ -1013,30 +1247,7 @@ class SdDocumentBuilder {
|
|
|
1013
1247
|
},
|
|
1014
1248
|
};
|
|
1015
1249
|
// ========================================================================
|
|
1016
|
-
//
|
|
1017
|
-
// ========================================================================
|
|
1018
|
-
#getTextFromElement = (element) => {
|
|
1019
|
-
let text = '';
|
|
1020
|
-
// Heading trong Model chứa các text node con
|
|
1021
|
-
for (const child of element.getChildren()) {
|
|
1022
|
-
if (child.is('$text') || child.is('$textProxy')) {
|
|
1023
|
-
text += child.data;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
return text;
|
|
1027
|
-
};
|
|
1028
|
-
#getTextFromRange = (range) => {
|
|
1029
|
-
let text = '';
|
|
1030
|
-
for (const item of range.getItems()) {
|
|
1031
|
-
// TextProxy là một phần của Text Node nằm trong Range
|
|
1032
|
-
if (item.is('$textProxy') || item.is('$text')) {
|
|
1033
|
-
text += item.data;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
return text;
|
|
1037
|
-
};
|
|
1038
|
-
// ========================================================================
|
|
1039
|
-
// COMMENT MANAGEMENT
|
|
1250
|
+
// 2. QUẢN LÝ COMMENT
|
|
1040
1251
|
// ========================================================================
|
|
1041
1252
|
comment = {
|
|
1042
1253
|
/**
|
|
@@ -1182,72 +1393,78 @@ class SdDocumentBuilder {
|
|
|
1182
1393
|
}
|
|
1183
1394
|
},
|
|
1184
1395
|
};
|
|
1185
|
-
//
|
|
1186
|
-
//
|
|
1187
|
-
//
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1396
|
+
// ========================================================================
|
|
1397
|
+
// 3. QUẢN LÝ VARIABLE
|
|
1398
|
+
// ========================================================================
|
|
1399
|
+
variable = {
|
|
1400
|
+
/**
|
|
1401
|
+
* Lấy tất cả variabes trong document
|
|
1402
|
+
* @returns Danh sách tất cả variables
|
|
1403
|
+
*/
|
|
1404
|
+
all: () => {
|
|
1405
|
+
if (!this.#editor)
|
|
1406
|
+
return [];
|
|
1407
|
+
const model = this.#editor.model;
|
|
1408
|
+
const root = model.document.getRoot();
|
|
1409
|
+
if (!root)
|
|
1410
|
+
return [];
|
|
1411
|
+
const variables = [];
|
|
1412
|
+
try {
|
|
1413
|
+
const range = model.createRangeIn(root);
|
|
1414
|
+
for (const item of range.getItems()) {
|
|
1415
|
+
// Sử dụng item.is('element', 'variable') là chính xác
|
|
1416
|
+
if (item.is('element', 'variable')) {
|
|
1417
|
+
variables.push({
|
|
1418
|
+
id: item.getAttribute('id'),
|
|
1419
|
+
uuid: item.getAttribute('uuid'),
|
|
1420
|
+
value: item.getAttribute('value'),
|
|
1421
|
+
display: item.getAttribute('display'),
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
catch (e) {
|
|
1427
|
+
console.error(e);
|
|
1428
|
+
return [];
|
|
1429
|
+
}
|
|
1430
|
+
return variables;
|
|
1431
|
+
},
|
|
1432
|
+
/**
|
|
1433
|
+
* Scroll tới vị trí của variable
|
|
1434
|
+
* @param uuid - uuid của variable FE sẽ tự sinh sau mỗi lần drop vào editor
|
|
1435
|
+
*/
|
|
1436
|
+
scroll: (uuid) => {
|
|
1437
|
+
if (!this.#editor)
|
|
1438
|
+
return;
|
|
1439
|
+
const model = this.#editor.model;
|
|
1440
|
+
const root = model.document.getRoot();
|
|
1441
|
+
if (!root)
|
|
1442
|
+
return;
|
|
1443
|
+
let targetElement = null;
|
|
1444
|
+
const range = model.createRangeIn(root);
|
|
1445
|
+
for (const item of range.getItems()) {
|
|
1446
|
+
if (item.is('element', 'variable') && item.getAttribute('uuid') === uuid) {
|
|
1447
|
+
targetElement = item;
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
if (targetElement) {
|
|
1452
|
+
const viewElement = this.#editor.editing.mapper.toViewElement(targetElement);
|
|
1453
|
+
if (viewElement) {
|
|
1454
|
+
const domElement = this.#editor.editing.view.domConverter.viewToDom(viewElement);
|
|
1455
|
+
if (domElement) {
|
|
1456
|
+
domElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1457
|
+
model.change(writer => {
|
|
1458
|
+
writer.setSelection(targetElement, 'on');
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
else {
|
|
1464
|
+
console.warn(`Variable với id ${uuid} không tìm thấy trong tài liệu.`);
|
|
1465
|
+
}
|
|
1466
|
+
},
|
|
1467
|
+
};
|
|
1251
1468
|
// ========================================================================
|
|
1252
1469
|
// 4. HÀM EXPORT DOCX (FULL HEADER/FOOTER + PAGE NUMBER)
|
|
1253
1470
|
// ========================================================================
|
|
@@ -1352,22 +1569,13 @@ class SdDocumentBuilder {
|
|
|
1352
1569
|
// Thêm '\ufeff' (BOM) để fix lỗi font tiếng Việt
|
|
1353
1570
|
const blob = new Blob(['\ufeff', fullHtml], { type: 'application/msword' });
|
|
1354
1571
|
SdUtilities.downloadBlob(blob, fileName);
|
|
1355
|
-
// 3. Convert & Save
|
|
1356
|
-
// asBlob(fullHtml, {
|
|
1357
|
-
// orientation: orientation as 'portrait' | 'landscape',
|
|
1358
|
-
// margins: { top: 720, right: 720, bottom: 720, left: 720 },
|
|
1359
|
-
// }).then(blob => {
|
|
1360
|
-
// if (blob instanceof Blob) {
|
|
1361
|
-
// SdUtilities.downloadBlob(blob, fileName);
|
|
1362
|
-
// }
|
|
1363
|
-
// });
|
|
1364
1572
|
}
|
|
1365
1573
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SdDocumentBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1366
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: SdDocumentBuilder, isStandalone: true, selector: "sd-document-builder", inputs: { option: "option", _disabled: ["disabled", "_disabled"] }, outputs: { contentChange: "contentChange" }, ngImport: i0, template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center;padding-bottom:20px}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%}:host ::ng-deep .ck-editor .ck-editor__top,:host ::ng-deep .ck-editor .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400}:host ::ng-deep .ck-content.ck-focused{outline:none!important;border-color:#d1d5db!important}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content img{max-width:100%!important;height:auto!important;object-fit:contain}:host ::ng-deep .ck-content p{margin-left:0!important;margin-right:0!important;margin-bottom:var(--ck-spacing-large);text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important}\n", "
|
|
1574
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.17", type: SdDocumentBuilder, isStandalone: true, selector: "sd-document-builder", inputs: { option: "option", _disabled: ["disabled", "_disabled"] }, outputs: { contentChange: "contentChange" }, ngImport: i0, template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";::ng-deep .ck-editor{--ck-font-size-base: 11px !important;--ck-icon-size: 16px !important;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.5;--ck-spacing-small: 2px !important;--ck-spacing-standard: 4px !important;--ck-spacing-large: 8px !important}::ng-deep .ck-editor .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}::ng-deep .ck-editor .ck-editor__top .ck-sticky-panel__content{border:none!important}::ng-deep .ck-editor .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}::ng-deep .ck-editor .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}::ng-deep .ck-editor .ck-toolbar{min-height:32px!important;padding:2px!important}::ng-deep .ck-editor .ck-button{padding:2px 4px!important;min-height:24px!important}::ng-deep .ck-editor .ck-dropdown__button{min-height:24px!important}::ng-deep .ck.ck-toolbar{background:#f8f9fa!important;border-bottom:1px solid #e0e0e0!important}\n", "@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center;padding-bottom:20px}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%}:host ::ng-deep .ck-editor .ck-editor__top,:host ::ng-deep .ck-editor .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400}:host ::ng-deep .ck-content.ck-focused{outline:none!important;border-color:#d1d5db!important}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content img{max-width:100%!important;height:auto!important;object-fit:contain}:host ::ng-deep .ck-content p{margin-left:0!important;margin-right:0!important;margin-bottom:var(--ck-spacing-large);text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", ":host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}:host ::ng-deep .ck-comment-marker:hover{background-color:#ffeb3bcc}:host ::ng-deep .ck-comment-marker.active-highlight{background-color:#ffeb3b;outline:2px dashed #f57f17}\n", "@charset \"UTF-8\";::ng-deep .ck-clipboard-drop-target-line{display:none!important}:host ::ng-deep .variable-widget{background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9!important;border-radius:4px;padding:2px 6px;font-weight:600;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10px;-webkit-user-select:none;user-select:none;display:inline-block;margin:0 4px;vertical-align:middle;font-size:0;cursor:default}:host ::ng-deep .variable-widget:before{content:attr(data-display);font-size:10px}:host ::ng-deep .variable-widget:hover{background-color:#bbdefb;box-shadow:0 1px 2px #0000001a}:host ::ng-deep .variable-widget.ck-widget_selected{outline:2px solid #2196f3;background-color:#bbdefb}:host ::ng-deep .ck.ck-content .ck-widget,:host ::ng-deep .ck.ck-content .ck-widget:hover,:host ::ng-deep .ck.ck-content .ck-widget:focus,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected:hover{outline:none!important;box-shadow:none!important}\n", ":host ::ng-deep .ck-editor__editable .ck-widget.table{float:none!important;display:block!important;max-width:100%!important;width:100%!important;margin:0!important;clear:both}:host ::ng-deep .ck-editor__editable table{table-layout:auto!important;width:100%!important;border-collapse:collapse;margin:0!important}:host ::ng-deep .ck-editor__editable table td,:host ::ng-deep .ck-editor__editable table th{word-wrap:break-word;white-space:normal!important;padding:.4em!important}:host ::ng-deep .ck-editor__editable table td img,:host ::ng-deep .ck-editor__editable table th img{max-width:100%;height:auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: CKEditorModule }, { kind: "component", type: i1.CKEditorComponent, selector: "ckeditor", inputs: ["editor", "config", "data", "tagName", "watchdog", "editorWatchdogConfig", "disableWatchdog", "disableTwoWayDataBinding", "disabled"], outputs: ["ready", "change", "blur", "focus", "error"] }] });
|
|
1367
1575
|
}
|
|
1368
1576
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: SdDocumentBuilder, decorators: [{
|
|
1369
1577
|
type: Component,
|
|
1370
|
-
args: [{ selector: 'sd-document-builder', standalone: true, imports: [CommonModule, CKEditorModule], template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center;padding-bottom:20px}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%}:host ::ng-deep .ck-editor .ck-editor__top,:host ::ng-deep .ck-editor .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400}:host ::ng-deep .ck-content.ck-focused{outline:none!important;border-color:#d1d5db!important}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content img{max-width:100%!important;height:auto!important;object-fit:contain}:host ::ng-deep .ck-content p{margin-left:0!important;margin-right:0!important;margin-bottom:var(--ck-spacing-large);text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important}\n", "
|
|
1578
|
+
args: [{ selector: 'sd-document-builder', standalone: true, imports: [CommonModule, CKEditorModule], template: "<div class=\"builder-container\">\n <ckeditor\n style=\"width: 100%\"\n [editor]=\"Editor\" \n [config]=\"config\" \n (ready)=\"onReady($event)\"\n [disabled]=\"disabled\">\n </ckeditor>\n</div>", styles: ["@charset \"UTF-8\";::ng-deep .ck-editor{--ck-font-size-base: 11px !important;--ck-icon-size: 16px !important;--ck-content-font-family: \"Times New Roman\", serif !important;--ck-content-font-size: 13pt;--ck-content-line-height: 1.5;--ck-spacing-small: 2px !important;--ck-spacing-standard: 4px !important;--ck-spacing-large: 8px !important}::ng-deep .ck-editor .ck-editor__top{position:sticky;top:0;z-index:100;width:100%;min-width:600px;margin-bottom:10px}::ng-deep .ck-editor .ck-editor__top .ck-sticky-panel__content{border:none!important}::ng-deep .ck-editor .ck-editor__top .ck-toolbar{background:#fff!important;box-shadow:0 4px 6px -1px #0000001a!important;padding:8px!important}::ng-deep .ck-editor .ck-editor__top .ck-toolbar .ck-toolbar__items{display:flex;justify-content:center;flex-wrap:wrap;align-items:center}::ng-deep .ck-editor .ck-toolbar{min-height:32px!important;padding:2px!important}::ng-deep .ck-editor .ck-button{padding:2px 4px!important;min-height:24px!important}::ng-deep .ck-editor .ck-dropdown__button{min-height:24px!important}::ng-deep .ck.ck-toolbar{background:#f8f9fa!important;border-bottom:1px solid #e0e0e0!important}\n", "@charset \"UTF-8\";.builder-container{background-color:#f3f4f6;height:100%;overflow-y:auto;width:100%;display:flex;flex-direction:column;align-items:center;padding-bottom:20px}:host{display:inline-block}:host ::ng-deep .ck-editor{display:flex;flex-direction:column;align-items:center;width:100%}:host ::ng-deep .ck-editor .ck-editor__top,:host ::ng-deep .ck-editor .ck-editor__main{border:none!important;box-shadow:none!important}:host ::ng-deep .ck-content{background-color:#fff;width:210mm;min-height:1123px;padding:20mm!important;box-sizing:border-box!important;box-shadow:0 10px 15px -3px #0000001a}:host ::ng-deep .ck-content h1,:host ::ng-deep .ck-content h2,:host ::ng-deep .ck-content h3,:host ::ng-deep .ck-content h4,:host ::ng-deep .ck-content h5,:host ::ng-deep .ck-content h6{font-weight:400}:host ::ng-deep .ck-content.ck-focused{outline:none!important;border-color:#d1d5db!important}:host ::ng-deep .ck-content.landscape{width:297mm}:host ::ng-deep .ck-content>*{max-width:100%!important;box-sizing:border-box!important}:host ::ng-deep .ck-content img{max-width:100%!important;height:auto!important;object-fit:contain}:host ::ng-deep .ck-content p{margin-left:0!important;margin-right:0!important;margin-bottom:var(--ck-spacing-large);text-indent:0}:host ::ng-deep .ck-content ul,:host ::ng-deep .ck-content ol{padding-left:20px!important;margin-left:0!important}\n", ":host ::ng-deep .ck-heading-highlight{background-color:#fef08a}\n", ":host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;transition:background-color .2s;cursor:pointer}:host ::ng-deep .ck-comment-marker:hover{background-color:#ffeb3bcc}:host ::ng-deep .ck-comment-marker.active-highlight{background-color:#ffeb3b;outline:2px dashed #f57f17}\n", "@charset \"UTF-8\";::ng-deep .ck-clipboard-drop-target-line{display:none!important}:host ::ng-deep .variable-widget{background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9!important;border-radius:4px;padding:2px 6px;font-weight:600;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10px;-webkit-user-select:none;user-select:none;display:inline-block;margin:0 4px;vertical-align:middle;font-size:0;cursor:default}:host ::ng-deep .variable-widget:before{content:attr(data-display);font-size:10px}:host ::ng-deep .variable-widget:hover{background-color:#bbdefb;box-shadow:0 1px 2px #0000001a}:host ::ng-deep .variable-widget.ck-widget_selected{outline:2px solid #2196f3;background-color:#bbdefb}:host ::ng-deep .ck.ck-content .ck-widget,:host ::ng-deep .ck.ck-content .ck-widget:hover,:host ::ng-deep .ck.ck-content .ck-widget:focus,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected,:host ::ng-deep .ck.ck-content .ck-widget.ck-widget_selected:hover{outline:none!important;box-shadow:none!important}\n", ":host ::ng-deep .ck-editor__editable .ck-widget.table{float:none!important;display:block!important;max-width:100%!important;width:100%!important;margin:0!important;clear:both}:host ::ng-deep .ck-editor__editable table{table-layout:auto!important;width:100%!important;border-collapse:collapse;margin:0!important}:host ::ng-deep .ck-editor__editable table td,:host ::ng-deep .ck-editor__editable table th{word-wrap:break-word;white-space:normal!important;padding:.4em!important}:host ::ng-deep .ck-editor__editable table td img,:host ::ng-deep .ck-editor__editable table th img{max-width:100%;height:auto}\n"] }]
|
|
1371
1579
|
}], propDecorators: { option: [{
|
|
1372
1580
|
type: Input,
|
|
1373
1581
|
args: [{ required: true }]
|