1
- /*
1
+ // This file is included by the activities on www.interactivities.ws
2
2
3
- Protocol:
3
+ // The following starts out with splice-iframe.js, and then translates VitalSource EPUB to SPLICE
4
4
5
- data.
6
- query (request from parent to child) 'docHeight', 'getContent', 'restoreState' (LTIHub v1)
7
- (request from child to parent) 'docHeight', 'send', 'retrieve' (LTIHub v2)
8
- request (response) the request
5
+ window . addEventListener ( 'load' , event => {
9
6
7
+ const scores = { }
8
+ const states = { }
9
+ let callbacks = undefined
10
+ let docHeight = 0
10
11
11
- state (restoreState request from parent to child, v1)
12
- state (getContent response from child to parent, v1)
13
-
14
- nonce (request from child to parent) a nonce to be returned with
15
- the response, for non-void requests ('retrieve', v2)
16
-
17
- docHeight (docHeight request, response from child to parent, v1)
18
-
19
- score (getContent response from child to parent, v1)
20
-
21
- param (request or response) parameter object (v2)
22
-
23
-
24
- v2 details:
25
-
26
- Always from child to parent
27
-
28
- { query: 'docHeight', param: { docHeight: ... } }
29
- No qid because applies to whole frame
30
- No response
31
-
32
- { query: 'send', param: { qid: ..., state: ..., score: ... }}
33
- Score between 0 and 1
34
- No response
12
+ const sendDocHeight = ( ) => {
13
+ if ( window . self === window . top ) return // not iframe
35
14
36
- { query: 'retrieve', nonce: ..., param: { qid: ... } }
37
- Response: { request: ..., param: state }
38
-
39
- TODO: Why not param: { state: ..., score: ... }
40
- If we want that, must adapt workAssignment.js, receiveMessage.js, codecheck.js
41
-
42
- */
43
-
44
- if ( window . self !== window . top ) { // iframe
45
- if ( ! ( 'EPUB' in window ) )
46
- window . EPUB = { }
47
- if ( ! ( 'Education' in window . EPUB ) ) {
48
- window . EPUB . Education = {
49
- nonceMap : { } ,
50
- retrieveCallback : undefined , // LTIHub v1
51
- retrieve : ( request , callback ) => {
52
- window . EPUB . Education . retrieveCallback = callback // LTIHub v1
53
- if ( 'stateToRestore' in window . EPUB . Education ) { // LTIHub v1, restore data already arrived
54
- callback ( { data : [ { data : window . EPUB . Education . stateToRestore } ] } )
55
- delete window . EPUB . Education . stateToRestore
56
- return
57
- }
58
- // Register callback
59
- const nonce = generateUUID ( )
60
- window . EPUB . Education . nonceMap [ nonce ] = callback
61
- if ( window . EPUB . Education . version !== 1 ) { // LTIHub v1
62
- // Pass request and nonce to parent
63
- // TODO: VitalSource format
64
- const qid = request . filters [ 0 ] . activityIds [ 0 ]
65
- const param = { qid }
66
- const data = { query : 'retrieve' , param, nonce }
67
- window . parent . postMessage ( data , '*' )
68
- }
69
- const MESSAGE_TIMEOUT = 5000
70
- setTimeout ( ( ) => {
71
- if ( 'stateToRestore' in window . EPUB . Education ) { // LTIHub v1, restore data already arrived and delivered
72
- delete window . EPUB . Education . stateToRestore
73
- return
74
- }
75
-
76
- if ( ! ( nonce in window . EPUB . Education . nonceMap ) ) return
77
- delete window . EPUB . Education . nonceMap [ nonce ]
78
- // TODO: VitalSource format
79
- callback ( { data : [ { data : null } ] } )
80
- } , MESSAGE_TIMEOUT )
81
- } ,
82
- send : ( request , callback ) => {
83
- if ( window . EPUB . Education . version === 1 ) return // LTIHub v1
84
- // TODO: VitalSource format
85
- const param = { state : request . data [ 0 ] . state . data , score : request . data [ 0 ] . results [ 0 ] . score , qid : request . data [ 0 ] . activityId }
86
- const data = { query : 'send' , param }
15
+ window . scrollTo ( 0 , 0 )
16
+ const SEND_DOCHEIGHT_DELAY = 100
17
+ setTimeout ( ( ) => {
18
+ let newDocHeight = document . body . scrollHeight + document . body . offsetTop
19
+ if ( docHeight != newDocHeight ) {
20
+ docHeight = newDocHeight
21
+ const data = { subject : 'lti.frameResize' , message_id : generateUUID ( ) , height : docHeight }
87
22
window . parent . postMessage ( data , '*' )
88
- sendDocHeight ( )
89
- } ,
90
- }
23
+ if ( window . SPLICE . logging ) console . log ( 'postMessage to parent' , data )
24
+ }
25
+ } , SEND_DOCHEIGHT_DELAY )
91
26
}
92
27
93
28
// https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
94
-
95
- function generateUUID ( ) { // Public Domain/MIT
29
+ const generateUUID = ( ) => { // Public Domain/MIT
96
30
var d = new Date ( ) . getTime ( )
97
31
var d2 = ( performance && performance . now && ( performance . now ( ) * 1000 ) ) || 0 // Time in microseconds since page-load or 0 if unsupported
98
32
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' . replace ( / [ x y ] / g, c => {
@@ -108,87 +42,103 @@ if (window.self !== window.top) { // iframe
108
42
} )
109
43
}
110
44
111
- let element = undefined
112
- let docHeight = 0
113
-
114
- function sendDocHeight ( ) {
115
- window . scrollTo ( 0 , 0 )
116
- const SEND_DOCHEIGHT_DELAY = 100
117
- if ( window . EPUB . Education . version === 1 ) return // TODO
118
- setTimeout ( ( ) => {
119
- const container = element === undefined ? document . body : element . closest ( 'li' ) . parentNode
120
- // When using container = document.documentElement, the document grows without bound
121
- // TODO: Why does this work in codecheck.js but not here?
122
- let newDocHeight = container . scrollHeight + container . offsetTop
123
- if ( docHeight != newDocHeight ) {
124
- docHeight = newDocHeight
125
- const data = { query : 'docHeight' , param : { docHeight } }
126
- window . parent . postMessage ( data , '*' )
127
- }
128
- } , SEND_DOCHEIGHT_DELAY )
45
+ if ( ! ( 'SPLICE' in window ) ) {
46
+ window . SPLICE = {
47
+ logging : true ,
48
+ getScore : ( location , callback ) => {
49
+ if ( callbacks === undefined ) {
50
+ callback ( states [ location ] )
51
+ } else {
52
+ callbacks . push ( { location, callback} )
53
+ }
54
+ } ,
55
+ reportScoreAndState : ( location , score , state ) => {
56
+ scores [ location ] = score
57
+ states [ location ] = state
58
+ if ( window . self === window . top ) return // not iframe
59
+ let averageScore = 0
60
+ let n = 0
61
+ for ( const location in scores ) {
62
+ averageScore += scores [ location ] ;
63
+ n ++
64
+ }
65
+ if ( n > 0 ) averageScore /= n
66
+ const message = { subject : 'SPLICE.reportScoreAndState' , message_id : generateUUID ( ) , score : averageScore , state : states }
67
+ window . parent . postMessage ( message , '*' )
68
+ if ( window . SPLICE . logging ) console . log ( 'postMessage to parent' , message )
69
+ } ,
70
+ sendEvent : ( location , name , data ) => {
71
+ const message = { subject : 'SPLICE.sendEvent' , message_id : generateUUID ( ) , name, data }
72
+ window . parent . postMessage ( message , '*' )
73
+ if ( window . SPLICE . logging ) console . log ( 'postMessage to parent' , message )
74
+ }
75
+ }
129
76
}
130
-
131
- window . addEventListener ( 'load' , event => {
132
- const interactiveElements = [ ...document . querySelectorAll ( 'div, ol' ) ] .
133
- filter ( e => {
134
- const ty = e . tagName
135
- const cl = e . getAttribute ( 'class' )
136
- return cl && ( ty === 'div' && cl . indexOf ( 'horstmann_' ) == 0 || ty === 'ol' && ( cl . indexOf ( 'multiple-choice' ) == 0 || cl . indexOf ( 'horstmann_ma' ) == 0 ) )
137
- } )
138
- element = interactiveElements [ 0 ]
139
- sendDocHeight ( )
140
- document . body . style . overflow = 'hidden'
141
- // ResizeObserver did not work
142
- const mutationObserver = new MutationObserver ( sendDocHeight ) ;
143
- mutationObserver . observe ( element === undefined ? document . body : element , { childList : true , subtree : true } )
144
- } )
145
77
146
- window . addEventListener ( "message" , event => {
78
+ if ( window . self !== window . top ) { // iframe
79
+ const message = { subject : 'SPLICE.getState' , message_id : generateUUID ( ) }
80
+ window . parent . postMessage ( message , '*' )
81
+ if ( window . SPLICE . logging ) console . log ( 'postMessage to parent' , message )
82
+ callbacks = [ ]
83
+ const MESSAGE_TIMEOUT = 5000
84
+ setTimeout ( ( ) => {
85
+ if ( callbacks === undefined ) return
86
+ for ( const { location, callback } of callbacks ) {
87
+ scores [ location ] = 0
88
+ states [ location ] = undefined
89
+ callback ( undefined , { code : 'timeout' } )
90
+ }
91
+ callbacks = undefined
92
+ } , MESSAGE_TIMEOUT )
93
+ }
94
+
95
+ window . addEventListener ( 'message' , event => {
147
96
if ( ! ( event . data instanceof Object ) ) return
148
- if ( 'request' in event . data ) { // It's a response
149
- const request = event . data . request
150
- if ( request . query === 'retrieve' ) { // LTIHub v2
151
- const state = event . data . param
152
- // TODO Old VitalSource API
153
- // let state = response.data[0].data
154
- const arg = { data : [ { data : state } ] }
155
- if ( request . nonce in window . EPUB . Education . nonceMap ) {
156
- // If not, already timed out
157
- window . EPUB . Education . nonceMap [ request . nonce ] ( arg )
158
- delete window . EPUB . Education . nonceMap [ request . nonce ]
97
+ if ( ! ( event . data . subject instanceof String && event . data . subject . startsWith ( 'SPLICE.' ) ) ) return
98
+ if ( event . data . subject . endsWith ( 'response' ) ) {
99
+ if ( window . SPLICE . logging ) console . log ( 'postmessage response' , event )
100
+ if ( event . data . subject === 'SPLICE.getState.response' ) {
101
+ if ( callbacks === undefined ) return // Already timed out
102
+ for ( const location in event . data . state ) {
103
+ scores [ location ] = 0
104
+ states [ location ] = event . data . state [ location ]
159
105
}
106
+
107
+ for ( const { location, callback } of callbacks ) {
108
+ callback ( states [ location ] , event . data . error )
109
+ }
110
+ callbacks = undefined
160
111
}
161
- // Handle other responses
162
- } else { // It's a request
163
- if ( event . data . query === 'docHeight' ) { // LTIHub v1
164
- const docHeight = document . body . children [ 0 ] . scrollHeight
165
- document . documentElement . style . height = docHeight + 'px'
166
- document . body . style . height = docHeight + 'px'
167
- document . body . style . overflow = 'auto'
168
- let response = { request : event . data , docHeight }
169
- event . source . postMessage ( response , '*' )
170
- window . EPUB . Education . version = 1
171
- }
172
- else if ( event . data . query === 'getContent' ) { // LTIHub v1
173
- const docHeight = document . body . children [ 0 ] . scrollHeight
174
- document . documentElement . style . height = docHeight + 'px'
175
- document . body . style . height = docHeight + 'px'
176
- const id = element . closest ( 'li' ) . id
177
- const score = { correct : Math . min ( element . correct , element . maxscore ) , errors : element . errors , maxscore : element . maxscore , activity : id }
178
- let response = { request : event . data , score : score , state : element . state }
179
- event . source . postMessage ( response , '*' )
180
- } else if ( event . data . query === 'restoreState' ) { // LTIHub v1
181
- window . EPUB . Education . stateToRestore = event . data . state
182
- /*
183
- It is possible that the element already made a
184
- retrieve request (which goes unanswered by the parent).
185
- */
186
- if ( window . EPUB . Education . retrieveCallback !== undefined ) { // retrieve request already made
187
- window . EPUB . Education . retrieve ( undefined , window . EPUB . Education . retrieveCallback )
188
- // delete window.EPUB.Education.retrieveCallback
189
- // Not deleting--in the instructor view assignments, called more than once
190
- }
191
- }
112
+ // Handle other responses
192
113
}
114
+ // TODO: SPLICE messages from children
193
115
} , false )
194
- }
116
+
117
+ sendDocHeight ( )
118
+ document . body . style . overflow = 'hidden'
119
+ // ResizeObserver did not work
120
+ const mutationObserver = new MutationObserver ( sendDocHeight ) ;
121
+ mutationObserver . observe ( document . body , { childList : true , subtree : true } )
122
+
123
+ // Translating EPUB to SPLICE
124
+ if ( ! ( 'EPUB' in window ) )
125
+ window . EPUB = { }
126
+ if ( ! ( 'Education' in window . EPUB ) ) {
127
+ window . EPUB . Education = {
128
+ retrieve : ( request , callback ) => {
129
+ const location = request . filters [ 0 ] . activityIds [ 0 ]
130
+ SPLICE . getState ( location , ( location , state ) => {
131
+ callback ( { data : [ { data : state } ] } )
132
+ } )
133
+ } ,
134
+ send : ( request , callback ) => {
135
+ const location = request . data [ 0 ] . activityId
136
+ const score = request . data [ 0 ] . results [ 0 ] . score
137
+ const state = request . data [ 0 ] . state . data
138
+ SPLICE . reportScoreAndState ( location , score , state )
139
+ } ,
140
+ }
141
+ }
142
+
143
+ } )
144
+
0 commit comments