@sd-angular/core 19.0.0-beta.42 → 19.0.0-beta.44

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.
@@ -6,6 +6,7 @@ export declare class CkCommentPlugin extends Plugin {
6
6
  static get requires(): (typeof ContextualBalloon)[];
7
7
  static readonly PENDING_MARKER_ID = "__pending_comment__";
8
8
  static readonly DEFAULT_SEARCH_RANGE = 5;
9
+ static readonly DEFAULT_MAX_TEXT_LENGTH = 1000;
9
10
  static readonly DEFAULT_COLORS: CkCommentColors;
10
11
  init(): void;
11
12
  /**
@@ -35,9 +35,15 @@ export interface CkCommentConfig {
35
35
  onRemoveComment?: (id: string | number) => void;
36
36
  onChange?: (comments: CkComment[]) => void;
37
37
  onCancelPending?: () => void;
38
+ onError?: (error: {
39
+ code: string;
40
+ message: string;
41
+ data?: any;
42
+ }) => void;
38
43
  searchRange?: number;
39
44
  debug?: boolean;
40
45
  colors?: CkCommentColors;
46
+ maxTextLength?: number;
41
47
  }
42
48
  /**
43
49
  * Data returned when user selects text for comment
@@ -24,6 +24,8 @@ export interface SdMiniEditorOption {
24
24
  placeholder?: string;
25
25
  /** Chiều cao editor (mặc định: auto) */
26
26
  height?: string;
27
+ /** Chiều cao tối đa của editor (ví dụ: '300px') */
28
+ maxHeight?: string;
27
29
  /** Bật/tắt mention plugin */
28
30
  enableMention?: boolean;
29
31
  /** Cấu hình mention */
@@ -3135,19 +3135,21 @@ class CkCommentPlugin extends Plugin {
3135
3135
  #selectedId = null;
3136
3136
  #pendingId = null; // ID cho pending highlight
3137
3137
  #isCreatingPending = false; // Flag để prevent clearing pending khi đang tạo
3138
+ #isProcessingClick = false; // Flag để prevent duplicate click events
3138
3139
  #balloon;
3139
3140
  #config = {};
3140
3141
  // Hằng số ID cho pending marker
3141
3142
  static PENDING_MARKER_ID = '__pending_comment__';
3142
3143
  // Số node tìm kiếm mặc định khi path không chính xác
3143
3144
  static DEFAULT_SEARCH_RANGE = 5;
3145
+ // Độ dài text tối đa để tạo marker
3146
+ static DEFAULT_MAX_TEXT_LENGTH = 1000;
3144
3147
  // Màu sắc mặc định cho markers
3145
3148
  static DEFAULT_COLORS = {
3146
3149
  marker: 'rgba(59, 130, 246, 0.2)',
3147
3150
  markerSelected: 'rgba(59, 130, 246, 0.5)',
3148
3151
  markerPending: 'rgba(245, 158, 11, 0.4)',
3149
3152
  markerModified: 'rgba(255, 193, 7, 0.4)',
3150
- markerBroken: 'rgba(220, 53, 69, 0.3)',
3151
3153
  };
3152
3154
  /**
3153
3155
  * Debug log - chỉ log khi debug config là true
@@ -3208,7 +3210,13 @@ class CkCommentPlugin extends Plugin {
3208
3210
  let hasValidContent = false;
3209
3211
  if (range && !isCollapsed) {
3210
3212
  const text = this.#getTextFromRange(range);
3211
- hasValidContent = text.trim().length > 0;
3213
+ const trimmedText = text.trim();
3214
+ const maxTextLength = this.#config.maxTextLength ?? _a.DEFAULT_MAX_TEXT_LENGTH;
3215
+ // Kiểm tra: có content, không phải chỉ khoảng trắng, và không vượt quá max length
3216
+ hasValidContent = trimmedText.length > 0 && trimmedText.length <= maxTextLength;
3217
+ if (trimmedText.length > maxTextLength) {
3218
+ this.#log(`Độ dài text vượt quá giới hạn: ${trimmedText.length} > ${maxTextLength}`);
3219
+ }
3212
3220
  }
3213
3221
  view.isEnabled = hasValidContent;
3214
3222
  });
@@ -3261,22 +3269,32 @@ class CkCommentPlugin extends Plugin {
3261
3269
  };
3262
3270
  }
3263
3271
  const comment = self.#comments.get(commentId);
3264
- // Build CSS variables based on status
3265
- const cssVars = [`--comment-bg: ${colors.marker}`];
3272
+ // Build CSS variables based on status - ALWAYS set the correct variable for the status
3273
+ let cssVars = [];
3266
3274
  if (comment) {
3275
+ // Add status class
3267
3276
  classes.push(`ck-comment-${comment.status}`);
3268
- if (commentId === self.#selectedId) {
3269
- classes.push('ck-comment-selected');
3270
- cssVars.push(`--comment-selected-bg: ${colors.markerSelected}`);
3271
- }
3272
- // Add status-specific colors
3277
+ // Set CSS variable based on status
3273
3278
  if (comment.status === 'modified') {
3274
- cssVars.push(`--comment-modified-bg: ${colors.markerModified}`);
3279
+ cssVars = [`--comment-modified-bg: ${colors.markerModified}`];
3275
3280
  }
3276
3281
  else if (comment.status === 'broken') {
3277
- cssVars.push(`--comment-broken-bg: ${colors.markerBroken}`);
3282
+ cssVars = [`--comment-broken-bg: ${colors.markerBroken}`];
3283
+ }
3284
+ else {
3285
+ // normal status
3286
+ cssVars = [`--comment-bg: ${colors.marker}`];
3287
+ }
3288
+ // Add selected state if needed
3289
+ if (commentId === self.#selectedId) {
3290
+ classes.push('ck-comment-selected');
3291
+ cssVars.push(`--comment-selected-bg: ${colors.markerSelected}`);
3278
3292
  }
3279
3293
  }
3294
+ else {
3295
+ // No comment found - use default
3296
+ cssVars = [`--comment-bg: ${colors.marker}`];
3297
+ }
3280
3298
  return {
3281
3299
  classes: classes,
3282
3300
  attributes: {
@@ -3292,7 +3310,7 @@ class CkCommentPlugin extends Plugin {
3292
3310
  // ========================================================================
3293
3311
  #setupMarkerClickHandler() {
3294
3312
  const viewDocument = this.editor.editing.view.document;
3295
- // Lắng nghe cả click và mousedown để đảm bảo bắt được event
3313
+ // Lắng nghe cả click và mousedown trên CKEditor view
3296
3314
  viewDocument.on('mousedown', (evt, data) => {
3297
3315
  this.#log('Mousedown event triggered, data:', data);
3298
3316
  this.#handleMarkerClick(evt, data);
@@ -3301,8 +3319,58 @@ class CkCommentPlugin extends Plugin {
3301
3319
  this.#log('Click event triggered, data:', data);
3302
3320
  this.#handleMarkerClick(evt, data);
3303
3321
  });
3322
+ // Thêm DOM event listener như fallback để đảm bảo bắt được click
3323
+ // Sử dụng editor's editable DOM element
3324
+ const editableElement = this.editor.ui.getEditableElement();
3325
+ if (editableElement) {
3326
+ editableElement.addEventListener('click', (domEvent) => {
3327
+ this.#log('DOM click event triggered');
3328
+ this.#handleDomMarkerClick(domEvent, editableElement);
3329
+ });
3330
+ }
3331
+ }
3332
+ /**
3333
+ * Handle DOM click event (fallback)
3334
+ */
3335
+ #handleDomMarkerClick(domEvent, rootElement) {
3336
+ // Prevent duplicate if already processing
3337
+ if (this.#isProcessingClick) {
3338
+ this.#log('DOM click skipped - already processing');
3339
+ return;
3340
+ }
3341
+ let targetElement = domEvent.target;
3342
+ // Traverse up to find marker element
3343
+ while (targetElement && targetElement !== rootElement) {
3344
+ if (targetElement.classList?.contains('ck-comment-marker')) {
3345
+ const commentId = targetElement.getAttribute('data-comment-id');
3346
+ this.#log('DOM click found marker with commentId:', commentId);
3347
+ if (commentId) {
3348
+ this.#isProcessingClick = true;
3349
+ this.selectComment(commentId, false);
3350
+ domEvent.stopPropagation();
3351
+ domEvent.preventDefault();
3352
+ // Reset flag after a short delay
3353
+ setTimeout(() => {
3354
+ this.#isProcessingClick = false;
3355
+ }, 50);
3356
+ }
3357
+ return;
3358
+ }
3359
+ targetElement = targetElement.parentElement;
3360
+ }
3361
+ // Click outside markers - clear selection
3362
+ if (this.#selectedId) {
3363
+ this.#log('DOM click outside markers, clearing selection');
3364
+ this.#selectedId = null;
3365
+ this.#refreshView();
3366
+ }
3304
3367
  }
3305
3368
  #handleMarkerClick(evt, data) {
3369
+ // Prevent duplicate if already processing
3370
+ if (this.#isProcessingClick) {
3371
+ this.#log('View click skipped - already processing');
3372
+ return;
3373
+ }
3306
3374
  const viewElement = data.target;
3307
3375
  let element = viewElement;
3308
3376
  this.#log('Target element:', element, 'hasClass:', typeof element?.hasClass);
@@ -3313,8 +3381,13 @@ class CkCommentPlugin extends Plugin {
3313
3381
  if (hasMarkerClass) {
3314
3382
  const commentId = element.getAttribute('data-comment-id');
3315
3383
  this.#log('Found marker with commentId:', commentId);
3384
+ this.#isProcessingClick = true;
3316
3385
  this.selectComment(commentId, false);
3317
3386
  evt.stop();
3387
+ // Reset flag after a short delay
3388
+ setTimeout(() => {
3389
+ this.#isProcessingClick = false;
3390
+ }, 50);
3318
3391
  return;
3319
3392
  }
3320
3393
  element = element.parent;
@@ -3348,9 +3421,11 @@ class CkCommentPlugin extends Plugin {
3348
3421
  if (!selection.isCollapsed) {
3349
3422
  const range = selection.getFirstRange();
3350
3423
  if (range) {
3351
- // Chỉ hiện balloon khi selection có content không phải khoảng trắng
3424
+ // Chỉ hiện balloon khi selection có content không phải khoảng trắng và không vượt quá max length
3352
3425
  const text = this.#getTextFromRange(range);
3353
- if (text.trim().length > 0) {
3426
+ const trimmedText = text.trim();
3427
+ const maxTextLength = this.#config.maxTextLength ?? _a.DEFAULT_MAX_TEXT_LENGTH;
3428
+ if (trimmedText.length > 0 && trimmedText.length <= maxTextLength) {
3354
3429
  this.#showBalloon(range);
3355
3430
  }
3356
3431
  else {
@@ -3459,6 +3534,7 @@ class CkCommentPlugin extends Plugin {
3459
3534
  JSON.stringify(comment.endPath) !== JSON.stringify(newEndPath);
3460
3535
  const textChanged = currentText !== comment.currentText;
3461
3536
  if (pathChanged || textChanged) {
3537
+ const oldStatus = comment.status;
3462
3538
  hasChanges = true;
3463
3539
  comment.startPath = newStartPath;
3464
3540
  comment.endPath = newEndPath;
@@ -3473,13 +3549,16 @@ class CkCommentPlugin extends Plugin {
3473
3549
  else {
3474
3550
  comment.status = 'modified';
3475
3551
  }
3552
+ this.#log(`Comment ${id} status changed: ${oldStatus} -> ${comment.status}`, `\n originalText: "${comment.originalText}"`, `\n currentText: "${currentText}"`, `\n textChanged: ${textChanged}`);
3476
3553
  }
3477
3554
  }
3478
3555
  else {
3479
3556
  // Không tìm thấy marker - bị hỏng
3480
3557
  if (comment.status !== 'broken') {
3558
+ const oldStatus = comment.status;
3481
3559
  hasChanges = true;
3482
3560
  comment.status = 'broken';
3561
+ this.#log(`Comment ${id} marker not found, status changed: ${oldStatus} -> broken`);
3483
3562
  }
3484
3563
  }
3485
3564
  });
@@ -3666,6 +3745,18 @@ class CkCommentPlugin extends Plugin {
3666
3745
  if (!trimmedText) {
3667
3746
  return null;
3668
3747
  }
3748
+ // Kiểm tra độ dài text tối đa
3749
+ const maxTextLength = this.#config.maxTextLength ?? _a.DEFAULT_MAX_TEXT_LENGTH;
3750
+ if (trimmedText.length > maxTextLength) {
3751
+ this.#warn(`Text too long: ${trimmedText.length} > ${maxTextLength}`);
3752
+ // Fire error callback
3753
+ this.#config.onError?.({
3754
+ code: 'TEXT_TOO_LONG',
3755
+ message: `Văn bản quá dài (${trimmedText.length} ký tự). Tối đa ${maxTextLength} ký tự.`,
3756
+ data: { textLength: trimmedText.length, maxLength: maxTextLength },
3757
+ });
3758
+ return null;
3759
+ }
3669
3760
  // Tính toán số ký tự cần trim ở đầu và cuối
3670
3761
  const leadingWhitespace = text.length - text.trimStart().length;
3671
3762
  const trailingWhitespace = text.length - text.trimEnd().length;
@@ -3792,16 +3883,49 @@ class CkCommentPlugin extends Plugin {
3792
3883
  }
3793
3884
  #scrollToComment(id) {
3794
3885
  const marker = this.editor.model.markers.get(`comment:${id}`);
3795
- if (!marker)
3886
+ if (!marker) {
3887
+ this.#warn('Marker not found for scroll:', id);
3796
3888
  return;
3889
+ }
3797
3890
  const editor = this.editor;
3798
- // Scroll đến marker range (không set selection - không bôi đen)
3799
- const viewRange = editor.editing.mapper.toViewRange(marker.getRange());
3800
- const domRange = editor.editing.view.domConverter.viewRangeToDom(viewRange);
3801
- // Scroll element into view
3802
- const domElement = domRange.startContainer.parentElement;
3803
- if (domElement) {
3804
- domElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
3891
+ const STICKY_OFFSET = 100; // Offset cho sticky toolbar
3892
+ try {
3893
+ // Get the model range from marker
3894
+ const modelRange = marker.getRange();
3895
+ // Convert model range to view range
3896
+ const viewRange = editor.editing.mapper.toViewRange(modelRange);
3897
+ // Convert view range to DOM range
3898
+ const domRange = editor.editing.view.domConverter.viewRangeToDom(viewRange);
3899
+ this.#log('DOM range start:', domRange.startContainer, domRange.startOffset);
3900
+ // Get the start position of the range
3901
+ let targetElement = domRange.startContainer;
3902
+ // If it's a text node, get its parent
3903
+ if (targetElement.nodeType === Node.TEXT_NODE) {
3904
+ targetElement = targetElement.parentElement;
3905
+ }
3906
+ this.#log('Target element:', targetElement);
3907
+ // Find .builder-container
3908
+ let scrollContainer = targetElement;
3909
+ while (scrollContainer && !scrollContainer.classList?.contains('builder-container')) {
3910
+ scrollContainer = scrollContainer.parentElement;
3911
+ }
3912
+ this.#log('Found scroll container:', scrollContainer);
3913
+ if (!scrollContainer) {
3914
+ this.#warn('No builder-container found');
3915
+ return;
3916
+ }
3917
+ // Get the target element's position relative to the container
3918
+ const containerRect = scrollContainer.getBoundingClientRect();
3919
+ const targetRect = targetElement.getBoundingClientRect();
3920
+ const relativeTop = targetRect.top - containerRect.top + scrollContainer.scrollTop - STICKY_OFFSET;
3921
+ this.#log('Container top:', containerRect.top);
3922
+ this.#log('Target top:', targetRect.top);
3923
+ this.#log('Current scroll:', scrollContainer.scrollTop);
3924
+ this.#log('Relative top to scroll:', relativeTop);
3925
+ scrollContainer.scrollTo({ top: relativeTop, behavior: 'smooth' });
3926
+ }
3927
+ catch (e) {
3928
+ this.#warn('Error scrolling to comment:', e);
3805
3929
  }
3806
3930
  }
3807
3931
  #refreshView() {