From d7802dc10e520a587013de3aca77434707f88b8f Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Fri, 15 Mar 2019 01:11:23 +0100 Subject: [PATCH 01/41] embed htmlwidgets in iframes --- DESCRIPTION | 2 +- R/paged.R | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 75e451d9..343327bf 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -18,7 +18,7 @@ Description: Use the paged media properties in CSS and the JavaScript running headers, etc. Applications of this package include books, letters, reports, papers, business cards, resumes, and posters. Imports: rmarkdown (>= 1.11), bookdown (>= 0.8), htmltools, jsonlite, later, - processx, servr (>= 0.13), httpuv, xfun + processx, servr (>= 0.13), httpuv, xfun, knitr, htmlwidgets Suggests: testit, xaringan License: MIT + file LICENSE URL: https://github.com/rstudio/pagedown diff --git a/R/paged.R b/R/paged.R index 92e3fd5d..b26e4255 100644 --- a/R/paged.R +++ b/R/paged.R @@ -112,7 +112,61 @@ html_format = function( pagedown_dependency(xfun::with_ext(css2, '.css'), .pagedjs) )) } - html_document2( + format = html_document2( ..., css = css, template = template, pandoc_args = c(.pandoc_args, pandoc_args) ) + if (isTRUE(.pagedjs)) format$knitr$opts_chunk[['render']] = paged_render + widget_file(reset = TRUE) + format } + +paged_render = function(x, options, ...) { + if (inherits(x, 'htmlwidget')) { + class(x) = c('iframedwidget', class(x)) + } + knitr::knit_print(x, options) +} + +knit_print.iframedwidget = function(x, options, ...) { + class(x) = tail(class(x), -1) + d = options$fig.path + if (!dir.exists(d)) dir.create(d, recursive = TRUE) + selfcontained = knitr::opts_knit$get('self.contained') + selfcontained = FALSE + f = save_widget(d, x, selfcontained, options) + src = NULL + srcdoc = NULL + if (isTRUE(selfcontained)) { + srcdoc = paste0(collapse = '\n', readLines(f)) + file.remove(f) + } else { + src = f + } + knitr::knit_print(htmltools::tags$iframe( + src = src, srcdoc = srcdoc, + #width = options$out.width.px, height = options$out.height.px, + is = "iframed-widget" + )) +} + +save_widget = function(directory, widget, selfcontained, options) { + old_wd = setwd(directory) + on.exit({ + setwd(old_wd) + }) + f = widget_file() + htmlwidgets::saveWidget( + widget = widget, file = f, selfcontained = selfcontained, + knitrOptions = options + ) + return(paste0(directory, f)) +} + +widget_file = (function() { + n = 0L + function(reset = FALSE) { + if (reset) n <<- -1L + n <<- n + 1L + sprintf('widget%i.html', n) + } +})() From 69f806ee0fd235eb0abc86f32fbba3eb046ecaee Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Fri, 15 Mar 2019 03:30:28 +0100 Subject: [PATCH 02/41] resize widgets --- R/paged.R | 2 +- inst/resources/js/config.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/R/paged.R b/R/paged.R index b26e4255..d7f4e61e 100644 --- a/R/paged.R +++ b/R/paged.R @@ -144,7 +144,7 @@ knit_print.iframedwidget = function(x, options, ...) { } knitr::knit_print(htmltools::tags$iframe( src = src, srcdoc = srcdoc, - #width = options$out.width.px, height = options$out.height.px, + width = options$out.width.px, height = options$out.height.px, is = "iframed-widget" )) } diff --git a/inst/resources/js/config.js b/inst/resources/js/config.js index 0393a8b4..124e7999 100644 --- a/inst/resources/js/config.js +++ b/inst/resources/js/config.js @@ -103,6 +103,12 @@ appendShortTitles2() ]); await runMathJax(); + let iframedWidgets = document.querySelectorAll('[is="iframed-widget"]'); + for (const widget of iframedWidgets) { + if (widget.ready) { + await widget.ready; + } + } }, after: () => { // pagedownListener is a binder added by the chrome_print function @@ -118,3 +124,32 @@ } }; })(); + +if (customElements) { + customElements.define( + 'iframed-widget', + class extends HTMLIFrameElement { + constructor() { + super(); + let pr = new Promise(resolve => { + this.addEventListener('load', () => {resolve();}); + }); + this.ready = pr.then(() => {this.resize();}); + //this.ready().then(() => {this.resize();}); + } + + resize() { + let docEl = this.contentWindow.document.documentElement; + let contentHeight = docEl.scrollHeight; + let contentWidth = docEl.scrollWidth; + + let scaleFactor = this.getBoundingClientRect().width / (contentWidth+2); + this.style.transformOrigin = "top left"; + this.style.transform = "scale(" + scaleFactor + ")"; + this.width = contentWidth; + this.height = contentHeight + 2; + } + }, + {extends: 'iframe'} + ); +} From 67637b11439e3da8c94ca52454775f0f560aaad1 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Sat, 16 Mar 2019 09:21:41 +0100 Subject: [PATCH 03/41] use responsive iframes --- R/paged.R | 9 +++-- inst/resources/js/config.js | 72 +++++++++++++++++++++++-------------- 2 files changed, 52 insertions(+), 29 deletions(-) diff --git a/R/paged.R b/R/paged.R index d7f4e61e..13268411 100644 --- a/R/paged.R +++ b/R/paged.R @@ -142,10 +142,9 @@ knit_print.iframedwidget = function(x, options, ...) { } else { src = f } - knitr::knit_print(htmltools::tags$iframe( + knitr::knit_print(iframe_widget( src = src, srcdoc = srcdoc, - width = options$out.width.px, height = options$out.height.px, - is = "iframed-widget" + width = options$out.width.px, height = options$out.height.px )) } @@ -170,3 +169,7 @@ widget_file = (function() { sprintf('widget%i.html', n) } })() + +iframe_widget = function(...) { + htmltools::tag('iframe-htmlwidget', list(...)) +} diff --git a/inst/resources/js/config.js b/inst/resources/js/config.js index 124e7999..49949222 100644 --- a/inst/resources/js/config.js +++ b/inst/resources/js/config.js @@ -103,12 +103,9 @@ appendShortTitles2() ]); await runMathJax(); - let iframedWidgets = document.querySelectorAll('[is="iframed-widget"]'); - for (const widget of iframedWidgets) { - if (widget.ready) { - await widget.ready; - } - } + let iframedWidgets = document.getElementsByTagName('iframe-htmlwidget'); + let widgetsReady = Promise.all([...iframedWidgets].map(el => {return el['ready'];})); + await widgetsReady; }, after: () => { // pagedownListener is a binder added by the chrome_print function @@ -126,30 +123,53 @@ })(); if (customElements) { - customElements.define( - 'iframed-widget', - class extends HTMLIFrameElement { + customElements.define('iframe-htmlwidget', + class extends HTMLElement { constructor() { super(); - let pr = new Promise(resolve => { - this.addEventListener('load', () => {resolve();}); + let h = this.getAttribute('height'); + let w = this.getAttribute('width'); + let shadowRoot = this.attachShadow({mode: 'open'}); + shadowRoot.innerHTML = ` + +
+ `; + let iframe = shadowRoot.querySelector('iframe'); + iframe.frameBorder = 0; + iframe.width = w; + iframe.height = h; + let loaded = new Promise(resolve => { + iframe.addEventListener('load', () => {resolve();}) }); - this.ready = pr.then(() => {this.resize();}); - //this.ready().then(() => {this.resize();}); - } - - resize() { - let docEl = this.contentWindow.document.documentElement; - let contentHeight = docEl.scrollHeight; - let contentWidth = docEl.scrollWidth; + if (this.hasAttribute('src')) { + iframe.src = this.getAttribute('src'); + } + if (this.hasAttribute('srcdoc')) { + iframe.srcdoc = this.getAttribute('srcdoc'); + } + this.ready = loaded.then(() => { + let docEl = iframe.contentWindow.document.documentElement; + let contentHeight = docEl.scrollHeight; + let contentWidth = docEl.scrollWidth; - let scaleFactor = this.getBoundingClientRect().width / (contentWidth+2); - this.style.transformOrigin = "top left"; - this.style.transform = "scale(" + scaleFactor + ")"; - this.width = contentWidth; - this.height = contentHeight + 2; + let widthScaleFactor = this.getBoundingClientRect().width / contentWidth; + let heightScaleFactor = this.getBoundingClientRect().height / contentHeight; + let scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); + iframe.style.transformOrigin = "top left"; + iframe.style.transform = "scale(" + scaleFactor + ")"; + iframe.width = contentWidth; + iframe.height = contentHeight; + let container = iframe.parentElement; + container.style.width = iframe.getBoundingClientRect().width + 'px'; + container.style.height = iframe.getBoundingClientRect().height + 'px'; + }); } - }, - {extends: 'iframe'} + } ); } From 9669d002ad62e085db93cd99fe4a9d239b43b7b0 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Sun, 17 Mar 2019 03:17:46 +0100 Subject: [PATCH 04/41] resize the iframe in the connectedCallback() instead of in the constructor function --- R/paged.R | 22 +++++++++---- inst/resources/js/config.js | 64 ++++++++++++++++++++----------------- 2 files changed, 49 insertions(+), 37 deletions(-) diff --git a/R/paged.R b/R/paged.R index 13268411..59fde8ea 100644 --- a/R/paged.R +++ b/R/paged.R @@ -122,12 +122,12 @@ html_format = function( paged_render = function(x, options, ...) { if (inherits(x, 'htmlwidget')) { - class(x) = c('iframedwidget', class(x)) + class(x) = c('iframehtmlwidget', class(x)) } - knitr::knit_print(x, options) + knitr::knit_print(x, options, ...) } -knit_print.iframedwidget = function(x, options, ...) { +knit_print.iframehtmlwidget = function(x, options, ...) { class(x) = tail(class(x), -1) d = options$fig.path if (!dir.exists(d)) dir.create(d, recursive = TRUE) @@ -142,9 +142,11 @@ knit_print.iframedwidget = function(x, options, ...) { } else { src = f } - knitr::knit_print(iframe_widget( + dims = c(options$out.width.px, options$out.height.px) + dims = ifelse(contains_numeric(dims), paste0(dims, 'px'), dims) + knitr::knit_print(responsive_iframe( src = src, srcdoc = srcdoc, - width = options$out.width.px, height = options$out.height.px + width = dims[1], height = dims[2] )) } @@ -170,6 +172,12 @@ widget_file = (function() { } })() -iframe_widget = function(...) { - htmltools::tag('iframe-htmlwidget', list(...)) +responsive_iframe = function(...) { + htmltools::div( + htmltools::tag('responsive-iframe', list(...)), + style = "overflow:hidden;") +} + +contains_numeric = function(x) { + !is.na(suppressWarnings(as.numeric(x))) } diff --git a/inst/resources/js/config.js b/inst/resources/js/config.js index 49949222..05efe409 100644 --- a/inst/resources/js/config.js +++ b/inst/resources/js/config.js @@ -103,8 +103,8 @@ appendShortTitles2() ]); await runMathJax(); - let iframedWidgets = document.getElementsByTagName('iframe-htmlwidget'); - let widgetsReady = Promise.all([...iframedWidgets].map(el => {return el['ready'];})); + let iframeHTMLWidgets = document.getElementsByTagName('responsive-iframe'); + let widgetsReady = Promise.all([...iframeHTMLWidgets].map(el => {return el['ready'];})); await widgetsReady; }, after: () => { @@ -122,53 +122,57 @@ }; })(); +// Define a custom element if (customElements) { - customElements.define('iframe-htmlwidget', + customElements.define('responsive-iframe', class extends HTMLElement { constructor() { - super(); - let h = this.getAttribute('height'); - let w = this.getAttribute('width'); - let shadowRoot = this.attachShadow({mode: 'open'}); + super(); // compulsory + let shadowRoot = this.attachShadow({mode: 'open'}); // we must use shadow DOM in the constructor + // Populate the shadow DOM: shadowRoot.innerHTML = ` -
+
+ +
`; - let iframe = shadowRoot.querySelector('iframe'); - iframe.frameBorder = 0; - iframe.width = w; - iframe.height = h; - let loaded = new Promise(resolve => { - iframe.addEventListener('load', () => {resolve();}) - }); - if (this.hasAttribute('src')) { - iframe.src = this.getAttribute('src'); - } - if (this.hasAttribute('srcdoc')) { - iframe.srcdoc = this.getAttribute('srcdoc'); - } - this.ready = loaded.then(() => { + this.ready = new Promise(resolve => {this.finished = resolve;}) + } + connectedCallback() { + let iframe = this.shadowRoot.querySelector('iframe'); + let container = this.shadowRoot.querySelector('div'); + container.style.width = this.getAttribute('width'); + container.style.height = this.getAttribute('height'); + + iframe.addEventListener('load', () => { let docEl = iframe.contentWindow.document.documentElement; let contentHeight = docEl.scrollHeight; let contentWidth = docEl.scrollWidth; - let widthScaleFactor = this.getBoundingClientRect().width / contentWidth; - let heightScaleFactor = this.getBoundingClientRect().height / contentHeight; + let widthScaleFactor = container.getBoundingClientRect().width / contentWidth; + let heightScaleFactor = container.getBoundingClientRect().height / contentHeight; let scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); iframe.style.transformOrigin = "top left"; iframe.style.transform = "scale(" + scaleFactor + ")"; iframe.width = contentWidth; iframe.height = contentHeight; - let container = iframe.parentElement; + container.style.width = iframe.getBoundingClientRect().width + 'px'; + this.parentElement.style.width = iframe.getBoundingClientRect().width + 'px'; container.style.height = iframe.getBoundingClientRect().height + 'px'; + this.parentElement.style.height = iframe.getBoundingClientRect().height + 'px'; + this.finished(); }); + + if (this.hasAttribute('srcdoc')) { + iframe.srcdoc = this.getAttribute('srcdoc'); + } + if (this.hasAttribute('src')) { + console.log('attribut src trouvé'); + iframe.src = this.getAttribute('src'); + } } } ); From 0af5aaec5f9d714f0a250c34d7031ef0a7954a24 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Sun, 17 Mar 2019 04:15:01 +0100 Subject: [PATCH 05/41] store computed size --- R/paged.R | 2 +- inst/resources/js/config.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/R/paged.R b/R/paged.R index 59fde8ea..fddad340 100644 --- a/R/paged.R +++ b/R/paged.R @@ -175,7 +175,7 @@ widget_file = (function() { responsive_iframe = function(...) { htmltools::div( htmltools::tag('responsive-iframe', list(...)), - style = "overflow:hidden;") + style = "overflow:hidden;break-inside:avoid;") } contains_numeric = function(x) { diff --git a/inst/resources/js/config.js b/inst/resources/js/config.js index 05efe409..ac582a81 100644 --- a/inst/resources/js/config.js +++ b/inst/resources/js/config.js @@ -161,8 +161,10 @@ if (customElements) { container.style.width = iframe.getBoundingClientRect().width + 'px'; this.parentElement.style.width = iframe.getBoundingClientRect().width + 'px'; + this.setAttribute('width', container.style.width); container.style.height = iframe.getBoundingClientRect().height + 'px'; this.parentElement.style.height = iframe.getBoundingClientRect().height + 'px'; + this.setAttribute('height', container.style.height); this.finished(); }); @@ -170,7 +172,6 @@ if (customElements) { iframe.srcdoc = this.getAttribute('srcdoc'); } if (this.hasAttribute('src')) { - console.log('attribut src trouvé'); iframe.src = this.getAttribute('src'); } } From 09fd137f6911390a0f41c4bfb79884efec595ebf Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Sun, 17 Mar 2019 16:03:57 +0100 Subject: [PATCH 06/41] always use self contained widgets: otherwise we should implement networkidle methods for chrome_print --- R/paged.R | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/R/paged.R b/R/paged.R index fddad340..4758eae6 100644 --- a/R/paged.R +++ b/R/paged.R @@ -133,7 +133,7 @@ knit_print.iframehtmlwidget = function(x, options, ...) { if (!dir.exists(d)) dir.create(d, recursive = TRUE) selfcontained = knitr::opts_knit$get('self.contained') selfcontained = FALSE - f = save_widget(d, x, selfcontained, options) + f = save_widget(d, x, options) src = NULL srcdoc = NULL if (isTRUE(selfcontained)) { @@ -150,14 +150,17 @@ knit_print.iframehtmlwidget = function(x, options, ...) { )) } -save_widget = function(directory, widget, selfcontained, options) { +save_widget = function(directory, widget, options) { old_wd = setwd(directory) on.exit({ setwd(old_wd) }) f = widget_file() htmlwidgets::saveWidget( - widget = widget, file = f, selfcontained = selfcontained, + widget = widget, file = f, + # since chrome_print() does not handle network requests, use a self contained html file + # In order to use selcontained = FALSE, we should implement a networkidle option first in chrome_print() + selfcontained = TRUE, knitrOptions = options ) return(paste0(directory, f)) From ab726bf70f5752322062f899624a0804d39e7643 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Sun, 17 Mar 2019 17:30:19 +0100 Subject: [PATCH 07/41] create the footprint div in the class declaration --- R/paged.R | 6 ++---- inst/resources/js/config.js | 23 ++++++++++++++++++++++- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/R/paged.R b/R/paged.R index 4758eae6..8f93b122 100644 --- a/R/paged.R +++ b/R/paged.R @@ -159,7 +159,7 @@ save_widget = function(directory, widget, options) { htmlwidgets::saveWidget( widget = widget, file = f, # since chrome_print() does not handle network requests, use a self contained html file - # In order to use selcontained = FALSE, we should implement a networkidle option first in chrome_print() + # In order to use selcontained = FALSE, we should implement first a networkidle option in chrome_print() selfcontained = TRUE, knitrOptions = options ) @@ -176,9 +176,7 @@ widget_file = (function() { })() responsive_iframe = function(...) { - htmltools::div( - htmltools::tag('responsive-iframe', list(...)), - style = "overflow:hidden;break-inside:avoid;") + htmltools::tag('responsive-iframe', list(...)) } contains_numeric = function(x) { diff --git a/inst/resources/js/config.js b/inst/resources/js/config.js index ac582a81..d514228c 100644 --- a/inst/resources/js/config.js +++ b/inst/resources/js/config.js @@ -135,18 +135,39 @@ if (customElements) { div {overflow: hidden;}
- +
`; this.ready = new Promise(resolve => {this.finished = resolve;}) } connectedCallback() { + // First, we embed the element in a div footprint + // This footprint will take room before Paged.js begins parsing the document + // Since the constructor is called a second time after Paged.js builds the document, + // we also must test if the footprint div already exists + if (!this.parentElement.classList.contains('responsive-iframe-footprint')) { + let footprint = document.createElement('div'); + footprint.style.overflow = 'hidden'; + footprint.style.breakInside = 'avoid'; + footprint.className = 'responsive-iframe-footprint'; + this.insertAdjacentElement('beforebegin', footprint); + footprint.appendChild(this); + } + let iframe = this.shadowRoot.querySelector('iframe'); let container = this.shadowRoot.querySelector('div'); container.style.width = this.getAttribute('width'); container.style.height = this.getAttribute('height'); iframe.addEventListener('load', () => { + // The load event fires twice: + // 1st time when the iframe is attached (therefore the iframe document does not exist) + // 2nd time when the document is loaded + if (!iframe.contentWindow) { + // This is the 1st time that the load event fires, the document does not exist + // Quit early: + return; + } let docEl = iframe.contentWindow.document.documentElement; let contentHeight = docEl.scrollHeight; let contentWidth = docEl.scrollWidth; From ca05b1077a409e0bc07bdac919b2a23244085b28 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Sun, 17 Mar 2019 17:31:09 +0100 Subject: [PATCH 08/41] wait until HTMLWidgets are built --- inst/resources/js/chrome_print.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/inst/resources/js/chrome_print.js b/inst/resources/js/chrome_print.js index e3329afa..92d5e2fc 100644 --- a/inst/resources/js/chrome_print.js +++ b/inst/resources/js/chrome_print.js @@ -40,5 +40,12 @@ ); }); - window.pagedownReady = Promise.all([MathJaxReady, HTMLWidgetsReady, document.fonts.ready]); + let responsiveIFramesReady = new Promise(resolve => { + window.addEventListener('load', () => { + let responsiveIFrames = document.getElementsByTagName('responsive-iframe'); + Promise.all([...responsiveIFrames].map(el => {return el['ready'];})).then(resolve()); + }); + }); + + window.pagedownReady = Promise.all([MathJaxReady, HTMLWidgetsReady, document.fonts.ready, responsiveIFramesReady]); })(); From ede1e3c5bad7f12d0204f4103841525e63f76796 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Sun, 17 Mar 2019 19:38:16 +0100 Subject: [PATCH 09/41] use the footprint div to size the iframe --- inst/resources/js/config.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/inst/resources/js/config.js b/inst/resources/js/config.js index d514228c..045493b2 100644 --- a/inst/resources/js/config.js +++ b/inst/resources/js/config.js @@ -145,14 +145,19 @@ if (customElements) { // This footprint will take room before Paged.js begins parsing the document // Since the constructor is called a second time after Paged.js builds the document, // we also must test if the footprint div already exists - if (!this.parentElement.classList.contains('responsive-iframe-footprint')) { - let footprint = document.createElement('div'); + let footprint; + if (this.parentElement.classList.contains('responsive-iframe-footprint')) { + footprint = this.parentElement; + } else { + footprint = document.createElement('div'); footprint.style.overflow = 'hidden'; footprint.style.breakInside = 'avoid'; footprint.className = 'responsive-iframe-footprint'; this.insertAdjacentElement('beforebegin', footprint); footprint.appendChild(this); } + footprint.style.width = this.getAttribute('width'); + footprint.style.height = this.getAttribute('height'); let iframe = this.shadowRoot.querySelector('iframe'); let container = this.shadowRoot.querySelector('div'); @@ -172,8 +177,8 @@ if (customElements) { let contentHeight = docEl.scrollHeight; let contentWidth = docEl.scrollWidth; - let widthScaleFactor = container.getBoundingClientRect().width / contentWidth; - let heightScaleFactor = container.getBoundingClientRect().height / contentHeight; + let widthScaleFactor = footprint.getBoundingClientRect().width / contentWidth; + let heightScaleFactor = footprint.getBoundingClientRect().height / contentHeight; let scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); iframe.style.transformOrigin = "top left"; iframe.style.transform = "scale(" + scaleFactor + ")"; @@ -181,10 +186,10 @@ if (customElements) { iframe.height = contentHeight; container.style.width = iframe.getBoundingClientRect().width + 'px'; - this.parentElement.style.width = iframe.getBoundingClientRect().width + 'px'; + footprint.style.width = iframe.getBoundingClientRect().width + 'px'; this.setAttribute('width', container.style.width); container.style.height = iframe.getBoundingClientRect().height + 'px'; - this.parentElement.style.height = iframe.getBoundingClientRect().height + 'px'; + footprint.style.height = iframe.getBoundingClientRect().height + 'px'; this.setAttribute('height', container.style.height); this.finished(); }); From 31d324851bb328412fc8ff2f61fe4f573190543b Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Mon, 18 Mar 2019 02:41:09 +0100 Subject: [PATCH 10/41] use self_contained and out.extra --- R/paged.R | 55 ++++++++++++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/R/paged.R b/R/paged.R index 8f93b122..5d669c7b 100644 --- a/R/paged.R +++ b/R/paged.R @@ -100,7 +100,7 @@ pagedown_dependency = function(css = NULL, js = FALSE) { } html_format = function( - ..., css, template, pandoc_args = NULL, .dependencies = NULL, + ..., css, template, self_contained = TRUE, pandoc_args = NULL, .dependencies = NULL, .pagedjs = FALSE, .pandoc_args = NULL ) { css2 = grep('[.]css$', css, value = TRUE, invert = TRUE) @@ -113,40 +113,44 @@ html_format = function( )) } format = html_document2( - ..., css = css, template = template, pandoc_args = c(.pandoc_args, pandoc_args) + ..., css = css, template = template, + self_contained = self_contained, pandoc_args = c(.pandoc_args, pandoc_args) ) - if (isTRUE(.pagedjs)) format$knitr$opts_chunk[['render']] = paged_render + if (isTRUE(.pagedjs)) format$knitr$opts_chunk[['render']] = paged_render(self_contained) widget_file(reset = TRUE) format } -paged_render = function(x, options, ...) { - if (inherits(x, 'htmlwidget')) { - class(x) = c('iframehtmlwidget', class(x)) +paged_render = function(self_contained) { + function(x, options, ...) { + if (inherits(x, 'htmlwidget')) { + class(x) = c('iframehtmlwidget', class(x)) + } + knitr::knit_print(x, options, ..., self_contained = self_contained) } - knitr::knit_print(x, options, ...) } -knit_print.iframehtmlwidget = function(x, options, ...) { +knit_print.iframehtmlwidget = function(x, options, ..., self_contained) { class(x) = tail(class(x), -1) d = options$fig.path - if (!dir.exists(d)) dir.create(d, recursive = TRUE) - selfcontained = knitr::opts_knit$get('self.contained') - selfcontained = FALSE + if (!dir.exists(d)) { + dir.create(d, recursive = TRUE) + if (self_contained) on.exit({ + unlink(d, recursive = TRUE) # doesn't work, don't understand why + }, add = TRUE) + } f = save_widget(d, x, options) src = NULL srcdoc = NULL - if (isTRUE(selfcontained)) { + if (self_contained) { srcdoc = paste0(collapse = '\n', readLines(f)) file.remove(f) } else { src = f } - dims = c(options$out.width.px, options$out.height.px) - dims = ifelse(contains_numeric(dims), paste0(dims, 'px'), dims) knitr::knit_print(responsive_iframe( - src = src, srcdoc = srcdoc, - width = dims[1], height = dims[2] + src = src, srcdoc = srcdoc, width = options$out.width.px, + height = options$out.height.px, extra.attr = options$out.extra )) } @@ -154,7 +158,7 @@ save_widget = function(directory, widget, options) { old_wd = setwd(directory) on.exit({ setwd(old_wd) - }) + }, add = TRUE) f = widget_file() htmlwidgets::saveWidget( widget = widget, file = f, @@ -175,10 +179,15 @@ widget_file = (function() { } })() -responsive_iframe = function(...) { - htmltools::tag('responsive-iframe', list(...)) -} - -contains_numeric = function(x) { - !is.na(suppressWarnings(as.numeric(x))) +responsive_iframe = function(width = NULL, height = NULL, ..., extra.attr = '') { + width = htmltools::validateCssUnit(width) + height = htmltools::validateCssUnit(height) + tag = htmltools::tag('responsive-iframe', c(list(width = width, height = height), list(...))) + if (length(extra.attr) == 0) extra.attr = '' + extra.attr = strsplit(extra.attr, ' ')[[1]] + extra.attr = strsplit(extra.attr, '=') + names(extra.attr) = lapply(extra.attr, `[`, 1) + extra.attr = lapply(extra.attr, `[`, 2) + extra.attr = lapply(extra.attr, function(x) eval(parse(text = x))) + do.call(htmltools::tagAppendAttributes, c(list(tag = tag), extra.attr)) } From 6702aca6aeb3e9a455ca71a2a7a199b2b8980a4d Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Mon, 18 Mar 2019 02:49:29 +0100 Subject: [PATCH 11/41] don't ajust width --- inst/resources/js/config.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/inst/resources/js/config.js b/inst/resources/js/config.js index 045493b2..508e377d 100644 --- a/inst/resources/js/config.js +++ b/inst/resources/js/config.js @@ -186,18 +186,16 @@ if (customElements) { iframe.height = contentHeight; container.style.width = iframe.getBoundingClientRect().width + 'px'; - footprint.style.width = iframe.getBoundingClientRect().width + 'px'; - this.setAttribute('width', container.style.width); container.style.height = iframe.getBoundingClientRect().height + 'px'; footprint.style.height = iframe.getBoundingClientRect().height + 'px'; - this.setAttribute('height', container.style.height); + this.setAttribute('height', footprint.style.height); this.finished(); }); - if (this.hasAttribute('srcdoc')) { + if (this.hasAttribute('srcdoc') && (iframe.srcdoc.length === 0)) { iframe.srcdoc = this.getAttribute('srcdoc'); } - if (this.hasAttribute('src')) { + if (this.hasAttribute('src') && (iframe.src.length === 0)) { iframe.src = this.getAttribute('src'); } } From a05e359ef3b261e4841cb490cd7d18092bcc4f9c Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Tue, 19 Mar 2019 02:14:20 +0100 Subject: [PATCH 12/41] adjust sizing --- DESCRIPTION | 2 +- R/paged.R | 37 ++++++++--- inst/resources/js/config.js | 81 ----------------------- inst/resources/js/responsiveiframe.js | 93 +++++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 92 deletions(-) create mode 100644 inst/resources/js/responsiveiframe.js diff --git a/DESCRIPTION b/DESCRIPTION index 343327bf..71a00825 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -18,7 +18,7 @@ Description: Use the paged media properties in CSS and the JavaScript running headers, etc. Applications of this package include books, letters, reports, papers, business cards, resumes, and posters. Imports: rmarkdown (>= 1.11), bookdown (>= 0.8), htmltools, jsonlite, later, - processx, servr (>= 0.13), httpuv, xfun, knitr, htmlwidgets + processx, servr (>= 0.13), httpuv, xfun, knitr, htmlwidgets, xml2 Suggests: testit, xaringan License: MIT + file LICENSE URL: https://github.com/rstudio/pagedown diff --git a/R/paged.R b/R/paged.R index 5d669c7b..e165d66b 100644 --- a/R/paged.R +++ b/R/paged.R @@ -163,7 +163,7 @@ save_widget = function(directory, widget, options) { htmlwidgets::saveWidget( widget = widget, file = f, # since chrome_print() does not handle network requests, use a self contained html file - # In order to use selcontained = FALSE, we should implement first a networkidle option in chrome_print() + # In order to use selcontained = FALSE, we should implement a networkidle option in chrome_print() selfcontained = TRUE, knitrOptions = options ) @@ -180,14 +180,31 @@ widget_file = (function() { })() responsive_iframe = function(width = NULL, height = NULL, ..., extra.attr = '') { - width = htmltools::validateCssUnit(width) - height = htmltools::validateCssUnit(height) - tag = htmltools::tag('responsive-iframe', c(list(width = width, height = height), list(...))) if (length(extra.attr) == 0) extra.attr = '' - extra.attr = strsplit(extra.attr, ' ')[[1]] - extra.attr = strsplit(extra.attr, '=') - names(extra.attr) = lapply(extra.attr, `[`, 1) - extra.attr = lapply(extra.attr, `[`, 2) - extra.attr = lapply(extra.attr, function(x) eval(parse(text = x))) - do.call(htmltools::tagAppendAttributes, c(list(tag = tag), extra.attr)) + extra.attr = as_html_attrs(extra.attr) + tag = htmltools::tag('responsive-iframe', c(extra.attr, list(...))) + width = css_declaration('width', htmltools::validateCssUnit(width)) + height = css_declaration('height', htmltools::validateCssUnit(height)) + tag = do.call( + htmltools::tagAppendAttributes, + c(list(tag = tag), list(style = width, style = height)) + ) + htmltools::attachDependencies( + tag, + htmltools::htmlDependency( + 'responsiveiframe', packageVersion('pagedown'), src = pkg_resource(), + script = 'js/responsiveiframe.js', all_files = FALSE + ) + ) +} + +as_html_attrs = function(string) { + doc = xml2::read_html(sprintf('

', string)) + node = xml2::xml_find_first(doc, './/p') + xml2::xml_attrs(node) +} + +css_declaration = function(property, value) { + if (is.null(value)) return('') + paste0(property, ':', value, ';') } diff --git a/inst/resources/js/config.js b/inst/resources/js/config.js index 508e377d..1ebf2eb8 100644 --- a/inst/resources/js/config.js +++ b/inst/resources/js/config.js @@ -121,84 +121,3 @@ } }; })(); - -// Define a custom element -if (customElements) { - customElements.define('responsive-iframe', - class extends HTMLElement { - constructor() { - super(); // compulsory - let shadowRoot = this.attachShadow({mode: 'open'}); // we must use shadow DOM in the constructor - // Populate the shadow DOM: - shadowRoot.innerHTML = ` - -

- -
- `; - this.ready = new Promise(resolve => {this.finished = resolve;}) - } - connectedCallback() { - // First, we embed the element in a div footprint - // This footprint will take room before Paged.js begins parsing the document - // Since the constructor is called a second time after Paged.js builds the document, - // we also must test if the footprint div already exists - let footprint; - if (this.parentElement.classList.contains('responsive-iframe-footprint')) { - footprint = this.parentElement; - } else { - footprint = document.createElement('div'); - footprint.style.overflow = 'hidden'; - footprint.style.breakInside = 'avoid'; - footprint.className = 'responsive-iframe-footprint'; - this.insertAdjacentElement('beforebegin', footprint); - footprint.appendChild(this); - } - footprint.style.width = this.getAttribute('width'); - footprint.style.height = this.getAttribute('height'); - - let iframe = this.shadowRoot.querySelector('iframe'); - let container = this.shadowRoot.querySelector('div'); - container.style.width = this.getAttribute('width'); - container.style.height = this.getAttribute('height'); - - iframe.addEventListener('load', () => { - // The load event fires twice: - // 1st time when the iframe is attached (therefore the iframe document does not exist) - // 2nd time when the document is loaded - if (!iframe.contentWindow) { - // This is the 1st time that the load event fires, the document does not exist - // Quit early: - return; - } - let docEl = iframe.contentWindow.document.documentElement; - let contentHeight = docEl.scrollHeight; - let contentWidth = docEl.scrollWidth; - - let widthScaleFactor = footprint.getBoundingClientRect().width / contentWidth; - let heightScaleFactor = footprint.getBoundingClientRect().height / contentHeight; - let scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); - iframe.style.transformOrigin = "top left"; - iframe.style.transform = "scale(" + scaleFactor + ")"; - iframe.width = contentWidth; - iframe.height = contentHeight; - - container.style.width = iframe.getBoundingClientRect().width + 'px'; - container.style.height = iframe.getBoundingClientRect().height + 'px'; - footprint.style.height = iframe.getBoundingClientRect().height + 'px'; - this.setAttribute('height', footprint.style.height); - this.finished(); - }); - - if (this.hasAttribute('srcdoc') && (iframe.srcdoc.length === 0)) { - iframe.srcdoc = this.getAttribute('srcdoc'); - } - if (this.hasAttribute('src') && (iframe.src.length === 0)) { - iframe.src = this.getAttribute('src'); - } - } - } - ); -} diff --git a/inst/resources/js/responsiveiframe.js b/inst/resources/js/responsiveiframe.js new file mode 100644 index 00000000..4cea3f9f --- /dev/null +++ b/inst/resources/js/responsiveiframe.js @@ -0,0 +1,93 @@ +// A responsive iframe +// Works only with a same-origin html file +if (customElements) { + customElements.define('responsive-iframe', + class extends HTMLElement { + constructor() { + super(); // compulsory + let shadowRoot = this.attachShadow({mode: 'open'}); // we must use shadow DOM in the constructor + // Populate the shadow DOM: + shadowRoot.innerHTML = ` + +
+ +
+ `; + this.ready = new Promise(resolve => {this.finished = resolve;}); + } + connectedCallback() { + // Be aware that the connectedCallback() function can be called multiple times, + // see https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks + if (!this.hasAttribute('initial-width')) { + this.setAttribute('initial-width', this.style.width); + } + if (!this.hasAttribute('initial-height')) { + this.setAttribute('initial-height', this.style.height); + } + + // First, we embed the element in a footprint div. + // This footprint will take room before Paged.js begins parsing the document. + // Since the constructor is also called after Paged.js builds the document, + // we also must test if the footprint div already exists. + let footprint; + if (!this.parentElement.classList.contains('responsive-iframe-footprint')) { + // The footprint div does not exist yet, create it. + footprint = document.createElement('div'); + footprint.style = this.style.cssText; + this.removeAttribute('style'); + footprint.style.boxSizing="content-box"; + footprint.style.breakInside = 'avoid'; + footprint.className = 'responsive-iframe-footprint'; + this.insertAdjacentElement('beforebegin', footprint); + footprint.appendChild(this); + } else { + // The footprint div already exists. + footprint = this.parentElement; + } + + let iframe = this.shadowRoot.querySelector('iframe'); + let container = this.shadowRoot.querySelector('div'); + container.style.width = footprint.style.width; + container.style.height = footprint.style.height; + + iframe.addEventListener('load', () => { + // The load event fires twice: + // 1st time when the iframe is attached (therefore the iframe document does not exist) + // 2nd time when the document is loaded + if (!iframe.contentWindow) { + // This is the 1st time that the load event fires, the document does not exist + // Quit early: + return; + } + let docEl = iframe.contentWindow.document.documentElement; // this works only with same-origin content + let contentHeight = docEl.scrollHeight; + let contentWidth = docEl.scrollWidth; + + let widthScaleFactor = parseFloat(footprint.style.width) / contentWidth; + let heightScaleFactor = parseFloat(footprint.style.height) / contentHeight; + let scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); + scaleFactor = Math.floor(scaleFactor * 1e6) / 1e6; + iframe.style.transformOrigin = "top left"; + iframe.style.transform = "scale(" + scaleFactor + ")"; + iframe.width = contentWidth; + iframe.height = contentHeight; + + container.style.width = iframe.getBoundingClientRect().width + 'px'; + container.style.height = iframe.getBoundingClientRect().height + 'px'; + footprint.style.width = iframe.getBoundingClientRect().width + 'px'; + footprint.style.height = iframe.getBoundingClientRect().height + 'px'; + this.finished(); + }); + + if (this.hasAttribute('srcdoc') && (iframe.srcdoc.length === 0)) { + iframe.srcdoc = this.getAttribute('srcdoc'); + } + if (this.hasAttribute('src') && (iframe.src.length === 0)) { + iframe.src = this.getAttribute('src'); + } + } + } + ); +} From 91e277c7d03b25355ec9391775b533beb8269322 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Tue, 19 Mar 2019 13:57:26 +0100 Subject: [PATCH 13/41] position elements --- inst/resources/js/responsiveiframe.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/inst/resources/js/responsiveiframe.js b/inst/resources/js/responsiveiframe.js index 4cea3f9f..5a27e58e 100644 --- a/inst/resources/js/responsiveiframe.js +++ b/inst/resources/js/responsiveiframe.js @@ -9,10 +9,11 @@ if (customElements) { // Populate the shadow DOM: shadowRoot.innerHTML = `
- +
`; this.ready = new Promise(resolve => {this.finished = resolve;}); @@ -37,11 +38,13 @@ if (customElements) { footprint = document.createElement('div'); footprint.style = this.style.cssText; this.removeAttribute('style'); - footprint.style.boxSizing="content-box"; + footprint.style.boxSizing='content-box'; footprint.style.breakInside = 'avoid'; + footprint.style.position = 'relative'; footprint.className = 'responsive-iframe-footprint'; this.insertAdjacentElement('beforebegin', footprint); footprint.appendChild(this); + this.setAttribute('style', 'position: absolute;'); } else { // The footprint div already exists. footprint = this.parentElement; From 0ccb23393ffe8a956168a06e7c196a2b5778fecf Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Tue, 19 Mar 2019 18:06:50 +0100 Subject: [PATCH 14/41] add a support for cross-origins urls --- inst/resources/js/responsiveiframe.js | 48 +++++++++++++++++---------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/inst/resources/js/responsiveiframe.js b/inst/resources/js/responsiveiframe.js index 5a27e58e..704d69c2 100644 --- a/inst/resources/js/responsiveiframe.js +++ b/inst/resources/js/responsiveiframe.js @@ -1,5 +1,5 @@ // A responsive iframe -// Works only with a same-origin html file +// Works only with a same-origin url if (customElements) { customElements.define('responsive-iframe', class extends HTMLElement { @@ -64,24 +64,38 @@ if (customElements) { // Quit early: return; } - let docEl = iframe.contentWindow.document.documentElement; // this works only with same-origin content - let contentHeight = docEl.scrollHeight; - let contentWidth = docEl.scrollWidth; - let widthScaleFactor = parseFloat(footprint.style.width) / contentWidth; - let heightScaleFactor = parseFloat(footprint.style.height) / contentHeight; - let scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); - scaleFactor = Math.floor(scaleFactor * 1e6) / 1e6; - iframe.style.transformOrigin = "top left"; - iframe.style.transform = "scale(" + scaleFactor + ")"; - iframe.width = contentWidth; - iframe.height = contentHeight; + let contentHeight, contentWidth; + try { + // this works only with a same-origin url + // with a cross-origin url, we get an error + let docEl = iframe.contentWindow.document.documentElement; + contentWidth = docEl.scrollWidth; + contentHeight = docEl.scrollHeight; + } + catch(e) { + // cross-origin url: + // we cannot find the size of the html page + // use a default resolution + contentWidth = 1024; + contentHeight = 768; + } + finally { + let widthScaleFactor = parseFloat(footprint.style.width) / contentWidth; + let heightScaleFactor = parseFloat(footprint.style.height) / contentHeight; + let scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); + scaleFactor = Math.floor(scaleFactor * 1e6) / 1e6; + iframe.style.transformOrigin = "top left"; + iframe.style.transform = "scale(" + scaleFactor + ")"; + iframe.width = contentWidth; + iframe.height = contentHeight; - container.style.width = iframe.getBoundingClientRect().width + 'px'; - container.style.height = iframe.getBoundingClientRect().height + 'px'; - footprint.style.width = iframe.getBoundingClientRect().width + 'px'; - footprint.style.height = iframe.getBoundingClientRect().height + 'px'; - this.finished(); + container.style.width = iframe.getBoundingClientRect().width + 'px'; + container.style.height = iframe.getBoundingClientRect().height + 'px'; + footprint.style.width = iframe.getBoundingClientRect().width + 'px'; + footprint.style.height = iframe.getBoundingClientRect().height + 'px'; + this.finished(); + } }); if (this.hasAttribute('srcdoc') && (iframe.srcdoc.length === 0)) { From 5e2ef1593e48697f802deebc74332f0d58cccbd8 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Tue, 19 Mar 2019 20:21:08 +0100 Subject: [PATCH 15/41] simplify the custom element --- R/paged.R | 5 +- inst/resources/js/responsiveiframe.js | 69 ++++++++------------------- 2 files changed, 24 insertions(+), 50 deletions(-) diff --git a/R/paged.R b/R/paged.R index e165d66b..8deba5d8 100644 --- a/R/paged.R +++ b/R/paged.R @@ -182,7 +182,10 @@ widget_file = (function() { responsive_iframe = function(width = NULL, height = NULL, ..., extra.attr = '') { if (length(extra.attr) == 0) extra.attr = '' extra.attr = as_html_attrs(extra.attr) - tag = htmltools::tag('responsive-iframe', c(extra.attr, list(...))) + tag = htmltools::tag( + 'autoscaling-iframe', + c(extra.attr, list(...), list(htmltools::p("This browser does not support this content."))) + ) width = css_declaration('width', htmltools::validateCssUnit(width)) height = css_declaration('height', htmltools::validateCssUnit(height)) tag = do.call( diff --git a/inst/resources/js/responsiveiframe.js b/inst/resources/js/responsiveiframe.js index 704d69c2..0d1f5721 100644 --- a/inst/resources/js/responsiveiframe.js +++ b/inst/resources/js/responsiveiframe.js @@ -1,60 +1,34 @@ -// A responsive iframe -// Works only with a same-origin url +// An auto-scaling iframe if (customElements) { - customElements.define('responsive-iframe', + customElements.define('autoscaling-iframe', class extends HTMLElement { constructor() { super(); // compulsory - let shadowRoot = this.attachShadow({mode: 'open'}); // we must use shadow DOM in the constructor + let shadowRoot = this.attachShadow({mode: 'open'}); // Populate the shadow DOM: shadowRoot.innerHTML = ` -
- -
+ `; this.ready = new Promise(resolve => {this.finished = resolve;}); } connectedCallback() { // Be aware that the connectedCallback() function can be called multiple times, // see https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks - if (!this.hasAttribute('initial-width')) { - this.setAttribute('initial-width', this.style.width); - } - if (!this.hasAttribute('initial-height')) { - this.setAttribute('initial-height', this.style.height); - } - - // First, we embed the element in a footprint div. - // This footprint will take room before Paged.js begins parsing the document. - // Since the constructor is also called after Paged.js builds the document, - // we also must test if the footprint div already exists. - let footprint; - if (!this.parentElement.classList.contains('responsive-iframe-footprint')) { - // The footprint div does not exist yet, create it. - footprint = document.createElement('div'); - footprint.style = this.style.cssText; - this.removeAttribute('style'); - footprint.style.boxSizing='content-box'; - footprint.style.breakInside = 'avoid'; - footprint.style.position = 'relative'; - footprint.className = 'responsive-iframe-footprint'; - this.insertAdjacentElement('beforebegin', footprint); - footprint.appendChild(this); - this.setAttribute('style', 'position: absolute;'); - } else { - // The footprint div already exists. - footprint = this.parentElement; - } - let iframe = this.shadowRoot.querySelector('iframe'); - let container = this.shadowRoot.querySelector('div'); - container.style.width = footprint.style.width; - container.style.height = footprint.style.height; - iframe.addEventListener('load', () => { // The load event fires twice: // 1st time when the iframe is attached (therefore the iframe document does not exist) @@ -81,19 +55,16 @@ if (customElements) { contentHeight = 768; } finally { - let widthScaleFactor = parseFloat(footprint.style.width) / contentWidth; - let heightScaleFactor = parseFloat(footprint.style.height) / contentHeight; + let widthScaleFactor = parseFloat(this.style.width) / contentWidth; + let heightScaleFactor = parseFloat(this.style.height) / contentHeight; let scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); scaleFactor = Math.floor(scaleFactor * 1e6) / 1e6; - iframe.style.transformOrigin = "top left"; iframe.style.transform = "scale(" + scaleFactor + ")"; iframe.width = contentWidth; iframe.height = contentHeight; - container.style.width = iframe.getBoundingClientRect().width + 'px'; - container.style.height = iframe.getBoundingClientRect().height + 'px'; - footprint.style.width = iframe.getBoundingClientRect().width + 'px'; - footprint.style.height = iframe.getBoundingClientRect().height + 'px'; + this.style.width = iframe.getBoundingClientRect().width + 'px'; + this.style.height = iframe.getBoundingClientRect().height + 'px'; this.finished(); } }); From 6e34518144d604ee915b5ac8774f0384a7660903 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Tue, 19 Mar 2019 21:36:40 +0100 Subject: [PATCH 16/41] adjust custom element sizes --- inst/resources/js/responsiveiframe.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/inst/resources/js/responsiveiframe.js b/inst/resources/js/responsiveiframe.js index 0d1f5721..d3a26eae 100644 --- a/inst/resources/js/responsiveiframe.js +++ b/inst/resources/js/responsiveiframe.js @@ -9,10 +9,9 @@ if (customElements) { shadowRoot.innerHTML = ` - - `; - this.ready = new Promise($ => this.addEventListener('resized', $)); +if (customElements) {customElements.define('autoscaling-iframe', + class extends HTMLElement { + constructor() { + super(); // compulsory + let shadowRoot = this.attachShadow({mode: 'open'}); + // Populate the shadow DOM: + shadowRoot.innerHTML = ` + + + `; + let iframe = shadowRoot.querySelector('iframe'); + iframe.addEventListener('load', event => { + // The iframe fires twice the load event: + // 1st time when the iframe is attached (therefore the iframe document does not exist) + // 2nd time when the document is loaded + if (!event.currentTarget.contentWindow) { + // This is the 1st time that the load event fires, the document does not exist + // Quit early: + return; } - var currentObject = this; - function resize(event) { - let iframe = event.currentTarget; - // The load event fires twice: - // 1st time when the iframe is attached (therefore the iframe document does not exist) - // 2nd time when the document is loaded - if (!iframe.contentWindow) { - // This is the 1st time that the load event fires, the document does not exist - // Quit early: - return; - } - - let contentHeight, contentWidth; - try { - // this works only with a same-origin url - // with a cross-origin url, we get an error - let docEl = iframe.contentWindow.document.documentElement; - contentWidth = docEl.scrollWidth; - contentHeight = docEl.scrollHeight; - } - catch(e) { - // cross-origin url: - // we cannot find the size of the html page - // use a default resolution - contentWidth = 1024; - contentHeight = 768; - } - finally { - let widthScaleFactor = currentObject.clientWidth / contentWidth; - let heightScaleFactor = currentObject.clientHeight / contentHeight; - let scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); - scaleFactor = Math.floor(scaleFactor * 1e6) / 1e6; - iframe.style.transform = "scale(" + scaleFactor + ")"; - iframe.width = contentWidth; - iframe.height = contentHeight; + this.dispatchEvent(new Event('load')); + }); + this.addEventListener('load', e => e.currentTarget.resize(), {once: true, capture: true}); + this.ready = new Promise(resolve => { + this.addEventListener('resized', e => resolve(e.currentTarget)); + }); + } + connectedCallback() { + // Be aware that the connectedCallback() function can be called multiple times, + // see https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks + let iframe = this.shadowRoot.querySelector('iframe'); + if (this.hasAttribute('srcdoc') && (iframe.srcdoc.length === 0)) { + iframe.srcdoc = this.getAttribute('srcdoc'); + } + if (this.hasAttribute('src') && (iframe.src.length === 0)) { + iframe.src = this.getAttribute('src'); + } + } + resize() { + let iframe = this.shadowRoot.querySelector('iframe'); + let contentHeight, contentWidth; + try { + // this works only with a same-origin url + // with a cross-origin url, we get an error + let docEl = iframe.contentWindow.document.documentElement; + contentWidth = docEl.scrollWidth; + contentHeight = docEl.scrollHeight; + } + catch(e) { + // cross-origin url: + // we cannot find the size of the html page + // use a default resolution + contentWidth = 1024; + contentHeight = 768; + } + finally { + let widthScaleFactor = this.clientWidth / contentWidth; + let heightScaleFactor = this.clientHeight / contentHeight; + let scaleFactor = Math.min(widthScaleFactor, heightScaleFactor); + scaleFactor = Math.floor(scaleFactor * 1e6) / 1e6; + iframe.style.transform = "scale(" + scaleFactor + ")"; + iframe.width = contentWidth; + iframe.height = contentHeight; - currentObject.style.width = iframe.getBoundingClientRect().width + 'px'; - currentObject.style.height = iframe.getBoundingClientRect().height + 'px'; - currentObject.style.boxSizing = "content-box"; - } - iframe.removeEventListener('load', resize, true); - currentObject.dispatchEvent(new Event('resized')); - } + this.style.width = iframe.getBoundingClientRect().width + 'px'; + this.style.height = iframe.getBoundingClientRect().height + 'px'; + this.style.boxSizing = "content-box"; } + this.dispatchEvent(new Event('resized')); } - ); -} + } +);} From 979897be0c51e95196b71e8aefa27204cbcc207a Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Wed, 20 Mar 2019 17:32:04 +0100 Subject: [PATCH 21/41] implements methods --- inst/resources/js/autoscaling_iframe.js | 77 ++++++++++++++++++++----- inst/resources/js/chrome_print.js | 2 +- inst/resources/js/config.js | 2 +- 3 files changed, 65 insertions(+), 16 deletions(-) diff --git a/inst/resources/js/autoscaling_iframe.js b/inst/resources/js/autoscaling_iframe.js index c08df0dc..9e2759cf 100644 --- a/inst/resources/js/autoscaling_iframe.js +++ b/inst/resources/js/autoscaling_iframe.js @@ -1,4 +1,8 @@ // An auto-scaling iframe +// This object emits the following events: +// - load: this is the same event as the iframe +// - initialized: before the iframe load the source document +// - resized: this event fires when the auto-scaling has finished if (customElements) {customElements.define('autoscaling-iframe', class extends HTMLElement { constructor() { @@ -22,32 +26,76 @@ if (customElements) {customElements.define('autoscaling-iframe', `; let iframe = shadowRoot.querySelector('iframe'); + + // dispatch the iframe load events by the custom element iframe.addEventListener('load', event => { - // The iframe fires twice the load event: - // 1st time when the iframe is attached (therefore the iframe document does not exist) - // 2nd time when the document is loaded - if (!event.currentTarget.contentWindow) { - // This is the 1st time that the load event fires, the document does not exist - // Quit early: - return; - } this.dispatchEvent(new Event('load')); }); - this.addEventListener('load', e => e.currentTarget.resize(), {once: true, capture: true}); - this.ready = new Promise(resolve => { - this.addEventListener('resized', e => resolve(e.currentTarget)); + + // the first load event throws the initialized event + this.addEventListener('load', () => this.dispatchEvent(new Event('initialized')), {once: true}); + + this.initialized = new Promise(resolve => { + if (this.hasAttribute('initialized')) { + resolve(this); + } else { + this.addEventListener('initialized', e => { + this.setAttribute('initialized', ''); + resolve(e.currentTarget); + }); + } }); + + this.ready = new Promise($ => this.addEventListener('resized', e => $(e.currentTarget), {once: true})); } + connectedCallback() { // Be aware that the connectedCallback() function can be called multiple times, // see https://developer.mozilla.org/docs/Web/Web_Components/Using_custom_elements#Using_the_lifecycle_callbacks + return this.initialized.then(() => this.clear()) + .then(() => this.loadSource()) + .then(() => this.resize()); + } + clearSource(attr) { let iframe = this.shadowRoot.querySelector('iframe'); - if (this.hasAttribute('srcdoc') && (iframe.srcdoc.length === 0)) { - iframe.srcdoc = this.getAttribute('srcdoc'); + let pr; + if (iframe.hasAttribute(attr)) { + pr = new Promise($ => this.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); + iframe.removeAttribute(attr); + } else { + pr = Promise.resolve(this); } - if (this.hasAttribute('src') && (iframe.src.length === 0)) { + return pr; + } + clear() { + let iframe = this.shadowRoot.querySelector('iframe'); + // clear srcdoc first (important) + return this.clearSource('srcdoc').then(() => this.clearSource('src')); + } + loadSrc() { + let iframe = this.shadowRoot.querySelector('iframe'); + let pr; + if (this.hasAttribute('src')) { + pr = new Promise($ => this.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); iframe.src = this.getAttribute('src'); + } else { + pr = Promise.resolve(); } + return pr; + } + loadSource() { + let iframe = this.shadowRoot.querySelector('iframe'); + // load src first (important) + return this.loadSrc().then(() => { + let loadSrcdoc; + if (this.hasAttribute('srcdoc')) { + loadSrcdoc = new Promise($ => this.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); + iframe.srcdoc = this.getAttribute('srcdoc'); + } else { + loadSrcdoc = Promise.resolve(); + } + return loadSrcdoc; + }); } resize() { let iframe = this.shadowRoot.querySelector('iframe'); @@ -80,6 +128,7 @@ if (customElements) {customElements.define('autoscaling-iframe', this.style.boxSizing = "content-box"; } this.dispatchEvent(new Event('resized')); + return Promise.resolve(this); } } );} diff --git a/inst/resources/js/chrome_print.js b/inst/resources/js/chrome_print.js index 92d5e2fc..c3a74078 100644 --- a/inst/resources/js/chrome_print.js +++ b/inst/resources/js/chrome_print.js @@ -42,7 +42,7 @@ let responsiveIFramesReady = new Promise(resolve => { window.addEventListener('load', () => { - let responsiveIFrames = document.getElementsByTagName('responsive-iframe'); + let responsiveIFrames = document.getElementsByTagName('autoscaling-iframe'); Promise.all([...responsiveIFrames].map(el => {return el['ready'];})).then(resolve()); }); }); diff --git a/inst/resources/js/config.js b/inst/resources/js/config.js index 1ebf2eb8..49d54c9c 100644 --- a/inst/resources/js/config.js +++ b/inst/resources/js/config.js @@ -103,7 +103,7 @@ appendShortTitles2() ]); await runMathJax(); - let iframeHTMLWidgets = document.getElementsByTagName('responsive-iframe'); + let iframeHTMLWidgets = document.getElementsByTagName('autoscaling-iframe'); let widgetsReady = Promise.all([...iframeHTMLWidgets].map(el => {return el['ready'];})); await widgetsReady; }, From 2fe59c1c03b9d9d8995cbc82e4c57246012ed59d Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Wed, 20 Mar 2019 18:01:15 +0100 Subject: [PATCH 22/41] reduce the exposed methods --- inst/resources/js/autoscaling_iframe.js | 27 ++++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/inst/resources/js/autoscaling_iframe.js b/inst/resources/js/autoscaling_iframe.js index 9e2759cf..010af03c 100644 --- a/inst/resources/js/autoscaling_iframe.js +++ b/inst/resources/js/autoscaling_iframe.js @@ -56,21 +56,24 @@ if (customElements) {customElements.define('autoscaling-iframe', .then(() => this.loadSource()) .then(() => this.resize()); } - clearSource(attr) { - let iframe = this.shadowRoot.querySelector('iframe'); - let pr; - if (iframe.hasAttribute(attr)) { - pr = new Promise($ => this.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); - iframe.removeAttribute(attr); - } else { - pr = Promise.resolve(this); - } - return pr; - } + clear() { + const clearSource = (attr) => { + let iframe = this.shadowRoot.querySelector('iframe'); + let pr; + if (iframe.hasAttribute(attr)) { + pr = new Promise($ => this.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); + iframe.removeAttribute(attr); + } else { + pr = Promise.resolve(this); + } + return pr; + }; + + let iframe = this.shadowRoot.querySelector('iframe'); // clear srcdoc first (important) - return this.clearSource('srcdoc').then(() => this.clearSource('src')); + return clearSource('srcdoc').then(() => clearSource('src')); } loadSrc() { let iframe = this.shadowRoot.querySelector('iframe'); From cf281d997d1da1f85e1f9f2517ce689b3a1d8995 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Wed, 20 Mar 2019 18:02:12 +0100 Subject: [PATCH 23/41] factor out variable declarations --- inst/resources/js/autoscaling_iframe.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/inst/resources/js/autoscaling_iframe.js b/inst/resources/js/autoscaling_iframe.js index 010af03c..e3f91f01 100644 --- a/inst/resources/js/autoscaling_iframe.js +++ b/inst/resources/js/autoscaling_iframe.js @@ -58,8 +58,9 @@ if (customElements) {customElements.define('autoscaling-iframe', } clear() { + let iframe = this.shadowRoot.querySelector('iframe'); + const clearSource = (attr) => { - let iframe = this.shadowRoot.querySelector('iframe'); let pr; if (iframe.hasAttribute(attr)) { pr = new Promise($ => this.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); @@ -70,8 +71,6 @@ if (customElements) {customElements.define('autoscaling-iframe', return pr; }; - - let iframe = this.shadowRoot.querySelector('iframe'); // clear srcdoc first (important) return clearSource('srcdoc').then(() => clearSource('src')); } From 8c73b48885b1d4ef2edcccdb91c989f139cb5887 Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Wed, 20 Mar 2019 20:59:08 +0100 Subject: [PATCH 24/41] use more explicit names --- R/paged.R | 13 +++-- inst/resources/js/autoscaling_iframe.js | 70 ++++++++++++------------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/R/paged.R b/R/paged.R index 46ada9e3..2809947a 100644 --- a/R/paged.R +++ b/R/paged.R @@ -117,7 +117,7 @@ html_format = function( self_contained = self_contained, pandoc_args = c(.pandoc_args, pandoc_args) ) if (isTRUE(.pagedjs)) format$knitr$opts_chunk[['render']] = paged_render(self_contained) - widget_file(reset = TRUE) + iframe_file(reset = TRUE) format } @@ -159,7 +159,7 @@ save_widget = function(directory, widget, options) { on.exit({ setwd(old_wd) }, add = TRUE) - f = widget_file() + f = iframe_file() htmlwidgets::saveWidget( widget = widget, file = f, # since chrome_print() does not handle network requests, use a self contained html file @@ -170,12 +170,12 @@ save_widget = function(directory, widget, options) { return(paste0(directory, f)) } -widget_file = (function() { +iframe_file = (function() { n = 0L function(reset = FALSE) { if (reset) n <<- -1L n <<- n + 1L - sprintf('widget%i.html', n) + sprintf('iframe%i.html', n) } })() @@ -184,7 +184,10 @@ autoscaling_iframe = function(width = NULL, height = NULL, ..., extra.attr = '') extra.attr = as_html_attrs(extra.attr) tag = htmltools::tag( 'autoscaling-iframe', - c(extra.attr, list(...), list(htmltools::p("This browser does not support this content."))) + c(extra.attr, + list(...), + list(htmltools::p("This browser does not support this feature.")) + ) ) width = css_declaration('width', htmltools::validateCssUnit(width)) height = css_declaration('height', htmltools::validateCssUnit(height)) diff --git a/inst/resources/js/autoscaling_iframe.js b/inst/resources/js/autoscaling_iframe.js index e3f91f01..310b906c 100644 --- a/inst/resources/js/autoscaling_iframe.js +++ b/inst/resources/js/autoscaling_iframe.js @@ -1,8 +1,12 @@ // An auto-scaling iframe // This object emits the following events: +// - initialize: when the iframe is ready to load a document +// - clear: when the iframe sources are removed // - load: this is the same event as the iframe -// - initialized: before the iframe load the source document -// - resized: this event fires when the auto-scaling has finished +// - resize: this event fires when the auto-scaling has finished +// +// TODO crosstalk support +// setters/getters for width/height if (customElements) {customElements.define('autoscaling-iframe', class extends HTMLElement { constructor() { @@ -18,7 +22,6 @@ if (customElements) {customElements.define('autoscaling-iframe', overflow: hidden; } iframe { - position: absolute; transform-origin: top left; } @@ -27,26 +30,25 @@ if (customElements) {customElements.define('autoscaling-iframe', `; let iframe = shadowRoot.querySelector('iframe'); - // dispatch the iframe load events by the custom element - iframe.addEventListener('load', event => { - this.dispatchEvent(new Event('load')); - }); - - // the first load event throws the initialized event - this.addEventListener('load', () => this.dispatchEvent(new Event('initialized')), {once: true}); + // the first load event throws the initialize event + iframe.addEventListener( + 'load', + () => this.dispatchEvent(new Event('initialize')), + {once: true} + ); this.initialized = new Promise(resolve => { if (this.hasAttribute('initialized')) { resolve(this); } else { - this.addEventListener('initialized', e => { + this.addEventListener('initialize', e => { this.setAttribute('initialized', ''); resolve(e.currentTarget); }); } }); - this.ready = new Promise($ => this.addEventListener('resized', e => $(e.currentTarget), {once: true})); + this.ready = new Promise($ => this.addEventListener('resize', e => $(e.currentTarget), {once: true})); } connectedCallback() { @@ -63,7 +65,7 @@ if (customElements) {customElements.define('autoscaling-iframe', const clearSource = (attr) => { let pr; if (iframe.hasAttribute(attr)) { - pr = new Promise($ => this.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); + pr = new Promise($ => iframe.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); iframe.removeAttribute(attr); } else { pr = Promise.resolve(this); @@ -72,33 +74,31 @@ if (customElements) {customElements.define('autoscaling-iframe', }; // clear srcdoc first (important) - return clearSource('srcdoc').then(() => clearSource('src')); - } - loadSrc() { - let iframe = this.shadowRoot.querySelector('iframe'); - let pr; - if (this.hasAttribute('src')) { - pr = new Promise($ => this.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); - iframe.src = this.getAttribute('src'); - } else { - pr = Promise.resolve(); - } - return pr; + let res = clearSource('srcdoc').then(() => clearSource('src')); + res.then(() => this.dispatchEvent(new Event('clear'))); + return res; } + loadSource() { let iframe = this.shadowRoot.querySelector('iframe'); - // load src first (important) - return this.loadSrc().then(() => { - let loadSrcdoc; - if (this.hasAttribute('srcdoc')) { - loadSrcdoc = new Promise($ => this.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); - iframe.srcdoc = this.getAttribute('srcdoc'); + + const load = (attr) => { + let pr; + if (this.hasAttribute(attr)) { + pr = new Promise($ => iframe.addEventListener('load', e => $(e.currentTarget), {once: true, capture: true})); + iframe.src = this.getAttribute(attr); } else { - loadSrcdoc = Promise.resolve(); + pr = Promise.resolve(); } - return loadSrcdoc; - }); + return pr; + }; + + // load src first (important) + const res = load('src').then(() => load('srcdoc')); + res.then(() => this.dispatchEvent(new Event('load'))); + return res; } + resize() { let iframe = this.shadowRoot.querySelector('iframe'); let contentHeight, contentWidth; @@ -129,7 +129,7 @@ if (customElements) {customElements.define('autoscaling-iframe', this.style.height = iframe.getBoundingClientRect().height + 'px'; this.style.boxSizing = "content-box"; } - this.dispatchEvent(new Event('resized')); + this.dispatchEvent(new Event('resize')); return Promise.resolve(this); } } From 02cf582107753eac472b8dee377f992547cbdc7c Mon Sep 17 00:00:00 2001 From: Romain Lesur Date: Wed, 20 Mar 2019 23:59:42 +0100 Subject: [PATCH 25/41] avoid overflows --- inst/resources/js/autoscaling_iframe.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inst/resources/js/autoscaling_iframe.js b/inst/resources/js/autoscaling_iframe.js index 310b906c..694cedab 100644 --- a/inst/resources/js/autoscaling_iframe.js +++ b/inst/resources/js/autoscaling_iframe.js @@ -23,6 +23,9 @@ if (customElements) {customElements.define('autoscaling-iframe', } iframe { transform-origin: top left; + position: absolute; + top: 0; + left: 0; }