1.はじめに
GIGAスクール構想で児童・生徒が使うChromebook用に、縦書き原稿用紙の機能をウェブで提供しようと開発した作文ウェブアプリ「sakubun」。これまで、初回に原稿用紙をどうやって実現したか、2回目には作成した原稿の保存・ダウンロード機能の実装をご紹介してきました。3回目はページネーションをどうやって実装したかをご紹介します。
2.ページネーションとは
ページネーションとは、ページ割りやページ送りのことで(正直、実装の必要性を感じるまでそんな言葉があることも知りませんでしたが……)、Googleで検索すると一番下に出る数字でページが選べるやつです。
原稿用紙というとやはり縦20字詰め、横20行の計400字詰めが全国標準なので、400字詰め原稿用紙1枚分を1ページにしようと思います。
3.なぜページネーション
そもそもなぜページネーションを実装しようと思ったのかといいますと、1回目の記事でご紹介した通り、原稿用紙のマス目をCSSで描いているのですが、横スクロールすると、マス目の表示が途切れてしまうという問題があったからです。このCSSをご紹介されていた懐かしの作文をCSSグラデーションで表現するで筆者のMasaki Osumiさんもご指摘されています。
そこで、開発当初は、起動時にbodyタグに30000pxという十分に長いwidthを与え、あらかじめ長い行数を確保しておくという泥縄的手法でごまかしていました。でもよく考えてみれば、ユーザーが長い原稿を書き、横幅が30000px以上に達すると当然、途切れてしまうわけです。
誰がどのように長い作文を打ち込んでも使えるアプリにしたい。そこで、原稿用紙が400字を超えた段階で次の原稿用紙が新しく自動生成され、ページめくりも行われるようにすれば、使いやすいものになるのでないかとページネーションの導入を思い立ったというわけです。
4.縦書きゆえの難点
縦書きですので、ページは当然、下ではなく左に伸びます。railsのkaminari、jQueryのpaginathing.jsなど既存のライブラリで使えるものがないかいろいろリサーチしましたが、縦書き用には使えそうもありません(もしかしたら使い方を探すことができなかっただけかもしれませんが……)。そこで一から自分で作ってみることにしました。
いざ自分で作ろうにも、何もないところからでは無理です。左に伸びる横長のものを、左に右にページを順番にめくれるようにしなければいけません。そこで参考にしたのは、スライドショーです。
5.ページネーションというスライドショー
画像をネガフィルム(デジタルカメラ世代にはなじみないかも)のように横一列に並べ、紙芝居のように表示枠の下を左右にスライドさせることで、順番に表示するスライドショー。これを原稿用紙に取り入れ、画面には400字詰め原稿用紙をちょうど表示できるだけの枠を設け、その下に従来の横長の原稿用紙を重ね、左右に20行ずつスライドすることでページ送りを実現します。
さらに何行まで原稿を打ち込んだかを変数で管理し、現在の原稿用紙枠から文字があふれた瞬間に自動的に左に新たに原稿を自動増幅してページ送りするようにJavaScriptを使って実装します。
6.実装へ
まず.sakubun
のdiv
の上に400字詰め原稿用紙の枠となる.frame
のdiv
をかぶせます。
<div class="sakubun">
<p id="article" contenteditable="true"></p>
</div>
↓
<div class="frame">
<div class="sakubun">
<p id="article" contenteditable="true"></p>
</div>
</div>
続いて原稿枠となる.frame
のサイズを求めます。
フォントサイズ28px、文字間0.5em(14px)、行間2em(56px)なので、
heght:(28px20字)+(14px20)+α =872px
width:(行間28px×20字) =1120px
親divとなる.frame
のCSSにposition:relative
を指定して配置のベースとし、子divの.sakubun
のCSSにposition:absolute
を指定して絶対配置します。縦書き原稿は右から左に書き進められるので、.frame
の右上を原点として.sakubun
の右端からの座標(right)をマイナス1120pxすることでページ送りを実現できるはずです。
.frame {
width: 1120px;
height: 872px;
position: relative;
overflow: hidden;
}
.sakubun {
width: 100%;
height: 100%;
overflow-x: scroll;
overflow-y: clip;
overflow-wrap: break-word;
font-family: "ヒラギノ明朝 ProN W3", "HiraMinProN-W3", "HG明朝E", "MS P明朝", "MS PMincho", "MS 明朝", serif;
font-size: 28px;
color: #333;
position: absolute;
letter-spacing: .5em;
line-height: 2em;
padding: 5px;
float: right;
-ms-writing-mode: tb-rl;
-webkit-writing-mode: vertical-rl;
writing-mode: vertical-rl;
}
JavaScriptで.sakubun
の要素を取得し、sakubun.style.right -= 1120 + 'px';
でrigtプロパティを1120引いたり足したりすれば左右にスライドすると考えたのですが、最初はうまくいかず、見事にはまりました。
6.1.はまったstyleプロパティ
はまりどころというのは、原因がシンプルなほどはまります。まず、JavaScriptでCSSのstyleを操作しようとしたのですが、style.right
はそもそも単位(px)のついた文字列なので、数値に直してから計算しないといけません。
さらに、styleプロパティ
を使ってJavaScriptでCSSを動的に変更しようとしたのですが、なぜかrightプロパティ
をうまく制御できません。MDN Web Docs「HTMLElement.style」によると、「要素のインラインのstyle属性で定義」と書いてあったので、HTMLの.sakubun
のdivタグにstyle="right: 0px"
とrightプロパティの初期値をstyle属性で記述したところ、うまく制御できるようになりました。
<div class="frame">
<div class="sakubun" style="right: 0px">
<p id="article" contenteditable="true"></p>
</div>
</div>
<div class="pagination">
<p class="next"><</p>
<p class="page_counter"></p>
<p class="prev">></p>
</div>
let current = 1; // 現在のページ番号
let latest = 1; // 更新中のページ番号
let start = 0; // 更新中ページの開始px
let end = 1120; // 更新中ページの終了px
const sakubun = document.querySelector('.sakubun');
const article = document.getElementById('article');
const next_btn = document.querySelector('.next');
const prev_btn = document.querySelector('.prev');
const page_counter = document.querySelector('.page_counter');
function moveRight() {
sakubun.style.right = ( parseInt( sakubun.style.right, 10 ) + 1120 ) + 'px';
current--;
displayPage();
}
function moveLeft() {
sakubun.style.right = ( parseInt( sakubun.style.right, 10 ) - 1120 ) + 'px';
current++;
displayPage();
}
next_btn.addEventListener('click', () => {
moveLeft();
});
prev_btn.addEventListener('click', () => {
moveRight();
});
// ページ数表示
function displayPage() {
if( current === 1 ) {
prev_btn_disable();
if( latest === 1 ) {
next_btn_disable();
} else {
next_btn_active();
}
} else if( current === latest ) {
prev_btn_active();
next_btn_disable();
} else {
next_btn_active();
prev_btn_active();
}
page_counter.textContent = current + '/' + latest;
}
// class付与・削除関数
prev_btn_active = () => {
prev_btn.classList.remove('disable');
}
prev_btn_disable = () => {
prev_btn.classList.add('disable');
}
next_btn_disable = () => {
next_btn.classList.add('disable');
}
next_btn_active = () => {
next_btn.classList.remove('disable');
}
6.2.原稿用紙よ更新せよ
次はページネーションの肝でもある原稿用紙が400字いっぱいになったら自動的に次の原稿用紙に切り替わる機能の実装です。
原稿が打ち込まれるcontenteditable属性の付いたpタグ(id="article"
)のイベントを拾い、JavaScriptでpタグのoffsetWidth
を監視します。もし更新中のページの範囲(変数end
)を超えたら原稿用紙の左に新たにマス目20行分を増やし、画面も追加した新ページに移行します。そのページの範囲(変数start
)を下回ったら1ページを消し、前のページに戻ります。
原稿用紙のマス目を増やしたり減らしたりする作業は、styleプロパティ
を使ってJavaScriptで.sakubun
のwidthプロパティ
の値を100%、200%、300%……と100%刻みで増やしたり減らしたりして実現します。
function updatePage() {
sakubun.style.width = ( latest * 100 ) + '%';
}
sakubun.addEventListener('input', () => {
if ( article.offsetWidth > end ) {
latest++;
start = ( latest - 1 ) * 1120;
end = latest * 1120;
updatePage();
moveLeft();
} else if ( article.offsetWidth < start ) {
latest--;
start = ( latest - 1 ) * 1120;
end = latest * 1120;
updatePage();
moveRight();
}
});
// ページ読み込み時の初期設定
function setPage() {
latest = Math.ceil( article.offsetWidth/1120 );
start = ( latest - 1 ) * 1120;
end = latest * 1120;
sakubun.style.width = ( latest * 100 ) + '%';
}
// DOMの構築が完了したタイミングでページネーション発火
window.addEventListener('load', () => {
setPage();
displayPage();
article.focus();
});
7.終わりに
ページネーションを実装した今回も縦書きゆえの困難に遭遇しました。困難に出合うたびに思うのは、WEBの世界をはじめ情報通信の世界では横書きが当然で、縦書きは廃れていってしまうのではという危機感です。
そして縦書き文化の担い手でもある日本語教育の現場にも、GIGAスクールによる横書き主流の波が押し寄せています。日本語文化を守る一助になればと思っています。