rsence-pre 3.0.0.12 → 3.0.0.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/client/js/controls/textcontrol/textcontrol.coffee +3 -2
- data/client/js/core/class/class.js +12 -2
- data/client/js/core/elem/elem.coffee +2 -0
- data/client/js/datetime/timesheet/themes/default/timesheet.css +28 -37
- data/client/js/datetime/timesheet/themes/default/timesheet.html +3 -3
- data/client/js/datetime/timesheet/timesheet.coffee +610 -0
- data/client/js/datetime/timesheet_item/themes/default/timesheet_item.css +33 -35
- data/client/js/datetime/timesheet_item/themes/default/timesheet_item.html +6 -10
- data/client/js/datetime/timesheet_item/timesheet_item.coffee +144 -0
- data/client/js/foundation/control/eventresponder/eventresponder.js +3 -4
- data/client/js/foundation/eventmanager/eventmanager.coffee +6 -0
- data/client/js/tables/table/table.coffee +89 -37
- data/client/js/tables/table/themes/default/table.css +7 -7
- data/client/js/tables/table/themes/default/table.html +3 -3
- metadata +4 -4
- data/client/js/datetime/timesheet/timesheet.js +0 -852
- data/client/js/datetime/timesheet_item/timesheet_item.js +0 -210
@@ -0,0 +1,610 @@
|
|
1
|
+
|
2
|
+
# Default locale (en) strings for HTimeSheet
|
3
|
+
HLocale.components.HTimeSheet =
|
4
|
+
strings:
|
5
|
+
newItemLabel: 'New item'
|
6
|
+
HTimeSheet = HControl.extend
|
7
|
+
|
8
|
+
componentName: 'timesheet'
|
9
|
+
markupElemNames: [ 'label', 'value', 'timeline' ]
|
10
|
+
|
11
|
+
defaultEvents:
|
12
|
+
draggable: true
|
13
|
+
click: true
|
14
|
+
doubleClick: true
|
15
|
+
resize: true
|
16
|
+
|
17
|
+
controlDefaults: HControlDefaults.extend
|
18
|
+
timeStart: 0 # 1970-01-01 00:00:00
|
19
|
+
timeEnd: 86399 # 1970-01-01 23:59:59
|
20
|
+
tzOffset: 0 # For custom timezone offsets in seconds; eg: 7200 => UTC+2
|
21
|
+
itemMinHeight: 16 # Smallest allowed size for an item (in pixels)
|
22
|
+
hideHours: false # Enable to hide the hours in the gutter
|
23
|
+
autoLabel: false # Automatically set the label to the date, when enabled
|
24
|
+
autoLabelFn: 'formatDate' # The name of the function to return formatted date/time
|
25
|
+
notchesPerHour: 4 # by default 1/4 of an hour precision (15 minutes)
|
26
|
+
snapToNotch: true # Snaps time to nearest notch/line
|
27
|
+
itemOffsetLeft: 64 # Theme settings; don't enter in options
|
28
|
+
itemOffsetRight: 0 # Theme settings; don't enter in options
|
29
|
+
itemOffsetTop: 20 # Theme settings; don't enter in options
|
30
|
+
itemOffsetBottom: 0 # Theme settings; don't enter in options
|
31
|
+
itemDisplayTime: true # Items display their time by default
|
32
|
+
allowClickCreate: false # Enable to allow clicking in empty areas to create new items
|
33
|
+
iconImage: 'timesheet_item_icons.png' # Icon resources for items
|
34
|
+
allowDoubleClickCreate: true # Double-clicking empty areas are shortcuts for new items by default
|
35
|
+
minDragSize: 5 # Minimum amount of pixels dragged required for accepting a drag
|
36
|
+
hourOffsetTop: -4 # Theme settings; don't enter in options
|
37
|
+
|
38
|
+
customOptions: ( _opt )->
|
39
|
+
@localeStrings = HLocale.components.HTimeSheet.strings
|
40
|
+
_opt.defaultLabel = @localeStrings.newItemLabel unless _opt.defaultLabel?
|
41
|
+
_opt.autoLabelFnOptions = { longWeekDay: true } unless _opt.autoLabelFnOptions?
|
42
|
+
unless _opt.dummyValue?
|
43
|
+
_opt.dummyValue =
|
44
|
+
label: ''
|
45
|
+
start: 0
|
46
|
+
color: '#000000'
|
47
|
+
|
48
|
+
themeSettings: ( _itemOffsetLeft, _itemOffsetTop, _itemOffsetRight, _itemOffsetBottom, _hourOffsetTop )->
|
49
|
+
if @options.hideHours
|
50
|
+
ELEM.addClassName( @elemId, 'nohours' )
|
51
|
+
@options.itemOffsetLeft = 0
|
52
|
+
else if _itemOffsetLeft?
|
53
|
+
@options.itemOffsetLeft = _itemOffsetLeft
|
54
|
+
@options.itemOffsetTop = _itemOffsetTop if _itemOffsetTop?
|
55
|
+
@options.itemOffsetRight = _itemOffsetRight if _itemOffsetRight?
|
56
|
+
@options.itemOffsetBottom = _itemOffsetBottom if _itemOffsetBottom?
|
57
|
+
@options.hourOffsetTop = _hourOffsetTop if _hourOffsetTop?
|
58
|
+
|
59
|
+
autoLabel: ->
|
60
|
+
_locale = HLocale.dateTime
|
61
|
+
_opt = @options
|
62
|
+
_label = _locale[_opt.autoLabelFn]( _opt.timeStart, _opt.autoLabelFnOptions )
|
63
|
+
if @label != _label
|
64
|
+
@label = _label
|
65
|
+
@refreshLabel()
|
66
|
+
|
67
|
+
clearHours: ->
|
68
|
+
for _hourItemId in @hourItems
|
69
|
+
ELEM.del( _hourItemId )
|
70
|
+
|
71
|
+
drawHours: ->
|
72
|
+
_hourParent = @markupElemIds.timeline
|
73
|
+
_lineParent = @markupElemIds.value
|
74
|
+
_dateStart = new Date( @options.timeStart * 1000 )
|
75
|
+
_dateEnd = new Date( @options.timeEnd * 1000 )
|
76
|
+
_hourStart = _dateStart.getUTCHours()
|
77
|
+
_hourEnd = _dateEnd.getUTCHours()
|
78
|
+
_hours = (_hourEnd - _hourStart) + 1
|
79
|
+
_rectHeight = ELEM.getSize( _hourParent )[1]
|
80
|
+
_topOffset = @options.itemOffsetTop
|
81
|
+
_height = _rectHeight - _topOffset - @options.itemOffsetBottom
|
82
|
+
_pxPerHour = _height / _hours
|
83
|
+
_notchesPerHour = @options.notchesPerHour
|
84
|
+
_pxPerLine = _pxPerHour / _notchesPerHour
|
85
|
+
_bottomPos = _rectHeight-_height-_topOffset-2
|
86
|
+
_pxPerNotch = _pxPerHour / _notchesPerHour
|
87
|
+
|
88
|
+
ELEM.setStyle( _hourParent, 'visibility', 'hidden', true )
|
89
|
+
ELEM.setStyle( @markupElemIds.value, 'bottom', _bottomPos+'px' )
|
90
|
+
|
91
|
+
@clearHours() if @hourItems?
|
92
|
+
|
93
|
+
@itemOptions =
|
94
|
+
notchHeight: _pxPerNotch
|
95
|
+
notches: _hours * _notchesPerHour
|
96
|
+
offsetTop: _topOffset
|
97
|
+
offsetBottom: _bottomPos
|
98
|
+
height: _height
|
99
|
+
|
100
|
+
@hourItems = []
|
101
|
+
|
102
|
+
for _hour in [_hourStart.._hourEnd]
|
103
|
+
_lineTop = Math.round( _topOffset + (_hour*_pxPerHour) )
|
104
|
+
if _hour != _hourStart
|
105
|
+
_hourTop = _lineTop + @options.hourOffsetTop
|
106
|
+
@hourItems.push( ELEM.make( @markupElemIds.timeline, 'div',
|
107
|
+
attr:
|
108
|
+
className: 'hour'
|
109
|
+
styles:
|
110
|
+
top: _hourTop+'px'
|
111
|
+
html: _hour+':00'
|
112
|
+
) )
|
113
|
+
@hourItems.push( ELEM.make( _lineParent, 'div',
|
114
|
+
attr:
|
115
|
+
className: 'line'
|
116
|
+
styles:
|
117
|
+
top: (_lineTop+1)+'px'
|
118
|
+
height: Math.round(_pxPerNotch-1)+'px'
|
119
|
+
) )
|
120
|
+
for i in [1..._notchesPerHour] by 1
|
121
|
+
_notchTop = Math.round(_lineTop + (_pxPerNotch*i))
|
122
|
+
@hourItems.push( ELEM.make( _lineParent, 'div',
|
123
|
+
attr:
|
124
|
+
className: 'notch'
|
125
|
+
styles:
|
126
|
+
top: (_notchTop+1)+'px'
|
127
|
+
height: Math.round(_pxPerNotch-1)+'px'
|
128
|
+
) )
|
129
|
+
ELEM.setStyle( @markupElemIds.timeline, 'visibility', 'inherit' );
|
130
|
+
|
131
|
+
# extra hook for refreshing; updates label and hours before doing common things
|
132
|
+
refresh: ->
|
133
|
+
if @drawn
|
134
|
+
@autoLabel() if @options.autoLabel
|
135
|
+
@drawHours()
|
136
|
+
@base()
|
137
|
+
|
138
|
+
# set the timezone offset (in seconds)
|
139
|
+
setTzOffset: (_tzOffset)->
|
140
|
+
@options.tzOffset = _tzOffset
|
141
|
+
@refresh()
|
142
|
+
|
143
|
+
# set the start timestamp of the timesheet
|
144
|
+
setTimeStart: (_timeStart)->
|
145
|
+
@options.timeStart = _timeStart
|
146
|
+
@refresh()
|
147
|
+
|
148
|
+
# set the end timestamp of the timesheet
|
149
|
+
setTimeEnd: (_timeEnd)->
|
150
|
+
@options.timeEnd = _timeEnd
|
151
|
+
@refresh()
|
152
|
+
|
153
|
+
# sets the range of timestams of the timesheet
|
154
|
+
setTimeRange: (_timeRange)->
|
155
|
+
if @typeChr(_timeRange) == 'a' and _timeRange.length == 2
|
156
|
+
@setTimeStart( _timeRange[0] )
|
157
|
+
@setTimeEnd( _timeRange[1] )
|
158
|
+
else if @typeChr(_timeRange) == 'h' and _timeRange.timeStart? and _timeRange.timeEnd?
|
159
|
+
@setTimeStart( _timeRange.timeStart )
|
160
|
+
@setTimeEnd( _timeRange.timeEnd )
|
161
|
+
|
162
|
+
# sets the timestamp of the timesheet
|
163
|
+
setDate: (_date)->
|
164
|
+
@setTimeRange( [ _date, _date + @options.timeEnd - @options.timeStart ] )
|
165
|
+
@refresh()
|
166
|
+
|
167
|
+
# draw decorations
|
168
|
+
drawSubviews: ->
|
169
|
+
@drawHours()
|
170
|
+
_options = @options
|
171
|
+
_minDuration = Math.round(3600/_options.notchesPerHour)
|
172
|
+
_dummyValue = @cloneObject( @options.dummyValue )
|
173
|
+
_dummyValue.duration = _minDuration
|
174
|
+
@dragPreviewRect = @rectFromValue(
|
175
|
+
start: _options.timeStart
|
176
|
+
duration: _minDuration
|
177
|
+
)
|
178
|
+
@minDuration = _minDuration
|
179
|
+
@dragPreview = HTimeSheetItem.new( @dragPreviewRect, @,
|
180
|
+
value: _dummyValue
|
181
|
+
visible: false
|
182
|
+
iconImage: @options.iconImage
|
183
|
+
displayTime: @options.itemDisplayTime
|
184
|
+
)
|
185
|
+
@dragPreview.setStyleOfPart('state','color','#fff')
|
186
|
+
|
187
|
+
# event listener for clicks, simulates double clicks in case of not double click aware browser
|
188
|
+
click: (x,y)->
|
189
|
+
_prevClickTime = false
|
190
|
+
_notCreated = not @clickCreated and not @doubleClickCreated and not @dragCreated
|
191
|
+
if not @startDragTime and @prevClickTime
|
192
|
+
_prevClickTime = @prevClickTime
|
193
|
+
else if @startDragTime
|
194
|
+
_prevClickTime = @startDragTime
|
195
|
+
if _notCreated and @options.allowClickCreate
|
196
|
+
@clickCreate( x,y )
|
197
|
+
@clickCreated = true
|
198
|
+
@doubleClickCreated = false
|
199
|
+
@prevClickTime = false
|
200
|
+
else if _notCreated and @options.allowDoubleClickCreate
|
201
|
+
_currTime = new Date().getTime()
|
202
|
+
if _prevClickTime
|
203
|
+
_timeDiff = _currTime - _prevClickTime
|
204
|
+
else
|
205
|
+
_timeDiff = -1
|
206
|
+
if _timeDiff > 150 and _timeDiff < 500 and not @doubleClickCreated
|
207
|
+
@clickCreate( x, y )
|
208
|
+
@clickCreated = false
|
209
|
+
@doubleClickCreated = true
|
210
|
+
@doubleClickSimCreated = true
|
211
|
+
else
|
212
|
+
@doubleClickCreated = false
|
213
|
+
@prevClickTime = _currTime
|
214
|
+
else
|
215
|
+
@clickCreated = false
|
216
|
+
@doubleClickCreated = false
|
217
|
+
@prevClickTime = false
|
218
|
+
|
219
|
+
# creates an item on click
|
220
|
+
clickCreate: (x,y)->
|
221
|
+
_startTime = @pxToTime( y-@pageY(), true )
|
222
|
+
_endTime = _startTime + @minDuration
|
223
|
+
@refreshDragPreview( _startTime, _endTime )
|
224
|
+
@dragPreview.bringToFront()
|
225
|
+
@dragPreview.show()
|
226
|
+
if @activateEditor( @dragPreview )
|
227
|
+
@editor.createItem( @cloneObject( @dragPreview.value ) )
|
228
|
+
else
|
229
|
+
@dragPreview.hide()
|
230
|
+
|
231
|
+
# event listener for double clicks
|
232
|
+
doubleClick: (x,y)->
|
233
|
+
@prevClickTime = false
|
234
|
+
@doubleClickCreated = false
|
235
|
+
_notCreated = not @clickCreated and not @doubleClickCreated and not @doubleClickSimCreated and not @dragCreated
|
236
|
+
if not @options.allowDoubleClickCreate and @options.allowClickCreate and _notCreated
|
237
|
+
@click( x, y )
|
238
|
+
else if @options.allowDoubleClickCreate and not @options.allowClickCreate and _notCreated
|
239
|
+
@clickCreate( x, y )
|
240
|
+
@clickCreated = false
|
241
|
+
@doubleClickCreated = true
|
242
|
+
else
|
243
|
+
@clickCreated = false
|
244
|
+
@doubleClickSimCreated = false
|
245
|
+
|
246
|
+
# update the preview area
|
247
|
+
refreshDragPreview: (_startTime, _endTime)->
|
248
|
+
@dragPreviewRect.setTop( @timeToPx( _startTime ) )
|
249
|
+
@dragPreviewRect.setBottom( @timeToPx( _endTime ) )
|
250
|
+
@dragPreviewRect.setHeight( @options.itemMinHeight ) if @dragPreviewRect.height < @options.itemMinHeight
|
251
|
+
@dragPreview.drawRect()
|
252
|
+
@dragPreview.value.start = _startTime
|
253
|
+
@dragPreview.value.duration = _endTime - _startTime
|
254
|
+
@dragPreview.refreshValue()
|
255
|
+
|
256
|
+
# drag & drop event listeners, used for dragging new timesheet items
|
257
|
+
startDrag: (x,y)->
|
258
|
+
@_startDragY = y
|
259
|
+
@startDragTime = @pxToTime( y-@pageY(), true )
|
260
|
+
@refreshDragPreview( @startDragTime, @startDragTime + @minDuration )
|
261
|
+
@dragPreview.bringToFront()
|
262
|
+
@dragPreview.show()
|
263
|
+
true
|
264
|
+
|
265
|
+
drag: (x,y)->
|
266
|
+
_dragTime = @pxToTime( y-@pageY() )
|
267
|
+
if _dragTime < @startDragTime
|
268
|
+
_startTime = _dragTime
|
269
|
+
_endTime = @startDragTime
|
270
|
+
else
|
271
|
+
_endTime = _dragTime
|
272
|
+
_startTime = @startDragTime
|
273
|
+
@refreshDragPreview( _startTime, _endTime )
|
274
|
+
true
|
275
|
+
|
276
|
+
endDrag: (x,y)->
|
277
|
+
_dragTime = @pxToTime( y-@pageY() )
|
278
|
+
_minDistanceSatisfied = Math.abs( @_startDragY - y ) >= @options.minDragSize
|
279
|
+
@dragPreview.hide()
|
280
|
+
if _dragTime != @startDragTime
|
281
|
+
if _minDistanceSatisfied
|
282
|
+
if @activateEditor( @dragPreview )
|
283
|
+
@dragCreated = true
|
284
|
+
@editor.createItem( @cloneObject( @dragPreview.value ) )
|
285
|
+
return true
|
286
|
+
@dragCreated = false
|
287
|
+
else
|
288
|
+
@dragCreated = false
|
289
|
+
@clickCreated = false
|
290
|
+
@startDragTime = false
|
291
|
+
@click(x, y)
|
292
|
+
return true
|
293
|
+
false
|
294
|
+
|
295
|
+
# a resize triggers refresh, of which the important part is refreshValue, which triggers redraw of the time sheet items
|
296
|
+
resize: ->
|
297
|
+
@base()
|
298
|
+
@refresh()
|
299
|
+
|
300
|
+
# snaps the time to grid
|
301
|
+
snapTime: (_timeSecs,_begin)->
|
302
|
+
_options = @options
|
303
|
+
_pxDate = new Date( Math.round(_timeSecs) * 1000 )
|
304
|
+
_snapSecs = Math.round( 3600 / _options.notchesPerHour )
|
305
|
+
_halfSnapSecs = _snapSecs * 0.5
|
306
|
+
_hourSecs = (_pxDate.getUTCMinutes()*60) + _pxDate.getUTCSeconds()
|
307
|
+
_remSecs = _hourSecs % _snapSecs
|
308
|
+
if _begin
|
309
|
+
_timeSecs -= _remSecs
|
310
|
+
else
|
311
|
+
if _remSecs > _halfSnapSecs
|
312
|
+
_timeSecs += _snapSecs-_remSecs
|
313
|
+
else
|
314
|
+
_timeSecs -= _remSecs
|
315
|
+
_timeSecs
|
316
|
+
|
317
|
+
# snaps the pixel to grid
|
318
|
+
snapPx: (_px)->
|
319
|
+
_timeSecs = @pxToTime( _px )
|
320
|
+
_timeSecs = @snapTime( _timeSecs )
|
321
|
+
@timeToPx( _timeSecs )
|
322
|
+
|
323
|
+
# activates the editor; _item is the timesheet item to edit
|
324
|
+
activateEditor: (_item)->
|
325
|
+
if @editor
|
326
|
+
_editor = @editor
|
327
|
+
_editor.setTimeSheetItem( _item )
|
328
|
+
_item.bringToFront()
|
329
|
+
_editor.bringToFront()
|
330
|
+
_editor.show()
|
331
|
+
return true
|
332
|
+
false
|
333
|
+
|
334
|
+
###
|
335
|
+
# = Description
|
336
|
+
# Sets the editor given as parameter as the editor of instance.
|
337
|
+
#
|
338
|
+
# = Parameters
|
339
|
+
# +_editor+::
|
340
|
+
###
|
341
|
+
setEditor: (_editor)-> @editor = _editor
|
342
|
+
|
343
|
+
###
|
344
|
+
# = Description
|
345
|
+
# Destructor; destroys the editor first and commences inherited die.
|
346
|
+
###
|
347
|
+
die: ->
|
348
|
+
@editor.die() if @editor
|
349
|
+
@editor = null
|
350
|
+
@base()
|
351
|
+
|
352
|
+
# converts pixels to time
|
353
|
+
pxToTime: (_px, _begin)->
|
354
|
+
_options = @options
|
355
|
+
_timeStart = _options.timeStart
|
356
|
+
_timeEnd = _options.timeEnd
|
357
|
+
_timeRange = _timeEnd - _timeStart
|
358
|
+
_itemOptions = @itemOptions
|
359
|
+
_top = _itemOptions.offsetTop+1
|
360
|
+
_height = _itemOptions.height
|
361
|
+
_pxPerSec = _height / _timeRange
|
362
|
+
_px -= _top
|
363
|
+
_timeSecs = _timeStart + ( _px / _pxPerSec )
|
364
|
+
_timeSecs = @snapTime( _timeSecs, _begin ) if @options.snapToNotch
|
365
|
+
if _timeSecs > _options.timeEnd
|
366
|
+
_timeSecs = _options.timeEnd
|
367
|
+
else if _timeSecs < _options.timeStart
|
368
|
+
_timeSecs = _options.timeStart
|
369
|
+
Math.round( _timeSecs )
|
370
|
+
|
371
|
+
# converts time to pixels
|
372
|
+
timeToPx: (_time, _begin)->
|
373
|
+
_time = @snapTime( _time, _begin ) if @options.snapToNotch
|
374
|
+
_options = @options
|
375
|
+
_timeStart = _options.timeStart
|
376
|
+
_timeEnd = _options.timeEnd
|
377
|
+
_time = _timeStart if _time < _timeStart
|
378
|
+
_time = _timeEnd if _time > _timeEnd
|
379
|
+
_timeRange = _timeEnd - _timeStart
|
380
|
+
_itemOptions = @itemOptions
|
381
|
+
_top = _itemOptions.offsetTop
|
382
|
+
_height = _itemOptions.height
|
383
|
+
_pxPerSec = _height / _timeRange
|
384
|
+
_timeSecs = _time - _timeStart
|
385
|
+
_px = _top + ( _timeSecs * _pxPerSec )
|
386
|
+
Math.round( _px )
|
387
|
+
|
388
|
+
# converts time to pixels for the rect
|
389
|
+
rectFromValue: (_value)->
|
390
|
+
_topPx = @timeToPx( _value.start )
|
391
|
+
_bottomPx = @timeToPx( _value.start + _value.duration )
|
392
|
+
_leftPx = @options.itemOffsetLeft
|
393
|
+
_rightPx = @rect.width - @options.itemOffsetRight - 2
|
394
|
+
if _topPx == 'underflow'
|
395
|
+
_topPx = _itemOptions.offsetTop
|
396
|
+
else if _topPx == 'overflow'
|
397
|
+
return false
|
398
|
+
if _bottomPx == 'underflow'
|
399
|
+
return false
|
400
|
+
else if _bottomPx == 'overflow'
|
401
|
+
_bottomPx = _itemOptions.offsetTop + _itemOptions.height
|
402
|
+
_rect = HRect.new( _leftPx, _topPx, _rightPx, _bottomPx )
|
403
|
+
if _rect.height < @options.itemMinHeight
|
404
|
+
_rect.setHeight( @options.itemMinHeight )
|
405
|
+
_rect
|
406
|
+
|
407
|
+
# creates a single time sheet item component
|
408
|
+
createTimeSheetItem: (_value)->
|
409
|
+
_rect = @rectFromValue( _value )
|
410
|
+
return false if rect == false
|
411
|
+
HTimeSheetItem.new( _rect, @,
|
412
|
+
value: _value
|
413
|
+
displayTime: @options.itemDisplayTime
|
414
|
+
events:
|
415
|
+
draggable: true
|
416
|
+
doubleClick: true
|
417
|
+
)
|
418
|
+
|
419
|
+
# calls createTimeSheetItem with each value of the timesheet value array
|
420
|
+
drawTimeSheetItems: ->
|
421
|
+
_data = @value
|
422
|
+
_items = @timeSheetItems
|
423
|
+
if @typeChr(_data) == 'a' and _data.length > 0
|
424
|
+
for _value in _data
|
425
|
+
_item = @createTimeSheetItem( _value )
|
426
|
+
_items.push( _item ) if _item
|
427
|
+
|
428
|
+
###
|
429
|
+
# =Description
|
430
|
+
# Create a new timeSheetItems if it hasn't been done already,
|
431
|
+
# otherwise destroy the items of the old one before proceeding.
|
432
|
+
###
|
433
|
+
_initTimeSheetItems: ->
|
434
|
+
@timeSheetItems = [] unless @timeSheetItems?
|
435
|
+
if @timeSheetItems.length > 0
|
436
|
+
for _timeSheetItem in @timeSheetItems
|
437
|
+
_timeSheetItem.die()
|
438
|
+
@timeSheetItems = []
|
439
|
+
|
440
|
+
# finds the index in the array which contains most sequential items
|
441
|
+
_findLargestSequence: (_arr)->
|
442
|
+
_index = 0
|
443
|
+
_length = 1
|
444
|
+
_maxLength = 1
|
445
|
+
_bestIndex = 0
|
446
|
+
for i in [1..._arr.length] by 1
|
447
|
+
# grow:
|
448
|
+
if _arr[i] - _arr[i-1] == 1 and _index == i-_length
|
449
|
+
_length++
|
450
|
+
# reset:
|
451
|
+
else
|
452
|
+
_index = i
|
453
|
+
_length = 1
|
454
|
+
if _length > _maxLength
|
455
|
+
_bestIndex = _index
|
456
|
+
_maxLength = _length
|
457
|
+
[ _bestIndex, _maxLength ]
|
458
|
+
|
459
|
+
# find the amount of overlapping time sheet items
|
460
|
+
_findOverlapCount: (_items)->
|
461
|
+
_overlaps = []
|
462
|
+
_testRects = @_getTestRects( _items )
|
463
|
+
for i in [0..._items.length] by 1
|
464
|
+
_overlaps[i] = 0
|
465
|
+
for i in [0...(_items.length-1)] by 1
|
466
|
+
for j in [(i+1)..._items.length] by 1
|
467
|
+
if _items[i].rect.intersects( _testRects[j] )
|
468
|
+
_overlaps[i]++
|
469
|
+
_overlaps[j]++
|
470
|
+
Math.max.apply( Math, _overlaps )
|
471
|
+
|
472
|
+
_getTestRects: (_items)->
|
473
|
+
_rects = []
|
474
|
+
for i in [0..._items.length] by 1
|
475
|
+
_rects[i] = HRect.new( _items[i].rect )
|
476
|
+
_rects[i].insetBy( 1, 1 )
|
477
|
+
_rects
|
478
|
+
|
479
|
+
# returns a sorted copy of the timeSheetItems array
|
480
|
+
_sortedTimeSheetItems: (_sortFn)->
|
481
|
+
unless _sortFn?
|
482
|
+
_sortFn = (a,b)-> b.rect.height - a.rect.height
|
483
|
+
_arr = []
|
484
|
+
_items = @timeSheetItems
|
485
|
+
_arr.push( _item ) for _item in _items
|
486
|
+
_arr.sort(_sortFn)
|
487
|
+
|
488
|
+
# Optimizes the left and right position of each timesheet item to fit
|
489
|
+
# NOTE: This method will require refactoring; it's way too long and complicated
|
490
|
+
_updateTimelineRects: ->
|
491
|
+
# loop indexes:
|
492
|
+
_options = @options
|
493
|
+
_rect = @rect
|
494
|
+
_availWidth = _rect.width - _options.itemOffsetRight - _options.itemOffsetLeft
|
495
|
+
_left = _options.itemOffsetLeft
|
496
|
+
# get a list of timesheet items sorted by height (larger to smaller order)
|
497
|
+
_items = @_sortedTimeSheetItems()
|
498
|
+
_itemCount = _items.length
|
499
|
+
# amount of items ovelapping (max, actual number might be smaller after optimization)
|
500
|
+
_overlapCount = @_findOverlapCount( _items )
|
501
|
+
_width = Math.floor( _availWidth / (_overlapCount+1) )
|
502
|
+
_maxCol = 0
|
503
|
+
_origColById = []
|
504
|
+
# No overlapping; nothing to do
|
505
|
+
return false unless _overlapCount
|
506
|
+
# move all items initially to one column right of the max overlaps
|
507
|
+
_leftPos = _left+(_width*(_overlapCount+1))
|
508
|
+
for i in [0..._itemCount] by 1
|
509
|
+
_itemRect = _items[i].rect
|
510
|
+
_itemRect.setLeft( _leftPos )
|
511
|
+
_itemRect.setRight( _leftPos+_width )
|
512
|
+
|
513
|
+
# optimize gaps by traversing each combination
|
514
|
+
# and finding the first column with no gaps
|
515
|
+
# the top-level loops three times in the following modes:
|
516
|
+
# 0: place items into the first vacant column and find the actual max columns
|
517
|
+
# 1: stretch columns to final column width
|
518
|
+
# 2: stretch columns to fit multiple columns, if space is vacant
|
519
|
+
for l in [0...3] by 1
|
520
|
+
for i in [0..._itemCount] by 1
|
521
|
+
_itemRect = _items[i].rect
|
522
|
+
# in mode 1, just the column widths are changed
|
523
|
+
if l == 1
|
524
|
+
_leftPos = _left + (_origColById[i]*_width)
|
525
|
+
_itemRect.setLeft( _leftPos )
|
526
|
+
_itemRect.setRight( _leftPos + _width )
|
527
|
+
continue
|
528
|
+
_overlapCols = []
|
529
|
+
_vacantCols = []
|
530
|
+
_testRects = @_getTestRects( _items )
|
531
|
+
_testRect = HRect.new( _itemRect )
|
532
|
+
# test each column position (modes 0 and 2)
|
533
|
+
for k in [0...(_overlapCount+1)] by 1
|
534
|
+
_leftPos = _left + (k*_width)
|
535
|
+
_testRect.setLeft( _leftPos )
|
536
|
+
_testRect.setRight( _leftPos + _width )
|
537
|
+
for j in [0..._itemCount] by 1
|
538
|
+
if i != j and _testRect.intersects( _testRects[j] )
|
539
|
+
_overlapCols.push( k ) unless ~_overlapCols.indexOf( k )
|
540
|
+
if not ~_vacantCols.indexOf( k ) and not ~_overlapCols.indexOf( k )
|
541
|
+
_vacantCols.push( k )
|
542
|
+
|
543
|
+
# on the first run (mode 0) place items into the first column:
|
544
|
+
if l == 0
|
545
|
+
_origCol = _vacantCols[0]
|
546
|
+
_origColById.push( _origCol )
|
547
|
+
_leftPos = _left+(_origCol*_width)
|
548
|
+
_rightPos = _leftPos + _width
|
549
|
+
_maxCol = _origCol if _maxCol < _origCol
|
550
|
+
else
|
551
|
+
# on mode 2: stretch to fill multiple column widths,
|
552
|
+
# because no item moving is done anymore at this stage, so we know what's free and what's not
|
553
|
+
if _vacantCols.length > 0
|
554
|
+
_optimalColAndLength = @_findLargestSequence( _vacantCols )
|
555
|
+
_col = _vacantCols[ _optimalColAndLength[0] ]
|
556
|
+
_colWidth = _optimalColAndLength[1]
|
557
|
+
else
|
558
|
+
_origCol = _origColById[i]
|
559
|
+
_col = _origCol
|
560
|
+
_colWidth = 1
|
561
|
+
_leftPos = _left+(_col*_width)
|
562
|
+
_rightPos = _leftPos+(_colWidth*_width)
|
563
|
+
_itemRect.setLeft( _leftPos )
|
564
|
+
_itemRect.setRight( _rightPos )
|
565
|
+
# after the first run (mode 0) we know the actual amount of columns, so adjust column width accordingly
|
566
|
+
if l == 0
|
567
|
+
_overlapCount = _maxCol
|
568
|
+
_width = Math.floor( _availWidth / (_maxCol+1) )
|
569
|
+
true
|
570
|
+
|
571
|
+
# draws the timeline (sub-routine of refreshValue)
|
572
|
+
drawTimeline: ->
|
573
|
+
@_initTimeSheetItems()
|
574
|
+
@drawTimeSheetItems()
|
575
|
+
@_updateTimelineRects()
|
576
|
+
# use the dimensions of the views
|
577
|
+
for _timeSheetItem in @timeSheetItems
|
578
|
+
_timeSheetItem.drawRect()
|
579
|
+
|
580
|
+
_sha: SHA.new(8)
|
581
|
+
|
582
|
+
###
|
583
|
+
# Each item looks like this, any extra attributes are allowed,
|
584
|
+
# but not used and not guaranteed to be preserved:
|
585
|
+
#
|
586
|
+
# { id: 'abcdef1234567890', # identifier, used in server to map id's
|
587
|
+
# label: 'Event title', # label of event title
|
588
|
+
# start: 1299248619, # epoch timestamp of event start
|
589
|
+
# duration: 3600, # duration of event in seconds
|
590
|
+
# locked: true, # when false, prevents editing the item
|
591
|
+
# icons: [ 1, 3, 6 ], # icon id's to display
|
592
|
+
# color: '#ffffff' # defaults to '#ffffff' if undefined
|
593
|
+
# }
|
594
|
+
#
|
595
|
+
# = Description
|
596
|
+
# Redraws and refreshes the values on timesheet.
|
597
|
+
#
|
598
|
+
###
|
599
|
+
refreshValue: ->
|
600
|
+
return unless @itemOptions
|
601
|
+
# optimization that ensures the rect and previous value are different before redrawing
|
602
|
+
_valueStr = @encodeObject( @value )
|
603
|
+
_rectStr = @rect.toString()
|
604
|
+
_timeRangeStr = @options.timeStart+':'+@options.timeEnd
|
605
|
+
_shaSum = @_sha.strSHA1( _valueStr+_rectStr+_timeRangeStr )
|
606
|
+
if @_prevSum != _shaSum
|
607
|
+
# the preview timesheet item is hidden when new data arrives (including what it created)
|
608
|
+
@dragPreview.hide()
|
609
|
+
@_prevSum = _shaSum
|
610
|
+
@drawTimeline()
|