Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
93632029
Commit
93632029
authored
Jun 13, 2017
by
Sean Roberts
Committed by
GitHub
Jun 13, 2017
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #430 from evidentpoint/initial-multi-frame-support
Multiple frame detection and injection
parents
0306b229
3db43465
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
407 additions
and
39 deletions
+407
-39
gulpfile.js
gulpfile.js
+1
-0
live-reload-server.js
scripts/gulp/live-reload-server.js
+107
-38
guest.coffee
src/annotator/guest.coffee
+2
-0
main.js
src/annotator/main.js
+11
-0
cross-frame.coffee
src/annotator/plugin/cross-frame.coffee
+47
-1
multi-frame-test.js
src/annotator/test/integration/multi-frame-test.js
+172
-0
frame-util.js
src/annotator/util/frame-util.js
+63
-0
frame-rpc.js
src/shared/frame-rpc.js
+1
-0
polyfills.js
src/shared/polyfills.js
+1
-0
bridge-test.coffee
src/shared/test/bridge-test.coffee
+2
-0
No files found.
gulpfile.js
View file @
93632029
...
...
@@ -327,6 +327,7 @@ gulp.task('serve-live-reload', ['serve-package'], function () {
var
LiveReloadServer
=
require
(
'./scripts/gulp/live-reload-server'
);
liveReloadServer
=
new
LiveReloadServer
(
3000
,
{
clientUrl
:
`http://
${
packageServerHostname
()}
:3001/hypothesis`
,
enableMultiFrameSupport
:
!!
process
.
env
.
MULTI_FRAME_SUPPORT
,
});
});
...
...
scripts/gulp/live-reload-server.js
View file @
93632029
...
...
@@ -4,11 +4,20 @@ var fs = require('fs');
var
gulpUtil
=
require
(
'gulp-util'
);
var
http
=
require
(
'http'
);
var
WebSocketServer
=
require
(
'websocket'
).
server
;
var
urlParser
=
require
(
'url'
);
function
changelog
Text
()
{
function
readme
Text
()
{
return
fs
.
readFileSync
(
'./README.md'
,
'utf-8'
);
}
function
licenseText
()
{
return
fs
.
readFileSync
(
'./LICENSE'
,
'utf-8'
);
}
function
changelogText
()
{
return
fs
.
readFileSync
(
'./CHANGELOG.md'
,
'utf-8'
);
}
/**
* @typedef Config
* @property {string} clientUrl - The URL of the client's boot script
...
...
@@ -33,43 +42,103 @@ function LiveReloadServer(port, config) {
function
listen
()
{
var
log
=
gulpUtil
.
log
;
var
server
=
http
.
createServer
(
function
(
req
,
response
)
{
var
content
=
`
<html>
<head>
<meta charset="UTF-8">
<title>Hypothesis Client Test</title>
</head>
<body>
<div data-hypothesis-trigger style="margin: 75px 0 0 75px;">
Number of annotations:
<span data-hypothesis-annotation-count>...</span>
</div>
<pre style="margin: 20px 75px 75px 75px;">
${
changelogText
()}
</pre>
<script>
var appHost = document.location.hostname;
window.hypothesisConfig = function () {
return {
liveReloadServer: 'ws://' + appHost + ':
${
port
}
',
// Open the sidebar when the page loads
openSidebar: true,
};
};
window.addEventListener('message', function (event) {
if (event.data.type && event.data.type === 'reloadrequest') {
window.location.reload();
}
});
var embedScript = document.createElement('script');
embedScript.src = '
${
config
.
clientUrl
}
';
document.body.appendChild(embedScript);
</script>
</body>
</html>
`
;
var
url
=
urlParser
.
parse
(
req
.
url
);
var
content
;
if
(
url
.
pathname
===
'/document/license'
)
{
content
=
`
<html>
<head>
<meta charset="UTF-8">
<title>Hypothesis in-line frame document - License</title>
</head>
<body>
<pre style="margin: 20px;">
${
licenseText
()}
</pre>
</body>
</html>
`
;
}
else
if
(
url
.
pathname
===
'/document/changelog'
)
{
content
=
`
<html>
<head>
<meta charset="UTF-8">
<title>Hypothesis in-line frame document - Changelog</title>
</head>
<body>
<pre style="margin: 20px;">
${
changelogText
()}
</pre>
</body>
</html>
`
;
}
else
{
var
multiFrameContent
=
config
.
enableMultiFrameSupport
?
`
<div style="margin: 10px 0 0 75px;">
<button id="add-test" style="padding: 0.6em; font-size: 0.75em">Toggle 2nd Frame</button>
</div>
<div style="margin: 10px 0 0 75px;">
<iframe id="iframe1" src="/document/license" style="width: 50%;height: 300px;"></iframe>
</div>
<div id="iframe2-container" style="margin: 10px 0 0 75px;">
</div>`
:
''
;
content
=
`
<html>
<head>
<meta charset="UTF-8">
<title>Hypothesis Client Test</title>
</head>
<body>
<div data-hypothesis-trigger style="margin: 75px 0 0 75px;">
Number of annotations:
<span data-hypothesis-annotation-count>...</span>
</div>
${
multiFrameContent
}
<pre style="margin: 20px 75px 75px 75px;">
${
readmeText
()}
</pre>
<script>
var appHost = document.location.hostname;
window.hypothesisConfig = function () {
return {
liveReloadServer: 'ws://' + appHost + ':
${
port
}
',
// Open the sidebar when the page loads
openSidebar: true,
// Needed for multi frame support
enableMultiFrameSupport:
${
config
.
enableMultiFrameSupport
}
,
embedScriptUrl: '
${
config
.
clientUrl
}
'
};
};
window.addEventListener('message', function (event) {
if (event.data.type && event.data.type === 'reloadrequest') {
window.location.reload();
}
});
var embedScript = document.createElement('script');
embedScript.src = '
${
config
.
clientUrl
}
';
document.body.appendChild(embedScript);
var iframeIsAdded = false;
document.querySelector('#add-test').addEventListener('click', function() {
if (!iframeIsAdded) {
var iframe1 = document.querySelector('#iframe1');
var iframeNew = iframe1.cloneNode();
iframeNew.src = "/document/changelog";
iframeNew.id = "iframe2";
iframeIsAdded = true;
document.querySelector('#iframe2-container').appendChild(iframeNew);
} else {
var iframe2 = document.querySelector('#iframe2');
iframe2.parentNode.removeChild(iframe2);
iframeIsAdded = false;
}
});
</script>
</body>
</html>
`
;
}
response
.
end
(
content
);
});
...
...
src/annotator/guest.coffee
View file @
93632029
...
...
@@ -81,6 +81,8 @@ module.exports = class Guest extends Delegator
this
.
anchors
=
[]
cfOptions
=
enableMultiFrameSupport
:
config
.
enableMultiFrameSupport
embedScriptUrl
:
config
.
embedScriptUrl
on
:
(
event
,
handler
)
=>
this
.
subscribe
(
event
,
handler
)
emit
:
(
event
,
args
...)
=>
...
...
src/annotator/main.js
View file @
93632029
...
...
@@ -20,6 +20,7 @@ if (window.wgxpath) {
var
$
=
require
(
'jquery'
);
// Applications
var
Guest
=
require
(
'./guest'
);
var
Sidebar
=
require
(
'./sidebar'
);
var
PdfSidebar
=
require
(
'./pdf-sidebar'
);
...
...
@@ -44,11 +45,21 @@ $.noConflict(true)(function() {
var
Klass
=
window
.
PDFViewerApplication
?
PdfSidebar
:
Sidebar
;
if
(
config
.
hasOwnProperty
(
'constructor'
))
{
Klass
=
config
.
constructor
;
delete
config
.
constructor
;
}
if
(
config
.
enableMultiFrameSupport
&&
config
.
subFrameInstance
)
{
Klass
=
Guest
;
// Other modules use this to detect if this
// frame context belongs to hypothesis.
// Needs to be a global property that's set.
window
.
__hypothesis_frame
=
true
;
}
config
.
pluginClasses
=
pluginClasses
;
window
.
annotator
=
new
Klass
(
document
.
body
,
config
);
...
...
src/annotator/plugin/cross-frame.coffee
View file @
93632029
...
...
@@ -3,7 +3,9 @@ Plugin = require('../plugin')
AnnotationSync
=
require
(
'../annotation-sync'
)
Bridge
=
require
(
'../../shared/bridge'
)
Discovery
=
require
(
'../../shared/discovery'
)
FrameUtil
=
require
(
'../util/frame-util'
)
debounce
=
require
(
'lodash.debounce'
)
# Extracts individual keys from an object and returns a new one.
extract
=
extract
=
(
obj
,
keys
...)
->
...
...
@@ -11,11 +13,17 @@ extract = extract = (obj, keys...) ->
ret
[
key
]
=
obj
[
key
]
for
key
in
keys
when
obj
.
hasOwnProperty
(
key
)
ret
# Find difference of two arrays
difference
=
(
arrayA
,
arrayB
)
->
arrayA
.
filter
(
x
)
->
!
arrayB
.
includes
(
x
)
# Class for establishing a messaging connection to the parent sidebar as well
# as keeping the annotation state in sync with the sidebar application, this
# frame acts as the bridge client, the sidebar is the server. This plugin
# can also be used to send messages through to the sidebar using the
# call method.
# call method. This plugin also enables the discovery and management of
# not yet known frames in a multiple frame scenario.
module
.
exports
=
class
CrossFrame
extends
Plugin
constructor
:
(
elem
,
options
)
->
super
...
...
@@ -28,11 +36,16 @@ module.exports = class CrossFrame extends Plugin
opts
=
extract
(
options
,
'on'
,
'emit'
)
annotationSync
=
new
AnnotationSync
(
bridge
,
opts
)
handledFrames
=
[]
this
.
pluginInit
=
->
onDiscoveryCallback
=
(
source
,
origin
,
token
)
->
bridge
.
createChannel
(
source
,
origin
,
token
)
discovery
.
startDiscovery
(
onDiscoveryCallback
)
if
options
.
enableMultiFrameSupport
_setupFrameDetection
()
this
.
destroy
=
->
# super doesnt work here :(
Plugin
::
destroy
.
apply
(
this
,
arguments
)
...
...
@@ -50,3 +63,36 @@ module.exports = class CrossFrame extends Plugin
this
.
onConnect
=
(
fn
)
->
bridge
.
onConnect
(
fn
)
_setupFrameDetection
=
->
_discoverOwnFrames
()
# Listen for DOM mutations, to know when frames are added / removed
observer
=
new
MutationObserver
(
debounce
(
_discoverOwnFrames
,
300
,
leading
:
true
))
observer
.
observe
(
elem
,
{
childList
:
true
,
subtree
:
true
});
_discoverOwnFrames
=
->
frames
=
FrameUtil
.
findFrames
(
elem
)
for
frame
in
frames
if
frame
not
in
handledFrames
_handleFrame
(
frame
)
handledFrames
.
push
(
frame
)
for
frame
,
i
in
difference
(
handledFrames
,
frames
)
_iframeUnloaded
(
frame
)
delete
handledFrames
[
i
]
_injectToFrame
=
(
frame
)
->
if
!
FrameUtil
.
hasHypothesis
(
frame
)
FrameUtil
.
injectHypothesis
(
frame
,
options
.
embedScriptUrl
)
frame
.
contentWindow
.
addEventListener
'unload'
,
->
_iframeUnloaded
(
frame
)
_handleFrame
=
(
frame
)
->
if
!
FrameUtil
.
isAccessible
(
frame
)
then
return
FrameUtil
.
isLoaded
frame
,
()
->
_injectToFrame
(
frame
)
_iframeUnloaded
=
(
frame
)
->
# TODO: Bridge call here not yet implemented, placeholder for now
bridge
.
call
(
'destroyFrame'
,
frame
.
src
);
src/annotator/test/integration/multi-frame-test.js
0 → 100644
View file @
93632029
'use strict'
;
var
proxyquire
=
require
(
'proxyquire'
);
var
isLoaded
=
require
(
'../../util/frame-util.js'
).
isLoaded
;
describe
(
'CrossFrame multi-frame scenario'
,
function
()
{
var
fakeAnnotationSync
;
var
fakeBridge
;
var
proxyAnnotationSync
;
var
proxyBridge
;
var
container
;
var
crossFrame
;
var
options
;
var
sandbox
=
sinon
.
sandbox
.
create
();
beforeEach
(
function
()
{
fakeBridge
=
{
createChannel
:
sandbox
.
stub
(),
call
:
sandbox
.
stub
(),
destroy
:
sandbox
.
stub
(),
};
fakeAnnotationSync
=
{};
proxyAnnotationSync
=
sandbox
.
stub
().
returns
(
fakeAnnotationSync
);
proxyBridge
=
sandbox
.
stub
().
returns
(
fakeBridge
);
var
CrossFrame
=
proxyquire
(
'../../plugin/cross-frame'
,
{
'../annotation-sync'
:
proxyAnnotationSync
,
'../../shared/bridge'
:
proxyBridge
,
});
container
=
document
.
createElement
(
'div'
);
document
.
body
.
appendChild
(
container
);
options
=
{
enableMultiFrameSupport
:
true
,
embedScriptUrl
:
'data:,'
,
// empty data uri
on
:
sandbox
.
stub
(),
emit
:
sandbox
.
stub
(),
};
crossFrame
=
new
CrossFrame
(
container
,
options
);
});
afterEach
(
function
()
{
sandbox
.
restore
();
crossFrame
.
destroy
();
container
.
parentNode
.
removeChild
(
container
);
});
it
(
'detects frames on page'
,
function
()
{
// Create a frame before initializing
var
validFrame
=
document
.
createElement
(
'iframe'
);
container
.
appendChild
(
validFrame
);
// Create another that mimics the sidebar frame
// This one should should not be detected
var
invalidFrame
=
document
.
createElement
(
'iframe'
);
invalidFrame
.
className
=
'h-sidebar-iframe'
;
container
.
appendChild
(
invalidFrame
);
// Now initialize
crossFrame
.
pluginInit
();
var
validFramePromise
=
new
Promise
(
function
(
resolve
)
{
isLoaded
(
validFrame
,
function
()
{
assert
(
validFrame
.
contentDocument
.
body
.
hasChildNodes
(),
'expected valid frame to be modified'
);
resolve
();
});
});
var
invalidFramePromise
=
new
Promise
(
function
(
resolve
)
{
isLoaded
(
invalidFrame
,
function
()
{
assert
(
!
invalidFrame
.
contentDocument
.
body
.
hasChildNodes
(),
'expected invalid frame to not be modified'
);
resolve
();
});
});
return
Promise
.
all
([
validFramePromise
,
invalidFramePromise
]);
});
it
(
'detects removed frames'
,
function
()
{
// Create a frame before initializing
var
frame
=
document
.
createElement
(
'iframe'
);
container
.
appendChild
(
frame
);
// Now initialize
crossFrame
.
pluginInit
();
// Remove the frame
frame
.
remove
();
assert
.
calledWith
(
fakeBridge
.
call
,
'destroyFrame'
);
});
it
(
'injects embed script in frame'
,
function
()
{
var
frame
=
document
.
createElement
(
'iframe'
);
container
.
appendChild
(
frame
);
crossFrame
.
pluginInit
();
return
new
Promise
(
function
(
resolve
)
{
isLoaded
(
frame
,
function
()
{
var
scriptElement
=
frame
.
contentDocument
.
querySelector
(
'script[src]'
);
assert
(
scriptElement
,
'expected embed script to be injected'
);
assert
.
equal
(
scriptElement
.
src
,
options
.
embedScriptUrl
,
'unexpected embed script source'
);
resolve
();
});
});
});
it
(
'excludes injection from already injected frames'
,
function
()
{
var
frame
=
document
.
createElement
(
'iframe'
);
frame
.
srcdoc
=
'<script>window.__hypothesis_frame = true;</script>'
;
container
.
appendChild
(
frame
);
crossFrame
.
pluginInit
();
return
new
Promise
(
function
(
resolve
)
{
isLoaded
(
frame
,
function
()
{
var
scriptElement
=
frame
.
contentDocument
.
querySelector
(
'script[src]'
);
assert
(
!
scriptElement
,
'expected embed script to not be injected'
);
resolve
();
});
});
});
it
(
'detects dynamically added frames'
,
function
()
{
// Initialize with no initial frame, unlike before
crossFrame
.
pluginInit
();
// Add a frame to the DOM
var
frame
=
document
.
createElement
(
'iframe'
);
container
.
appendChild
(
frame
);
return
new
Promise
(
function
(
resolve
)
{
// Yield to let the DOM and CrossFrame catch up
setTimeout
(
function
()
{
isLoaded
(
frame
,
function
()
{
assert
(
frame
.
contentDocument
.
body
.
hasChildNodes
(),
'expected dynamically added frame to be modified'
);
resolve
();
});
},
0
);
});
});
it
(
'detects dynamically removed frames'
,
function
()
{
// Create a frame before initializing
var
frame
=
document
.
createElement
(
'iframe'
);
container
.
appendChild
(
frame
);
// Now initialize
crossFrame
.
pluginInit
();
return
new
Promise
(
function
(
resolve
)
{
// Yield to let the DOM and CrossFrame catch up
setTimeout
(
function
()
{
frame
.
remove
();
// Yield again
setTimeout
(
function
()
{
assert
.
calledWith
(
fakeBridge
.
call
,
'destroyFrame'
);
resolve
();
},
0
);
},
0
);
});
});
});
\ No newline at end of file
src/annotator/util/frame-util.js
0 → 100644
View file @
93632029
'use strict'
;
// Find all iframes within this iframe only
function
findFrames
(
container
)
{
var
frames
=
Array
.
from
(
container
.
getElementsByTagName
(
'iframe'
));
return
frames
.
filter
(
isValid
);
}
// Check if the iframe has already been injected
function
hasHypothesis
(
iframe
)
{
return
iframe
.
contentWindow
.
__hypothesis_frame
===
true
;
}
// Inject embed.js into the iframe
function
injectHypothesis
(
iframe
,
scriptUrl
)
{
var
config
=
document
.
createElement
(
'script'
);
config
.
className
=
'js-hypothesis-config'
;
config
.
type
=
'application/json'
;
config
.
innerText
=
'{"enableMultiFrameSupport": true, "subFrameInstance": true}'
;
var
src
=
scriptUrl
;
var
embed
=
document
.
createElement
(
'script'
);
embed
.
className
=
'js-hypothesis-embed'
;
embed
.
async
=
true
;
embed
.
src
=
src
;
iframe
.
contentDocument
.
body
.
appendChild
(
config
);
iframe
.
contentDocument
.
body
.
appendChild
(
embed
);
}
// Check if we can access this iframe's document
function
isAccessible
(
iframe
)
{
try
{
return
!!
iframe
.
contentDocument
;
}
catch
(
e
)
{
return
false
;
}
}
// Check if this is an iframe that we want to inject embed.js into
function
isValid
(
iframe
)
{
// Currently only checks if it's not the h-sidebar
return
iframe
.
className
!==
'h-sidebar-iframe'
;
}
function
isLoaded
(
iframe
,
callback
)
{
if
(
iframe
.
contentDocument
.
readyState
!==
'complete'
)
{
iframe
.
addEventListener
(
'load'
,
function
()
{
callback
();
});
}
else
{
callback
();
}
}
module
.
exports
=
{
findFrames
:
findFrames
,
hasHypothesis
:
hasHypothesis
,
injectHypothesis
:
injectHypothesis
,
isAccessible
:
isAccessible
,
isValid
:
isValid
,
isLoaded
:
isLoaded
,
};
src/shared/frame-rpc.js
View file @
93632029
...
...
@@ -52,6 +52,7 @@ function RPC (src, dst, origin, methods) {
this
.
_onmessage
=
function
(
ev
)
{
if
(
self
.
_destroyed
)
return
;
if
(
self
.
dst
!==
ev
.
source
)
return
;
if
(
self
.
origin
!==
'*'
&&
ev
.
origin
!==
self
.
origin
)
return
;
if
(
!
ev
.
data
||
typeof
ev
.
data
!==
'object'
)
return
;
if
(
ev
.
data
.
protocol
!==
'frame-rpc'
)
return
;
...
...
src/shared/polyfills.js
View file @
93632029
...
...
@@ -6,6 +6,7 @@ require('core-js/es6/set');
require
(
'core-js/fn/array/find'
);
require
(
'core-js/fn/array/find-index'
);
require
(
'core-js/fn/array/from'
);
require
(
'core-js/fn/array/includes'
);
require
(
'core-js/fn/object/assign'
);
require
(
'core-js/fn/string/ends-with'
);
require
(
'core-js/fn/string/starts-with'
);
...
...
src/shared/test/bridge-test.coffee
View file @
93632029
...
...
@@ -160,6 +160,7 @@ describe 'Bridge', ->
}
event
=
{
source
:
fakeWindow
origin
:
'http://example.com'
data
:
data
}
...
...
@@ -182,6 +183,7 @@ describe 'Bridge', ->
}
event
=
{
source
:
fakeWindow
origin
:
'http://example.com'
data
:
data
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment