29
29
*/
30
30
import { EventEmitter } from '@osjs/event-emitter' ;
31
31
import { h , app } from 'hyperapp' ;
32
+ import { draggable , droppable } from '../../utils/dnd' ;
33
+ import { emToPx } from '../../utils/dom' ;
32
34
import { doubleTap } from '../../utils/input' ;
33
35
import { pathJoin } from '../../utils/vfs' ;
34
36
35
37
const tapper = doubleTap ( ) ;
36
38
39
+ //
40
+ // FIXME: Excessive render on drop events
41
+ //
42
+
43
+ // TODO: Needs real values
44
+ const ICON_WIDTH = 5.0 ; // ems
45
+ const ICON_HEIGHT = 6.5 ; // ems
46
+ const ICON_MARGIN = 0.5 ; // ems
47
+
37
48
const validVfsDrop = data => data && data . path ;
49
+ const validInternalDrop = data => data && data . internal ;
50
+
51
+ // TODO: Use internal storage
52
+ const loadIconPositions = ( ) => JSON . parse (
53
+ localStorage . getItem ( '___osjs_iconview_positions' ) || '[]'
54
+ ) ;
55
+
56
+ // TODO: Use internal storage
57
+ const saveIconPositions = positions =>
58
+ localStorage . setItem ( '___osjs_iconview_positions' , JSON . stringify ( positions || [ ] ) ) ;
38
59
39
60
const onDropAction = actions => ( ev , data , files , shortcut = true ) => {
40
61
if ( validVfsDrop ( data ) ) {
41
- actions . addEntry ( { entry : data , shortcut} ) ;
62
+ actions . addEntry ( { entry : data , shortcut, ev} ) ;
63
+ } else if ( validInternalDrop ( data ) ) {
64
+ actions . moveEntry ( { entry : data . internal , ev} ) ;
42
65
} else if ( files . length > 0 ) {
43
66
actions . uploadEntries ( files ) ;
44
67
}
@@ -47,6 +70,67 @@ const onDropAction = actions => (ev, data, files, shortcut = true) => {
47
70
const isRootElement = ev =>
48
71
ev . target && ev . target . classList . contains ( 'osjs-desktop-iconview__wrapper' ) ;
49
72
73
+ const calculateGridSizes = el => {
74
+ const { offsetWidth, offsetHeight} = el ;
75
+ // TODO: Might cause reflow, do cache here
76
+ const sizeX = emToPx ( ICON_WIDTH ) + ( emToPx ( ICON_MARGIN ) * 2 ) ;
77
+ const sizeY = emToPx ( ICON_HEIGHT ) + ( emToPx ( ICON_MARGIN ) * 2 ) ;
78
+ const cols = Math . floor ( offsetWidth / sizeX ) ;
79
+ const rows = Math . floor ( offsetHeight / sizeY ) ;
80
+ return [ rows , cols , sizeX , sizeY ] ;
81
+ } ;
82
+
83
+ const calculateIconPositions = ( entries , positions , cols ) => {
84
+ const savedPositions = entries . map ( entry => {
85
+ const key = entry . shortcut === false ? entry . filename : entry . shortcut ;
86
+ const found = positions . findIndex ( s => s . key === key ) ;
87
+ return found === - 1 ? undefined : positions [ found ] . position ;
88
+ } ) ;
89
+
90
+ return entries . map ( ( entry , index ) => {
91
+ const x = index % cols ;
92
+ const y = Math . floor ( index / cols ) ;
93
+ const _position = savedPositions [ index ] || [ x , y ] ;
94
+
95
+ return Object . assign ( entry , { _position} ) ;
96
+ } ) ;
97
+ } ;
98
+
99
+ const isIconPositionBusy = ( ev , { entries, grid : { sizeX, sizeY} } ) => {
100
+ const col = Math . floor ( ev . clientX / sizeX ) ;
101
+ const row = Math . floor ( ev . clientY / sizeY ) ;
102
+
103
+ return entries . findIndex ( e => {
104
+ return e . _position [ 0 ] === col &&
105
+ e . _position [ 1 ] === row ;
106
+ } ) !== - 1 ;
107
+ } ;
108
+
109
+ const createIconStyle = ( entry , index , { grid : { enabled, sizeX, sizeY} } ) => {
110
+ const [ left , top ] = entry . _position || [ 0 , 0 ] ;
111
+
112
+ return enabled ? {
113
+ position : 'absolute' ,
114
+ top : String ( top * sizeY ) + 'px' ,
115
+ left : String ( left * sizeX ) + 'px'
116
+ } : { } ;
117
+ } ;
118
+
119
+ const createGhostStyle = ( { ghost, grid : { enabled, sizeX, sizeY} } ) => {
120
+ const style = { } ;
121
+ if ( ghost instanceof Event ) {
122
+ const col = Math . floor ( ghost . clientX / sizeX ) ;
123
+ const row = Math . floor ( ghost . clientY / sizeY ) ;
124
+ style . top = String ( row * sizeY ) + 'px' ;
125
+ style . left = String ( col * sizeX ) + 'px' ;
126
+ }
127
+
128
+ return Object . assign ( {
129
+ position : enabled ? 'absolute' : undefined ,
130
+ display : ghost ? undefined : 'none'
131
+ } , style ) ;
132
+ } ;
133
+
50
134
const view = ( fileIcon , themeIcon , droppable ) => ( state , actions ) =>
51
135
h ( 'div' , {
52
136
class : 'osjs-desktop-iconview__wrapper' ,
@@ -80,6 +164,7 @@ const view = (fileIcon, themeIcon, droppable) => (state, actions) =>
80
164
} , [
81
165
...state . entries . map ( ( entry , index ) => {
82
166
return h ( 'div' , {
167
+ style : createIconStyle ( entry , index , state ) ,
83
168
class : 'osjs-desktop-iconview__entry' + (
84
169
state . selected === index
85
170
? ' osjs-desktop-iconview__entry--selected'
@@ -88,7 +173,12 @@ const view = (fileIcon, themeIcon, droppable) => (state, actions) =>
88
173
oncontextmenu : ev => actions . openContextMenu ( { ev, entry, index} ) ,
89
174
ontouchstart : ev => tapper ( ev , ( ) => actions . openEntry ( { ev, entry, index} ) ) ,
90
175
ondblclick : ev => actions . openEntry ( { ev, entry, index} ) ,
91
- onclick : ev => actions . selectEntry ( { ev, entry, index} )
176
+ onclick : ev => actions . selectEntry ( { ev, entry, index} ) ,
177
+ oncreate : el => {
178
+ draggable ( el , {
179
+ data : { internal : entry }
180
+ } ) ;
181
+ }
92
182
} , [
93
183
h ( 'div' , {
94
184
class : 'osjs-desktop-iconview__entry__inner'
@@ -115,9 +205,7 @@ const view = (fileIcon, themeIcon, droppable) => (state, actions) =>
115
205
} ) ,
116
206
h ( 'div' , {
117
207
class : 'osjs-desktop-iconview__entry osjs-desktop-iconview__entry--ghost' ,
118
- style : {
119
- display : state . ghost ? undefined : 'none'
120
- }
208
+ style : createGhostStyle ( state )
121
209
} )
122
210
] ) ;
123
211
@@ -207,6 +295,10 @@ export class DesktopIconView extends EventEmitter {
207
295
this . $root . style . left = `${ rect . left } px` ;
208
296
this . $root . style . bottom = `${ rect . bottom } px` ;
209
297
this . $root . style . right = `${ rect . right } px` ;
298
+
299
+ if ( this . iconview ) {
300
+ this . iconview . resize ( ) ;
301
+ }
210
302
}
211
303
212
304
_render ( settings ) {
@@ -238,20 +330,31 @@ export class DesktopIconView extends EventEmitter {
238
330
this . core . $root . appendChild ( this . $root ) ;
239
331
240
332
const root = settings . path ;
241
- const { droppable} = this . core . make ( 'osjs/dnd' ) ;
242
333
const { icon : fileIcon } = this . core . make ( 'osjs/fs' ) ;
243
334
const { icon : themeIcon } = this . core . make ( 'osjs/theme' ) ;
244
335
const { copy, readdir, readfile, writefile, unlink, mkdir} = this . core . make ( 'osjs/vfs' ) ;
245
336
const error = err => console . error ( err ) ;
246
337
const shortcuts = createShortcuts ( root , readfile , writefile ) ;
247
338
const read = readDesktopFolder ( root , readdir , shortcuts ) ;
248
339
340
+ const [ rows , cols , sizeX , sizeY ] = calculateGridSizes ( this . $root ) ;
341
+
249
342
this . iconview = app ( {
250
343
selected : - 1 ,
251
344
entries : [ ] ,
252
- ghost : false
345
+ positions : loadIconPositions ( ) ,
346
+ ghost : false ,
347
+ grid : {
348
+ enabled : settings . grid ,
349
+ rows,
350
+ cols,
351
+ sizeX,
352
+ sizeY
353
+ }
253
354
} , {
254
- setEntries : entries => ( { entries} ) ,
355
+ setEntries : entries => state => {
356
+ return { entries : calculateIconPositions ( entries , state . positions , state . grid . cols ) } ;
357
+ } ,
255
358
256
359
openDropContextMenu : ( { ev, data, files} ) => {
257
360
this . createDropContextMenu ( ev , data , files ) ;
@@ -292,7 +395,7 @@ export class DesktopIconView extends EventEmitter {
292
395
// TODO
293
396
} ,
294
397
295
- addEntry : ( { entry, shortcut} ) => ( state , actions ) => {
398
+ addEntry : ( { entry, shortcut, ev } ) => ( state , actions ) => {
296
399
const dest = `${ root } /${ entry . filename } ` ;
297
400
298
401
mkdir ( root )
@@ -325,12 +428,47 @@ export class DesktopIconView extends EventEmitter {
325
428
return { selected : - 1 } ;
326
429
} ,
327
430
431
+ moveEntry : ( { entry, ev} ) => ( state ) => {
432
+ if ( ! isIconPositionBusy ( ev , state ) ) {
433
+ const positions = state . positions ;
434
+ const key = entry . shortcut === false ? entry . filename : entry . shortcut ;
435
+ const found = positions . findIndex ( s => s . key === key ) ;
436
+ const col = Math . floor ( ev . clientX / sizeX ) ;
437
+ const row = Math . floor ( ev . clientY / sizeY ) ;
438
+ const position = [ col , row ] ;
439
+ const value = { key, position} ;
440
+
441
+ if ( found !== - 1 ) {
442
+ positions [ found ] = value ;
443
+ } else {
444
+ positions . push ( value ) ;
445
+ }
446
+
447
+ saveIconPositions ( positions ) ;
448
+
449
+ return {
450
+ positions,
451
+ entries : calculateIconPositions ( state . entries , positions , state . cols )
452
+ } ;
453
+ }
454
+ return { } ;
455
+ } ,
456
+
328
457
reload : ( ) => ( state , actions ) => {
329
458
read ( )
330
459
. then ( entries => entries . filter ( e => e . filename !== '..' ) )
331
460
. then ( entries => actions . setEntries ( entries ) ) ;
332
461
} ,
333
462
463
+ resize : ( ) => ( { grid : { enabled} } ) => {
464
+ const [ rows , cols , sizeX , sizeY ] = calculateGridSizes ( this . $root ) ;
465
+ return { grid : { enabled, rows, cols, sizeX, sizeY} } ;
466
+ } ,
467
+
468
+ toggleGrid : enabled => ( { grid} ) => {
469
+ return { grid : Object . assign ( grid , { enabled} ) } ;
470
+ } ,
471
+
334
472
setGhost : ev => {
335
473
return { ghost : ev } ;
336
474
}
0 commit comments