Разделить очень длинный текст на страницы в Flutter

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

Таким образом, учитывая размер контейнера для страницы и текста (с определенным стилем шрифта) в качестве входных данных, результатом должно быть общее количество страниц, необходимое для отображения этого текста.

Я понимаю, что текст может отображаться в ListView как упомянутое @pskink, но я хотел бы сделать страницу статичной, как в Kindle, и заранее отображать общее количество страниц, чтобы можно было переключиться на любую страницу по индексу.

Я также обнаружил связанную проблему флаттера, которая может быть связана с этим вопросом. Не уверен, если это ограничивает или нет.

Предоставьте достаточное количество LibTxt, чтобы сделать собственное расположение текста практичным

2 ответа

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

Поскольку компоновка нескольких абзацев только для того, чтобы определить точку отсечки для каждой страницы, обходится дорого, _getPageText()функционировать в отдельном изоляте. К сожалению, в настоящее время это невозможно, так как неосновные изоляты не могут использовать dart:uiпоэтому функция выполняется в основном изоляте.

Вот полный код, включая некоторые комментарии, которые вы можете запустить в DartPad:

      import 'dart:ui' as ui;
import 'dart:math' as math;

import 'package:flutter/material.dart';

const Color darkBlue = Color.fromARGB(255, 18, 32, 47);

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark().copyWith(
        scaffoldBackgroundColor: darkBlue,
      ),
      debugShowCheckedModeBanner: false,
      home: const Scaffold(
        body: Center(
          child: ExampleMultiPageText(),
        ),
      ),
    );
  }
}

class MultiPageText extends StatefulWidget {
  /// The entire text that has to be distributed across one or
  /// more pages.
  final String fullText;

  /// The [TextStyle] that is applied to the [fullText].
  final TextStyle textStyle;

  /// The size of the entire widget.
  ///
  /// This size's height is the upper limit for the [PageTextContainer]'s that contains
  /// each page text. If the [PageNavigatorMenu] is included (i.e. if
  /// [usePageNavigation] is `true`), its height (50 pixel) will be deducted
  /// leaving the only remaining height for the [PageTextContainer].
  final Size size;

  /// The padding that will be applied to the [PageTextContainer].
  final EdgeInsets paddingTextBox;

  /// Whether the [PageNavigatorMenu] will be rendered below the [PageTextContainer].
  final bool usePageNavigation;

  /// The [PageTextContainer]'s decoration.
  final BoxDecoration? decoration;

  /// Creates a widget that distributes the provided text across as many pages as necessary.
  ///
  /// Besides the TextContainer that holds the text for the given page, the widget can also
  /// contain a PageNavigatorMenu for navigating between the different pages.
  const MultiPageText({
    Key? key,
    required this.fullText,
    required this.size,
    this.textStyle = const TextStyle(
      fontSize: 10,
      color: Colors.white,
    ),
    this.paddingTextBox = const EdgeInsets.all(
      10,
    ),
    this.usePageNavigation = true,
    this.decoration,
  }) : super(key: key);

  @override
  State<MultiPageText> createState() => _MultiPageTextState();
}

class _MultiPageTextState extends State<MultiPageText> {
  int _currentPageIndex = 0;
  final double _pageNavigatorHeight = 40;
  final int _upperLayoutRunsLimit = 20;
  late List<String> _pages;
  late Size _availableSize;

  @override
  void initState() {
    _pages = _getPageTexts();
    super.initState();
  }

  List<String> _getPageTexts() {
    List<String> pages = <String>[];
    String remainingText = widget.fullText;
    _availableSize = _calculateAvailableSize(
      size: widget.size,
      padding: widget.paddingTextBox,
      usePageNavigation: widget.usePageNavigation,
    );
    double widthFactor = 0.5;
    int retries = 0;
    int pageCharacterLimit = _estimatePageCharacterLimit(
      size: _availableSize,
      textStyle: widget.textStyle,
      widthFactor: widthFactor,
    );
    while (remainingText.isNotEmpty) {
      final String pageTextEstimate = _getPageTextEstimate(
        text: remainingText,
        pageCharacterLimit: pageCharacterLimit,
      );
      final PageProperties pageProperties = _getPageText(
        text: pageTextEstimate,
        textStyle: widget.textStyle,
        size: _availableSize,
      );
      if (_shouldOptimizeEstimates(pageProperties.layoutRuns)) {
        // Optimize widthFactor for better pageTextEstimates
        widthFactor = _updateWidthFactor(
          widthFactor: widthFactor,
          layoutRuns: pageProperties.layoutRuns,
          upperLayoutRunsLimit: _upperLayoutRunsLimit,
        );
        // Update pageCharacterLimit
        pageCharacterLimit = _estimatePageCharacterLimit(
          size: _availableSize,
          textStyle: widget.textStyle,
          widthFactor: widthFactor,
        );
      }
      if (_performRetry(pageProperties.layoutRuns, retries)) {
        retries++;
        continue;
      }
      pages.add(pageProperties.text);
      remainingText =
          remainingText.substring(pageProperties.text.length).trimLeft();
      retries = 0;
    }
    return pages;
  }

  /// Calculates the available space for the [ui.ParagraphBuilder] (i.e. its width constraint).
  ///
  /// That means subtracting any padding of the enclosing [Container] as well as removing the
  /// height of the page navigation (only if [usePageNavigation] is `true`).
  Size _calculateAvailableSize({
    required Size size,
    required EdgeInsets padding,
    required bool usePageNavigation,
  }) {
    double availableHeight = size.height -
        (widget.paddingTextBox.top + widget.paddingTextBox.bottom);
    if (usePageNavigation) {
      availableHeight = availableHeight - _pageNavigatorHeight;
    }
    final double availableWidth =
        size.width - (widget.paddingTextBox.right + widget.paddingTextBox.left);
    return Size(availableWidth, availableHeight);
  }

  /// Updates the [widthFactor] based on the number of actual [layoutRuns].
  ///
  /// If the [upperLayoutRunsLimit] was exceeded, we want to tighten our character estimate
  /// (hence increase the [widthFactor] by `0.05`). Otherwise (i.e. if [layoutRuns] = `1`) the
  /// constraint should be loosened (decrease the [widthFactor] by `0.05`).
  double _updateWidthFactor({
    required double widthFactor,
    required int layoutRuns,
    required int upperLayoutRunsLimit,
  }) {
    final double newWidthFactor = layoutRuns >= upperLayoutRunsLimit
        ? widthFactor + 0.05
        : widthFactor - 0.05;
    return newWidthFactor;
  }

  /// (Over)Estimates the character limit for a given page.
  ///
  /// The [widthFactor] is automatically chosen and adjusted by the parent function
  /// so that the resulting maximum character will be slightly overestimated.
  int _estimatePageCharacterLimit({
    required Size size,
    required TextStyle textStyle,
    required double widthFactor,
  }) {
    final characterHeight = textStyle.fontSize!;
    final characterWidth = characterHeight * widthFactor;
    return ((size.height * size.width) / (characterHeight * characterWidth))
        .ceil();
  }

  /// Retrieves the part of the [text] that fits within the [pageCharacterLimit] starting
  /// from the beginning of the string.
  String _getPageTextEstimate({
    required String text,
    required int pageCharacterLimit,
  }) {
    final initialPageTextEstimate =
        text.substring(0, math.min(pageCharacterLimit + 1, text.length));
    final substringIndex =
        initialPageTextEstimate.lastIndexOf(RegExp(r"\s+\b|\b\s+|[\.?!]"));
    final pageTextEstimate =
        text.substring(0, math.min(substringIndex + 1, text.length));
    return pageTextEstimate;
  }

  /// Determines the final text for the given page and returns the respective
  /// [PageProperties] with the number of necessary `layoutRuns` for optimization
  /// and the [text] itself.
  PageProperties _getPageText({
    required String text,
    required TextStyle textStyle,
    required Size size,
  }) {
    double paragraphHeight = 10000;
    String currentText = text;
    int layoutRuns = 0;
    final RegExp regExp = RegExp(r"\S+[\W]*$");
    while (paragraphHeight > size.height) {
      final paragraph = ParagraphPainter.layoutParagraph(
          text: currentText, textStyle: textStyle, size: size);
      paragraphHeight = paragraph.height;
      if (paragraphHeight > size.height) {
        currentText = currentText.replaceFirst(regExp, '');
      }
      layoutRuns = layoutRuns + 1;
    }

    return PageProperties(currentText, layoutRuns);
  }

  bool _performRetry(int layoutRuns, int retries) {
    return layoutRuns == 1 && retries <= 0;
  }

  bool _shouldOptimizeEstimates(int layoutRuns) {
    return layoutRuns > _upperLayoutRunsLimit || layoutRuns == 1;
  }

  void _updatePageIndex(PageUpdateOperation pageUpdateOperation) {
    switch (pageUpdateOperation) {
      case PageUpdateOperation.first:
        setState(() {
          _currentPageIndex = 0;
        });
        break;
      case PageUpdateOperation.previous:
        setState(() {
          _currentPageIndex--;
        });
        break;
      case PageUpdateOperation.next:
        setState(() {
          _currentPageIndex++;
        });
        break;
      case PageUpdateOperation.last:
        setState(() {
          _currentPageIndex = _pages.length - 1;
        });
        break;
    }
  }

  @override
  Widget build(BuildContext context) {
    final pageTextContainer = PageTextContainer(
      text: _pages[_currentPageIndex],
      textStyle: widget.textStyle,
      padding: widget.paddingTextBox,
      size: _availableSize,
      decoration: widget.decoration,
    );
    return widget.usePageNavigation
        ? Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              pageTextContainer,
              PageNavigatorMenu(
                size: Size(widget.size.width, _pageNavigatorHeight),
                currentPageIndex: _currentPageIndex,
                pageCount: _pages.length,
                updatePageIndex: _updatePageIndex,
              ),
            ],
          )
        : pageTextContainer;
  }
}

class PageTextContainer extends StatelessWidget {
  final String text;
  final TextStyle textStyle;
  final EdgeInsets padding;
  final Size size;
  final BoxDecoration? decoration;

  const PageTextContainer({
    Key? key,
    required this.text,
    required this.textStyle,
    required this.padding,
    required this.size,
    this.decoration,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: padding,
      decoration: decoration,
      child: CustomPaint(
        painter: ParagraphPainter(
          pageText: text,
          textStyle: textStyle,
        ),
        child: SizedBox(
          height: size.height,
          width: size.width,
        ),
      ),
    );
  }
}

class PageNavigatorMenu extends StatelessWidget {
  final Size size;
  final int currentPageIndex;
  final int pageCount;
  final void Function(PageUpdateOperation) updatePageIndex;

  const PageNavigatorMenu({
    Key? key,
    required this.size,
    required this.currentPageIndex,
    required this.pageCount,
    required this.updatePageIndex,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      height: size.height,
      width: size.width,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          IconButton(
            icon: const Icon(
              Icons.first_page,
            ),
            onPressed: currentPageIndex > 0
                ? () => updatePageIndex(
                      PageUpdateOperation.first,
                    )
                : null,
          ),
          IconButton(
            icon: const Icon(
              Icons.navigate_before,
            ),
            onPressed: currentPageIndex > 0
                ? () => updatePageIndex(
                      PageUpdateOperation.previous,
                    )
                : null,
          ),
          Flexible(
            child: FittedBox(
              fit: BoxFit.scaleDown,
              child: Text(
                'Page ${currentPageIndex + 1}',
              ),
            ),
          ),
          IconButton(
            icon: const Icon(
              Icons.navigate_next,
            ),
            onPressed: currentPageIndex < pageCount - 1
                ? () => updatePageIndex(
                      PageUpdateOperation.next,
                    )
                : null,
          ),
          IconButton(
            icon: const Icon(
              Icons.last_page,
            ),
            onPressed: currentPageIndex < pageCount - 1
                ? () => updatePageIndex(
                      PageUpdateOperation.last,
                    )
                : null,
          ),
        ],
      ),
    );
  }
}

class ParagraphPainter extends CustomPainter {
  final String pageText;
  final TextStyle textStyle;

  ParagraphPainter({
    required this.pageText,
    required this.textStyle,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final paragraph = layoutParagraph(
      text: pageText,
      textStyle: textStyle,
      size: size,
    );
    canvas.drawParagraph(paragraph, Offset.zero);
  }

  static ui.Paragraph layoutParagraph({
    required String text,
    required TextStyle textStyle,
    required Size size,
  }) {
    final ui.ParagraphBuilder paragraphBuilder = ui.ParagraphBuilder(
      ui.ParagraphStyle(
        fontSize: textStyle.fontSize,
        fontFamily: textStyle.fontFamily,
        fontStyle: textStyle.fontStyle,
        fontWeight: textStyle.fontWeight,
        textAlign: TextAlign.left,
      ),
    )
      ..pushStyle(textStyle.getTextStyle())
      ..addText(text);
    final ui.Paragraph paragraph = paragraphBuilder.build()
      ..layout(
        ui.ParagraphConstraints(width: size.width),
      );
    return paragraph;
  }

  @override
  bool shouldRepaint(ParagraphPainter oldDelegate) =>
      oldDelegate.pageText != pageText || oldDelegate.textStyle != textStyle;
}

class PageProperties {
  final String text;
  final int layoutRuns;

  PageProperties(this.text, this.layoutRuns);

  @override
  String toString() {
    return '''PageProperties(
$text,
$layoutRuns
)''';
  }
}

enum PageUpdateOperation {
  first,
  previous,
  next,
  last,
}

// Call the widget
class ExampleMultiPageText extends StatelessWidget {
  const ExampleMultiPageText({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: MultiPageText(
        textStyle: const TextStyle(
          fontSize: 10,
          color: Colors.white,
        ),
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(10),
          border: Border.all(
            width: 1.0,
            color: Colors.grey,
          ),
        ),
        usePageNavigation: true,
        fullText:
            '''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam et mollis orci. Sed ullamcorper leo ipsum, sit amet feugiat neque aliquam at. Vestibulum vehicula elit eget metus iaculis ultrices. Nunc faucibus vehicula justo vitae portaPhasellus vestibulum lectus non tellus accumsan, non dictum tellus bibendum. Nulla ornare eros vitae bibendum pharetra. Fusce sit amet lobortis ex. Proin condimentum imperdiet erat, lacinia suscipit est efficitur sit amet. Nunc laoreet luctus tortor, in accumsan velit. Donec cursus velit vehicula maximus finibus. Donec quis euismod quam. In vel lacus fringilla, rhoncus eros nec, elementum massa. Donec luctus lobortis ullamcorper.

Aenean lacus ligula, rutrum ac felis in, dictum sagittis est. Integer finibus arcu magna, eget bibendum odio dignissim id. Mauris ornare ipsum maximus malesuada efficitur. Duis pulvinar neque a lectus fermentum accumsan non id arcu. Quisque congue lectus eu ante efficitur, ac semper lectus volutpat. Pellentesque dignissim turpis quam, venenatis facilisis sem rutrum non. Praesent tincidunt sodales dolor a maximus. Aliquam sit amet quam vel augue mattis luctus. Duis placerat condimentum aliquam. Quisque bibendum in ipsum non pretium. Nam lobortis libero quam, sed lacinia ex rhoncus non. Fusce viverra felis vitae finibus tincidunt. In hac habitasse platea dictumst. Praesent mollis, turpis at iaculis pulvinar, lectus enim feugiat mi, ultricies auctor lacus sapien sed ipsum.

Etiam ac mi risus. In dictum purus sapien, non tempus magna tempor vel. Suspendisse finibus lectus et sem laoreet dignissim.

Maecenas erat mi, ultrices non sollicitudin non, tristique a est. Vestibulum interdum diam nec justo eleifend tincidunt. Nulla non nulla at nulla suscipit congue.

Mauris est dui, molestie sed tempus ac, accumsan eget urna. Nullam sit amet bibendum lacus, a pellentesque nisl. Aliquam lorem eros, finibus id enim eget, faucibus ultricies erat. Sed sed pulvinar tellus, nec euismod lectus. Quisque libero metus, congue nec suscipit ut, tincidunt eget odio. Aliquam sit amet cursus magna. Nam aliquam ipsum at eleifend auctor. Fusce eu metus dui. Nulla non lacus eros. Cras elementum, ante et tristique faucibus, risus enim dignissim est, id iaculis augue turpis vel massa. Sed cursus ultricies lorem.''',
        size: const Size(
          200,
          350,
        ),
      ),
    );
  }
}

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

      var deviceData = MediaQuery.of(context);
var deviceHeight = deviceData.size.height;
var deviceWidth = deviceData.size.width - 60; // 60 - AppBar estimated height
var deviceDimension = deviceHeight * deviceWidth;

/// Compute estimated character limit per page
/// Estimated dimension of each character: textSize * (textSize * 0.8)
/// textSize width estimated dimension is 80% of its height
var pageCharLimit = (deviceDimension / (textSize * (textSize * 0.8))).round();
debugPrint('Character limit per page: $pageCharLimit');

/// Compute pageCount base from the computed pageCharLimit
var pageCount = (textLength / pageCharLimit).round();
debugPrint('Pages: $pageCount');

Затем разорвите текст из документа, используя String.substring(start,end)чтобы добавить его в список

      List<String> pageText = [];
var index = 0;
var startStrIndex = 0;
var endStrIndex = pageCharLimit;
while (index < pageCount) {
  /// Update the last index to the Document Text length
  if (index == pageCount - 1) endStrIndex = textLength;

  /// Add String on List<String>
  pageText.add(Document.text.substring(startStrIndex, endStrIndex));

  /// Update index of Document Text String to be added on [pageText]
  if (index < pageCount) {
    startStrIndex = endStrIndex;
    endStrIndex += pageCharLimit;
  }
  index++;
}

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

Вот полный образец.

      import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late int textLength;
  static const textSize = 16.0;

  @override
  void initState() {
    textLength = Document.text.length;
    debugPrint('Text Length: $textLength');
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    var deviceData = MediaQuery.of(context);
    var deviceHeight = deviceData.size.height;
    var deviceWidth =
        deviceData.size.width - 60; // 60 - AppBar estimated height
    var deviceDimension = deviceHeight * deviceWidth;

    /// Compute estimated character limit per page
    /// Estimated dimension of each character: textSize * (textSize * 0.8)
    /// textSize width estimated dimension is 80% of its height
    var pageCharLimit = (deviceDimension / (textSize * (textSize * 0.8))).round();
    debugPrint('Character limit per page: $pageCharLimit');

    /// Compute pageCount base from the computed pageCharLimit
    var pageCount = (textLength / pageCharLimit).round();
    debugPrint('Pages: $pageCount');

    List<String> pageText = [];
    var index = 0;
    var startStrIndex = 0;
    var endStrIndex = pageCharLimit;
    while (index < pageCount) {
      /// Update the last index to the Document Text length
      if (index == pageCount - 1) endStrIndex = textLength;

      /// Add String on List<String>
      pageText.add(Document.text.substring(startStrIndex, endStrIndex));

      /// Update index of Document Text String to be added on [pageText]
      if (index < pageCount) {
        startStrIndex = endStrIndex;
        endStrIndex += pageCharLimit;
      }
      index++;
    }

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: ListView.builder(
          itemCount: pageCount,
          itemBuilder: (BuildContext context, int index) {
            return Container(
              padding: const EdgeInsets.fromLTRB(0, 0, 0, 16.0),
              child: Card(
                child: Container(
                  padding: const EdgeInsets.all(16.0),
                  child: Text(
                    pageText[index],
                    style: const TextStyle(fontSize: textSize),
                  ),
                ),
              ),
            );
          }),
    );
  }
}

class Document {
  static const text =
      'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Nunc sed velit dignissim sodales ut eu sem. Enim nec dui nunc mattis enim ut tellus elementum sagittis. Augue lacus viverra vitae congue eu. Posuere morbi leo urna molestie at elementum eu. Sed faucibus turpis in eu mi bibendum neque egestas congue. Id volutpat lacus laoreet non curabitur gravida arcu. Ut tristique et egestas quis ipsum suspendisse ultrices gravida. Sit amet mattis vulputate enim nulla. Risus pretium quam vulputate dignissim suspendisse in. Vel pharetra vel turpis nunc eget lorem dolor sed. Ac turpis egestas maecenas pharetra convallis posuere morbi. Quam nulla porttitor massa id neque aliquam vestibulum. Ut tortor pretium viverra suspendisse potenti nullam ac tortor. Quam lacus suspendisse faucibus interdum posuere lorem ipsum. Posuere lorem ipsum dolor sit amet. Imperdiet nulla malesuada pellentesque elit eget gravida cum sociis.'
      '\n\nQuam elementum pulvinar etiam non quam lacus suspendisse faucibus. Congue quisque egestas diam in arcu cursus euismod quis. Felis donec et odio pellentesque diam volutpat. Maecenas accumsan lacus vel facilisis volutpat est velit egestas. Leo urna molestie at elementum. Facilisi nullam vehicula ipsum a arcu cursus vitae congue mauris. At imperdiet dui accumsan sit. Porttitor lacus luctus accumsan tortor posuere. Volutpat odio facilisis mauris sit amet massa vitae. Ut eu sem integer vitae justo eget magna fermentum iaculis. Volutpat diam ut venenatis tellus in metus vulputate eu scelerisque. Morbi enim nunc faucibus a pellentesque sit amet porttitor eget. Sed odio morbi quis commodo.'
      '\n\nEu mi bibendum neque egestas congue quisque egestas. Libero id faucibus nisl tincidunt. Nunc mi ipsum faucibus vitae aliquet nec ullamcorper. Tristique nulla aliquet enim tortor. Risus nec feugiat in fermentum posuere. Eu non diam phasellus vestibulum. Sit amet venenatis urna cursus. Amet venenatis urna cursus eget nunc scelerisque viverra mauris in. A arcu cursus vitae congue mauris rhoncus aenean. Maecenas sed enim ut sem viverra aliquet eget. Scelerisque purus semper eget duis at tellus at. Aliquam malesuada bibendum arcu vitae. Sed augue lacus viverra vitae congue eu. Sit amet est placerat in egestas erat imperdiet sed euismod. Venenatis tellus in metus vulputate eu scelerisque felis imperdiet proin. Volutpat consequat mauris nunc congue. Nec dui nunc mattis enim ut tellus elementum. Amet purus gravida quis blandit turpis cursus. Nisl suscipit adipiscing bibendum est ultricies integer quis auctor elit.'
      '\n\nNunc vel risus commodo viverra maecenas accumsan. Felis donec et odio pellentesque diam volutpat commodo. Sodales ut etiam sit amet nisl purus in mollis. Et netus et malesuada fames ac. Pretium aenean pharetra magna ac placerat vestibulum lectus mauris. Pulvinar pellentesque habitant morbi tristique. Nisl purus in mollis nunc sed id semper risus in. Elit ut aliquam purus sit amet luctus venenatis. Nulla aliquet enim tortor at. Amet luctus venenatis lectus magna fringilla urna porttitor rhoncus. Aliquam malesuada bibendum arcu vitae. Urna nec tincidunt praesent semper feugiat nibh. Vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Vel turpis nunc eget lorem dolor sed viverra. Bibendum neque egestas congue quisque egestas. Leo a diam sollicitudin tempor id eu. Consectetur lorem donec massa sapien. Consequat ac felis donec et odio. Sed velit dignissim sodales ut eu sem integer vitae justo.'
      '\n\nInteger vitae justo eget magna fermentum iaculis. Lorem ipsum dolor sit amet consectetur adipiscing elit ut. Id porta nibh venenatis cras sed felis eget velit aliquet. Non sodales neque sodales ut etiam. Nunc faucibus a pellentesque sit amet porttitor. Ultricies tristique nulla aliquet enim tortor. Cursus metus aliquam eleifend mi. Arcu non odio euismod lacinia at quis. Sed lectus vestibulum mattis ullamcorper velit sed. Tortor aliquam nulla facilisi cras. Quam vulputate dignissim suspendisse in est ante in nibh mauris. Pretium nibh ipsum consequat nisl vel pretium lectus. Eget lorem dolor sed viverra. Neque ornare aenean euismod elementum nisi quis eleifend.'
      '\n\nDiam vel quam elementum pulvinar etiam non quam lacus suspendisse. Dui vivamus arcu felis bibendum ut tristique et. Gravida neque convallis a cras semper. Nisl nunc mi ipsum faucibus vitae aliquet. Vitae justo eget magna fermentum. Odio morbi quis commodo odio aenean sed adipiscing. Est ullamcorper eget nulla facilisi etiam dignissim. Dictum sit amet justo donec enim diam vulputate ut pharetra. Consequat id porta nibh venenatis cras sed felis eget. Ut porttitor leo a diam. Ipsum dolor sit amet consectetur adipiscing elit duis tristique sollicitudin. Vulputate enim nulla aliquet porttitor lacus luctus accumsan tortor posuere. Sit amet consectetur adipiscing elit duis tristique sollicitudin nibh sit. Phasellus faucibus scelerisque eleifend donec.'
      '\n\nAc feugiat sed lectus vestibulum mattis ullamcorper velit. Placerat duis ultricies lacus sed turpis tincidunt. Faucibus a pellentesque sit amet. Sagittis vitae et leo duis ut diam. Augue interdum velit euismod in pellentesque massa. At urna condimentum mattis pellentesque. Potenti nullam ac tortor vitae purus. Cursus mattis molestie a iaculis at erat pellentesque adipiscing. Tortor consequat id porta nibh venenatis cras. Sagittis nisl rhoncus mattis rhoncus urna. Elit eget gravida cum sociis natoque penatibus et. Vitae et leo duis ut diam quam. Eu turpis egestas pretium aenean pharetra. Morbi tincidunt ornare massa eget egestas purus. Eget nulla facilisi etiam dignissim diam quis enim lobortis scelerisque.'
      '\n\nFaucibus ornare suspendisse sed nisi lacus sed viverra tellus. Dignissim diam quis enim lobortis scelerisque fermentum. Turpis tincidunt id aliquet risus. Quam adipiscing vitae proin sagittis nisl rhoncus mattis rhoncus urna. Dolor magna eget est lorem ipsum dolor. Nam aliquam sem et tortor consequat id porta nibh venenatis. At augue eget arcu dictum varius duis at consectetur. Felis eget velit aliquet sagittis id. At elementum eu facilisis sed odio. Habitant morbi tristique senectus et netus et malesuada fames ac. Vitae congue eu consequat ac felis donec et odio. Ipsum dolor sit amet consectetur adipiscing elit ut aliquam purus. Non arcu risus quis varius quam quisque id diam. Rhoncus urna neque viverra justo nec ultrices dui sapien eget. Accumsan in nisl nisi scelerisque eu ultrices vitae auctor.'
      '\n\nSed adipiscing diam donec adipiscing tristique risus. Massa vitae tortor condimentum lacinia quis vel eros. Non enim praesent elementum facilisis leo vel fringilla est ullamcorper. Rhoncus dolor purus non enim praesent elementum. Praesent semper feugiat nibh sed pulvinar proin. Nisl condimentum id venenatis a condimentum vitae sapien pellentesque habitant. Sit amet dictum sit amet justo donec enim diam. Consectetur adipiscing elit pellentesque habitant morbi tristique senectus. Et netus et malesuada fames ac turpis. Viverra aliquet eget sit amet tellus cras adipiscing enim. Tristique senectus et netus et. Sed lectus vestibulum mattis ullamcorper velit sed.'
      '\n\nIaculis urna id volutpat lacus. Imperdiet massa tincidunt nunc pulvinar sapien et. Posuere sollicitudin aliquam ultrices sagittis orci a. Eu volutpat odio facilisis mauris sit amet. Scelerisque eleifend donec pretium vulputate sapien nec sagittis aliquam. Sit amet nisl purus in mollis nunc sed id. Maecenas accumsan lacus vel facilisis volutpat est velit. Tortor at risus viverra adipiscing at in tellus. Arcu ac tortor dignissim convallis. Nisi scelerisque eu ultrices vitae auctor eu augue ut lectus. Consectetur adipiscing elit pellentesque habitant morbi tristique senectus. Quis lectus nulla at volutpat. Diam in arcu cursus euismod quis viverra nibh cras pulvinar. Libero id faucibus nisl tincidunt eget. Tellus in hac habitasse platea dictumst vestibulum rhoncus est pellentesque. Odio morbi quis commodo odio aenean sed. Vitae suscipit tellus mauris a diam maecenas. Non pulvinar neque laoreet suspendisse interdum consectetur. Libero nunc consequat interdum varius sit amet. Tincidunt id aliquet risus feugiat in ante.';
}

Демо

Другие вопросы по тегам