` — it owns the whole
* document there, and `
` reserves space more reliably than a fixed
* element when the footer text wraps to several lines.
*
* The popup appends its own `@page` rule *after* the styles it copies from this
* page, so route 2's margins keep winning inside the popup and the two
* mechanisms never fight.
*
* @since 4.9.1
*/
class PrintTemplate {
/**
* Vertical space reserved inside the printed page margins, in millimetres.
* Keep these in sync with the offsets in {@see self::styles()}.
*/
const HEADER_SPACE = 22;
const FOOTER_SPACE = 18;
const EDGE_SPACE = 12;
public function __construct() {
add_action( 'wp_footer', [ $this, 'render' ], 99 );
}
/**
* Whether the print template should be emitted on the current request.
*
* Defaults to every front-end page — a reader can print any page, and the
* whole point of this route is to cover the ones without a print button.
* Filter `betterdocs_print_template_enabled` to narrow it, e.g. to docs
* only:
*
* add_filter( 'betterdocs_print_template_enabled', function ( $on ) {
* return $on && ( is_singular( 'docs' ) || is_post_type_archive( 'docs' ) );
* } );
*
* @return bool
*/
public static function is_enabled() {
$enabled = ! is_admin() && ! is_feed() && ! is_embed();
return (bool) apply_filters( 'betterdocs_print_template_enabled', $enabled );
}
/**
* Resolved logo URL for the printed page, or '' when disabled/unavailable.
*
* Resolution order: the `print_logo` setting, the theme's Custom Logo, the
* Elementor kit's site logo (themes without custom-logo support keep theirs
* there), then the Site Icon.
*
* @return string
*/
public static function get_logo() {
$enabled = apply_filters(
'betterdocs_print_enable_logo',
betterdocs()->settings->get( 'print_enable_logo', false )
);
if ( ! $enabled ) {
return '';
}
$logo = '';
$setting = betterdocs()->settings->get( 'print_logo' );
if ( ! empty( $setting['url'] ) ) {
$logo = (string) $setting['url'];
}
if ( '' === $logo ) {
$logo_id = (int) get_theme_mod( 'custom_logo' );
if ( ! $logo_id ) {
$kit_id = (int) get_option( 'elementor_active_kit' );
if ( $kit_id ) {
$kit_settings = get_post_meta( $kit_id, '_elementor_page_settings', true );
if ( ! empty( $kit_settings['site_logo']['id'] ) ) {
$logo_id = (int) $kit_settings['site_logo']['id'];
}
}
}
if ( $logo_id ) {
$logo = (string) wp_get_attachment_image_url( $logo_id, 'medium' );
}
}
if ( '' === $logo && has_site_icon() ) {
$logo = get_site_icon_url( 512 );
}
return (string) apply_filters( 'betterdocs_print_logo', $logo );
}
/**
* Resolved footer markup for the printed page, or '' when disabled.
*
* Falls back to "© " when the setting is left empty.
*
* @return string
*/
public static function get_footer() {
$enabled = apply_filters(
'betterdocs_print_enable_footer',
betterdocs()->settings->get( 'print_enable_footer', false )
);
if ( ! $enabled ) {
return '';
}
$footer = (string) betterdocs()->settings->get( 'print_footer_text', '' );
if ( '' === trim( $footer ) ) {
$footer = sprintf( '© %1$s %2$s', gmdate( 'Y' ), get_bloginfo( 'name' ) );
}
return (string) apply_filters( 'betterdocs_print_footer', $footer );
}
/**
* Fallback print CSS, used when the table wrap below never runs.
*
* `position: fixed` is what makes a browser repeat an element on every
* printed page, and the negative offsets lift the boxes out of the content
* flow into the margin `@page` reserves for them.
*
* This is a fallback and not the primary mechanism because Chrome drops one
* instance at each end: measured over an 8-page print, the header rendered
* on pages 1-7 and the footer on pages 2-8 — the last page lost its logo and
* the first page lost its footer. Laying the boxes out inside the page area
* instead (`top:0`/`bottom:0`) does repeat on every page, but then the text
* runs underneath them. `transform: translateY()` in place of the negative
* offsets behaves identically to the negative offsets. The correct CSS
* answer — `@page` margin boxes such as `@top-center` — is implemented by no
* browser, so {@see self::script()} does the job properly and this rule set
* only covers the case where its events never fire.
*
* @param bool $has_logo
* @param bool $has_footer
* @return string
*/
protected static function styles( $has_logo, $has_footer ) {
$top = $has_logo ? self::HEADER_SPACE : self::EDGE_SPACE;
$bottom = $has_footer ? self::FOOTER_SPACE : self::EDGE_SPACE;
$css = '.betterdocs-print-running-header,.betterdocs-print-running-footer{display:none}'
. '@media print{'
. sprintf( '@page{margin:%1$dmm %2$dmm %3$dmm}', $top, self::EDGE_SPACE, $bottom )
. '.betterdocs-print-running-header,.betterdocs-print-running-footer{'
. 'display:block;position:fixed;left:0;right:0;margin:0;padding:0;'
. 'text-align:center;background:none;border:0}';
if ( $has_logo ) {
$css .= '.betterdocs-print-running-header{top:-16mm;height:14mm;line-height:0}'
. '.betterdocs-print-running-header img{'
. 'max-height:14mm;max-width:60%;width:auto;height:auto;display:inline-block}';
}
if ( $has_footer ) {
$css .= '.betterdocs-print-running-footer{'
. 'bottom:-13mm;height:11mm;overflow:hidden;'
. 'font-size:9pt;line-height:1.35;color:#555}'
. '.betterdocs-print-running-footer p{margin:0;font-size:inherit;line-height:inherit}';
}
return $css . '}';
}
/**
* Print CSS for the wrapped (table) layout.
*
* Emitted inert as `media="not all"`; {@see self::script()} flips it to
* `print` once it has actually built the table, and back again afterwards.
* Because this sheet comes after the fallback one, its `@page` margin and
* its `position: static` reset both win while the wrap is in place.
*
* Browsers repeat `table-header-group` / `table-footer-group` on every page
* *and* keep the flowing content clear of them, which is the behaviour the
* fixed-position fallback cannot deliver.
*
* @param bool $has_logo
* @param bool $has_footer
* @return string
*/
protected static function wrapped_styles( $has_logo, $has_footer ) {
$css = sprintf( '@page{margin:%1$dmm}', self::EDGE_SPACE )
. '.betterdocs-print-running-header,.betterdocs-print-running-footer{'
. 'display:block;position:static;top:auto;bottom:auto;left:auto;right:auto;'
. 'height:auto;transform:none;text-align:center}'
. '#betterdocs-print-layout{width:100%;border-collapse:collapse;border:0}'
. '#betterdocs-print-layout>thead>tr>td,'
. '#betterdocs-print-layout>tfoot>tr>td,'
. '#betterdocs-print-layout>tbody>tr>td{padding:0;border:0}'
. '#betterdocs-print-layout>thead{display:table-header-group}'
. '#betterdocs-print-layout>tfoot{display:table-footer-group}';
if ( $has_logo ) {
$css .= '.betterdocs-print-running-header{padding:0 0 6mm;line-height:0}'
. '.betterdocs-print-running-header img{'
. 'max-height:16mm;max-width:60%;width:auto;height:auto;display:inline-block}';
}
if ( $has_footer ) {
$css .= '.betterdocs-print-running-footer{'
. 'padding:5mm 0 0;font-size:9pt;line-height:1.35;color:#555}'
. '.betterdocs-print-running-footer p{margin:0;font-size:inherit;line-height:inherit}';
}
return $css;
}
/**
* The wrap/unwrap helper.
*
* Inlined rather than shipped as a bundle on purpose: it has to be present
* on every front-end page, and a separate request site-wide for a
* print-only helper of this size costs more than it saves.
*
* On `beforeprint` it moves the page's rendered content into a single table
* cell, with the logo as `` and the footer as ``, then
* activates {@see self::wrapped_styles()}. On `afterprint` it puts
* everything back. `matchMedia('print')` covers Safari, which has no
* `beforeprint`; the `state` guard makes a double fire harmless.
*
* @return string
*/
protected static function script() {
return <<<'JS'
(function(){
"use strict";
var SKIP={SCRIPT:1,STYLE:1,LINK:1,TEMPLATE:1,NOSCRIPT:1},state=null;
function cell(parent,tag){
var section=document.createElement(tag),row=document.createElement("tr"),td=document.createElement("td");
row.appendChild(td);section.appendChild(row);parent.appendChild(section);return td;
}
function wrap(){
if(state||!document.body){return;}
var header=document.querySelector(".betterdocs-print-running-header");
var footer=document.querySelector(".betterdocs-print-running-footer");
if(!header&&!footer){return;}
var body=document.body,content=[],node=body.firstChild;
while(node){
if(node!==header&&node!==footer&&!(node.nodeType===1&&SKIP[node.tagName])){content.push(node);}
node=node.nextSibling;
}
if(!content.length){return;}
var table=document.createElement("table");
table.id="betterdocs-print-layout";
if(header){cell(table,"thead").appendChild(header);}
if(footer){cell(table,"tfoot").appendChild(footer);}
var host=cell(table,"tbody");
body.insertBefore(table,content[0]);
for(var i=0;i.
*
* @return void
*/
public function render() {
if ( ! self::is_enabled() ) {
return;
}
$logo = self::get_logo();
$footer = self::get_footer();
if ( '' === $logo && '' === $footer ) {
return;
}
$has_logo = '' !== $logo;
$has_footer = '' !== $footer;
if ( $has_logo ) {
printf(
'',
esc_url( $logo )
);
}
if ( $has_footer ) {
printf(
'',
wp_kses_post( $footer )
);
}
printf(
'',
self::styles( $has_logo, $has_footer )
);
printf(
'',
self::wrapped_styles( $has_logo, $has_footer )
);
wp_print_inline_script_tag( self::script(), [ 'id' => 'betterdocs-print-template-js' ] );
}
}