2929 */
3030import { EventEmitter } from '@osjs/event-emitter' ;
3131import { h , app } from 'hyperapp' ;
32+ import { draggable , droppable } from '../../utils/dnd' ;
33+ import { emToPx } from '../../utils/dom' ;
3234import { doubleTap } from '../../utils/input' ;
3335import { pathJoin } from '../../utils/vfs' ;
3436
3537const tapper = doubleTap ( ) ;
3638
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+
3748const 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 || [ ] ) ) ;
3859
3960const onDropAction = actions => ( ev , data , files , shortcut = true ) => {
4061 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} ) ;
4265 } else if ( files . length > 0 ) {
4366 actions . uploadEntries ( files ) ;
4467 }
@@ -47,6 +70,67 @@ const onDropAction = actions => (ev, data, files, shortcut = true) => {
4770const isRootElement = ev =>
4871 ev . target && ev . target . classList . contains ( 'osjs-desktop-iconview__wrapper' ) ;
4972
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+
50134const view = ( fileIcon , themeIcon , droppable ) => ( state , actions ) =>
51135 h ( 'div' , {
52136 class : 'osjs-desktop-iconview__wrapper' ,
@@ -80,6 +164,7 @@ const view = (fileIcon, themeIcon, droppable) => (state, actions) =>
80164 } , [
81165 ...state . entries . map ( ( entry , index ) => {
82166 return h ( 'div' , {
167+ style : createIconStyle ( entry , index , state ) ,
83168 class : 'osjs-desktop-iconview__entry' + (
84169 state . selected === index
85170 ? ' osjs-desktop-iconview__entry--selected'
@@ -88,7 +173,12 @@ const view = (fileIcon, themeIcon, droppable) => (state, actions) =>
88173 oncontextmenu : ev => actions . openContextMenu ( { ev, entry, index} ) ,
89174 ontouchstart : ev => tapper ( ev , ( ) => actions . openEntry ( { ev, entry, index} ) ) ,
90175 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+ }
92182 } , [
93183 h ( 'div' , {
94184 class : 'osjs-desktop-iconview__entry__inner'
@@ -115,9 +205,7 @@ const view = (fileIcon, themeIcon, droppable) => (state, actions) =>
115205 } ) ,
116206 h ( 'div' , {
117207 class : 'osjs-desktop-iconview__entry osjs-desktop-iconview__entry--ghost' ,
118- style : {
119- display : state . ghost ? undefined : 'none'
120- }
208+ style : createGhostStyle ( state )
121209 } )
122210 ] ) ;
123211
@@ -207,6 +295,10 @@ export class DesktopIconView extends EventEmitter {
207295 this . $root . style . left = `${ rect . left } px` ;
208296 this . $root . style . bottom = `${ rect . bottom } px` ;
209297 this . $root . style . right = `${ rect . right } px` ;
298+
299+ if ( this . iconview ) {
300+ this . iconview . resize ( ) ;
301+ }
210302 }
211303
212304 _render ( settings ) {
@@ -238,20 +330,31 @@ export class DesktopIconView extends EventEmitter {
238330 this . core . $root . appendChild ( this . $root ) ;
239331
240332 const root = settings . path ;
241- const { droppable} = this . core . make ( 'osjs/dnd' ) ;
242333 const { icon : fileIcon } = this . core . make ( 'osjs/fs' ) ;
243334 const { icon : themeIcon } = this . core . make ( 'osjs/theme' ) ;
244335 const { copy, readdir, readfile, writefile, unlink, mkdir} = this . core . make ( 'osjs/vfs' ) ;
245336 const error = err => console . error ( err ) ;
246337 const shortcuts = createShortcuts ( root , readfile , writefile ) ;
247338 const read = readDesktopFolder ( root , readdir , shortcuts ) ;
248339
340+ const [ rows , cols , sizeX , sizeY ] = calculateGridSizes ( this . $root ) ;
341+
249342 this . iconview = app ( {
250343 selected : - 1 ,
251344 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+ }
253354 } , {
254- setEntries : entries => ( { entries} ) ,
355+ setEntries : entries => state => {
356+ return { entries : calculateIconPositions ( entries , state . positions , state . grid . cols ) } ;
357+ } ,
255358
256359 openDropContextMenu : ( { ev, data, files} ) => {
257360 this . createDropContextMenu ( ev , data , files ) ;
@@ -292,7 +395,7 @@ export class DesktopIconView extends EventEmitter {
292395 // TODO
293396 } ,
294397
295- addEntry : ( { entry, shortcut} ) => ( state , actions ) => {
398+ addEntry : ( { entry, shortcut, ev } ) => ( state , actions ) => {
296399 const dest = `${ root } /${ entry . filename } ` ;
297400
298401 mkdir ( root )
@@ -325,12 +428,47 @@ export class DesktopIconView extends EventEmitter {
325428 return { selected : - 1 } ;
326429 } ,
327430
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+
328457 reload : ( ) => ( state , actions ) => {
329458 read ( )
330459 . then ( entries => entries . filter ( e => e . filename !== '..' ) )
331460 . then ( entries => actions . setEntries ( entries ) ) ;
332461 } ,
333462
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+
334472 setGhost : ev => {
335473 return { ghost : ev } ;
336474 }
0 commit comments