При создании фронтенд-приложения мы иногда сталкиваемся с необходимостью распечатать представление, которое видно на экране. Кроме того, нам необходимо внести некоторые изменения, чтобы распечатанный документ выглядел не как снимок экрана, а как вид, персонализированный для печатной копии. В этой статье мы рассмотрим одно из таких персонализированных изменений, связанных с появлением верхнего и нижнего колонтитула на каждой печатаемой странице так, чтобы она выглядела как официальная копия.

Однако способ добиться этого довольно прост. Нам просто нужно обернуть содержимое для печати внутри таблицы и включить заголовок, который будет отображаться внутри тега thead, и нижний колонтитул внутри тега tfoot. Наш подход основан на CSS, JavaScript и функциях печати браузера и не требует дополнительных пакетов. Следовательно, он применим не только для приложения React, но и для других интерфейсных фреймворков JavaScript. Однако недостатком является то, что это сильно зависит от совместимости браузера.

Теперь давайте рассмотрим реализацию, сначала создав приложение React.

npx create-react-app react-print

Затем создайте компонент, который принимает содержимое для печати и обертывает его верхним и нижним колонтитулом. Компонент отображает кнопку, которая запускает печать при нажатии, и таблицу с содержимым заголовка внутри thead, содержимым для печати внутри tbody и содержимым нижнего колонтитула внутри tfoot. В демонстрационных целях мы покажем логотип реакции и текст в качестве заголовка и простой текст в качестве нижнего колонтитула.

import logo from './../logo.svg';
const PrintComponent = ({children}) => {
    const printAction = () => {
        window.print()
    }
    return (<>
          <button className={"print-preview-button"} onClick={printAction}>{"Print Preview"}</button>
        <table className="print-component">
            <thead>
                <tr>
                <th>
                <img src={logo} height={"40px"} width={"40px"} alt="logo" />
                <div>
                {"Page Header"}
                </div>
                </th>
                </tr>
            </thead>
            <tbody>
                <tr>
                <td>
                    {children}
                </td>
                </tr>
            </tbody>
            <tfoot className="table-footer">
                <tr>
                <td>
                {"Page footer"}
                </td>
                </tr>
            </tfoot>
        </table>
        </>
    )
}

export default PrintComponent

Кнопка изначально будет видна и скрыта для страницы печати, тогда как содержимое изначально скрыто и будет видно только для страницы печати. Поэтому, учитывая это соображение, вот простая таблица стилей для приложения.

.print-preview-button {
  @media print {
    display: none;
  }
}

.print-component {
  display: none;
  @media print {
    display: table;
    .table-footer > tr > td{
      text-align: center;
      background-color: grey;
      color: white;
    }
  }
}

Просто импортировав компонент внутри основного App.js и передав необходимое содержимое в качестве дочернего компонента компоненту, мы можем увидеть результат после нажатия кнопки

import './App.css';
import PrintComponent from './Components/Print';

function App() {
  return (
    <div>
      <PrintComponent>
        <div>
          {`What is Lorem Ipsum?
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

Why do we use it?
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).


Where does it come from?
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.

The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.

Where can I get some?
There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.`}
        </div>
<div>
          {`What is Lorem Ipsum?
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.

Why do we use it?
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout. The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English. Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).

Where does it come from?
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of "de Finibus Bonorum et Malorum" (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, "Lorem ipsum dolor sit amet..", comes from a line in section 1.10.32.

The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from "de Finibus Bonorum et Malorum" by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.

Where can I get some?
There are many variations of passages of Lorem Ipsum available, but the majority have suffered alteration in some form, by injected humour, or randomised words which don't look even slightly believable. If you are going to use a passage of Lorem Ipsum, you need to be sure there isn't anything embarrassing hidden in the middle of text. All the Lorem Ipsum generators on the Internet tend to repeat predefined chunks as necessary, making this the first true generator on the Internet. It uses a dictionary of over 200 Latin words, combined with a handful of model sentence structures, to generate Lorem Ipsum which looks reasonable. The generated Lorem Ipsum is therefore always free from repetition, injected humour, or non-characteristic words etc.`}
        </div>
      </PrintComponent>
    </div>
  );
}

export default App;

Нажав кнопку, мы видим, что верхний и нижний колонтитулы появляются на обеих страницах, как показано на изображении ниже.

Нам удалось отобразить верхний и нижний колонтитулы на всех печатных страницах, однако есть небольшая проблема. Как мы видим на второй странице, нижний колонтитул находится не внизу страницы, а в том месте, где заканчивается контент. Для тех, чьи требования приемлемы, дальнейшая настройка может не требоваться. Однако для тех, кто предпочитает, чтобы нижний колонтитул всегда отображался внизу страницы, нам нужно применить некоторые приемы, описанные ниже.

Хитрость, позволяющая добиться этого, находится прямо перед нами. Как показано на изображении, первая страница имеет нижний колонтитул внизу страницы. Это означает, что если мы установим подходящую высоту для компонента, нижний колонтитул появится внизу страницы. Однако у нас есть некоторые проблемы с этим. Прежде всего, какая это подходящая высота? Более того, изначально контент установлен как display: none, поэтому его высота равна 0. Мы будем иметь дело со всем этим в следующем подходе.

  • Получите высоту, которую будет занимать контент
  • Установите эту высоту для печати

ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: Следующая реализация была сделана для режима портретной печати в формате A4. Хотя подход тот же, используемые цифры будут варьироваться в зависимости от предпочтений.

Для портретного режима формата А4 идеально подойдет высота 1045 пикселей для 1 страницы. Хотя это приблизительное значение, попробуйте изменить его, если оно не соответствует потребностям. Это отправная точка расчета и будет зависеть от этой цифры. Аналогично, ширина 720 пикселей — идеальная ширина для той же конфигурации с полями по умолчанию.

Теперь мы рассчитаем общую высоту, которую будет занимать контент при заданной ширине. Прежде чем отобразится предварительный просмотр печати, мы добавляем класс к компоненту печати, который меняет часть его стиля, чтобы мы могли определить его высоту. Класс имеет следующий стиль

.temp-class-for-height {
// media not print so that it doesn't interfere with print style
  @media not print {
  // for A4 portrait
  width: 720px;
  visibility: hidden;
  display: table;
  }
}
const printElement = document.getElementById("print-component")
printElement.classList.add("temp-class-for-height")
const height = printElement.clientHeight

На этом этапе высота, которую мы получаем, имеет один верхний и нижний колонтитул, но на странице печати она будет присутствовать для каждой страницы. Итак, нам необходимо определить общее количество необходимых страниц. Поскольку мы уже установили стандарт высоты каждой печатной страницы, мы можем определить общее количество следующим образом.

const numberOfPage = Math.ceil(height / PAGE_HEIGHT)

Теперь, если количество страниц равно 1, нам не нужно выполнять дальнейшие расчеты, поскольку верхний и нижний колонтитулы уже учитываются для одного однократного появления. Однако в случае нескольких случаев требуемую высоту можно рассчитать ниже.

let requiredHeight = heightWithSingleHeader
if (numberOfPage > 1) {
    const headerHeight = printElement.getElementsByTagName("thead")?.[0]?.clientHeight || 0
    const footerHeight = printElement.getElementsByTagName("tfoot")?.[0]?.clientHeight || 0
    requiredHeight -= (numberOfPage - 1) * (headerHeight + footerHeight) 
}

Теперь мы просто установим высоту компонента печати на рассчитанную высоту и запустим печать.

printElement.style.height = `${requiredHeight}px`
window.print()

К этому времени мы увидим предварительный просмотр печати в нужном формате. Однако манипуляции, которые мы недавно проделали для определения высоты, можно отменить, поскольку это были временные изменения. Поэтому мы удалим добавленный класс и установим высоту автоматически после отображения предварительного просмотра, чтобы она была чем-то похожа на исходный макет.

printElement.classList.remove("temp-class-for-height")
printElement.style.height = `auto`

Все эти изменения были выполнены внутри функции, которая обрабатывала действие печати, поэтому окончательные изменения будут следующими:

const printAction = () => {
  const PAGE_HEIGHT = 1045
  const printElement = document.getElementById("print-component")
  if (printElement) {
      printElement.classList.add("temp-class-for-height")
      const height = printElement.clientHeight
      const numberOfPage = Math.ceil(height / PAGE_HEIGHT)
      const heightWithSingleHeader = numberOfPage*PAGE_HEIGHT
      let requiredHeight = heightWithSingleHeader
      if (numberOfPage > 1) {
          const headerHeight = printElement.getElementsByTagName("thead")?.[0]?.clientHeight
          const footerHeight = printElement.getElementsByTagName("tfoot")?.[0]?.clientHeight
          requiredHeight -= (numberOfPage - 1) * (headerHeight + footerHeight) 
      }
      printElement.style.height = `${requiredHeight}px`
      printElement.classList.remove("temp-class-for-height")
  }
  window.print()
  if (printElement) {
      printElement.style.height = `auto`
  }
}

Окончательный результат второй печатной страницы показан ниже, с нижним колонтитулом внизу, несмотря на то, что контент не такой длинный.

Надеюсь, этот трюк поможет тем, кто ищет решение в подобном случае.