@djvlc/sandbox 1.0.0 → 1.0.1

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/dist/index.cjs CHANGED
@@ -51,6 +51,7 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
51
51
  MessageType2["COMPONENT_CONTEXT_MENU"] = "component_context_menu";
52
52
  MessageType2["COMPONENT_DRAG_START"] = "component_drag_start";
53
53
  MessageType2["COMPONENT_DRAG_END"] = "component_drag_end";
54
+ MessageType2["COMPONENT_DROP"] = "component_drop";
54
55
  MessageType2["ERROR"] = "error";
55
56
  MessageType2["LOG"] = "log";
56
57
  return MessageType2;
@@ -290,6 +291,14 @@ var SandboxClient = class {
290
291
  this.runtime = null;
291
292
  this.selectedComponents = /* @__PURE__ */ new Set();
292
293
  this.hoveredComponent = null;
294
+ this.dragState = {
295
+ isDragging: false,
296
+ draggedComponentId: null,
297
+ draggedElement: null,
298
+ dropTarget: null,
299
+ dropPosition: null,
300
+ dropIndicator: null
301
+ };
293
302
  // ==================== 私有方法 ====================
294
303
  this.handleMessage = async (event) => {
295
304
  const message = event.data;
@@ -351,6 +360,12 @@ var SandboxClient = class {
351
360
  this.options = {
352
361
  targetOrigin: "*",
353
362
  debug: false,
363
+ enableDrag: true,
364
+ dragOptions: {
365
+ dragOpacity: 0.5,
366
+ showDropIndicator: true,
367
+ dropIndicatorColor: "#1890ff"
368
+ },
354
369
  ...options
355
370
  };
356
371
  }
@@ -447,17 +462,19 @@ var SandboxClient = class {
447
462
  * 注入编辑器样式
448
463
  */
449
464
  injectEditorStyles() {
465
+ const indicatorColor = this.options.dragOptions?.dropIndicatorColor || "#1890ff";
450
466
  const style = document.createElement("style");
467
+ style.id = "djvlc-editor-styles";
451
468
  style.textContent = `
452
469
  /* \u9009\u4E2D\u72B6\u6001 */
453
470
  [data-component-id].djvlc-selected {
454
- outline: 2px solid #1890ff !important;
471
+ outline: 2px solid ${indicatorColor} !important;
455
472
  outline-offset: 2px;
456
473
  }
457
474
 
458
475
  /* \u60AC\u505C\u72B6\u6001 */
459
476
  [data-component-id].djvlc-hovered {
460
- outline: 1px dashed #1890ff !important;
477
+ outline: 1px dashed ${indicatorColor} !important;
461
478
  outline-offset: 1px;
462
479
  }
463
480
 
@@ -471,39 +488,300 @@ var SandboxClient = class {
471
488
  pointer-events: auto;
472
489
  }
473
490
 
491
+ /* \u53EF\u62D6\u62FD\u72B6\u6001 */
492
+ [data-component-id][draggable="true"] {
493
+ cursor: grab;
494
+ }
495
+
496
+ [data-component-id][draggable="true"]:active {
497
+ cursor: grabbing;
498
+ }
499
+
500
+ /* \u62D6\u62FD\u4E2D\u72B6\u6001 */
501
+ [data-component-id].djvlc-dragging {
502
+ opacity: ${this.options.dragOptions?.dragOpacity || 0.5};
503
+ }
504
+
505
+ /* \u653E\u7F6E\u76EE\u6807\u72B6\u6001 */
506
+ [data-component-id].djvlc-drop-target {
507
+ outline: 2px dashed ${indicatorColor} !important;
508
+ outline-offset: 4px;
509
+ }
510
+
474
511
  /* \u62D6\u62FD\u5360\u4F4D\u7B26 */
475
512
  .djvlc-drop-indicator {
476
513
  position: absolute;
477
- background: #1890ff;
478
- opacity: 0.5;
514
+ background: ${indicatorColor};
515
+ opacity: 0.8;
479
516
  pointer-events: none;
480
517
  z-index: 10000;
518
+ transition: all 0.1s ease;
481
519
  }
482
520
 
483
521
  .djvlc-drop-indicator.horizontal {
484
522
  height: 4px;
485
523
  width: 100%;
524
+ border-radius: 2px;
486
525
  }
487
526
 
488
527
  .djvlc-drop-indicator.vertical {
489
528
  width: 4px;
490
529
  height: 100%;
530
+ border-radius: 2px;
531
+ }
532
+
533
+ /* \u653E\u7F6E\u63D0\u793A\u7BAD\u5934 */
534
+ .djvlc-drop-indicator::before {
535
+ content: '';
536
+ position: absolute;
537
+ width: 8px;
538
+ height: 8px;
539
+ background: ${indicatorColor};
540
+ border-radius: 50%;
541
+ left: -4px;
542
+ top: -2px;
543
+ }
544
+
545
+ .djvlc-drop-indicator::after {
546
+ content: '';
547
+ position: absolute;
548
+ width: 8px;
549
+ height: 8px;
550
+ background: ${indicatorColor};
551
+ border-radius: 50%;
552
+ right: -4px;
553
+ top: -2px;
491
554
  }
492
555
  `;
493
556
  document.head.appendChild(style);
494
557
  }
558
+ /**
559
+ * 初始化拖拽事件监听
560
+ */
561
+ initDragEventListeners() {
562
+ if (!this.options.enableDrag) return;
563
+ this.makeComponentsDraggable();
564
+ document.addEventListener("dragstart", (e) => {
565
+ const target = e.target;
566
+ const componentEl = this.findComponentElement(target);
567
+ if (!componentEl) return;
568
+ const componentId = componentEl.getAttribute("data-component-id");
569
+ if (!componentId) return;
570
+ this.dragState.isDragging = true;
571
+ this.dragState.draggedComponentId = componentId;
572
+ this.dragState.draggedElement = componentEl;
573
+ componentEl.classList.add("djvlc-dragging");
574
+ e.dataTransfer?.setData("text/plain", componentId);
575
+ e.dataTransfer?.setData("application/json", JSON.stringify({
576
+ componentId,
577
+ componentType: componentEl.getAttribute("data-component-type")
578
+ }));
579
+ if (e.dataTransfer) {
580
+ e.dataTransfer.effectAllowed = "move";
581
+ }
582
+ this.log("debug", "Drag start:", componentId);
583
+ });
584
+ document.addEventListener("dragover", (e) => {
585
+ if (!this.dragState.isDragging) return;
586
+ e.preventDefault();
587
+ const target = e.target;
588
+ const componentEl = this.findComponentElement(target);
589
+ if (!componentEl || componentEl === this.dragState.draggedElement) {
590
+ this.hideDropIndicator();
591
+ return;
592
+ }
593
+ const componentId = componentEl.getAttribute("data-component-id");
594
+ if (!componentId) return;
595
+ const rect = componentEl.getBoundingClientRect();
596
+ const y = e.clientY;
597
+ const threshold = rect.height / 3;
598
+ let position;
599
+ if (y < rect.top + threshold) {
600
+ position = "before";
601
+ } else if (y > rect.bottom - threshold) {
602
+ position = "after";
603
+ } else {
604
+ position = "inside";
605
+ }
606
+ if (this.dragState.dropTarget !== componentEl || this.dragState.dropPosition !== position) {
607
+ this.dragState.dropTarget?.classList.remove("djvlc-drop-target");
608
+ this.dragState.dropTarget = componentEl;
609
+ this.dragState.dropPosition = position;
610
+ if (position === "inside") {
611
+ componentEl.classList.add("djvlc-drop-target");
612
+ this.hideDropIndicator();
613
+ } else {
614
+ componentEl.classList.remove("djvlc-drop-target");
615
+ this.showDropIndicator(componentEl, position);
616
+ }
617
+ }
618
+ });
619
+ document.addEventListener("dragleave", (e) => {
620
+ const target = e.target;
621
+ const componentEl = this.findComponentElement(target);
622
+ if (componentEl && componentEl === this.dragState.dropTarget) {
623
+ const relatedTarget = e.relatedTarget;
624
+ if (!relatedTarget || !componentEl.contains(relatedTarget)) {
625
+ componentEl.classList.remove("djvlc-drop-target");
626
+ }
627
+ }
628
+ });
629
+ document.addEventListener("drop", (e) => {
630
+ if (!this.dragState.isDragging) return;
631
+ e.preventDefault();
632
+ const { draggedComponentId, dropTarget, dropPosition } = this.dragState;
633
+ if (draggedComponentId && dropTarget && dropPosition) {
634
+ const targetComponentId = dropTarget.getAttribute("data-component-id");
635
+ if (targetComponentId && targetComponentId !== draggedComponentId) {
636
+ this.log("info", "Drop:", draggedComponentId, dropPosition, targetComponentId);
637
+ this.send("component_drop" /* COMPONENT_DROP */, {
638
+ componentId: draggedComponentId,
639
+ targetComponentId,
640
+ position: dropPosition
641
+ });
642
+ }
643
+ }
644
+ this.endDrag();
645
+ });
646
+ document.addEventListener("dragend", () => {
647
+ this.endDrag();
648
+ });
649
+ }
650
+ /**
651
+ * 使组件可拖拽
652
+ */
653
+ makeComponentsDraggable() {
654
+ const components = document.querySelectorAll("[data-component-id]");
655
+ components.forEach((el) => {
656
+ el.draggable = true;
657
+ });
658
+ const observer = new MutationObserver((mutations) => {
659
+ mutations.forEach((mutation) => {
660
+ mutation.addedNodes.forEach((node) => {
661
+ if (node instanceof HTMLElement) {
662
+ if (node.hasAttribute("data-component-id")) {
663
+ node.draggable = true;
664
+ }
665
+ node.querySelectorAll("[data-component-id]").forEach((el) => {
666
+ el.draggable = true;
667
+ });
668
+ }
669
+ });
670
+ });
671
+ });
672
+ observer.observe(document.body, {
673
+ childList: true,
674
+ subtree: true
675
+ });
676
+ }
677
+ /**
678
+ * 显示放置指示器
679
+ */
680
+ showDropIndicator(target, position) {
681
+ if (!this.options.dragOptions?.showDropIndicator) return;
682
+ let indicator = this.dragState.dropIndicator;
683
+ if (!indicator) {
684
+ indicator = document.createElement("div");
685
+ indicator.className = "djvlc-drop-indicator horizontal";
686
+ document.body.appendChild(indicator);
687
+ this.dragState.dropIndicator = indicator;
688
+ }
689
+ const rect = target.getBoundingClientRect();
690
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
691
+ const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
692
+ indicator.style.left = `${rect.left + scrollLeft}px`;
693
+ indicator.style.width = `${rect.width}px`;
694
+ if (position === "before") {
695
+ indicator.style.top = `${rect.top + scrollTop - 2}px`;
696
+ } else {
697
+ indicator.style.top = `${rect.bottom + scrollTop - 2}px`;
698
+ }
699
+ indicator.style.display = "block";
700
+ }
701
+ /**
702
+ * 隐藏放置指示器
703
+ */
704
+ hideDropIndicator() {
705
+ if (this.dragState.dropIndicator) {
706
+ this.dragState.dropIndicator.style.display = "none";
707
+ }
708
+ }
709
+ /**
710
+ * 结束拖拽
711
+ */
712
+ endDrag() {
713
+ this.dragState.draggedElement?.classList.remove("djvlc-dragging");
714
+ this.dragState.dropTarget?.classList.remove("djvlc-drop-target");
715
+ this.hideDropIndicator();
716
+ this.dragState = {
717
+ isDragging: false,
718
+ draggedComponentId: null,
719
+ draggedElement: null,
720
+ dropTarget: null,
721
+ dropPosition: null,
722
+ dropIndicator: this.dragState.dropIndicator
723
+ // 保留 indicator 元素
724
+ };
725
+ }
495
726
  async handleInit(data) {
496
727
  this.log("info", "Init with config:", data);
728
+ void data;
729
+ if (this.runtime) {
730
+ try {
731
+ await this.runtime.init();
732
+ } catch (error) {
733
+ this.log("error", "Failed to init runtime:", error);
734
+ throw error;
735
+ }
736
+ }
497
737
  }
498
738
  async handleLoadPage(data) {
499
739
  this.log("info", "Load page:", data.pageUid);
740
+ if (!this.runtime) {
741
+ throw new Error("Runtime not set");
742
+ }
743
+ try {
744
+ await this.runtime.load();
745
+ await this.runtime.render();
746
+ this.syncState();
747
+ } catch (error) {
748
+ this.log("error", "Failed to load page:", error);
749
+ throw error;
750
+ }
500
751
  }
501
752
  async handleUpdateSchema(data) {
502
753
  this.log("info", "Update schema", data.fullUpdate ? "(full)" : "(partial)");
754
+ if (!this.runtime) {
755
+ throw new Error("Runtime not set");
756
+ }
757
+ try {
758
+ if (data.fullUpdate) {
759
+ await this.runtime.load();
760
+ await this.runtime.render();
761
+ } else {
762
+ await this.runtime.load();
763
+ await this.runtime.render();
764
+ }
765
+ this.syncState();
766
+ } catch (error) {
767
+ this.log("error", "Failed to update schema:", error);
768
+ throw error;
769
+ }
503
770
  }
504
771
  async handleRefresh() {
505
772
  this.log("info", "Refresh page");
506
- window.location.reload();
773
+ if (this.runtime) {
774
+ try {
775
+ await this.runtime.load();
776
+ await this.runtime.render();
777
+ this.syncState();
778
+ } catch (error) {
779
+ this.log("error", "Failed to refresh:", error);
780
+ window.location.reload();
781
+ }
782
+ } else {
783
+ window.location.reload();
784
+ }
507
785
  }
508
786
  handleSelectComponent(data) {
509
787
  if (data.componentId) {
@@ -539,13 +817,20 @@ var SandboxClient = class {
539
817
  this.runtime?.updateComponent(data.componentId, data.props || {});
540
818
  }
541
819
  handleAddComponent(data) {
542
- this.log("info", "Add component:", data.component.type);
820
+ this.log("info", "Add component:", data.component.componentType);
821
+ this.log("warn", "addComponent not supported, need to reload page");
543
822
  }
544
823
  handleMoveComponent(data) {
545
824
  this.log("info", "Move component:", data.componentId, "to", data.targetIndex);
825
+ this.log("warn", "moveComponent not supported, need to reload page");
546
826
  }
547
827
  handleDeleteComponent(data) {
548
828
  this.log("info", "Delete component:", data.componentId);
829
+ this.log("warn", "deleteComponent not supported, need to reload page");
830
+ this.selectedComponents.delete(data.componentId);
831
+ if (this.hoveredComponent === data.componentId) {
832
+ this.hoveredComponent = null;
833
+ }
549
834
  }
550
835
  handleSyncVariables(data) {
551
836
  Object.entries(data.variables).forEach(([key, value]) => {
@@ -564,11 +849,12 @@ var SandboxClient = class {
564
849
  });
565
850
  }
566
851
  syncState() {
852
+ const runtimeState = this.runtime?.getState();
567
853
  const state = {
568
- phase: this.runtime?.getState().phase || "idle",
569
- page: this.runtime?.getState().page ? {
570
- pageUid: this.runtime.getState().page.pageUid,
571
- pageVersionId: this.runtime.getState().page.pageVersionId
854
+ phase: runtimeState?.phase || "idle",
855
+ page: runtimeState?.page ? {
856
+ pageUid: runtimeState.page.pageUid,
857
+ pageVersionId: runtimeState.page.pageVersionId
572
858
  } : void 0,
573
859
  selectedComponents: Array.from(this.selectedComponents),
574
860
  hoveredComponent: this.hoveredComponent
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- import { PageSchema, ComponentInstance } from '@djvlc/contracts-types';
1
+ import { PageSchema, ComponentNode } from '@djvlc/contracts-types';
2
+ import { DjvlcRuntime } from '@djvlc/runtime-core';
2
3
 
3
4
  /**
4
5
  * 沙箱通信协议
@@ -28,6 +29,7 @@ declare enum MessageType {
28
29
  COMPONENT_CONTEXT_MENU = "component_context_menu",
29
30
  COMPONENT_DRAG_START = "component_drag_start",
30
31
  COMPONENT_DRAG_END = "component_drag_end",
32
+ COMPONENT_DROP = "component_drop",
31
33
  ERROR = "error",
32
34
  LOG = "log"
33
35
  }
@@ -124,14 +126,14 @@ interface UpdateComponentMessageData {
124
126
  /** 更新的样式 */
125
127
  style?: Record<string, unknown>;
126
128
  /** 完整组件实例 */
127
- component?: ComponentInstance;
129
+ component?: ComponentNode;
128
130
  }
129
131
  /**
130
132
  * Add Component 消息数据
131
133
  */
132
134
  interface AddComponentMessageData {
133
135
  /** 组件实例 */
134
- component: ComponentInstance;
136
+ component: ComponentNode;
135
137
  /** 父组件 ID */
136
138
  parentId?: string;
137
139
  /** 插入位置索引 */
@@ -187,6 +189,17 @@ interface SyncStateMessageData {
187
189
  /** 悬停组件 */
188
190
  hoveredComponent: string | null;
189
191
  }
192
+ /**
193
+ * Component Drop 消息数据
194
+ */
195
+ interface ComponentDropMessageData {
196
+ /** 被拖拽的组件 ID */
197
+ componentId: string;
198
+ /** 目标组件 ID */
199
+ targetComponentId: string;
200
+ /** 放置位置 */
201
+ position: 'before' | 'after' | 'inside';
202
+ }
190
203
  /**
191
204
  * Error 消息数据
192
205
  */
@@ -329,20 +342,7 @@ declare class SandboxHost {
329
342
  * 沙箱客户端
330
343
  * 在 iframe 预览中使用,响应 Editor 的控制
331
344
  */
332
- /**
333
- * 运行时接口(避免直接依赖 @djvlc/runtime-core)
334
- */
335
- interface RuntimeLike {
336
- getState(): {
337
- phase: string;
338
- page: {
339
- pageUid: string;
340
- pageVersionId: string;
341
- } | null;
342
- };
343
- updateComponent(componentId: string, props: Record<string, unknown>): void;
344
- setVariable(key: string, value: unknown): void;
345
- }
345
+
346
346
  /**
347
347
  * 沙箱客户端配置
348
348
  */
@@ -351,6 +351,17 @@ interface SandboxClientOptions {
351
351
  targetOrigin?: string;
352
352
  /** 调试模式 */
353
353
  debug?: boolean;
354
+ /** 是否启用拖拽 */
355
+ enableDrag?: boolean;
356
+ /** 拖拽配置 */
357
+ dragOptions?: {
358
+ /** 拖拽时的透明度 */
359
+ dragOpacity?: number;
360
+ /** 是否显示放置指示器 */
361
+ showDropIndicator?: boolean;
362
+ /** 放置指示器颜色 */
363
+ dropIndicatorColor?: string;
364
+ };
354
365
  }
355
366
  /**
356
367
  * 沙箱客户端
@@ -360,11 +371,12 @@ declare class SandboxClient {
360
371
  private runtime;
361
372
  private selectedComponents;
362
373
  private hoveredComponent;
374
+ private dragState;
363
375
  constructor(options?: SandboxClientOptions);
364
376
  /**
365
377
  * 设置运行时实例
366
378
  */
367
- setRuntime(runtime: RuntimeLike): void;
379
+ setRuntime(runtime: DjvlcRuntime): void;
368
380
  /**
369
381
  * 连接到父窗口
370
382
  */
@@ -381,6 +393,26 @@ declare class SandboxClient {
381
393
  * 注入编辑器样式
382
394
  */
383
395
  injectEditorStyles(): void;
396
+ /**
397
+ * 初始化拖拽事件监听
398
+ */
399
+ initDragEventListeners(): void;
400
+ /**
401
+ * 使组件可拖拽
402
+ */
403
+ private makeComponentsDraggable;
404
+ /**
405
+ * 显示放置指示器
406
+ */
407
+ private showDropIndicator;
408
+ /**
409
+ * 隐藏放置指示器
410
+ */
411
+ private hideDropIndicator;
412
+ /**
413
+ * 结束拖拽
414
+ */
415
+ private endDrag;
384
416
  private handleMessage;
385
417
  private handleInit;
386
418
  private handleLoadPage;
@@ -403,4 +435,4 @@ declare class SandboxClient {
403
435
  private log;
404
436
  }
405
437
 
406
- export { type AddComponentMessageData, type BaseMessage, type ComponentEventMessageData, type DeleteComponentMessageData, type ErrorMessageData, type HoverComponentMessageData, type InitMessageData, type LoadPageMessageData, MessageType, type MoveComponentMessageData, type ReadyMessageData, type ResponseMessage, SandboxClient, type SandboxClientOptions, SandboxHost, type SandboxHostOptions, type SelectComponentMessageData, type SyncStateMessageData, type UpdateComponentMessageData, type UpdateSchemaMessageData, createMessage, createResponse, generateMessageId, isDjvlcMessage };
438
+ export { type AddComponentMessageData, type BaseMessage, type ComponentDropMessageData, type ComponentEventMessageData, type DeleteComponentMessageData, type ErrorMessageData, type HoverComponentMessageData, type InitMessageData, type LoadPageMessageData, MessageType, type MoveComponentMessageData, type ReadyMessageData, type ResponseMessage, SandboxClient, type SandboxClientOptions, SandboxHost, type SandboxHostOptions, type SelectComponentMessageData, type SyncStateMessageData, type UpdateComponentMessageData, type UpdateSchemaMessageData, createMessage, createResponse, generateMessageId, isDjvlcMessage };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { PageSchema, ComponentInstance } from '@djvlc/contracts-types';
1
+ import { PageSchema, ComponentNode } from '@djvlc/contracts-types';
2
+ import { DjvlcRuntime } from '@djvlc/runtime-core';
2
3
 
3
4
  /**
4
5
  * 沙箱通信协议
@@ -28,6 +29,7 @@ declare enum MessageType {
28
29
  COMPONENT_CONTEXT_MENU = "component_context_menu",
29
30
  COMPONENT_DRAG_START = "component_drag_start",
30
31
  COMPONENT_DRAG_END = "component_drag_end",
32
+ COMPONENT_DROP = "component_drop",
31
33
  ERROR = "error",
32
34
  LOG = "log"
33
35
  }
@@ -124,14 +126,14 @@ interface UpdateComponentMessageData {
124
126
  /** 更新的样式 */
125
127
  style?: Record<string, unknown>;
126
128
  /** 完整组件实例 */
127
- component?: ComponentInstance;
129
+ component?: ComponentNode;
128
130
  }
129
131
  /**
130
132
  * Add Component 消息数据
131
133
  */
132
134
  interface AddComponentMessageData {
133
135
  /** 组件实例 */
134
- component: ComponentInstance;
136
+ component: ComponentNode;
135
137
  /** 父组件 ID */
136
138
  parentId?: string;
137
139
  /** 插入位置索引 */
@@ -187,6 +189,17 @@ interface SyncStateMessageData {
187
189
  /** 悬停组件 */
188
190
  hoveredComponent: string | null;
189
191
  }
192
+ /**
193
+ * Component Drop 消息数据
194
+ */
195
+ interface ComponentDropMessageData {
196
+ /** 被拖拽的组件 ID */
197
+ componentId: string;
198
+ /** 目标组件 ID */
199
+ targetComponentId: string;
200
+ /** 放置位置 */
201
+ position: 'before' | 'after' | 'inside';
202
+ }
190
203
  /**
191
204
  * Error 消息数据
192
205
  */
@@ -329,20 +342,7 @@ declare class SandboxHost {
329
342
  * 沙箱客户端
330
343
  * 在 iframe 预览中使用,响应 Editor 的控制
331
344
  */
332
- /**
333
- * 运行时接口(避免直接依赖 @djvlc/runtime-core)
334
- */
335
- interface RuntimeLike {
336
- getState(): {
337
- phase: string;
338
- page: {
339
- pageUid: string;
340
- pageVersionId: string;
341
- } | null;
342
- };
343
- updateComponent(componentId: string, props: Record<string, unknown>): void;
344
- setVariable(key: string, value: unknown): void;
345
- }
345
+
346
346
  /**
347
347
  * 沙箱客户端配置
348
348
  */
@@ -351,6 +351,17 @@ interface SandboxClientOptions {
351
351
  targetOrigin?: string;
352
352
  /** 调试模式 */
353
353
  debug?: boolean;
354
+ /** 是否启用拖拽 */
355
+ enableDrag?: boolean;
356
+ /** 拖拽配置 */
357
+ dragOptions?: {
358
+ /** 拖拽时的透明度 */
359
+ dragOpacity?: number;
360
+ /** 是否显示放置指示器 */
361
+ showDropIndicator?: boolean;
362
+ /** 放置指示器颜色 */
363
+ dropIndicatorColor?: string;
364
+ };
354
365
  }
355
366
  /**
356
367
  * 沙箱客户端
@@ -360,11 +371,12 @@ declare class SandboxClient {
360
371
  private runtime;
361
372
  private selectedComponents;
362
373
  private hoveredComponent;
374
+ private dragState;
363
375
  constructor(options?: SandboxClientOptions);
364
376
  /**
365
377
  * 设置运行时实例
366
378
  */
367
- setRuntime(runtime: RuntimeLike): void;
379
+ setRuntime(runtime: DjvlcRuntime): void;
368
380
  /**
369
381
  * 连接到父窗口
370
382
  */
@@ -381,6 +393,26 @@ declare class SandboxClient {
381
393
  * 注入编辑器样式
382
394
  */
383
395
  injectEditorStyles(): void;
396
+ /**
397
+ * 初始化拖拽事件监听
398
+ */
399
+ initDragEventListeners(): void;
400
+ /**
401
+ * 使组件可拖拽
402
+ */
403
+ private makeComponentsDraggable;
404
+ /**
405
+ * 显示放置指示器
406
+ */
407
+ private showDropIndicator;
408
+ /**
409
+ * 隐藏放置指示器
410
+ */
411
+ private hideDropIndicator;
412
+ /**
413
+ * 结束拖拽
414
+ */
415
+ private endDrag;
384
416
  private handleMessage;
385
417
  private handleInit;
386
418
  private handleLoadPage;
@@ -403,4 +435,4 @@ declare class SandboxClient {
403
435
  private log;
404
436
  }
405
437
 
406
- export { type AddComponentMessageData, type BaseMessage, type ComponentEventMessageData, type DeleteComponentMessageData, type ErrorMessageData, type HoverComponentMessageData, type InitMessageData, type LoadPageMessageData, MessageType, type MoveComponentMessageData, type ReadyMessageData, type ResponseMessage, SandboxClient, type SandboxClientOptions, SandboxHost, type SandboxHostOptions, type SelectComponentMessageData, type SyncStateMessageData, type UpdateComponentMessageData, type UpdateSchemaMessageData, createMessage, createResponse, generateMessageId, isDjvlcMessage };
438
+ export { type AddComponentMessageData, type BaseMessage, type ComponentDropMessageData, type ComponentEventMessageData, type DeleteComponentMessageData, type ErrorMessageData, type HoverComponentMessageData, type InitMessageData, type LoadPageMessageData, MessageType, type MoveComponentMessageData, type ReadyMessageData, type ResponseMessage, SandboxClient, type SandboxClientOptions, SandboxHost, type SandboxHostOptions, type SelectComponentMessageData, type SyncStateMessageData, type UpdateComponentMessageData, type UpdateSchemaMessageData, createMessage, createResponse, generateMessageId, isDjvlcMessage };
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ var MessageType = /* @__PURE__ */ ((MessageType2) => {
19
19
  MessageType2["COMPONENT_CONTEXT_MENU"] = "component_context_menu";
20
20
  MessageType2["COMPONENT_DRAG_START"] = "component_drag_start";
21
21
  MessageType2["COMPONENT_DRAG_END"] = "component_drag_end";
22
+ MessageType2["COMPONENT_DROP"] = "component_drop";
22
23
  MessageType2["ERROR"] = "error";
23
24
  MessageType2["LOG"] = "log";
24
25
  return MessageType2;
@@ -258,6 +259,14 @@ var SandboxClient = class {
258
259
  this.runtime = null;
259
260
  this.selectedComponents = /* @__PURE__ */ new Set();
260
261
  this.hoveredComponent = null;
262
+ this.dragState = {
263
+ isDragging: false,
264
+ draggedComponentId: null,
265
+ draggedElement: null,
266
+ dropTarget: null,
267
+ dropPosition: null,
268
+ dropIndicator: null
269
+ };
261
270
  // ==================== 私有方法 ====================
262
271
  this.handleMessage = async (event) => {
263
272
  const message = event.data;
@@ -319,6 +328,12 @@ var SandboxClient = class {
319
328
  this.options = {
320
329
  targetOrigin: "*",
321
330
  debug: false,
331
+ enableDrag: true,
332
+ dragOptions: {
333
+ dragOpacity: 0.5,
334
+ showDropIndicator: true,
335
+ dropIndicatorColor: "#1890ff"
336
+ },
322
337
  ...options
323
338
  };
324
339
  }
@@ -415,17 +430,19 @@ var SandboxClient = class {
415
430
  * 注入编辑器样式
416
431
  */
417
432
  injectEditorStyles() {
433
+ const indicatorColor = this.options.dragOptions?.dropIndicatorColor || "#1890ff";
418
434
  const style = document.createElement("style");
435
+ style.id = "djvlc-editor-styles";
419
436
  style.textContent = `
420
437
  /* \u9009\u4E2D\u72B6\u6001 */
421
438
  [data-component-id].djvlc-selected {
422
- outline: 2px solid #1890ff !important;
439
+ outline: 2px solid ${indicatorColor} !important;
423
440
  outline-offset: 2px;
424
441
  }
425
442
 
426
443
  /* \u60AC\u505C\u72B6\u6001 */
427
444
  [data-component-id].djvlc-hovered {
428
- outline: 1px dashed #1890ff !important;
445
+ outline: 1px dashed ${indicatorColor} !important;
429
446
  outline-offset: 1px;
430
447
  }
431
448
 
@@ -439,39 +456,300 @@ var SandboxClient = class {
439
456
  pointer-events: auto;
440
457
  }
441
458
 
459
+ /* \u53EF\u62D6\u62FD\u72B6\u6001 */
460
+ [data-component-id][draggable="true"] {
461
+ cursor: grab;
462
+ }
463
+
464
+ [data-component-id][draggable="true"]:active {
465
+ cursor: grabbing;
466
+ }
467
+
468
+ /* \u62D6\u62FD\u4E2D\u72B6\u6001 */
469
+ [data-component-id].djvlc-dragging {
470
+ opacity: ${this.options.dragOptions?.dragOpacity || 0.5};
471
+ }
472
+
473
+ /* \u653E\u7F6E\u76EE\u6807\u72B6\u6001 */
474
+ [data-component-id].djvlc-drop-target {
475
+ outline: 2px dashed ${indicatorColor} !important;
476
+ outline-offset: 4px;
477
+ }
478
+
442
479
  /* \u62D6\u62FD\u5360\u4F4D\u7B26 */
443
480
  .djvlc-drop-indicator {
444
481
  position: absolute;
445
- background: #1890ff;
446
- opacity: 0.5;
482
+ background: ${indicatorColor};
483
+ opacity: 0.8;
447
484
  pointer-events: none;
448
485
  z-index: 10000;
486
+ transition: all 0.1s ease;
449
487
  }
450
488
 
451
489
  .djvlc-drop-indicator.horizontal {
452
490
  height: 4px;
453
491
  width: 100%;
492
+ border-radius: 2px;
454
493
  }
455
494
 
456
495
  .djvlc-drop-indicator.vertical {
457
496
  width: 4px;
458
497
  height: 100%;
498
+ border-radius: 2px;
499
+ }
500
+
501
+ /* \u653E\u7F6E\u63D0\u793A\u7BAD\u5934 */
502
+ .djvlc-drop-indicator::before {
503
+ content: '';
504
+ position: absolute;
505
+ width: 8px;
506
+ height: 8px;
507
+ background: ${indicatorColor};
508
+ border-radius: 50%;
509
+ left: -4px;
510
+ top: -2px;
511
+ }
512
+
513
+ .djvlc-drop-indicator::after {
514
+ content: '';
515
+ position: absolute;
516
+ width: 8px;
517
+ height: 8px;
518
+ background: ${indicatorColor};
519
+ border-radius: 50%;
520
+ right: -4px;
521
+ top: -2px;
459
522
  }
460
523
  `;
461
524
  document.head.appendChild(style);
462
525
  }
526
+ /**
527
+ * 初始化拖拽事件监听
528
+ */
529
+ initDragEventListeners() {
530
+ if (!this.options.enableDrag) return;
531
+ this.makeComponentsDraggable();
532
+ document.addEventListener("dragstart", (e) => {
533
+ const target = e.target;
534
+ const componentEl = this.findComponentElement(target);
535
+ if (!componentEl) return;
536
+ const componentId = componentEl.getAttribute("data-component-id");
537
+ if (!componentId) return;
538
+ this.dragState.isDragging = true;
539
+ this.dragState.draggedComponentId = componentId;
540
+ this.dragState.draggedElement = componentEl;
541
+ componentEl.classList.add("djvlc-dragging");
542
+ e.dataTransfer?.setData("text/plain", componentId);
543
+ e.dataTransfer?.setData("application/json", JSON.stringify({
544
+ componentId,
545
+ componentType: componentEl.getAttribute("data-component-type")
546
+ }));
547
+ if (e.dataTransfer) {
548
+ e.dataTransfer.effectAllowed = "move";
549
+ }
550
+ this.log("debug", "Drag start:", componentId);
551
+ });
552
+ document.addEventListener("dragover", (e) => {
553
+ if (!this.dragState.isDragging) return;
554
+ e.preventDefault();
555
+ const target = e.target;
556
+ const componentEl = this.findComponentElement(target);
557
+ if (!componentEl || componentEl === this.dragState.draggedElement) {
558
+ this.hideDropIndicator();
559
+ return;
560
+ }
561
+ const componentId = componentEl.getAttribute("data-component-id");
562
+ if (!componentId) return;
563
+ const rect = componentEl.getBoundingClientRect();
564
+ const y = e.clientY;
565
+ const threshold = rect.height / 3;
566
+ let position;
567
+ if (y < rect.top + threshold) {
568
+ position = "before";
569
+ } else if (y > rect.bottom - threshold) {
570
+ position = "after";
571
+ } else {
572
+ position = "inside";
573
+ }
574
+ if (this.dragState.dropTarget !== componentEl || this.dragState.dropPosition !== position) {
575
+ this.dragState.dropTarget?.classList.remove("djvlc-drop-target");
576
+ this.dragState.dropTarget = componentEl;
577
+ this.dragState.dropPosition = position;
578
+ if (position === "inside") {
579
+ componentEl.classList.add("djvlc-drop-target");
580
+ this.hideDropIndicator();
581
+ } else {
582
+ componentEl.classList.remove("djvlc-drop-target");
583
+ this.showDropIndicator(componentEl, position);
584
+ }
585
+ }
586
+ });
587
+ document.addEventListener("dragleave", (e) => {
588
+ const target = e.target;
589
+ const componentEl = this.findComponentElement(target);
590
+ if (componentEl && componentEl === this.dragState.dropTarget) {
591
+ const relatedTarget = e.relatedTarget;
592
+ if (!relatedTarget || !componentEl.contains(relatedTarget)) {
593
+ componentEl.classList.remove("djvlc-drop-target");
594
+ }
595
+ }
596
+ });
597
+ document.addEventListener("drop", (e) => {
598
+ if (!this.dragState.isDragging) return;
599
+ e.preventDefault();
600
+ const { draggedComponentId, dropTarget, dropPosition } = this.dragState;
601
+ if (draggedComponentId && dropTarget && dropPosition) {
602
+ const targetComponentId = dropTarget.getAttribute("data-component-id");
603
+ if (targetComponentId && targetComponentId !== draggedComponentId) {
604
+ this.log("info", "Drop:", draggedComponentId, dropPosition, targetComponentId);
605
+ this.send("component_drop" /* COMPONENT_DROP */, {
606
+ componentId: draggedComponentId,
607
+ targetComponentId,
608
+ position: dropPosition
609
+ });
610
+ }
611
+ }
612
+ this.endDrag();
613
+ });
614
+ document.addEventListener("dragend", () => {
615
+ this.endDrag();
616
+ });
617
+ }
618
+ /**
619
+ * 使组件可拖拽
620
+ */
621
+ makeComponentsDraggable() {
622
+ const components = document.querySelectorAll("[data-component-id]");
623
+ components.forEach((el) => {
624
+ el.draggable = true;
625
+ });
626
+ const observer = new MutationObserver((mutations) => {
627
+ mutations.forEach((mutation) => {
628
+ mutation.addedNodes.forEach((node) => {
629
+ if (node instanceof HTMLElement) {
630
+ if (node.hasAttribute("data-component-id")) {
631
+ node.draggable = true;
632
+ }
633
+ node.querySelectorAll("[data-component-id]").forEach((el) => {
634
+ el.draggable = true;
635
+ });
636
+ }
637
+ });
638
+ });
639
+ });
640
+ observer.observe(document.body, {
641
+ childList: true,
642
+ subtree: true
643
+ });
644
+ }
645
+ /**
646
+ * 显示放置指示器
647
+ */
648
+ showDropIndicator(target, position) {
649
+ if (!this.options.dragOptions?.showDropIndicator) return;
650
+ let indicator = this.dragState.dropIndicator;
651
+ if (!indicator) {
652
+ indicator = document.createElement("div");
653
+ indicator.className = "djvlc-drop-indicator horizontal";
654
+ document.body.appendChild(indicator);
655
+ this.dragState.dropIndicator = indicator;
656
+ }
657
+ const rect = target.getBoundingClientRect();
658
+ const scrollTop = window.scrollY || document.documentElement.scrollTop;
659
+ const scrollLeft = window.scrollX || document.documentElement.scrollLeft;
660
+ indicator.style.left = `${rect.left + scrollLeft}px`;
661
+ indicator.style.width = `${rect.width}px`;
662
+ if (position === "before") {
663
+ indicator.style.top = `${rect.top + scrollTop - 2}px`;
664
+ } else {
665
+ indicator.style.top = `${rect.bottom + scrollTop - 2}px`;
666
+ }
667
+ indicator.style.display = "block";
668
+ }
669
+ /**
670
+ * 隐藏放置指示器
671
+ */
672
+ hideDropIndicator() {
673
+ if (this.dragState.dropIndicator) {
674
+ this.dragState.dropIndicator.style.display = "none";
675
+ }
676
+ }
677
+ /**
678
+ * 结束拖拽
679
+ */
680
+ endDrag() {
681
+ this.dragState.draggedElement?.classList.remove("djvlc-dragging");
682
+ this.dragState.dropTarget?.classList.remove("djvlc-drop-target");
683
+ this.hideDropIndicator();
684
+ this.dragState = {
685
+ isDragging: false,
686
+ draggedComponentId: null,
687
+ draggedElement: null,
688
+ dropTarget: null,
689
+ dropPosition: null,
690
+ dropIndicator: this.dragState.dropIndicator
691
+ // 保留 indicator 元素
692
+ };
693
+ }
463
694
  async handleInit(data) {
464
695
  this.log("info", "Init with config:", data);
696
+ void data;
697
+ if (this.runtime) {
698
+ try {
699
+ await this.runtime.init();
700
+ } catch (error) {
701
+ this.log("error", "Failed to init runtime:", error);
702
+ throw error;
703
+ }
704
+ }
465
705
  }
466
706
  async handleLoadPage(data) {
467
707
  this.log("info", "Load page:", data.pageUid);
708
+ if (!this.runtime) {
709
+ throw new Error("Runtime not set");
710
+ }
711
+ try {
712
+ await this.runtime.load();
713
+ await this.runtime.render();
714
+ this.syncState();
715
+ } catch (error) {
716
+ this.log("error", "Failed to load page:", error);
717
+ throw error;
718
+ }
468
719
  }
469
720
  async handleUpdateSchema(data) {
470
721
  this.log("info", "Update schema", data.fullUpdate ? "(full)" : "(partial)");
722
+ if (!this.runtime) {
723
+ throw new Error("Runtime not set");
724
+ }
725
+ try {
726
+ if (data.fullUpdate) {
727
+ await this.runtime.load();
728
+ await this.runtime.render();
729
+ } else {
730
+ await this.runtime.load();
731
+ await this.runtime.render();
732
+ }
733
+ this.syncState();
734
+ } catch (error) {
735
+ this.log("error", "Failed to update schema:", error);
736
+ throw error;
737
+ }
471
738
  }
472
739
  async handleRefresh() {
473
740
  this.log("info", "Refresh page");
474
- window.location.reload();
741
+ if (this.runtime) {
742
+ try {
743
+ await this.runtime.load();
744
+ await this.runtime.render();
745
+ this.syncState();
746
+ } catch (error) {
747
+ this.log("error", "Failed to refresh:", error);
748
+ window.location.reload();
749
+ }
750
+ } else {
751
+ window.location.reload();
752
+ }
475
753
  }
476
754
  handleSelectComponent(data) {
477
755
  if (data.componentId) {
@@ -507,13 +785,20 @@ var SandboxClient = class {
507
785
  this.runtime?.updateComponent(data.componentId, data.props || {});
508
786
  }
509
787
  handleAddComponent(data) {
510
- this.log("info", "Add component:", data.component.type);
788
+ this.log("info", "Add component:", data.component.componentType);
789
+ this.log("warn", "addComponent not supported, need to reload page");
511
790
  }
512
791
  handleMoveComponent(data) {
513
792
  this.log("info", "Move component:", data.componentId, "to", data.targetIndex);
793
+ this.log("warn", "moveComponent not supported, need to reload page");
514
794
  }
515
795
  handleDeleteComponent(data) {
516
796
  this.log("info", "Delete component:", data.componentId);
797
+ this.log("warn", "deleteComponent not supported, need to reload page");
798
+ this.selectedComponents.delete(data.componentId);
799
+ if (this.hoveredComponent === data.componentId) {
800
+ this.hoveredComponent = null;
801
+ }
517
802
  }
518
803
  handleSyncVariables(data) {
519
804
  Object.entries(data.variables).forEach(([key, value]) => {
@@ -532,11 +817,12 @@ var SandboxClient = class {
532
817
  });
533
818
  }
534
819
  syncState() {
820
+ const runtimeState = this.runtime?.getState();
535
821
  const state = {
536
- phase: this.runtime?.getState().phase || "idle",
537
- page: this.runtime?.getState().page ? {
538
- pageUid: this.runtime.getState().page.pageUid,
539
- pageVersionId: this.runtime.getState().page.pageVersionId
822
+ phase: runtimeState?.phase || "idle",
823
+ page: runtimeState?.page ? {
824
+ pageUid: runtimeState.page.pageUid,
825
+ pageVersionId: runtimeState.page.pageVersionId
540
826
  } : void 0,
541
827
  selectedComponents: Array.from(this.selectedComponents),
542
828
  hoveredComponent: this.hoveredComponent
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djvlc/sandbox",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "DJV 低代码平台沙箱通信模块",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -17,8 +17,8 @@
17
17
  "dist"
18
18
  ],
19
19
  "scripts": {
20
- "build": "tsup src/index.ts --format esm,cjs --dts --clean",
21
- "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
20
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean --no-sourcemap",
21
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch --no-sourcemap",
22
22
  "test": "vitest run --passWithNoTests",
23
23
  "test:watch": "vitest",
24
24
  "lint": "eslint src --ext .ts",
@@ -26,7 +26,8 @@
26
26
  "clean": "rimraf dist"
27
27
  },
28
28
  "dependencies": {
29
- "@djvlc/contracts-types": "^1.4.0"
29
+ "@djvlc/contracts-types": "1.8.3",
30
+ "@djvlc/runtime-core": "1.0.1"
30
31
  },
31
32
  "devDependencies": {
32
33
  "@types/node": "^20.10.0",