@sd-angular/core 19.0.0-beta.1 → 19.0.0-beta.10

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.
Files changed (82) hide show
  1. package/assets/scss/core/bootstrap.scss +25 -0
  2. package/assets/scss/core/form.scss +4 -10
  3. package/components/document-builder/src/document-builder.component.d.ts +23 -6
  4. package/components/document-builder/src/document-builder.config.d.ts +21 -0
  5. package/components/document-builder/src/document-builder.model.d.ts +1 -0
  6. package/components/document-builder/src/document-builder.utils.d.ts +10 -0
  7. package/components/document-builder/src/plugins/heading/heading.plugin.d.ts +4 -0
  8. package/components/document-builder/src/plugins/{image-upload.plugin.d.ts → image-upload/image-upload.plugin.d.ts} +0 -4
  9. package/components/document-builder/src/plugins/index.d.ts +6 -5
  10. package/components/table/src/models/table-item.model.d.ts +2 -1
  11. package/components/table/src/models/table-option.model.d.ts +2 -1
  12. package/components/workflow/src/models/index.d.ts +1 -0
  13. package/fesm2022/sd-angular-core-components-badge.mjs +2 -2
  14. package/fesm2022/sd-angular-core-components-badge.mjs.map +1 -1
  15. package/fesm2022/sd-angular-core-components-document-builder.mjs +721 -513
  16. package/fesm2022/sd-angular-core-components-document-builder.mjs.map +1 -1
  17. package/fesm2022/sd-angular-core-components-table.mjs +366 -77
  18. package/fesm2022/sd-angular-core-components-table.mjs.map +1 -1
  19. package/fesm2022/sd-angular-core-components-workflow.mjs +23 -23
  20. package/fesm2022/sd-angular-core-components-workflow.mjs.map +1 -1
  21. package/fesm2022/sd-angular-core-forms-autocomplete.mjs +24 -2
  22. package/fesm2022/sd-angular-core-forms-autocomplete.mjs.map +1 -1
  23. package/fesm2022/sd-angular-core-forms-date.mjs +15 -3
  24. package/fesm2022/sd-angular-core-forms-date.mjs.map +1 -1
  25. package/fesm2022/sd-angular-core-forms-datetime.mjs +17 -3
  26. package/fesm2022/sd-angular-core-forms-datetime.mjs.map +1 -1
  27. package/fesm2022/sd-angular-core-forms-input-number.mjs +18 -3
  28. package/fesm2022/sd-angular-core-forms-input-number.mjs.map +1 -1
  29. package/fesm2022/sd-angular-core-forms-input.mjs +20 -6
  30. package/fesm2022/sd-angular-core-forms-input.mjs.map +1 -1
  31. package/fesm2022/sd-angular-core-forms-radio.mjs +17 -2
  32. package/fesm2022/sd-angular-core-forms-radio.mjs.map +1 -1
  33. package/fesm2022/sd-angular-core-forms-select.mjs +15 -2
  34. package/fesm2022/sd-angular-core-forms-select.mjs.map +1 -1
  35. package/fesm2022/sd-angular-core-forms-textarea.mjs +21 -2
  36. package/fesm2022/sd-angular-core-forms-textarea.mjs.map +1 -1
  37. package/fesm2022/sd-angular-core-modules-auth.mjs +137 -0
  38. package/fesm2022/sd-angular-core-modules-auth.mjs.map +1 -0
  39. package/fesm2022/sd-angular-core-modules-layout.mjs +1 -1
  40. package/fesm2022/sd-angular-core-modules-layout.mjs.map +1 -1
  41. package/fesm2022/sd-angular-core-modules.mjs +1 -0
  42. package/fesm2022/sd-angular-core-modules.mjs.map +1 -1
  43. package/fesm2022/sd-angular-core-pipes.mjs +21 -1
  44. package/fesm2022/sd-angular-core-pipes.mjs.map +1 -1
  45. package/fesm2022/sd-angular-core-services-confirm.mjs +60 -25
  46. package/fesm2022/sd-angular-core-services-confirm.mjs.map +1 -1
  47. package/fesm2022/sd-angular-core-utilities-extensions.mjs +66 -1
  48. package/fesm2022/sd-angular-core-utilities-extensions.mjs.map +1 -1
  49. package/fesm2022/sd-angular-core-utilities-models.mjs +12 -3
  50. package/fesm2022/sd-angular-core-utilities-models.mjs.map +1 -1
  51. package/forms/autocomplete/src/autocomplete.component.d.ts +5 -1
  52. package/forms/date/src/date.component.d.ts +4 -1
  53. package/forms/datetime/src/datetime.component.d.ts +4 -1
  54. package/forms/input/src/input.component.d.ts +6 -4
  55. package/forms/input-number/src/input-number.component.d.ts +4 -1
  56. package/forms/radio/src/radio.component.d.ts +5 -1
  57. package/forms/select/src/select.component.d.ts +5 -1
  58. package/forms/textarea/src/textarea.component.d.ts +3 -1
  59. package/modules/auth/configurations/auth.configuration.d.ts +19 -0
  60. package/modules/auth/configurations/index.d.ts +1 -0
  61. package/modules/auth/guards/auth.guard.d.ts +11 -0
  62. package/modules/auth/guards/index.d.ts +2 -0
  63. package/modules/auth/guards/portal.guard.d.ts +11 -0
  64. package/modules/auth/index.d.ts +3 -0
  65. package/modules/auth/services/auth.model.d.ts +8 -0
  66. package/modules/auth/services/auth.service.d.ts +17 -0
  67. package/modules/auth/services/index.d.ts +2 -0
  68. package/modules/index.d.ts +1 -0
  69. package/package.json +56 -52
  70. package/pipes/index.d.ts +1 -0
  71. package/pipes/src/empty.pipe.d.ts +7 -0
  72. package/sd-angular-core-19.0.0-beta.10.tgz +0 -0
  73. package/services/confirm/src/lib/components/dialog-confirm/dialog-confirm.component.d.ts +8 -0
  74. package/services/confirm/src/lib/confirm.service.d.ts +14 -0
  75. package/utilities/extensions/index.d.ts +1 -0
  76. package/utilities/extensions/src/color.extension.d.ts +20 -0
  77. package/utilities/models/src/maybe-async.model.d.ts +1 -0
  78. package/utilities/models/src/pattern.model.d.ts +2 -2
  79. /package/components/document-builder/src/plugins/{comment.plugin.d.ts → comment/comment.plugin.d.ts} +0 -0
  80. /package/components/document-builder/src/plugins/{page-orientation.plugin.d.ts → page-orientation/page-orientation.plugin.d.ts} +0 -0
  81. /package/components/document-builder/src/plugins/{table-fit.plugin.d.ts → table-fit/table-fit.plugin.d.ts} +0 -0
  82. /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
- // Icon khổ dọc (Mặc định cũ)
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
- const componentFactory = editor.ui.componentFactory;
145
- // Đăng ký nút tên là 'pageOrientation'
146
- componentFactory.add('pageOrientation', locale => {
147
- const view = new ButtonView(locale);
148
- this.buttonView = view;
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
- // --- 1. CONVERSION: MODEL MARKER -> VIEW CSS ---
210
- // Biến Marker thành Highlight màu vàng
211
- editor.conversion.for('editingDowncast').markerToHighlight({
212
- model: 'comment', // Khớp với prefix của markerId (ví dụ: comment:12345)
213
- view: {
214
- classes: 'ck-comment-marker', // Class CSS sẽ được gắn vào thẻ <span> bao quanh text
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
- // --- 3. ĐĂNG KÝ UI COMPONENT: 'addCommentBtn' ---
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
- // ẨN BUTTON NẾU KHÔNG onAddComment
167
+ // Ẩn button nếu không 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, // Mặc định disable
179
+ isEnabled: false,
239
180
  });
240
- // 4. Logic Enable/Disable: Dựa theo Selection
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
- // 5. Logic Execute: Khi bấm nút
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
- // BẮN EVENT RA NGOÀI - KHÔNG TỰ ADD MARKER
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
- allowAttributes: ['id', 'value', 'display', 'data'],
251
+ allowWhere: '$text',
252
+ isInline: true,
253
+ isObject: true,
254
+ allowAttributes: ['id', 'uuid', 'value', 'display'],
283
255
  });
284
- // 2. Conversion: Model -> View (Hiển thị ra HTML)
285
- conversion.for('downcast').elementToElement({
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
- // 3. Conversion: View -> Model (Khi load data hoặc paste)
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
- // Ngăn trình duyệt xử mặc định
317
+ // data.dropRange 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
- let variable = JSON.parse(jsonData);
344
- const config = editor.config;
345
- const getOption = config.get('getOption');
346
- const option = getOption?.();
347
- if (option?.onDropVariable) {
348
- const result = await SdResolveMaybeAsync(option.onDropVariable(variable, 0));
349
- //* Hỗ trợ dữ liệu có sẵn sẽ chỉ cần nhận vào boolean
350
- if (typeof result === 'boolean') {
351
- if (!result)
352
- return;
353
- }
354
- else {
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
- throw new Error('Dữ liệu variable không hợp lệ');
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
- // Xác định vị trí thả chính xác (Quan trọng)
365
- // data.dropRange vị trí con chuột trên View khi thả
366
- const viewRange = data.dropRange;
367
- const modelRange = editor.editing.mapper.toModelRange(viewRange);
368
- editor.model.change(writer => {
369
- // A. Chèn biến
370
- const variableElem = writer.createElement('variable', {
371
- id: variable.id,
372
- value: variable.value,
373
- display: variable.display,
374
- data: variable.data,
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
- editor.model.insertContent(variableElem, modelRange);
377
- // B. Đặt con trỏ ra sau biến
378
- writer.setSelection(variableElem, 'after');
379
- // C. [QUAN TRỌNG] XÓA SẠCH CÁC MARKER DROP
380
- // Thay vì chỉ xóa 'drop-target', ta duyệt tìm tất cả marker có tên bắt đầu bằng 'drop-target'
381
- // đôi khi CKEditor tạo ra các biến thể khác nhau
382
- for (const marker of editor.model.markers) {
383
- if (marker.name.startsWith('drop-target')) {
384
- writer.removeMarker(marker);
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
+ // 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
- // FIX LỖI: Focus lại vào editor để xóa các artifact của việc kéo thả
389
- editor.editing.view.focus();
390
- // [BỔ SUNG] XÓA CLASS RÁC TRÊN VIEW (NẾU MARKER KHÔNG HẾT)
391
- // Đôi khi View chưa kịp render lại, ta ép xóa class thủ công trên root nếu cần
392
- // (Thường bước C trên đủ, nhưng đây chốt chặn cuối cùng bằng JS)
393
- const viewRoot = editor.editing.view.document.getRoot();
394
- if (viewRoot) {
395
- editor.editing.view.change(viewWriter => {
396
- // Loại bỏ class decoration nếu nó bị dính vào root (hiếm gặp nhưng có thể)
397
- viewWriter.removeClass('ck-clipboard-drop-target-position', viewRoot);
398
- viewWriter.removeClass('ck-clipboard-drop-target-line', viewRoot);
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 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
- PageOrientationPlugin,
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
- 'tableColumn',
735
- 'tableRow',
736
- 'mergeTableCells',
737
- '|',
738
- 'tableProperties', // <--- Nút chỉnh thuộc tính bảng (Viền, Màu, Width)
739
- 'tableCellProperties',
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
- // https://onemount.atlassian.net/browse/SM-1862
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
- // Đăng ký sự kiện lắng nghe Selection để làm Comment
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
- // ĐĂNG SỰ KIỆN LẮNG NGHE THAY ĐỔI NỘI DUNG
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
- // Kho lưu trữ tham chiếu Model của Heading (để phục vụ scroll)
937
- #headingElementsMap = new Map();
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
- // HÀM LẤY DANH SÁCH HEADING (TOC)
941
- // ========================================================================
942
- getHeadings: () => {
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
- // HÀM SCROLL TO HEADING
980
- // ========================================================================
981
- scrollToHeading: (id) => {
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
- const editor = this.#editor;
988
- const view = editor.editing.view;
989
- // 2. Chuyển đổi Model Element -> View Element
990
- // mapper.toViewElement sẽ trả về thẻ HTML ảo (View Element) tương ứng (ví dụ thẻ <h2>)
991
- const viewElement = editor.editing.mapper.toViewElement(modelElement);
1208
+ this.#editor.model.change(writer => {
1209
+ // Xóa marker
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
- // 3. Từ View Element ảo -> Lấy ra DOM thật (HTMLElement)
994
- const domElement = view.domConverter.mapViewToDom(viewElement);
1228
+ const domElement = this.#editor.editing.view.domConverter.viewToDom(viewElement);
995
1229
  if (domElement) {
996
- // 4. Dùng hàm native của trình duyệt để cuộn
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
- // HELPER: LẤY TEXT TỪ ELEMENT (Đệ quy nhẹ)
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 COMMENT
1040
1251
  // ========================================================================
1041
1252
  comment = {
1042
1253
  /**
@@ -1182,72 +1393,78 @@ class SdDocumentBuilder {
1182
1393
  }
1183
1394
  },
1184
1395
  };
1185
- // exportDocx(fileName: string = 'document.docx'): void {
1186
- // if (!this.#editor) return;
1187
- // // 1. Kiểm tra xem Editor đang ở chế độ Landscape hay Portrait
1188
- // // (Dựa vào class 'landscape' mà Plugin PageOrientation đã toggle trên View)
1189
- // const rootElement = this.#editor.editing.view.document.getRoot();
1190
- // const isLandscape = rootElement?.hasClass('landscape');
1191
- // const orientation = isLandscape ? 'landscape' : 'portrait';
1192
- // // 2. Lấy nội dung HTML
1193
- // const contentHtml = this.#editor.getData();
1194
- // // 3. Chuẩn bị HTML với CSS @page động
1195
- // const fullHtml = `
1196
- // <!DOCTYPE html>
1197
- // <html>
1198
- // <head>
1199
- // <meta charset="UTF-8">
1200
- // <style>
1201
- // /* --- CẤU HÌNH KHỔ GIẤY DỰA TRÊN TRẠNG THÁI --- */
1202
- // @page {
1203
- // size: A4 ${orientation}; /* Thêm portrait hoặc landscape vào đây */
1204
- // margin: 20mm;
1205
- // }
1206
- // body {
1207
- // font-family: 'Times New Roman', serif;
1208
- // font-size: 13pt;
1209
- // line-height: 1.5;
1210
- // }
1211
- // /* --- STYLE CHO BẢNG BIỂU --- */
1212
- // table {
1213
- // width: 100%;
1214
- // border-collapse: collapse;
1215
- // }
1216
- // td, th {
1217
- // border: 1px solid black;
1218
- // padding: 5px;
1219
- // }
1220
- // /* --- STYLE CHO BIẾN --- */
1221
- // .variable-widget {
1222
- // color: #1565c0;
1223
- // background-color: #e3f2fd;
1224
- // font-weight: bold;
1225
- // border: 1px solid #90caf9;
1226
- // padding: 0 4px;
1227
- // border-radius: 4px;
1228
- // }
1229
- // /* --- ẨN COMMENT KHI IN --- */
1230
- // .ck-comment-marker {
1231
- // background-color: transparent;
1232
- // border: none;
1233
- // }
1234
- // </style>
1235
- // </head>
1236
- // <body>
1237
- // ${contentHtml}
1238
- // </body>
1239
- // </html>
1240
- // `;
1241
- // // 3. Convert sang Blob (Dạng file Binary)
1242
- // asBlob(fullHtml, {
1243
- // orientation: 'portrait', // 'portrait' hoặc 'landscape'
1244
- // margins: { top: 720, right: 720, bottom: 720, left: 720 }, // Đơn vị twips (1440 twips = 1 inch)
1245
- // }).then(blob => {
1246
- // if (blob instanceof Blob) {
1247
- // SdUtilities.downloadBlob(blob, fileName);
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", "@charset \"UTF-8\";:host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;cursor:pointer;transition:background-color .2s}: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\";:host ::ng-deep .variable-widget{background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9;border-radius:4px;padding:2px 6px;font-weight:600;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10px;cursor:default;-webkit-user-select:none;user-select:none;display:inline-block;margin:0 2px;vertical-align:middle;font-size:0}: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-clipboard-drop-target-line{display:none!important}\n", "@charset \"UTF-8\";: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", "@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"], 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"] }] });
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", "@charset \"UTF-8\";:host ::ng-deep .ck-comment-marker{background-color:#ffeb3b80;border-bottom:2px solid #fbc02d;cursor:pointer;transition:background-color .2s}: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\";:host ::ng-deep .variable-widget{background-color:#e3f2fd;color:#1976d2;border:1px solid #90caf9;border-radius:4px;padding:2px 6px;font-weight:600;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;font-size:10px;cursor:default;-webkit-user-select:none;user-select:none;display:inline-block;margin:0 2px;vertical-align:middle;font-size:0}: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-clipboard-drop-target-line{display:none!important}\n", "@charset \"UTF-8\";: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", "@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"] }]
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 }]