using afIoc
using afReflux
using gfx
using fwt
** (View) - A HTML viewer for HTTP and file resources.
class HtmlViewer : View {
@Inject private IframeBlocker iframeBlocker
@Inject private AppStash stash
@Inject private GlobalCommands globalCommands
@Inject private Scope scope
@Inject private Reflux reflux
private Browser browser
private Label statusBar
private Obj? resolvedContent
@NoDoc
protected new make(|This| in) : super(in) {
content = EdgePane() {
it.center = browser = Browser() {
it.onHyperlink.add |e| { this->onHyperlink(e) }
it.onTitleText.add |e| { this->onTitleText(e) }
it.onStatusText.add |e| { this->onStatusText(e) }
it.onLoad.add |e| { this->onLoad(e) }
}
it.bottom = EdgePane() {
it.top = BorderPane {
it.border = Border("1,0,0 $Desktop.sysNormShadow")
}
it.center = statusBar = Label()
}
}
}
** Returns 'true'.
override Bool reuseView(Resource resource) { true }
** Scrolls the page to an ID. Handy for when setting the HTML via the text attribute.
**
** scrollToId("#myId")
Void scrollToId(Str id) {
browser.execute("var ele = document.getElementById(${id.toCode}); if (ele) window.scrollTo(ele.offsetLeft, ele.offsetTop);")
}
@NoDoc
override Void onActivate() {
enableCmds
// fudge to prevent blank tabs when panel switches to / from a single pane
if (resolvedContent != null)
Desktop.callLater(50ms) |->| {
if (resolvedContent is Str) {
browser.html = resolvedContent
browser.focus
}
if (resolvedContent is Uri) {
if (browser.url != resolvedContent) {
browser.url = resolvedContent
browser.focus
}
}
}
}
private Void enableCmds() {
if (resource is FileResource) {
globalCommands["afReflux.cmdSaveAs"].addInvoker("afExplorer.imageViewer", |Event? e| { this->onSaveAs() } )
globalCommands["afReflux.cmdSaveAs"].addEnabler("afExplorer.imageViewer", | ->Bool| { true } )
}
}
@NoDoc
override Void onDeactivate() {
globalCommands["afReflux.cmdSaveAs"].removeEnabler("afExplorer.htmlViewer")
globalCommands["afReflux.cmdSaveAs"].removeInvoker("afExplorer.htmlViewer")
try {
// we want to keep the scrollTop when switching between views,
// but clear it when closing the tab - so when re-opened, we're at the top again!
if (stash["${resource?.uri}.htmlViewer.clear"] == true)
stash.remove("${resource.uri}.htmlViewer.clear")
else {
scrollTop := browser.evaluate("return document.documentElement ? document.documentElement.scrollTop : 0;")
stash["${resource?.uri}.htmlViewer.scrollTop"] = scrollTop
}
} catch (Err err)
typeof.pod.log.warn("JS Err: ${err.msg}")
}
@NoDoc
override Bool confirmClose(Bool force) {
stash.remove("${resource.uri}.htmlViewer.scrollTop")
stash["${resource?.uri}.htmlViewer.clear"] = true
return true
}
@NoDoc
override Void load(Resource resource) {
super.load(resource)
enableCmds
resolvedContent = resolveResource(resource)
if (resolvedContent is Uri)
browser.url = resolvedContent
else if (resolvedContent is Str)
// this delay prevents a blank browser when going to or from a single tab pane
Desktop.callLater(50ms) |->| {
browser.html = resolvedContent
}
else
throw Err("Resource should resolve to either a URI or a Str, not: $resolvedContent")
// set focus so browser responds to scroll events
// see http://fantom.org/sidewalk/topic/2024#c13355
Desktop.callLater(50ms) |->| {
browser.focus
}
}
@NoDoc
override Void refresh(Resource? resource := null) {
if (resource != null && resource == this.resource) {
load(resource)
return
}
if (resource == null && this.resource != null) {
browser.refresh
return
}
}
** Hook for subclasses to convert the resource into either a URI or a Str.
** Returns 'resource.uri' by default.
virtual Obj resolveResource(Resource resource) {
resource.uri
}
** Callback for when the Browser's page loads.
virtual Void onLoad(Event event) {
scrollTop := stash["${resource?.uri}.htmlViewer.scrollTop"]
if (scrollTop != null)
browser.execute("window.scrollTo(0, ${scrollTop});")
}
** Callback for when the Browser's status text changes.
virtual Void onStatusText(Event event) {
try {
url := event.data.toStr.toUri
if (url.scheme == "about")
event.data = normaliseBrowserUrl(this.resource.uri, url).toStr
} catch {}
statusBar.text = event.data
}
** Callback for when the Browser's title text changes.
virtual Void onTitleText(Event event) {
// don't show useless titles!
if (event.data != "about:blank") {
// set resource name first so it gets picked up by the window
if (resource is HttpResource)
((HttpResource) resource).name = event.data
// this triggers a frame update
name = event.data
}
}
** Callback for when the 'afReflux.cmdSaveAs' 'GlobalCommand' is activated.
** Default implementation is to perform the *save as*.
@NoDoc
virtual Void onSaveAs() {
fileResource := (FileResource) resource
file := (File?) FileDialog {
it.mode = FileDialogMode.saveFile
it.dir = fileResource.file.parent
it.name = fileResource.file.name
it.filterExts = ["*.${fileResource.file.ext}", "*.*"]
}.open(reflux.window)
if (file != null) {
fileResource.file.copyTo(file)
fileRes := scope.build(FileResource#, [file])
reflux.loadResource(fileRes)
isDirty = false // mark as not dirty so confirmClose() doesn't give a dialog
reflux.closeView(this, true)
// refresh any views on the containing directory
dirRes := scope.build(FolderResource#, [file.parent])
reflux.refresh(dirRes)
}
}
** Callback for normalising Browser URIs into Reflux URIs.
virtual Uri normaliseBrowserUrl(Uri resourceUri, Uri url) {
// anchors on the same page are defined as `about:blank#anchor`
if (url.scheme == "about" && url.name == "blank" && url.frag != null)
url = (resourceUri.parent ?: resourceUri).plusName(resourceUri.name + "#" + url.frag)
// IE gives relative links the scheme 'about' so resolve it relative to the current resource
if (url.scheme == "about")
url = Url(resourceUri + url.pathOnly).plusQuery(url.queryStr).plusFrag(url.frag).toUri
return url
}
private Void onHyperlink(Event event) {
// don't hyperlink in place, instead we route the hyperlink through reflux to save
// the URI in the history and give consistent navigation
url := (Uri) event.data
echo(url)
// if a shitty url, cancel the event
if (iframeBlocker.block(url)) {
event.data = null
return
}
// doesn't work 'cos stoopid Fandoc emits `className#wot` when it should be `#wot`
// TODO: rewrite Fandoc generator
// url = normaliseBrowserUrl(url)
// if (Url(resource.uri).minusFrag == Url(url).minusFrag)
// return
// the other work around
if (url.scheme == "about" && url.name == "blank" && url.frag != null)
return
// actually, why would we want a blank page?
// fantom-lang.org seems to redirect to this (prob dodgy javascript!) so ignore it
if (url.scheme == "about" && url.name == "blank")
return
// normalise AFTER the above fudge
url = normaliseBrowserUrl(this.resource.uri, url)
// anything beyond this point will be routed through `Reflux.load()`
// so cancel the link event in the browser
event.data = null
// route the URI through reflux so it gets stored in the history
// CTRL opens link in new tab
ctx := null as LoadCtx
if (event.key != null && event.key.isCtrl)
ctx = LoadCtx() { newTab = true}
reflux.load(url.toStr, ctx)
}
}