rsence-pre 3.0.0.12 → 3.0.0.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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()
|