22

假设我们有一个像罗密欧与朱丽叶这样的长文本,我们想在一个简单的电子阅读器中呈现它(没有动画,只有页面和自定义字体大小)。有什么方法可以做到这一点?

到目前为止我想出了什么:

  • 使用 css3 列,可以将整个文本加载到内存中,使其样式化,使得单个列占用整个页面的大小。事实证明,这样做非常难以控制,并且需要将整个文本加载到内存中。
  • 使用 css3 区域(任何主流浏览器都不支持)将构成与以前的解决方案相同的基本概念,主要区别在于它不会那么难以控制(因为每个“列”都是一个自包含的元素)。
  • 在画布上绘制文本可以让您准确知道文本的结束位置,从而根据该文本绘制下一页。优点之一是您只需要将所有文本加载到当前页面(仍然很糟糕,但更好)。缺点是无法与文本交互(如选择文本)。
  • 将每个单词放在一个元素中,并为每个元素赋予一个唯一的 id(或在 javascript 中保留一个逻辑引用),接下来用于document.elementFromPoint查找页面上最后一个元素(单词)并从该单词开始显示下一页。尽管这是唯一一个在我看来实际上是现实的,但由此产生的开销必须是巨大的。

然而,这些似乎都不可接受(首先没有给予足够的控制以使其工作,第二个还不支持,第三个很难并且没有文本选择,第四个给出了一个荒谬的开销),所以任何好的方法我还没有想到,或者解决上述方法的一个或多个缺点的方法(是的,我知道这是一个相当开放的问题,但它越开放,产生任何相关答案的机会就越高)?

4

6 回答 6

14

SVG 可能非常适合您的文本分页

  • SVG 文本实际上是文本——与仅显示文本图片的画布不同。

  • SVG 文本是可读的、可选择的、可搜索的。

  • SVG 文本本身不会自动换行,但使用 javascript 很容易解决这个问题。

  • 灵活的页面大小是可能的,因为页面格式是在 javascript 中完成的。

  • 分页不依赖于浏览器的格式。

  • 文本下载小而高效。只需要下载当前页面的文本。

以下是如何进行 SVG 分页的详细信息和一个 Demo:

http://jsfiddle.net/m1erickson/Lf4Vt/

在此处输入图像描述

第 1 部分:从服务器上的数据库中有效地获取大约一页的单词

将整个文本存储在数据库中,每行 1 个单词。

每行(单词)按单词的顺序顺序索引(单词#1 的索引==1,单词#2 的索引==2,等等)。

例如,这将以正确的词序获取整个文本:

// select the entire text of Romeo and Juliet
// “order by wordIndex” causes the words to be in proper order

Select word from RomeoAndJuliet order by wordIndex

如果您假设任何页面在格式化时包含大约 250 个单词,那么此数据库查询将获取第 1 页的前 250 个单词的文本

// select the first 250 words for page#1

Select top 250 word from RomeoAndJuliet order by wordIndex

现在好的部分!

假设第 1 页在格式化后使用了 212 个单词。然后,当您准备好处理 page#2 时,您可以从 word#213 开始再获取 250 个单词。这导致快速有效的数据获取。

// select 250 more words for page#2
// “where wordIndex>212” causes the fetched words
// to begin with the 213th word in the text

Select top 250 word from RomeoAndJuliet order by wordIndex where wordIndex>212

第 2 部分:将获取的单词格式化为适合指定页面宽度的文本行

每行文本必须包含足够的单词来填充指定的页面,但不能更多。

以单个单词开始第 1 行,然后一次添加 1 个单词,直到文本适合指定的页面宽度。

安装第一行后,我们向下移动一个行高并开始第 2 行。

将单词放在一行上需要测量一行上添加的每个额外单词。当下一个单词超出行宽时,多余的单词将移至下一行。

可以使用 Html Canvasescontext.measureText方法测量一个单词。

此代码将采用一组单词(例如从数据库中获取的 250 个单词),并将格式化尽可能多的单词以填充页面大小。

maxWidth是一行文本的最大像素宽度。

maxLines是适合一页的最大行数。

function textToLines(words,maxWidth,maxLines,x,y){

    var lines=[];

    while(words.length>0 && lines.length<=maxLines){
        var line=getOneLineOfText(words,maxWidth);
        words=words.splice(line.index+1);
        lines.push(line);
        wordCount+=line.index+1;
    }

    return(lines);
}

function getOneLineOfText(words,maxWidth){
    var line="";
    var space="";
    for(var i=0;i<words.length;i++){
        var testWidth=ctx.measureText(line+" "+words[i]).width;
        if(testWidth>maxWidth){return({index:i-1,text:line});}
        line+=space+words[i];
        space=" ";
    }
    return({index:words.length-1,text:line});
}

第 3 部分:使用 SVG 显示文本行

SVG 文本元素是一个真正的 html 元素,可以阅读、选择和搜索。

SVG Text 元素中的每一行文本都使用 SVG Tspan 元素显示。

此代码采用在第 2 部分中格式化的文本行,并使用 SVG 将这些行显示为文本页面。

function drawSvg(lines,x){
    var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    var sText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    sText.setAttributeNS(null, 'font-family', 'verdana');
    sText.setAttributeNS(null, 'font-size', "14px");
    sText.setAttributeNS(null, 'fill', '#000000');
    for(var i=0;i<lines.length;i++){
        var sTSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
        sTSpan.setAttributeNS(null, 'x', x);
        sTSpan.setAttributeNS(null, 'dy', lineHeight+"px");
        sTSpan.appendChild(document.createTextNode(lines[i].text));
        sText.appendChild(sTSpan);
    }
    svg.appendChild(sText);
    $page.append(svg);
}

这是完整的代码,以防 Demo 链接中断:

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
    body{ background-color: ivory; }
    .page{border:1px solid red;}
</style>
<script>
$(function(){

    var canvas=document.createElement("canvas");
    var ctx=canvas.getContext("2d");
    ctx.font="14px verdana";

    var pageWidth=250;
    var pageHeight=150;
    var pagePaddingLeft=10;
    var pagePaddingRight=10;
    var approxWordsPerPage=500;        
    var lineHeight=18;
    var maxLinesPerPage=parseInt(pageHeight/lineHeight)-1;
    var x=pagePaddingLeft;
    var y=lineHeight;
    var maxWidth=pageWidth-pagePaddingLeft-pagePaddingRight;
    var text="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.";

    // # words that have been displayed 
    //(used when ordering a new page of words)
    var wordCount=0;

    // size the div to the desired page size
    $pages=$(".page");
    $pages.width(pageWidth)
    $pages.height(pageHeight);


    // Test: Page#1

    // get a reference to the page div
    var $page=$("#page");
    // use html canvas to word-wrap this page
    var lines=textToLines(getNextWords(wordCount),maxWidth,maxLinesPerPage,x,y);
    // create svg elements for each line of text on the page
    drawSvg(lines,x);

    // Test: Page#2 (just testing...normally there's only 1 full-screen page)
    var $page=$("#page2");
    var lines=textToLines(getNextWords(wordCount),maxWidth,maxLinesPerPage,x,y);
    drawSvg(lines,x);

    // Test: Page#3 (just testing...normally there's only 1 full-screen page)
    var $page=$("#page3");
    var lines=textToLines(getNextWords(wordCount),maxWidth,maxLinesPerPage,x,y);
    drawSvg(lines,x);


    // fetch the next page of words from the server database
    // (since we've specified the starting point in the entire text
    //  we only have to download 1 page of text as needed
    function getNextWords(nextWordIndex){
        // Eg: select top 500 word from romeoAndJuliet 
        //     where wordIndex>=nextwordIndex
        //     order by wordIndex
        //
        // But here for testing, we just hardcode the entire text 
        var testingText="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.";
        var testingWords=testingText.split(" ");
        var words=testingWords.splice(nextWordIndex,approxWordsPerPage);

        // 
        return(words);    
    }


    function textToLines(words,maxWidth,maxLines,x,y){

        var lines=[];

        while(words.length>0 && lines.length<=maxLines){
            var line=getLineOfText(words,maxWidth);
            words=words.splice(line.index+1);
            lines.push(line);
            wordCount+=line.index+1;
        }

        return(lines);
    }

    function getLineOfText(words,maxWidth){
        var line="";
        var space="";
        for(var i=0;i<words.length;i++){
            var testWidth=ctx.measureText(line+" "+words[i]).width;
            if(testWidth>maxWidth){return({index:i-1,text:line});}
            line+=space+words[i];
            space=" ";
        }
        return({index:words.length-1,text:line});
    }

    function drawSvg(lines,x){
        var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
        var sText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
        sText.setAttributeNS(null, 'font-family', 'verdana');
        sText.setAttributeNS(null, 'font-size', "14px");
        sText.setAttributeNS(null, 'fill', '#000000');
        for(var i=0;i<lines.length;i++){
            var sTSpan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
            sTSpan.setAttributeNS(null, 'x', x);
            sTSpan.setAttributeNS(null, 'dy', lineHeight+"px");
            sTSpan.appendChild(document.createTextNode(lines[i].text));
            sText.appendChild(sTSpan);
        }
        svg.appendChild(sText);
        $page.append(svg);
    }

}); // end $(function(){});
</script>
</head>
<body>
    <h4>Text split into "pages"<br>(Selectable & Searchable)</h4>
    <div id="page" class="page"></div>
    <h4>Page 2</h4>
    <div id="page2" class="page"></div>
    <h4>Page 3</h4>
    <div id="page3" class="page"></div>
</body>
</html>
于 2014-05-09T04:37:44.987 回答
9

See my answer to Wrap text every 2500 characters in a for pagination using PHP or javascript. I ended up with http://jsfiddle.net/Eric/WTPzn/show

Quoting the original post:

Just set your HTML to:

<div id="target">...</div>

Add some css for pages:

#target {
    white-space: pre-wrap; /* respect line breaks */
}
.individualPage {
    border: 1px solid black;
    padding: 5px;    
}

And then use the following code:

var contentBox = $('#target');
//get the text as an array of word-like things
var words = contentBox.text().split(' ');

function paginate() {
    //create a div to build the pages in
    var newPage = $('<div class="individualPage" />');
    contentBox.empty().append(newPage);

    //start off with no page text
    var pageText = null;
    for(var i = 0; i < words.length; i++) {
        //add the next word to the pageText
        var betterPageText = pageText ? pageText + ' ' + words[i]
                                      : words[i];
        newPage.text(betterPageText);

        //Check if the page is too long
        if(newPage.height() > $(window).height()) {
            //revert the text
            newPage.text(pageText);

            //and insert a copy of the page at the start of the document
            newPage.clone().insertBefore(newPage);

            //start a new page
            pageText = null;
        } else {
            //this longer text still fits
            pageText = betterPageText;             
        }
    }    
}

$(window).resize(paginate).resize();
于 2012-08-30T20:45:48.630 回答
3

我有一个非常简单、可变的 css 标记和 3 个非常短的 js 函数的解决方案。

首先,我创建了两个 div 元素,其中一个隐藏但包含整个文本,另一个显示但为空。HTML看起来像这样:

<div id="originalText">
some text here
</div>
<div id="paginatedText"></div>

这两个的CSS是:

#originalText{
    display: none; // hides the container
}

#paginatedText{
    width: 300px;
    height: 400px;
    background: #aaa;
}

我还为类名页面准备了 css,如下所示:

.page{
    padding: 0;
    width: 298;
    height: 398px; // important to define this one
    border: 1px solid #888;
}

真正重要的部分是定义高度,否则当我们稍后填写单词时页面只会被拉伸。


现在是重要的部分。JavaScript函数。评论应该不言自明。

function paginateText() {
    var text = document.getElementById("originalText").innerHTML; // gets the text, which should be displayed later on
    var textArray = text.split(" "); // makes the text to an array of words
    createPage(); // creates the first page
    for (var i = 0; i < textArray.length; i++) { // loops through all the words
        var success = appendToLastPage(textArray[i]); // tries to fill the word in the last page
        if (!success) { // checks if word could not be filled in last page
            createPage(); // create new empty page
            appendToLastPage(textArray[i]); // fill the word in the new last element
        }
    }
}

function createPage() {
    var page = document.createElement("div"); // creates new html element
    page.setAttribute("class", "page"); // appends the class "page" to the element
    document.getElementById("paginatedText").appendChild(page); // appends the element to the container for all the pages
}

function appendToLastPage(word) {
    var page = document.getElementsByClassName("page")[document.getElementsByClassName("page").length - 1]; // gets the last page
    var pageText = page.innerHTML; // gets the text from the last page
    page.innerHTML += word + " "; // saves the text of the last page
    if (page.offsetHeight < page.scrollHeight) { // checks if the page overflows (more words than space)
        page.innerHTML = pageText; //resets the page-text
        return false; // returns false because page is full
    } else {
        return true; // returns true because word was successfully filled in the page
    }
}

最后我只是paginateText

paginateText();

整个脚本适用于每个文本和页面的每种样式。

因此,您可以更改字体和字体大小,甚至页面的大小。

我也有一个jsfiddle 里面的所有东西。

如果我忘记了什么或者您有任何问题,请随时发表评论并提出建议或提出问题。

于 2014-05-13T19:42:51.643 回答
3

我还没有足够的代表发表评论,但我只想说 Eric 的回答效果很好。我正在创建一个电子阅读器,除了它读取 HTML 文件,您可以将它用于未准备好发布的文本。有两个页面可以看到,它们只有在您按下按钮时才会调整大小。

我做了很多修改。不过,我只发现了一个小缺陷。当您检查最后一个单词是否从页面边缘掉下来时,您需要将该单词添加回列表中。简单地说,在 if 语句的第一种情况下,放入 i--; 行。为了回去把那个词放在下一页。

这是我的修改:

  1. 用参数(内容,目标)把它全部变成一个函数。
  2. 添加了一个变量 backUpContent,以便在我调整页面大小时重用。
  3. 将 newPage 更改为不可见的 testPage 并添加了一个数组 page[i],其中包含每个页面的内容,以便在排序页面后轻松来回。
  4. 在 else 语句的第一部分添加了行“pC++;”,一个页面计数器。
  5. 将 .text 更改为 .html,这样它就不会将标签视为其文本等价物。
  6. 我围绕 1 或 2 个内容变化的 div 设计了它,而不是许多隐藏和显示的 div。
  7. 还有更多的插入我还没有得到。

如果您想将整段内容保留在同一页面上,请更改行

pageText + ' ' + words[i]

pageText + '</p><p>' + words[i]

和线

words = content.split(' ');

words = content.split('</p><p>');

但是,只有当您确定每个这样的元素都小到可以放在一页上时,您才应该使用它。

埃里克的解决方案正是我所缺少的。我本来打算问自己的问题,但在输入了几乎所有问题后,我终于在建议中找到了这个页面。不过,这个问题的措辞有点令人困惑。

谢谢埃里克!

于 2014-10-27T01:06:03.713 回答
2

另一个想法是使用CSS 列来拆分 html 内容,这个重排是由浏览器自己完成的,所以它会非常快,下一步是将每个页面内容插入到 dom 中,我通过复制整个列并滚动每个页面来做到这一点裁剪的窗口,请参见 codepen 示例:

https://codepen.io/alphakevin/pen/eXqbQP

const pageWidth = 320;
const content = document.getElementById('content');
const totalWidth = content.scrollWidth;
const totalPages = totalWidth / pageWidth;
console.log('totalPages', totalPages);

let contentVisible = true;
const button = document.getElementById('btn-content');
const buttonText = document.getElementById('btn-content-text');
const showHideContent = () => {
  contentVisible = !contentVisible;
  content.style.display = contentVisible ? 'block' : 'none';
  buttonText.innerText = contentVisible ? 'Hide' : 'Show';
}
button.addEventListener('click', showHideContent);

const html = content.innerHTML;
const container = document.getElementById('container');
// console.log('content', content);
for (let p = 0; p < totalPages; p++) {
  const page = document.createElement('div');
  page.innerHTML = html;
  page.className = 'page';
  page.style.cssText = `
    width: ${totalWidth}px;
    transform: translateX(-${p * pageWidth}px);
  `;
  const pageClip = document.createElement('div');
  pageClip.className = 'page-clip';
  pageClip.appendChild(page);
  const pageWrapper = document.createElement('div');
  pageWrapper.className = 'page-wrapper';
  pageWrapper.appendChild(pageClip);
  container.appendChild(pageWrapper);
}

showHideContent();

这非常适合少量分页内容,但不适用于大内容,您会得到很多永远不会显示的浪费的 DOM 元素。

但我认为必须有更好的想法,比如结合其他答案,使用 javascript 来帮助拆分列结果。

作为参考,请查看分页媒体解决方案

https://codepen.io/julientaq/pen/MBryxr

于 2019-05-15T02:48:42.467 回答
-5

这很简单,不需要 javascript。从CSS2paged media type开始支持。有关受支持的属性,请参阅http://www.w3.org/TR/CSS21/page.html(或当前 CSS3 模块)。

于 2012-08-30T17:38:31.913 回答