@@ -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,15 +31,73 @@ 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+ }
82+
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+ }
4093
94+ export function writeHookOnlyToClipboard ( hookItems : ClipboardHookItem [ ] ) {
95+ const payload = buildEmptyPayload ( ) ;
96+ payload . hookItems = hookItems ;
97+ writeToClipboard ( payload ) ;
98+ }
99+
100+ export class GridCompOperator {
41101 static copyComp ( editorState : EditorState , compRecords : Record < string , Comp > ) {
42102 const oldUi = editorState . getUIComp ( ) . getComp ( ) ;
43103 if ( ! oldUi ) {
@@ -65,24 +125,54 @@ 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);
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+ writeToClipboard ( payload ) ;
156+
77157 return true ;
78158 }
79159
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" ) ) ;
160+ static pasteFromPayload ( editorState : EditorState , payload : LowcoderClipboardPayload ) : boolean {
161+ if ( payload . gridItems . length === 0 ) {
84162 return false ;
85163 }
164+ return this . doPaste (
165+ editorState ,
166+ payload . gridItems ,
167+ payload . sourcePositionParams || DEFAULT_POSITION_PARAMS ,
168+ ) ;
169+ }
170+
171+ private static doPaste (
172+ editorState : EditorState ,
173+ items : ClipboardGridItem [ ] ,
174+ sourcePositionParams : PositionParams ,
175+ ) : boolean {
86176 const oldUi = editorState . getUIComp ( ) . getComp ( ) ;
87177 if ( ! oldUi ) {
88178 messageInstance . info ( trans ( "gridCompOperator.notSupport" ) ) ;
@@ -93,18 +183,8 @@ export class GridCompOperator {
93183 messageInstance . warning ( trans ( "gridCompOperator.noContainerSelected" ) ) ;
94184 return false ;
95185 }
186+
96187 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- }
108188
109189 const selectedSimpleContainer =
110190 selectedContainer . realSimpleContainer ( Object . keys ( selectedComps ) [ 0 ] ) ??
@@ -115,43 +195,39 @@ export class GridCompOperator {
115195 const multiAddActions : Array < CustomAction < any > > = [ ] ;
116196 const copyLayouts : Layout = { } ;
117197 const copyCompNames = new Set < string > ( ) ;
118- // log.debug("pasteComps. sourceContainer: ", this.sourceContainer, " targetContainer: ", selectedContainer);
119- this . copyComps . forEach ( ( comp ) => {
198+
199+ items . forEach ( ( item ) => {
120200 const key = genRandomKey ( ) ;
121201 const { w, h } = switchLayoutWH (
122- comp . layout . w ,
123- comp . layout . h ,
124- this . sourcePositionParams ,
202+ item . layout . w ,
203+ item . layout . h ,
204+ sourcePositionParams ,
125205 selectedSimpleContainer . children . positionParams . getView ( )
126206 ) ;
127207 copyLayouts [ key ] = {
128- ...comp . layout ,
208+ ...item . layout ,
129209 i : key ,
130- x : comp . layout . x + baseX ,
131- y : comp . layout . y + baseY ,
210+ x : item . layout . x + baseX ,
211+ y : item . layout . y + baseY ,
132212 w,
133213 h,
134214 isDragging : true ,
135215 } ;
136- const itemComp = comp . item as GridItemComp ;
137- const compType = itemComp . children . compType . getView ( ) ;
138- const compInfo = parseCompType ( compType ) ;
216+ const compInfo = parseCompType ( item . compType ) ;
139217 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 ( ) ;
218+ const compJSONValue = item . isContainer
219+ ? remapContainerPasteValue ( item . comp , nameGenerator )
220+ : item . comp ;
146221 copyCompNames . add ( compName ) ;
147222 multiAddActions . push (
148223 ( oldUi . realSimpleContainer ( ) as SimpleContainerComp ) . children . items . addAction ( key , {
149- compType : compType ,
224+ compType : item . compType ,
150225 comp : compJSONValue ,
151226 name : compName ,
152227 } )
153228 ) ;
154229 } ) ;
230+
155231 selectedSimpleContainer . dispatch (
156232 multiChangeAction ( {
157233 layout : selectedSimpleContainer . children . layout . changeValueAction ( {
@@ -281,3 +357,46 @@ export class GridCompOperator {
281357 messageInstance . success ( trans ( "comp.upgradeSuccess" ) ) ;
282358 }
283359}
360+
361+ /**
362+ * Remap keys and names inside a serialized container JSON.
363+ * Mirrors what SimpleContainerComp.getPasteValue() does, but operates on
364+ * plain JSON so it works for cross-app clipboard payloads where we don't
365+ * have live comp instances.
366+ */
367+ function remapContainerPasteValue ( compJson : any , nameGenerator : NameGenerator ) : any {
368+ if ( ! compJson || typeof compJson !== "object" ) return compJson ;
369+
370+ const items = compJson . items ;
371+ const layout = compJson . layout ;
372+ if ( ! items || typeof items !== "object" ) return compJson ;
373+
374+ const keyMap : Record < string , string > = { } ;
375+ Object . keys ( items ) . forEach ( ( oldKey ) => {
376+ keyMap [ oldKey ] = genRandomKey ( ) ;
377+ } ) ;
378+
379+ const newItems : Record < string , any > = { } ;
380+ Object . entries ( items ) . forEach ( ( [ oldKey , itemValue ] : [ string , any ] ) => {
381+ const newKey = keyMap [ oldKey ] ;
382+ const compType = itemValue ?. compType ;
383+ const newName = compType ? nameGenerator . genItemName ( compType ) : genRandomKey ( ) ;
384+ const innerComp = itemValue ?. comp ;
385+ const remappedComp = innerComp ?. items
386+ ? remapContainerPasteValue ( innerComp , nameGenerator )
387+ : innerComp ;
388+ newItems [ newKey ] = { ...itemValue , name : newName , comp : remappedComp } ;
389+ } ) ;
390+
391+ let newLayout = layout ;
392+ if ( layout && typeof layout === "object" ) {
393+ const remapped : Record < string , any > = { } ;
394+ Object . entries ( layout ) . forEach ( ( [ oldKey , layoutItem ] : [ string , any ] ) => {
395+ const newKey = keyMap [ oldKey ] || oldKey ;
396+ remapped [ newKey ] = { ...layoutItem , i : newKey } ;
397+ } ) ;
398+ newLayout = remapped ;
399+ }
400+
401+ return { ...compJson , items : newItems , layout : newLayout } ;
402+ }
0 commit comments