@@ -3,9 +3,11 @@ import { SimpleContainerComp } from "comps/comps/containerBase/simpleContainerCo
33import { GridItemComp } from "comps/comps/gridItemComp" ;
44import { remoteComp } from "comps/comps/remoteComp/remoteComp" ;
55import { EditorState } from "comps/editorState" ;
6+ import { NameGenerator } from "comps/utils" ;
67import { trans } from "i18n" ;
78import {
89 calcPasteBaseXY ,
10+ DEFAULT_POSITION_PARAMS ,
911 Layout ,
1012 LayoutItem ,
1113 moveToZero ,
@@ -29,16 +31,74 @@ import { genRandomKey } from "./idGenerator";
2931import { getLatestVersion , getRemoteCompType , parseCompType } from "./remote" ;
3032import { APPLICATION_VIEW_URL } from "@lowcoder-ee/constants/routesURL" ;
3133
32- export type CopyCompType = {
34+ const CLIPBOARD_TYPE = "lowcoder-components" ;
35+ const CLIPBOARD_VERSION = 1 ;
36+
37+ export interface ClipboardGridItem {
38+ compType : string ;
39+ comp : any ;
40+ name : string ;
3341 layout : LayoutItem ;
34- item : Comp ;
35- } ;
42+ isContainer : boolean ;
43+ }
3644
37- export class GridCompOperator {
38- private static copyComps : CopyCompType [ ] = [ ] ;
39- private static sourcePositionParams : PositionParams ;
45+ export interface LowcoderClipboardPayload {
46+ type : typeof CLIPBOARD_TYPE ;
47+ version : number ;
48+ timestamp : number ;
49+ gridItems : ClipboardGridItem [ ] ;
50+ hookItems : ClipboardHookItem [ ] ;
51+ sourcePositionParams : PositionParams ;
52+ }
53+
54+ export interface ClipboardHookItem {
55+ compType : string ;
56+ comp : any ;
57+ name : string ;
58+ fullValue : any ;
59+ }
60+
61+ async function writeToClipboard ( payload : LowcoderClipboardPayload ) : Promise < boolean > {
62+ try {
63+ const json = JSON . stringify ( payload ) ;
64+ await navigator . clipboard . writeText ( json ) ;
65+ return true ;
66+ } catch {
67+ return false ;
68+ }
69+ }
70+
71+ export async function readFromClipboard ( ) : Promise < LowcoderClipboardPayload | null > {
72+ try {
73+ const text = await navigator . clipboard . readText ( ) ;
74+ if ( ! text ) return null ;
75+ const parsed = JSON . parse ( text ) ;
76+ if ( parsed ?. type !== CLIPBOARD_TYPE || ! parsed ?. version ) return null ;
77+ return parsed as LowcoderClipboardPayload ;
78+ } catch {
79+ return null ;
80+ }
81+ }
4082
41- static copyComp ( editorState : EditorState , compRecords : Record < string , Comp > ) {
83+ function buildEmptyPayload ( ) : LowcoderClipboardPayload {
84+ return {
85+ type : CLIPBOARD_TYPE ,
86+ version : CLIPBOARD_VERSION ,
87+ timestamp : Date . now ( ) ,
88+ gridItems : [ ] ,
89+ hookItems : [ ] ,
90+ sourcePositionParams : DEFAULT_POSITION_PARAMS ,
91+ } ;
92+ }
93+
94+ export async function writeHookOnlyToClipboard ( hookItems : ClipboardHookItem [ ] ) : Promise < boolean > {
95+ const payload = buildEmptyPayload ( ) ;
96+ payload . hookItems = hookItems ;
97+ return writeToClipboard ( payload ) ;
98+ }
99+
100+ export class GridCompOperator {
101+ static async copyComp ( editorState : EditorState , compRecords : Record < string , Comp > ) : Promise < boolean > {
42102 const oldUi = editorState . getUIComp ( ) . getComp ( ) ;
43103 if ( ! oldUi ) {
44104 messageInstance . info ( trans ( "gridCompOperator.notSupport" ) ) ;
@@ -65,24 +125,58 @@ export class GridCompOperator {
65125 layout : layout [ key ] ,
66126 } ) ) ;
67127
68- const toCopyComps = Object . values ( compMap ) . filter ( ( item ) => ! ! item . item && ! ! item . layout ) ;
69- if ( ! toCopyComps || _ . size ( toCopyComps ) <= 0 ) {
128+ const validComps = Object . values ( compMap ) . filter ( ( item ) => ! ! item . item && ! ! item . layout ) ;
129+ if ( ! validComps || _ . size ( validComps ) <= 0 ) {
70130 messageInstance . info ( trans ( "gridCompOperator.selectAtLeastOneComponent" ) ) ;
71131 return false ;
72132 }
73- this . copyComps = toCopyComps ;
74- this . sourcePositionParams = simpleContainer . children . positionParams . getView ( ) ;
75133
76- // log.debug( "copyComp. toCopyComps: ", this.copyComps, " sourcePositionParams: ", this.sourcePositionParams);
77- return true ;
134+ const sourcePositionParams = simpleContainer . children . positionParams . getView ( ) ;
135+ const nameGenerator = editorState . getNameGenerator ( ) ;
136+
137+ const gridItems : ClipboardGridItem [ ] = validComps . map ( ( comp ) => {
138+ const itemComp = comp . item as GridItemComp ;
139+ const compType = itemComp . children . compType . getView ( ) ;
140+ const name = itemComp . children . name . getView ( ) ;
141+ const innerComp = itemComp . children . comp ;
142+ const isContainerComp = isContainer ( innerComp ) ;
143+ const compJSON = isContainerComp
144+ ? {
145+ ...innerComp . toJsonValue ( ) ,
146+ ...innerComp . getPasteValue ( nameGenerator ) as Record < string , any > ,
147+ }
148+ : innerComp . toJsonValue ( ) ;
149+ return { compType, comp : compJSON , name, layout : comp . layout , isContainer : isContainerComp } ;
150+ } ) ;
151+
152+ const payload = buildEmptyPayload ( ) ;
153+ payload . sourcePositionParams = sourcePositionParams ;
154+ payload . gridItems = gridItems ;
155+ const written = await writeToClipboard ( payload ) ;
156+ if ( written ) {
157+ messageInstance . success ( trans ( "gridCompOperator.copyCompsSuccess" , { compNum : gridItems . length } ) ) ;
158+ } else {
159+ messageInstance . error ( trans ( "gridCompOperator.clipboardWriteError" ) ) ;
160+ }
161+ return written ;
78162 }
79163
80- // FALK TODO: How can we enable Copy and Paste of components across Browser Tabs / Windows?
81- static pasteComp ( editorState : EditorState ) {
82- if ( ! this . copyComps || _ . size ( this . copyComps ) <= 0 || ! this . sourcePositionParams ) {
83- messageInstance . info ( trans ( "gridCompOperator.selectCompFirst" ) ) ;
164+ static pasteFromPayload ( editorState : EditorState , payload : LowcoderClipboardPayload ) : boolean {
165+ if ( payload . gridItems . length === 0 ) {
84166 return false ;
85167 }
168+ return this . doPaste (
169+ editorState ,
170+ payload . gridItems ,
171+ payload . sourcePositionParams || DEFAULT_POSITION_PARAMS ,
172+ ) ;
173+ }
174+
175+ private static doPaste (
176+ editorState : EditorState ,
177+ items : ClipboardGridItem [ ] ,
178+ sourcePositionParams : PositionParams ,
179+ ) : boolean {
86180 const oldUi = editorState . getUIComp ( ) . getComp ( ) ;
87181 if ( ! oldUi ) {
88182 messageInstance . info ( trans ( "gridCompOperator.notSupport" ) ) ;
@@ -93,18 +187,8 @@ export class GridCompOperator {
93187 messageInstance . warning ( trans ( "gridCompOperator.noContainerSelected" ) ) ;
94188 return false ;
95189 }
190+
96191 const selectedComps = editorState . selectedComps ( ) ;
97- const isSelectingContainer =
98- _ . size ( selectedComps ) === 1 &&
99- ( Object . values ( selectedComps ) [ 0 ] as GridItemComp ) ?. children ?. comp === selectedContainer ;
100- if ( _ . size ( this . copyComps ) === 1 ) {
101- const { item } = this . copyComps [ 0 ] ;
102- // Special case: To paste a container, and the container is currently selected, paste it outside the selected container
103- if ( isContainer ( ( item as GridItemComp ) . children . comp ) && isSelectingContainer ) {
104- selectedContainer =
105- editorState . findContainer ( Object . keys ( selectedComps ) [ 0 ] ) ?? selectedContainer ;
106- }
107- }
108192
109193 const selectedSimpleContainer =
110194 selectedContainer . realSimpleContainer ( Object . keys ( selectedComps ) [ 0 ] ) ??
@@ -115,43 +199,39 @@ export class GridCompOperator {
115199 const multiAddActions : Array < CustomAction < any > > = [ ] ;
116200 const copyLayouts : Layout = { } ;
117201 const copyCompNames = new Set < string > ( ) ;
118- // log.debug("pasteComps. sourceContainer: ", this.sourceContainer, " targetContainer: ", selectedContainer);
119- this . copyComps . forEach ( ( comp ) => {
202+
203+ items . forEach ( ( item ) => {
120204 const key = genRandomKey ( ) ;
121205 const { w, h } = switchLayoutWH (
122- comp . layout . w ,
123- comp . layout . h ,
124- this . sourcePositionParams ,
206+ item . layout . w ,
207+ item . layout . h ,
208+ sourcePositionParams ,
125209 selectedSimpleContainer . children . positionParams . getView ( )
126210 ) ;
127211 copyLayouts [ key ] = {
128- ...comp . layout ,
212+ ...item . layout ,
129213 i : key ,
130- x : comp . layout . x + baseX ,
131- y : comp . layout . y + baseY ,
214+ x : item . layout . x + baseX ,
215+ y : item . layout . y + baseY ,
132216 w,
133217 h,
134218 isDragging : true ,
135219 } ;
136- const itemComp = comp . item as GridItemComp ;
137- const compType = itemComp . children . compType . getView ( ) ;
138- const compInfo = parseCompType ( compType ) ;
220+ const compInfo = parseCompType ( item . compType ) ;
139221 const compName = nameGenerator . genItemName ( compInfo . compName ) ;
140- const compJSONValue = isContainer ( itemComp . children . comp )
141- ? {
142- ...itemComp . children . comp . toJsonValue ( ) ,
143- ...itemComp . children . comp . getPasteValue ( nameGenerator ) as Record < string , any > ,
144- }
145- : itemComp . children . comp . toJsonValue ( ) ;
222+ const compJSONValue = item . isContainer
223+ ? remapContainerPasteValue ( item . comp , nameGenerator )
224+ : item . comp ;
146225 copyCompNames . add ( compName ) ;
147226 multiAddActions . push (
148227 ( oldUi . realSimpleContainer ( ) as SimpleContainerComp ) . children . items . addAction ( key , {
149- compType : compType ,
228+ compType : item . compType ,
150229 comp : compJSONValue ,
151230 name : compName ,
152231 } )
153232 ) ;
154233 } ) ;
234+
155235 selectedSimpleContainer . dispatch (
156236 multiChangeAction ( {
157237 layout : selectedSimpleContainer . children . layout . changeValueAction ( {
@@ -164,6 +244,7 @@ export class GridCompOperator {
164244 } )
165245 ) ;
166246 editorState . setSelectedCompNames ( copyCompNames ) ;
247+ messageInstance . success ( trans ( "gridCompOperator.pasteCompsSuccess" , { compNum : copyCompNames . size } ) ) ;
167248 return true ;
168249 }
169250
@@ -198,10 +279,11 @@ export class GridCompOperator {
198279 window . open ( APPLICATION_VIEW_URL ( applicationId , "edit" ) )
199280 }
200281
201- static cutComp ( editorState : EditorState , compRecords : Record < string , Comp > ) {
202- this . copyComp ( editorState , compRecords ) &&
203- this . doDelete ( editorState , compRecords ) &&
204- messageInstance . info ( trans ( "gridCompOperator.cutCompsSuccess" , { pasteKey, undoKey } ) ) ;
282+ static async cutComp ( editorState : EditorState , compRecords : Record < string , Comp > ) {
283+ const copied = await this . copyComp ( editorState , compRecords ) ;
284+ if ( copied && this . doDelete ( editorState , compRecords ) ) {
285+ messageInstance . info ( trans ( "gridCompOperator.cutCompsSuccess" , { pasteKey, undoKey } ) ) ;
286+ }
205287 }
206288
207289 private static doDelete ( editorState : EditorState , compRecords : Record < string , Comp > ) : boolean {
@@ -281,3 +363,46 @@ export class GridCompOperator {
281363 messageInstance . success ( trans ( "comp.upgradeSuccess" ) ) ;
282364 }
283365}
366+
367+ /**
368+ * Remap keys and names inside a serialized container JSON.
369+ * Mirrors what SimpleContainerComp.getPasteValue() does, but operates on
370+ * plain JSON so it works for cross-app clipboard payloads where we don't
371+ * have live comp instances.
372+ */
373+ function remapContainerPasteValue ( compJson : any , nameGenerator : NameGenerator ) : any {
374+ if ( ! compJson || typeof compJson !== "object" ) return compJson ;
375+
376+ const items = compJson . items ;
377+ const layout = compJson . layout ;
378+ if ( ! items || typeof items !== "object" ) return compJson ;
379+
380+ const keyMap : Record < string , string > = { } ;
381+ Object . keys ( items ) . forEach ( ( oldKey ) => {
382+ keyMap [ oldKey ] = genRandomKey ( ) ;
383+ } ) ;
384+
385+ const newItems : Record < string , any > = { } ;
386+ Object . entries ( items ) . forEach ( ( [ oldKey , itemValue ] : [ string , any ] ) => {
387+ const newKey = keyMap [ oldKey ] ;
388+ const compType = itemValue ?. compType ;
389+ const newName = compType ? nameGenerator . genItemName ( compType ) : genRandomKey ( ) ;
390+ const innerComp = itemValue ?. comp ;
391+ const remappedComp = innerComp ?. items
392+ ? remapContainerPasteValue ( innerComp , nameGenerator )
393+ : innerComp ;
394+ newItems [ newKey ] = { ...itemValue , name : newName , comp : remappedComp } ;
395+ } ) ;
396+
397+ let newLayout = layout ;
398+ if ( layout && typeof layout === "object" ) {
399+ const remapped : Record < string , any > = { } ;
400+ Object . entries ( layout ) . forEach ( ( [ oldKey , layoutItem ] : [ string , any ] ) => {
401+ const newKey = keyMap [ oldKey ] || oldKey ;
402+ remapped [ newKey ] = { ...layoutItem , i : newKey } ;
403+ } ) ;
404+ newLayout = remapped ;
405+ }
406+
407+ return { ...compJson , items : newItems , layout : newLayout } ;
408+ }
0 commit comments