Web上に付箋を絵馬のように貼って並べる、フレームワーク非依存のTypeScriptライブラリ。
付箋の作成・編集・自由配置・接続線描画を提供する。1ファイルで完結するスタンドアロンHTML版もあり、ブラウザで開くだけで使える。
wema を使った Wiki に wema-kake(npm: @kanf/wema-kake)がある。保存と同期まで含めて付箋のボードを使いたいなら、wema-kake で Wiki として使うを参照。
デモページでそのまま使うか、Releases から wema.html をダウンロードしてブラウザで開く。
- 付箋の追加・編集・ドラッグ移動・リサイズ・削除
- 接続線(Edge)の作成・スタイル編集
- 接続線を空白にドロップ → そこに新しい付箋を自動作成して接続
- 部分木の折り畳み → 付箋にカーソルを乗せると折り畳みボタン(−)が出現。クリックで全ての接続先とその子孫を一括非表示。数字バッジをクリックで再展開
- リッチテキスト(太字、取り消し線、テキスト色、箇条書き、チェックボックス、リンク、画像、Embed)
- リストの段下げと段上げ(Tab / Shift+Tab)
- 引用 → 付箋の文字を選ぶと出るツールバーの引用ボタン(❞)で、選んだ文字を引用した付箋を隣に作り、元の付箋から矢印でつなぐ
- 複数選択・整列・均等配置・自動レイアウト
- Undo/Redo(Ctrl+Z / Ctrl+Y)
- autoSize モード(コンテンツに合わせて付箋サイズを自動調整)
- 長い付箋を畳んで表示(3 行以上の付箋は冒頭だけを見せ、「続きを読む」で開く)
- リサイズハンドルのダブルクリックで、付箋をコンテンツに合うサイズへ 1 回だけ変更
- 5色のカラーパレット
- IndexedDB による自動保存
- JSON エクスポート/インポート
wema-kake は、wema を使った Wiki。1 ページが 1 枚のボードで、付箋を置き、線でつなぎ、ページの中に子ページを置いて整理する。Cloudflare Workers の上で動き、複数のブラウザの間で同期し、Claude などの LLM からも MCP で読み書きできる。
wema-kake も npm パッケージ(@kanf/wema-kake)として配っている。雛形から自分のリポジトリを作り、自分の Worker としてデプロイする。
npx @kanf/wema-kake init my-wiki
cd my-wiki
npm run devデプロイと Cloudflare Access の設定の手順は、wema-kake の README にある。
wema はブラウザの中だけで動き、保存先や同期の仕組みを持たない。wema-kake は、そこに次のものを足している。
| wema が受け持つ | wema-kake が足す |
|---|---|
| 付箋と接続線の表示と編集 | ページという単位と、ページ間のリンク、ページの階層 |
| Undo / Redo、パンとズーム | サーバーへの保存と、複数のブラウザの間の同期(リアルタイム同期の API を使う) |
| 変更の内容を表すデータ(デルタ) | 全文検索、画像の保存、認証 |
| LLM 向けの MCP サーバーと、その操作の取り消し |
wema を自分のアプリに組み込むときの実例としても読める。
npm install @kanf/wemaimport { WemaBoard } from '@kanf/wema';
import '@kanf/wema/style.css';
const board = new WemaBoard({
container: document.getElementById('board'),
});
// 付箋を追加
const note = board.addNote({ x: 100, y: 80, text: 'Hello!' });
// 接続線を追加
const note2 = board.addNote({ x: 400, y: 80, text: 'World!' });
board.addEdge(note.id, note2.id);
// データをエクスポート
const data = board.exportData();
// イベントを購読
board.on('change', ({ data }) => {
console.log('Board changed:', data);
});const board = new WemaBoard({
container: HTMLElement, // マウント先
data?: WemaBoardData, // 初期データ
defaultNoteWidth?: number, // default: 200
defaultNoteHeight?: number, // default: 150
defaultNoteColor?: string, // default: '#FFF9C4'
createOnDblClick?: boolean, // default: true
readOnly?: boolean, // default: false
viewOnly?: boolean, // default: false
theme?: NoteTheme, // default: 'default' ('default' | 'card')
onImageUpload?: (file: File) => Promise<string>, // 画像のアップロード先 URL を返す
onLinkClick?: (url: string, event: MouseEvent) => boolean | void, // 付箋内のリンクのクリックを処理する
renderNote?: (note: WemaNote, container: HTMLElement) => boolean | void, // 付箋の中身を利用側が描く
renderNoteAccessory?: (note: WemaNote, container: HTMLElement) => boolean | void, // 付箋のすぐ下に、利用側の小さな要素(印やボタン)を並べる
renderNoteFooter?: (note: WemaNote, container: HTMLElement) => boolean | void, // 縦に積んでいる間、付箋の下に利用側の要素を描く
noteLinks?: boolean | { text?: (peer: WemaNote, edge: WemaEdge) => string | undefined | void, padding?: number }, // default: false(縦に積んでいる間、つながっている付箋を付箋の下に出す)
labels?: Partial<WemaLabels>, // 画面の文言(ボタンの説明、接続線のポップアップの見出しなど)。省略したキーは英語
foldLabels?: { more?: string, less?: string }, // 畳んだ付箋を開閉するリンクの文言。labels の readMore / showLess と同じもの
wheelPan?: boolean, // default: true(ホイールで表示位置を動かす)
wheelZoom?: boolean, // default: true(Ctrl / Cmd + ホイールで拡大・縮小する)
emptyDrag?: 'select' | 'pan', // default: 'select'(空いている場所の左ドラッグ。'pan' でパン、選択は Shift か Ctrl / Cmd + ドラッグ)
panMargin?: number, // default: 200(操作でパンできる範囲。付箋の外側に見せる余白のピクセル数)
pageScroll?: boolean, // default: false(ボードをページと一緒にスクロールさせる。狭い画面向け)
stacked?: { x?, y?, width?, gap?, order? }, // 付箋を縦 1 列に積んで表示する(データは変えない)
minZoom?: number, // default: 0.25
maxZoom?: number, // default: 2
});onImageUpload を指定すると、付箋に挿入する画像を data URL で埋め込まず、返された URL の <img> として挿入する。アップロードが完了してから挿入し、失敗したときは挿入せずに image:error イベントを発火する。未指定のときは従来どおり data URL で埋め込む。
付箋内のリンクは、クリックすると新しいタブで開く。onLinkClick を指定すると、クリックのたびにリンク先の URL とクリックイベントを渡して呼び出す。true を返した場合は新しいタブを開かないので、同じタブでの遷移などを利用側で行える。それ以外を返した場合は、指定しないときと同じく新しいタブで開く。
const board = new WemaBoard({
container,
onLinkClick: (url) => {
const target = new URL(url);
if (target.origin !== location.origin) return false; // サイト外は新しいタブで開く
router.push(target.pathname + target.search + target.hash);
return true;
},
});- 渡される URL は、ブラウザが解決した絶対 URL(
href属性の値そのままではない)。サイト内のリンクかどうかは、オリジンを比べて判定すること。//other.example/xのように/で始まっていても別サイトを指す書き方があるので、文字列の先頭では判定できない - 呼び出されるのは、URL の安全性の検査(
http/https/mailto/tel、および相対リンク)を通ったリンクだけ。検査を通らないリンクは、クリックしても何も開かない - 新しいタブで開くときも、同じ解決済みの URL を使う
- ブラウザ既定の遷移は wema が止めている(
event.defaultPreventedはtrue) - readOnly / viewOnly でも呼び出される
wema が描く UI の文言は、何も指定しなければ英語で出る。対象は、付箋のポップアップ、接続線のポップアップ、文字のツールバー、画像の上のボタン、畳んだ付箋の開閉のリンク。labels オプションで差し替えられる。日本語の文言は jaLabels として同梱している。
import { WemaBoard, jaLabels } from '@kanf/wema';
const board = new WemaBoard({ container, labels: jaLabels });一部だけを変えるときは、変えるキーだけを渡す。渡さなかったキーは英語のまま出る。
const board = new WemaBoard({
container,
labels: {
...jaLabels,
duplicate: 'コピーを作る',
deleteNotes: (count) => `選んだ ${count} 枚を削除`, // 数の入る文言は関数で渡す
},
});- キーの一覧は、型
WemaLabelsにある。英語の文言はenLabelsとして取り出せる。自分で訳を持つときは、const labels: WemaLabels = { ... }と型を付けておくと、wema が文言を足したときに、訳していないキーが型エラーになる - アイコンだけのボタンには、同じ文言を
title(ポインタを乗せたときの説明)とaria-label(読み上げ)の両方に入れる - 文言は HTML として解釈しない
- 読むのは、ボードを作るときの 1 回だけ。作ったあとで言語を切り替えるには、ボードを作り直す
- どの言語にするかは、利用側が決めて渡す。wema は、ブラウザの言語設定を見ない
- 畳んだ付箋のリンクの文言(
readMore/showLess)は、foldLabelsオプションでも指定できる。両方を渡したときはfoldLabelsが使われる - UMD 版では
Wema.jaLabels/Wema.enLabels
| メソッド | 説明 |
|---|---|
addNote(params?) |
付箋を追加 |
updateNote(id, params) |
付箋を更新 |
deleteNote(id) |
付箋を削除 |
getNote(id) |
IDで取得 |
getNotes() |
全付箋を取得 |
refreshNote(id) |
renderNote オプションで描いた付箋と、renderNoteAccessory / renderNoteFooter オプションで付箋の下に描いた要素を描き直す。noteLinks オプションでは、その付箋と、つながっている付箋のボタンを描き直す |
resizeNotesToContent(noteIds) |
付箋をコンテンツに合うサイズへ 1 回だけ変更する(autoSize は変えない) |
付箋の foldable を true にすると、本文が 3 行以上のときに冒頭だけを表示する。1 行目と 2 行目をそのまま見せ、3 行目は薄くして、その上に続きを開くリンクを重ねる。開くと、リンクは本文の下へ移り、畳み直すリンクに変わる。2 行以下の付箋は、ふつうの付箋と同じ表示になる。
const board = new WemaBoard({
container,
labels: { readMore: '続きを読む', showLess: '折り畳む' }, // リンクの文言(jaLabels にも入っている)
});
board.addNote({ text: '長い本文…', foldable: true });
board.updateNote(noteId, { foldable: true }); // あとから切り替える付箋のポップアップにも、切り替えのボタンがある。
- 行数は、表示上の行で数える。改行がなくても、付箋の幅で折り返して 3 行以上になれば畳む
foldableの付箋の高さは、表示している内容から決まる(閉じているときは約 3 行分、開いているときは本文の全体)。heightは wema が計測して書き込む値になり、updateNote()やリサイズハンドルで指定できるのは幅だけになる。接続線、整列、自動レイアウトは、その時点の表示の大きさに従う- 開いているかどうかは、表示だけの状態。 データには入らない。
note:updateとhistory:commitを出さず、Undo の対象外。ボードを読み込んだ直後は、すべて閉じている。ロックモードと参照モードでも開閉できる - 開閉で高さが変わると
note:measureとchangeが発火する。changeのdataのheightは、その時点の表示の高さになる - 本文を編集しているあいだは、閉じている付箋も一時的に開く。フォーカスが外れると元の状態へ戻る
foldableをfalseに戻すと、高さは本文の全体が収まる値になる(非表示の付箋は計測できないので、そのときの高さのまま残る)renderNoteで利用側が描いた付箋は畳まない- 「部分木の折り畳み」(接続線の
collapsed。接続先の付箋を隠す)とは別の機能
付箋の meta は、利用側が自由に使える項目。wema は中身を読まず、保存してそのまま返す。本文(text)とは別なので、本文を編集しても変わらない。
const note = board.addNote({ text: '子ページ', meta: { page: 'child-1' } });
board.getNote(note.id)?.meta; // { page: 'child-1' }
board.updateNote(note.id, { meta: { page: 'child-2' } }); // 全体を置き換える
board.updateNote(note.id, { meta: undefined }); // 取り除く- 値は文字列だけ(
Record<string, string>)。文字列以外の値は保存時に落とす。入れ子のデータを持たせたいときは、利用側で JSON の文字列にする - 返される
metaは凍結されている(書き換えると例外になる)。変えるときは、新しいオブジェクトをupdateNote()に渡す。updateNote()はmetaの全体を置き換える(キーごとのマージはしない) getNote()/getNotes()/exportData()/importData()、付箋のイベント、history:commitのデルタ、applyRemote()、Undo / Redo を通して値が保たれる- 中身が同じ
metaを渡した更新は、変更として扱わない(履歴にもhistory:commitにも載らない) - 付箋を複製しても
metaは引き継がない。 複製した付箋は、本文、色、大きさだけを引き継ぐ - 大きさと形の上限は設けていない。同期する場合は、受け取る側(サーバー)で検証すること
renderNote を指定すると、付箋ごとに呼び出す。container に DOM を作って true を返すと、その付箋は本文の代わりに container の中身を表示する。それ以外を返した付箋は、ふつうの付箋のまま本文を表示する。
const board = new WemaBoard({
container,
renderNote: (note, el) => {
const slug = note.meta?.page;
if (!slug) return false; // ふつうの付箋
const title = document.createElement('strong');
title.textContent = pages.get(slug)?.title ?? slug;
const open = document.createElement('button');
open.textContent = '開く';
open.addEventListener('click', () => router.push(`/p/${slug}`));
el.append(title, open);
return true;
},
});
// 付箋のデータ以外(ページの表示名など)が変わったら、描き直しを頼む
board.refreshNote(noteId);- 呼び出すのは、付箋を作ったとき(
importData()、applyRemote()、Undo での復活を含む)、付箋のtextまたはmetaが変わったとき、refreshNote(id)を呼んだとき。移動、リサイズ、色の変更では呼ばない containerは、呼び出しのたびに空にしてから渡す。前回付けたイベントリスナーは要素ごと消える- 利用側が描いた付箋は、本文を編集できない。ポップアップにも、本文の書式(リスト、画像、埋め込み)のボタンを出さない。本文(
text)はデータに残り、exportData()はそのまま返す - それ以外はふつうの付箋と同じ。移動、リサイズ、選択、削除、接続線、整列、自動レイアウト、絞り込み、折り畳みの対象になる
containerの中をドラッグすると、付箋が動く。リンク、ボタン、入力欄(a/button/input/select/textarea/label/summary)と、data-wema-no-drag属性を付けた要素の上では、ドラッグを始めないcontainerの中のクリックは、付箋の選択にもなる(ポップアップが出る)。出したくないときは、利用側のハンドラでevent.stopPropagation()を呼ぶcontainerの中身は、wema のサニタイズを通さない。 他人が書いた文字列(metaや本文を含む)を出すときは、innerHTMLではなくtextContentを使うこと- readOnly / viewOnly でも呼び出し、中のボタンやリンクは押せる
containerの中の入力欄(input/textarea/select、contenteditableの要素)にフォーカスがあるあいだ、ボードのキーボードショートカット(Delete、Undo / Redo、Space)は働かないrenderNoteが例外を投げたときは、コンソールにエラーを出し、その付箋をふつうの付箋として表示する。付箋の作成やimportData()は止まらないrefreshNote()はデータを変えない。イベントも履歴も出さない
付箋の本文はそのままにして、付箋のすぐ下に、印やボタンを置ける。作者の印、「いいね」のボタンと数のように、付箋のデータの外にあるものを付箋の近くで見せるために使う。container に DOM を入れて true を返すと表示する。それ以外を返した付箋には、何も置かない。
const board = new WemaBoard({
container,
renderNoteAccessory: (note, el) => {
const info = infoOf(note.id); // 利用側が、付箋の id ごとに持っている値
if (!info) return false; // 何も置かない
if (info.byAgent) {
const badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = 'AI';
el.append(badge);
}
const like = document.createElement('button');
like.dataset.wemaFocusKey = 'like'; // 描き直しても、フォーカスを残す
like.textContent = `♥ ${info.likes}`;
like.addEventListener('click', () => sendLike(note.id));
el.append(like);
return true;
},
});
// 「いいね」の数が届いたら、この領域だけを描き直す
board.refreshNoteAccessory(noteId);| メソッド | 説明 |
|---|---|
refreshNoteAccessory(id) |
renderNoteAccessory で描いた要素だけを描き直す。renderNote と、付箋の下のボタン(noteLinks / renderNoteFooter)は描き直さない。無い付箋の ID を渡しても、何もしない |
-
置く場所は、付箋の枠のすぐ下(外側)。 幅は付箋と同じ。付箋の箱の大きさは変わらない
-
付箋のデータは変わらない。
width/height、getNote()、exportData()は、この要素の有無で変わらない。note:update/note:measure/history:commit/changeも発火しない -
縦に積んでいても、いなくても表示する。 積んでいないときは場所を取らないので、下の付箋に重なることがある。積んでいる間(
setStacked())は、列がこの要素の高さを空ける。noteLinks/renderNoteFooterの要素は、この要素の下に並ぶ -
noteLinks、renderNoteFooter、renderNoteと一緒に使える。renderNoteで利用側が描いた付箋にも置ける -
containerは、要素を横に並べる入れ物(display: flex、折り返しあり、間隔 4px)。並べ方は、.wema-note-accessoryに CSS を書いて変える。右に寄せるならjustify-content: flex-end- 付箋の下辺の中央には、折り畳みのボタンとアンカーが出る。要素は左右のどちらかに寄せるとよい
- 付箋との間隔は、
.wema-note-accessoryのpadding-topで空ける。負のmargin-topで付箋の縁に重ねると、右下のリサイズハンドルや下辺のアンカーを要素が覆い、押せなくなる
-
ポインタを受けるのは、
containerに直接入れた要素だけ。 要素の無い部分は、クリックを下の付箋やボードへ通す。要素を自前の行で包むと、その行が幅いっぱいにクリックを受けるので、包むなら行にpointer-events: none、中の要素にpointer-events: autoを指定する -
中の要素のクリック、押下、キー入力は、付箋の選択、ドラッグ、編集、ボードのショートカットにならない
-
ホバー中と選択中は、CSS で見分けられる。
containerは付箋の要素(.wema-note)の中にあり、選択中の付箋にはwema-note-selectedクラスが付く(このクラス名は変えない)/* 作者の名前は、ホバーか選択のときだけ見せる */ .wema-note-accessory .author { visibility: hidden; } .wema-note:hover .wema-note-accessory .author, .wema-note-selected .wema-note-accessory .author { visibility: visible; }
- 積んでいる間は、ホバーで領域の高さを変えない。 高さを変えると、wema が列を積み直すので、ポインタの下で付箋が動く。隠し方は 2 つあり、どちらにも向き不向きがある
visibility: hidden(上の例): 隠していても高さを取る。ホバーで出しても列は動かない。そのかわり、積んでいる間は、何も見えていない付箋の下にも、その分の間隔が空くdisplay: none: 隠している間は高さを取らないので、間隔は空かない。出すと列が動くので、積んでいる間は、ホバーでは出さず、付箋を選んだときやボタンを押したときだけ出す
opacityで透明にした要素は、見えないままクリックを受けて、下の付箋の操作を止める。隠す目的には使わない
- 積んでいる間は、ホバーで領域の高さを変えない。 高さを変えると、wema が列を積み直すので、ポインタの下で付箋が動く。隠し方は 2 つあり、どちらにも向き不向きがある
-
呼び出すのは、付箋を作ったとき(
importData()、applyRemote()、Undo での復活を含む)、付箋のtextまたはmetaが変わったとき、refreshNote(id)かrefreshNoteAccessory(id)を呼んだとき。readOnly / viewOnly でも呼ぶ -
呼び出しは、きっかけになった処理が終わってから、まとめて行う(ブラウザが描く前)。同じ付箋は、何度頼んでも 1 回だけ呼ぶ。関数の中で、
new WemaBoard()の戻り値や、利用側が持つ値を読める。数が届くたびにrefreshNoteAccessory()を呼んで構わない- 例外: 積んでいる間は、付箋の画面上の位置を読むメソッド(
revealNotes()/centerContent()/fitToContent())が、呼んだ時点で、頼まれている分を先に描く(列の位置が決まらないため)。積んでいないときは、この例外は無い
- 例外: 積んでいる間は、付箋の画面上の位置を読むメソッド(
-
containerは、呼び出すたびに、空にしてから渡す。data-wema-focus-key属性を付けたボタンは、描き直したあと、同じ値のボタンへフォーカスを戻す -
containerの中身は、wema のサニタイズを通さない。 他人が書いた文字列(作者の名前など)を出すときは、innerHTMLではなくtextContentを使うこと -
付箋を選択したときのポップアップは、この要素の下に出る
-
重なり順は付箋に従う。手前にある別の付箋(後から触った付箋)の下に隠れることがある。ズームでは、付箋と一緒に拡大、縮小される
-
revealNotes()/centerContent()/fitToContent()と、pageScrollのページの高さは、この要素を付箋の一部として扱う -
描き直しても、要素の高さが変わらなければ、列の積み直しは起きない
-
renderNoteAccessoryの中から、描いている付箋にrefreshNoteAccessory()/refreshNote()を呼んでも、何もしない(呼び出しが止まらなくなるのを防ぐため) -
renderNoteAccessoryが例外を投げたときは、コンソールにエラーを出し、その付箋には何も置かない
| メソッド | 説明 |
|---|---|
addEdge(from, to, params?) |
接続線を追加 |
updateEdge(id, params) |
接続線を更新(線種・矢印・太さ・折り畳み等) |
deleteEdge(id) |
接続線を削除 |
getEdges() |
全接続線を取得 |
getEdgesOf(noteId) |
指定付箋に接続された線を取得 |
getSelectedEdge() |
選択中の接続線IDを取得 |
| メソッド | 説明 |
|---|---|
select(noteIds) |
付箋を選択 |
selectAll() |
全選択 |
getSelection() |
選択中のIDを取得 |
非表示の付箋(折り畳みや絞り込みで隠れているもの)は選択できない。
選択中の付箋の要素(.wema-note)には、wema-note-selected クラスが付く。利用側の CSS で、選択中だけ見せるものを書ける(「付箋に利用側の小さな要素を並べる」の例)。
| メソッド | 説明 |
|---|---|
setNoteFilter(noteIds) |
指定した付箋だけを表示する。null で解除 |
getNoteFilter() |
表示対象の ID を取得。絞り込んでいなければ null |
isNoteHidden(id) |
付箋が非表示か(絞り込みの対象外、または折り畳んだ接続線の先)。存在しない ID には false |
board.setNoteFilter(matchedIds); // 一致した付箋だけを残す
board.setNoteFilter(null); // 全部表示に戻す- 付箋の表示と非表示が変わると、
note:visibilityが発火する(「イベント」の節)。折り畳みのバッジを押して開いたときも発火する - 表示対象でない付箋と、その付箋につながる接続線を非表示にする
- データは変えない。
note:*/edge:*/history:commit/changeは発火せず、Undo 履歴にも積まれない。exportData()は全部の付箋と接続線を返す - 非表示になった付箋と接続線は選択から外れ、付箋は全選択やラバーバンド選択の対象にもならない
- ID を指定して呼ぶメソッド(
updateNote/deleteNote/alignNotes/autoLayout(noteIds)など)は、非表示の付箋にも作用する getNoteFilter()が返すのは現在の表示対象で、setNoteFilter()に渡した配列そのものではない(絞り込み中に作成した付箋が加わり、削除した付箋の ID も残る)- 折り畳み(
collapsed)と両立する。表示対象の付箋どうしをつなぐ接続線が折り畳まれていれば、その先の付箋は表示対象でも隠れる。両端のどちらかが表示対象でない接続線の折り畳みは、絞り込みの間は無視する(展開するボタンを出せないため) - readOnly / viewOnly でも使える
- 絞り込み中にユーザーが作成した付箋は、表示対象に加わる。
applyRemote()で届いた付箋と、Undo / Redo で復活した付箋は加わらず、もう一度setNoteFilter()を呼ぶまで非表示のまま - 絞り込み中に引数なしで
autoLayout()を呼ぶと、表示対象の付箋だけを配置する importData()を呼ぶと絞り込みは解除される
ボードはコンテナの大きさで表示し、はみ出した付箋へは表示位置を動かして届く。倍率を下げれば、広い範囲を一度に表示できる。
| メソッド | 説明 |
|---|---|
getViewport() |
表示位置と倍率 { x, y, zoom } を取得 |
setViewport({ x?, y?, zoom? }) |
表示位置と倍率を設定 |
zoomTo(zoom, center?) |
倍率を設定する。center({ clientX, clientY })の下にある点を画面上で動かさない。省略時はボードの中央 |
revealNotes(noteIds, options?) |
指定した付箋が見えるよう、必要な分だけ表示位置を動かす。倍率は変えない |
centerContent(options?) |
付箋全体(絞り込み中は表示対象)がボードの中央に来るよう動かす。倍率は変えない。options.noteIds で対象を絞れる |
fitToContent(options?) |
付箋全体(絞り込み中は表示対象)がボードに収まる倍率と位置にする。options.noteIds で対象を絞れる |
board.setNoteFilter(matchedIds);
board.fitToContent(); // 残った付箋が収まるように表示する
board.revealNotes([noteId]); // この付箋が見える位置まで動かす
board.zoomTo(1); // 等倍に戻すx/yは、ボードの内容を画面上でずらす量(ピクセル)。zoomは倍率(1 が等倍)。ボード座標(nx, ny)の付箋は、ボード要素の中の(nx * zoom + x, ny * zoom + y)に描かれる- 表示位置や倍率を変えても、付箋の座標と大きさ(
x/y/width/height)は変わらない - 倍率は
minZoom〜maxZoom(既定値 0.25〜2)に収める。範囲外の値を渡すと、範囲の端の値になる setViewport({ zoom })のようにzoomだけを渡すと、ボード座標の原点を中心に拡大・縮小する。画面上の点を中心にするにはzoomTo()を使うfitToContentは、付箋が少ないときに等倍より大きくしない。大きくしてよい場合はoptions.maxZoomで上限を指定する(ボードのmaxZoomを超えることはない)- 表示位置は各クライアントの表示状態として扱う。
note:*/edge:*/history:commit/changeは発火せず、Undo 履歴にも積まれない。発火するのはviewport:changeだけ exportData()は表示位置を含めず、importData()は表示位置を変えない。保存したい場合はgetViewport()で取得し、setViewport()で戻す- readOnly / viewOnly でも使える
revealNotes、centerContent、fitToContentは、非表示の付箋(折り畳み、絞り込み)を対象から外す。対象がボードに収まらないとき(fitToContentでは、最小の倍率でも収まらないとき)は、左上を表示する。options.paddingで、ボードの端との間隔を画面のピクセルで指定できる(既定値 24)- 座標を省略した
addNote()は、表示中の領域の左上から (100, 100) の位置に付箋を作る - 表示位置や倍率を変えると、付箋や接続線のポップアップなども一緒に動く。ポップアップ自体の大きさは変わらない
利用者の操作は次のとおり。
| 操作 | 通常 | ロック(readOnly) | 参照(viewOnly) |
|---|---|---|---|
| ホイール(Shift で横) | パン | パン | パン |
| Ctrl / Cmd + ホイール、トラックパッドのピンチ | ズーム | ズーム | ズーム |
| 中ボタンのドラッグ | パン | パン | パン |
| Space + 左ドラッグ | パン | パン | パン |
| 空いている場所の左ドラッグ | ラバーバンド選択(emptyDrag: 'pan' ではパン) |
パン | パン |
| 空いている場所の Shift + 左ドラッグ | ラバーバンド選択 | パン | ラバーバンド選択 |
| 空いている場所の Ctrl / Cmd + 左ドラッグ | ラバーバンド選択 | パン | パン(emptyDrag: 'pan' ではラバーバンド選択) |
| 空いている場所のダブルクリック | 付箋を作成 | その位置を中央へ | その位置を中央へ |
| リサイズハンドルのダブルクリック | 付箋を内容に合うサイズへ変更 | (ハンドルなし) | (ハンドルなし) |
- 中身がスクロールできる付箋の上では、ホイールは付箋の中身をスクロールする
- Ctrl / Cmd + ホイールは、ポインタの位置を中心に拡大・縮小する。ボードの上ではブラウザのページのズームが働かなくなる。
wheelZoom: falseで無効にできる - タッチ操作(2 本指のピンチ)でのズームは未対応
- ホイールでのパンは、ページのスクロールを止める。ボードをスクロールするページに埋め込む場合は、
wheelPan: falseで無効にできる - Space は、ポインタがボードの上にある間だけパンの合図になる。ボードにフォーカスが無くても使える。テキストの編集中や、入力欄・ボタンにフォーカスがあるときは、通常のキー入力として扱う
- 通常モードでも
createOnDblClick: falseを指定していれば、ダブルクリックはその位置を中央へ動かす emptyDrag: 'pan'を指定すると、通常モードでも、空いている場所の左ドラッグがパンになる。ラバーバンド選択は、Shift または Ctrl / Cmd を押しながらドラッグする。既定値は'select'(左ドラッグがラバーバンド選択)- 動かさずに離したとき(クリック)の選択の解除、ダブルクリックでの付箋の作成、編集中の付箋の確定は、
'select'のときと変わらない - このパンも下の
panMarginの制限を受ける。付箋全体がボードに収まっているあいだは、付箋がボードからはみ出さない範囲でしか動かない
- 動かさずに離したとき(クリック)の選択の解除、ダブルクリックでの付箋の作成、編集中の付箋の確定は、
- 操作でパンできる範囲は、付箋のある範囲の周囲までに限る。 表示中の付箋全体を囲む範囲の外側に見える余白は、上下左右とも
panMargin(既定値 200、画面のピクセル)まで。付箋全体がボードに収まるときは、付箋がボードからはみ出さない範囲で動かせる- 制限するのは上の表の操作(ホイール、ドラッグ、ダブルクリック、Ctrl / Cmd + ホイール)だけ。
setViewport()などのメソッドは、指定どおりの位置へ動かす - 範囲の外にある状態(メソッドで動かした、付箋を削除した、絞り込みを変えた)からは、付箋へ近づく方向にだけ動かせる。範囲の中へ勝手に戻すことはしない
- 付箋が 1 枚も表示されていないボードには、制限がない
panMargin: Infinityで制限をなくせる
- 制限するのは上の表の操作(ホイール、ドラッグ、ダブルクリック、Ctrl / Cmd + ホイール)だけ。
狭い画面では、ボードの中をパンするより、付箋を縦に積んでページ全体をスクロールさせるほうが読みやすい。pageScroll を有効にすると、ボードは自分の表示領域を持たず、付箋の高さまで伸びて、ページの一部として流れる。
| メソッド | 説明 |
|---|---|
setPageScroll(pageScroll) |
モードを切り替える。生成時は pageScroll オプションで指定する |
isPageScroll() |
モード中か |
const board = new WemaBoard({ container, pageScroll: window.innerWidth < 768 });
window.addEventListener('resize', () => {
const narrow = window.innerWidth < 768;
board.setPageScroll(narrow);
if (narrow) board.stackNotes({ x: 20, y: 56, width: window.innerWidth - 40, duration: 300 });
});モードの間は、次のようになる。
- ボードの高さは、表示中の付箋のいちばん下の端に 24px を足した値になる。付箋を動かす、足す、消す、隠すと追従する。付箋が少ないときは、コンテナの高さまで広がる(
min-height: 100%) - 表示位置と倍率は
{ x: 0, y: 0, zoom: 1 }に固定する。モードへ入るとき、この値へ戻す(モードを抜けても、前の位置へは戻さない) - ホイール、空いている場所のドラッグ、中ボタン、Space、ダブルクリックでは、ボードは動かない。ホイールはページをスクロールし、Ctrl / Cmd + ホイールとピンチはブラウザの拡大になる
setViewport()/zoomTo()/centerContent()/fitToContent()は何もしない。revealNotes()は、付箋が見える位置までページをスクロールする- 指での操作は、ページに渡す。 付箋の上から始めたスワイプでもページがスクロールし、指では付箋のドラッグ、リサイズ、ラバーバンド選択を始められない。タップ(選択、リンク、折り畳みボタン、開閉のリンク)は働く。マウスでの操作は、モードの外と変わらない
- 通常モードのダブルクリックでの付箋の作成、マウスでのラバーバンド選択は、これまでどおり働く
- 引用ボタンで作る付箋は、元の付箋の右隣ではなく、同じ列の下に置く
duration付きの移動で全体が短くなるときは、付箋が動き終わってからボードの高さを縮める(動いている途中でページのスクロール位置が跳ばないようにするため)
制限:
- ボード座標の負の位置(
x < 0やy < 0)にある付箋は、見えない。 表示位置を原点に固定するので、ボードの左と上の外にはスクロールできない。モードへ入る前に、付箋を 0 以上の位置へ動かしておく(stackNotes()にはxとyを渡す。省略すると、付箋のある範囲の左上が起点になる)
利用側で用意すること:
- コンテナの高さを固定しない。 ボードはコンテナからはみ出して伸びる(
overflow: visible)ので、コンテナをheight: autoにして、ページ(または外側の要素)をスクロールできるようにする - 付箋の並べ直しは行わない。縦に積むメソッドは 2 つある。データを変えない
setStacked()(次の節)と、データごと積むstackNotes()。stackNotes()で積んだあとに高さが変わる付箋(autoSize/foldable)があるときは、note:measureを受けて積み直す(「レイアウト・整列」の「高さが変わったときに積み直す」) - 表示だけの状態で、
note:*/edge:*/history:commit/changeは発火しない
保存や同期をしているボードで stackNotes() を呼ぶと、狭い画面で開いただけで、保存してある配置が縦 1 列に書き換わる。setStacked() は、付箋のデータを変えず、描く位置と幅だけを縦 1 列にする。
| メソッド | 説明 |
|---|---|
setStacked(stack, options?) |
付箋を縦 1 列に積んで表示する。stack に null を渡すと、データどおりの位置に戻す。生成時は stacked オプションで指定する |
getStacked() |
積み方(setStacked() に渡した stack)。積んでいないときは null |
stack は computeStackLayout() と同じ項目(x / y / width / gap / order)。x と y を省略すると 0 になる。options は { duration }。
const board = new WemaBoard({ container, data });
const layout = () => {
const narrow = window.innerWidth < 768;
board.setPageScroll(narrow);
board.setStacked(narrow ? { x: 12, y: 12, width: window.innerWidth - 24 } : null);
};
layout();
window.addEventListener('resize', layout);- 付箋のデータは変わらない。
getNote()/exportData()は、元の位置と大きさを返す。note:*/history:commit/changeは発火せず、Undo 履歴にも積まれない。ロックモードと参照モードでも切り替えられる - 並び順は、
orderの順、その後ろにデータの位置の上から順(同じ高さなら左から順) - 付箋の幅は
widthになる(autoSizeの付箋も同じ)。高さが内容で決まる付箋(autoSize/foldable)は、その幅での高さで描く。データのheightは、データの幅での高さのまま。高さを固定している付箋は、データの高さのまま描く(幅が狭くなって本文が収まらないときは、付箋の中でスクロールする) - 列が変わるたびに、ボードが積み直す。 付箋の作成と削除、絞り込みと接続線の折り畳み、データの変更(
updateNote()、applyRemote()、importData()、Undo)、描いている高さの変化(foldableの開閉、入力の途中)が対象。利用側がnote:measureを受けて呼び直す必要はない - 接続線は表示しない。 縦 1 列では、離れた付箋をつなぐ線が間の付箋の裏を通り、隙間に切れ端だけが見えるため。付箋のアンカーも出さないので、画面の操作では接続線を作れない。データは変わらず、
addEdge()/updateEdge()/deleteEdge()も働く。setStacked(null)で元どおり表示する- 折り畳んだ接続線(
collapsed)のバッジは表示する。押すと、隠れている付箋が列に入る
- 折り畳んだ接続線(
- 付箋のドラッグとリサイズはできない(マウスでも同じ)。描いている位置とデータの位置が別なので、動かした結果をデータに写せないため。ムーブハンドルとリサイズハンドルは表示しない。選択、本文の編集、付箋の作成と削除、ポップアップは、通常どおり働く
- ボードが位置を決める付箋(ダブルクリック、位置を省略した
addNote()、複製、引用)は、データの上では、既存の付箋のある範囲の左下(いちばん下の端の 20px 下)に置く。広い画面で開いたときに他の付箋と重ならず、列では最後に並ぶ。位置を渡したaddNote()は、その位置をデータに入れる - 位置や大きさを変えるメソッド(
moveNotes()/stackNotes()/alignNotes()など)は、これまでどおりデータを変える。表示は縦 1 列のままで、結果はsetStacked(null)のあとに見える options.duration(ミリ秒)を渡すと、付箋と接続線が、列へ入るとき(setStacked(stack, { duration: 300 }))と列から出るとき(setStacked(null, { duration: 300 }))にアニメーションする。アニメーションするのはその呼び出しだけで、ボードによる積み直しはすぐに反映する- 表示位置と倍率は変えない。ページスクロールモード(
pageScroll)とは別に切り替える。狭い画面では、上の例のように両方を使う
stackNotes() との使い分け:
stackNotes() |
setStacked() |
|
|---|---|---|
| 付箋のデータ | 位置と幅を書き換える | 変えない |
| イベントと履歴 | note:update と history:commit が出る(Undo 1 回分) |
出ない |
| その後の変化 | 追従しない(note:measure を受けて呼び直す) |
ボードが積み直す |
| ドラッグとリサイズ | できる | できない |
| 向いている場面 | 保存しないページ、配置そのものを縦 1 列にしたいとき | 保存や同期をしているボードを、狭い画面で読ませるとき |
積んでいる間は接続線が出ない。noteLinks を指定すると、接続線の代わりに、つながっている相手を付箋の下に並べる。
const board = new WemaBoard({
container,
data,
labels: jaLabels,
stacked: { x: 12, y: 12, width: window.innerWidth - 24 },
noteLinks: true,
});- 接続線 1 本につき、ボタンを 1 つ出す。中身は、向きの記号、接続線のラベル、相手の付箋の本文の冒頭(文字のある最初の行)
- 記号は、その付箋から見た向き。
→は相手へ向かう矢印、←は相手から来る矢印、↔は両端の矢印、―は矢印のない線 - 並び順は、
→、←、↔、―の順。同じ向きの中では、接続線の順
- 記号は、その付箋から見た向き。
- ボタンを押すと、相手の付箋が見える位置へ動かし(
revealNotes())、相手を少しの間だけ枠で強調する - 相手が非表示(絞り込み、接続線の折り畳み)のボタンは、押せない
- 描き直しは wema が行う。 接続線の作成、変更、削除、相手の付箋の本文と
metaの変更、相手の表示と非表示の変化が対象 - 表示するのは、
setStacked()で積んでいる間だけ。付箋のデータは変わらず、イベントと履歴は出ない(下のrenderNoteFooterと同じ) - 既定は OFF
相手の表示名は、text で差し替えられる。文字列を返すと、本文の冒頭の代わりに使う。それ以外を返した付箋は、本文の冒頭のままにする。
const board = new WemaBoard({
container,
noteLinks: {
// ページを指す付箋は、ページの表示名で出す
text: (peer) => (peer.meta?.page ? pages.get(peer.meta.page)?.title : undefined),
},
});
// 表示名の元(ページの表示名)が変わったら、その付箋を知らせる。
// その付箋を出している付箋(つながっている相手)のボタンを、wema が描き直す
board.refreshNote(noteId);-
textの戻り値と相手の本文は、文字として入れる(HTML として解釈しない) -
textが空の文字列を返したとき、または例外を投げたときは、本文の冒頭のままにする(例外はコンソールに出す) -
本文の冒頭には、付箋に表示されない文字を使わない(
video/audio/iframeの代替の文字、display: noneの要素) -
描き直しの間も、キーボードのフォーカスは同じ接続線のボタンに残る
-
色は CSS 変数で変える(
.wema-boardか、その外側の要素に指定する)変数 対象 既定値 --wema-note-link-backgroundボタンの背景 #fff--wema-note-link-colorボタンの文字 #333--wema-note-link-border-colorボタンの枠 rgba(0, 0, 0, 0.15)--wema-note-link-label-color接続線のラベル #666--wema-note-link-accent-color向きの記号と、相手の付箋の強調の枠 #4A90D9/* 暗い背景のページに合わせる */ .wema-board { --wema-note-link-background: #2a2a2a; --wema-note-link-color: #eee; --wema-note-link-border-color: rgba(255, 255, 255, 0.2); --wema-note-link-label-color: #aaa; --wema-note-link-accent-color: #7ab8ff; }
-
色以外(大きさ、角の丸み、並べ方)は、クラスに CSS を書いて変える
クラス 対象 .wema-note-links1 枚の付箋のボタンの並び .wema-note-linkボタン 1 つ .wema-note-link-arrow/-label/-textボタンの中の、向きの記号、接続線のラベル、相手の表示名 .wema-note-link-target押されたボタンの相手の付箋(強調している間だけ付く) -
ボタンを押して相手の付箋へ移るとき、付箋の周りに残す余白は
paddingで変える(revealNotes()のpaddingと同じ。既定は 24px)。ボードの端に固定表示の見出しなどが重なるときに、広げるnew WemaBoard({ container, noteLinks: { padding: 64 } });
-
読み上げ用の説明(
aria-label)と、本文のない付箋の表示は、labelsのlinkOut/linkIn/linkBoth/linkNone/linkLabeled/linkEmptyで変える。日本語はjaLabelsに入っている -
ボタンの構造ごと変えたいときは、下の
renderNoteFooterを使う。両方を渡したときはrenderNoteFooterが描き、noteLinksは何もしない
noteLinks のボタンでは足りないときは、renderNoteFooter で、付箋の下に利用側の DOM を描ける。container に DOM を作って true を返すと、付箋の枠のすぐ下に表示する。それ以外を返した付箋の下には、何も置かない。
積んでいないときにも出したい小さな要素(印やボタン)は、renderNoteAccessory に描く(「付箋に利用側の小さな要素を並べる」)。両方を渡したときは、renderNoteAccessory の要素の下に、この要素が並ぶ。
const board = new WemaBoard({
container,
data,
stacked: { x: 12, y: 12, width: window.innerWidth - 24 },
renderNoteFooter: (note, el) => {
const edges = board.getEdgesOf(note.id);
if (edges.length === 0) return false; // 何も置かない
for (const edge of edges) {
const out = edge.from === note.id;
const other = board.getNote(out ? edge.to : edge.from);
if (!other) continue;
const link = document.createElement('button');
link.textContent = (out ? '→ ' : '← ') + summaryOf(other);
link.disabled = board.isNoteHidden(other.id); // 非表示の付箋へは移れない
link.addEventListener('click', () => board.revealNotes([other.id]));
el.append(link);
}
return true;
},
});
// 描いた内容の元が変わったら、描き直しを頼む
const refreshEnds = ({ edge }) => {
board.refreshNote(edge.from);
board.refreshNote(edge.to);
};
board.on('edge:create', refreshEnds);
board.on('edge:update', refreshEnds);
board.on('edge:delete', refreshEnds);
const refreshConnected = (id) => {
for (const edge of board.getEdgesOf(id)) board.refreshNote(edge.from === id ? edge.to : edge.from);
};
board.on('note:update', ({ note, prev }) => {
if (note.text !== prev.text) refreshConnected(note.id);
});
board.on('note:visibility', ({ hidden, shown }) => {
for (const id of [...hidden, ...shown]) refreshConnected(id);
});- 表示するのは、
setStacked()で積んでいる間だけ。 積んでいない間は、renderNoteFooterを呼ばず、描いた要素も消す。付箋どうしの間隔が利用側の配置で決まり、要素の置き場がないため - 付箋のデータは変わらない。 付箋の
height、getNote()、exportData()は、この要素の有無で変わらない。note:update/note:measure/history:commit/changeも発火しない - 列は、この要素の高さを含めて積む(下の付箋は、この要素の下から始まる)。
gapは、この要素と下の付箋の間隔になる。高さが変わると、ボードが積み直す(描き直したとき、幅が変わったとき、利用側が中身を書き換えたとき)。付箋との間隔は、利用側がcontainerの中の余白で決める - 呼び出すのは、積み始めたとき(全部の付箋)と、積んでいる間に、付箋を作ったとき、付箋の
textまたはmetaが変わったとき、refreshNote(id)を呼んだとき - 呼び出しは、きっかけになった処理が終わってから、まとめて行う(ブラウザが描く前)。同じ付箋は、何度頼んでも 1 回だけ呼ぶ。関数の中から、その付箋の接続線(
getEdgesOf())や相手の付箋(getNote()/isNoteHidden())を調べられる。生成時のdataとstackedで積んだ付箋も同じで、関数の中でnew WemaBoard()の戻り値を使える。importData()とapplyRemote()では、接続線を読み込んだあとに呼ぶ。例外は、付箋の画面上の位置を読むメソッド(revealNotes()/centerContent()/fitToContent())で、呼んだ時点で、頼まれている分を先に描く containerは、付箋と同じ幅。呼び出すたびに、空にしてから渡す- 中のボタンにフォーカスがある間に描き直すと、ボタンごと作り直すので、フォーカスが外れる。ボタンに
data-wema-focus-key属性(そのボタンが何を指すかを表す文字列)を付けておくと、描き直したあと、同じ値のボタンへフォーカスを戻す
- 中のボタンにフォーカスがある間に描き直すと、ボタンごと作り直すので、フォーカスが外れる。ボタンに
containerの中のクリックやタップは、付箋の選択、編集の開始、ボードの操作にならない。中のボタンにフォーカスがあるときのキー入力も、ボードのショートカット(Delete、Undo / Redo)にならない- 付箋を選択したときのポップアップは、この要素の下に出す
renderNoteで利用側が描いた付箋の下にも描ける。readOnly / viewOnly でも呼び出すcontainerの中身は、wema のサニタイズを通さない。 他人が書いた文字列(相手の付箋の本文を含む)を出すときは、innerHTMLではなくtextContentを使うこと- 下向きの接続線を折り畳んだバッジは、この要素のある付箋では、付箋の下の右端に出す。
containerは、右端をバッジの分(28px)だけ空ける - 制限: 上向きの接続線(列で上にある付箋へ向かう線)を折り畳んだバッジは、付箋の上の中央に出て、1 つ上の付箋のこの要素の下端に 8px ほど重なる(
gapが既定の 16px のとき)。重ねたくないときは、gapを 24px 以上にするか、containerの下に余白を入れる renderNoteFooterが例外を投げたときは、コンソールにエラーを出し、その付箋の下には何も置かない。他の付箋の描画と積み直しは続けるrevealNotes()/centerContent()/fitToContent()は、この要素を付箋の一部として扱う(付箋とこの要素が見える位置へ動かす)
| メソッド | 説明 |
|---|---|
moveNotes(moves, options?) |
複数の付箋の位置と大きさをまとめて変える。moves は { id, x?, y?, width?, height? } の配列 |
alignNotes(noteIds, alignment, options?) |
付箋を整列(left/center/right/top/middle/bottom) |
distributeNotes(noteIds, direction, options?) |
付箋を均等配置(horizontal/vertical) |
autoLayout(noteIds?, options?) |
自動レイアウト(接続線から階層を作る。今の位置の左上を保つ。ボードの縦横比に合わせて折り返す) |
stackNotes(options?) |
付箋を縦 1 列に積む(狭い画面向け)。options は下の computeStackLayout() と同じ項目に、noteIds(省略時は表示中の付箋すべて)と duration を足したもの |
どのメソッドも、options.duration(ミリ秒)を渡すと、付箋を新しい位置までアニメーションで動かす。接続線も付箋についていく。
// ウィンドウの大きさに合わせて、全部の付箋を 300ms で動かす
board.moveNotes(
[{ id: 'blog', x: 20, y: 56, width: 335 }, { id: 'about', x: 20, y: 240, width: 335 }],
{ duration: 300 },
);
// 狭い画面では縦 1 列に積む
board.stackNotes({ x: 20, y: 56, width: 335, gap: 16, duration: 300 });- データは、呼び出した時点で新しい位置になる。 アニメーションするのは表示だけ。
note:updateは付箋 1 枚につき 1 回で、全体が Undo 1 回分になる。途中の位置では、イベントと履歴は出ない。getNote()とexportData()は、動いている途中でも終点の値を返す - 動いている途中の付箋をもう一度動かすと、いま表示されている位置から新しい終点へ向かう。ドラッグ、Undo、
updateNote()、applyRemote()で位置や大きさが変わったときは、アニメーションをやめて、その位置にすぐ表示する durationを省略したとき(既定値 0)と、利用者が OS で視差効果を減らす設定(prefers-reduced-motion: reduce)にしているときは、アニメーションしないmoveNotes()とstackNotes()は、updateNote()と同じく参照モード(viewOnly)でも働く(位置はモード終了時に元へ戻る。幅と高さは戻らない)。ロックモード(readOnly)では何もしない。alignNotes()/distributeNotes()/autoLayout()は、これまでどおりどちらのモードでも何もしない- 大きさが内容で決まる付箋は、その大きさを渡しても変わらない(
autoSizeの付箋のwidthとheight、foldableの付箋のheight)。note:updateも出ない。stackNotes()は、新しい幅での高さを測ってから積む stackNotes()は、非表示の付箋(絞り込み、接続線の折り畳み)を動かさない。noteIdsで指定しても同じ
stackNotes() は、呼んだ時点の高さで付箋を積む。ボードは積み方を覚えない(覚えて積み直すのは、データを変えない setStacked())。あとで高さが変わった付箋は、下の付箋に重なる(foldable の付箋を開いた、autoSize の付箋に入力した)。note:measure を受けて、同じ引数で stackNotes() を呼び直す。
const stack = () => board.stackNotes({ x: 20, y: 56, width: 335, gap: 16, duration: 200 });
stack();
board.on('note:measure', stack);stackNotes()は、位置の変わらない付箋を更新しない。呼び直しても、note:measureとchangeは連鎖しないdurationを渡すと、下の付箋は新しい位置までアニメーションで動く。高さが変わった付箋そのものは、すぐにその高さになる。開いた直後は、下の付箋が動き終わるまで重なって見える。重なりを見せたくないときはdurationを省く- 入力中は、高さが変わるたびに発火する(行が増えたときなど)。積み直しを間引くかどうかは、利用側で決める
- 積み直しで動いた付箋は
note:updateを出す。 通常モードでは、付箋を開いただけで Undo 1 回分が積まれ(Redo できる操作は消える)、Undo すると、下の付箋だけが前の位置へ戻る。開閉は Undo の対象外なので、付箋が重なるか、すき間が空く。入力中も、行が増えるたびに積まれる。参照モード(viewOnly)では位置を履歴に記録しないので、積まれない。読むための画面では、参照モードと組み合わせる - 画像の読み込みで付箋の高さが変わっても、
note:measureは発火しない(wema は、画像の読み込みで付箋を計測し直さない)。大きさの分からない画像を入れる付箋は、積み直しの対象にならない - ロックモード(readOnly)では
stackNotes()が何もしないので、積み直せない。開閉のリンクはロックモードでも働くため、開いた付箋は重なる orderを省略していても、並び順は変わらない。省略時は「いまの位置の上から順」に積むので、積んである付箋はその順のままになる
同じ計算を DOM なしで行う関数も export している。サーバー側など WemaBoard を作れない環境で使える。どれも入力を変更せず、付箋の新しい位置 { id, x, y } の配列を返す(computeStackLayout() は幅も入った { id, x, y, width })。
| 関数 | 説明 |
|---|---|
computeAlignment(notes, alignment) |
整列後の位置。全付箋の位置を返す |
computeDistribution(notes, direction) |
均等配置後の位置。両端を除く付箋の位置を返す |
computeAutoLayout(notes, edges, options?) |
自動レイアウト後の位置。options.noteIds で対象を絞れる。options.aspectRatio で、収めたい範囲の縦横比(幅 ÷ 高さ、既定値 1.6)を渡せる |
computeStackLayout(notes, options?) |
縦 1 列に積んだ位置と幅。options は x / y(1 枚目の左上。省略時は付箋のある範囲の左上)、width(全部の付箋の幅。省略時はそれぞれの幅のまま)、gap(付箋の間隔。既定値 16)、order(上から並べる順の ID の配列) |
computeStackLayout() の並び順は、order を省略すると、いまの位置の上から順(同じ高さなら左から順)になる。order に無い付箋は、order の後ろに同じ規則で並べる。列から漏れる付箋は無い。付箋を積む高さには、渡された height を使う。幅を変えると高さが変わる付箋(autoSize / foldable)があるときは、stackNotes() を使う。
import { computeAutoLayout } from '@kanf/wema';
const positions = computeAutoLayout(data.notes, data.edges);
// 横長の画面に収めたいとき
const wide = computeAutoLayout(data.notes, data.edges, { aspectRatio: 16 / 9 });自動レイアウトは、次のように配置する。
- 接続線でつながった付箋は、線の向きに上から下へ段を作る。つながっていないまとまり同士は、横に並べて折り返す
- 接続線のない付箋は、左から詰めて折り返し、まとまりの下か右に置く。付箋の大きさが違っていても、付箋同士は重ならない
- 折り返す幅と、接続線のない付箋を下と右のどちらに置くかは、全体が
aspectRatioの範囲に最も大きく収まるように選ぶ。autoLayout()は、ボードの要素の縦横比を使う - 接続線の鎖は折り返さない。段の数が多いボードは、縦横比を渡しても縦に長くなる
| メソッド | 説明 |
|---|---|
undo() |
元に戻す |
redo() |
やり直す |
canUndo() |
undo可能か |
canRedo() |
redo可能か |
batch(fn, options?) |
fn の中の操作を Undo 1 回分にまとめる。fn は同期関数。options.origin('user' / 'agent')は history:commit の origin になる |
| メソッド | 説明 |
|---|---|
applyRemote(deltas) |
他のクライアントやサーバーで起きた変更(HistoryDelta[])を適用する |
使い方はリアルタイム同期を参照。
| メソッド | 説明 |
|---|---|
setReadOnly(readOnly) |
読み取り専用モードを設定 |
isReadOnly() |
読み取り専用か |
setViewOnly(viewOnly) |
閲覧専用モードを設定(UIも非表示) |
isViewOnly() |
閲覧専用か |
setTheme(theme) |
テーマを設定('default' / 'card') |
getTheme() |
現在のテーマを取得 |
| メソッド | 説明 |
|---|---|
exportData() |
ボードデータをオブジェクトで返す |
importData(data) |
データを読み込み(現在の内容を置換) |
| イベント | ペイロード |
|---|---|
note:create |
{ note, origin } |
note:update |
{ note, prev, origin } |
note:delete |
{ note, origin } |
note:measure |
{ note, prev } |
note:visibility |
{ hidden, shown } |
note:select |
{ noteIds } |
edge:create |
{ edge, origin } |
edge:update |
{ edge, prev, origin } |
edge:delete |
{ edge, origin } |
readOnly:change |
{ readOnly } |
viewOnly:change |
{ viewOnly } |
history:change |
{ canUndo, canRedo } |
history:commit |
{ deltas, origin } |
image:error |
{ noteId, file, error } |
viewport:change |
{ x, y, zoom } |
change |
{ data } |
board.on('change', ({ data }) => { /* ... */ });
board.off('change', handler);note:*/edge:*のoriginは、このボードでの操作なら'local'、applyRemote()による変更なら'remote'note:measureは、大きさが内容で決まる付箋(autoSize/foldable)の大きさが、note:updateを伴わずに変わると発火する(入力の途中、foldableの開閉、非表示だった付箋の再表示)。prevは変わる前の付箋。changeと同じく、きっかけになった処理が終わってから発火し、同じタイミングの変化は、付箋ごとに 1 回だけ発火する。表示から決まる値の変化なので、Undo 履歴とhistory:commitには載らない。この大きさの変化は、その付箋の次のnote:updateのprevに載る。updateNote()の中で大きさが変わったときはnote:updateだけが発火し、ボードの読み込み(生成時のdataとimportData())では発火しないnote:visibilityは、付箋の表示と非表示が変わると発火する(絞り込み、接続線の折り畳みと展開。折り畳みのバッジを押したときを含む)。hiddenは非表示になった付箋の ID、shownは表示に戻った付箋の ID。非表示の状態で作られた付箋(絞り込み中にapplyRemote()で届いた付箋など)はhiddenに入り、削除した付箋はどちらにも入らない。changeと同じく、きっかけになった処理が終わってから発火し、前回の発火から変わった付箋だけを 1 回で知らせる。表示だけの変化なので、Undo 履歴とhistory:commitには載らない。ボードの読み込み(生成時のdataとimportData())では発火しない。いまの状態はisNoteHidden(id)で調べるhistory:commitは、ユーザー操作 1 回分(Undo 1 回分)が確定するたびに発火する。originは'user'/'undo'/'redo'/'agent'changeのdataには常にexportData()と同じ内容が入る。同じタイミングの変更は 1 回にまとめて発火する
history:commit で確定した変更を送り、受け取った変更を applyRemote() で適用する。通信と保存は利用側で実装する(ライブラリは関与しない)。
// 送信: 操作が確定するたびに差分を送る
board.on('history:commit', ({ deltas }) => {
socket.send(JSON.stringify(deltas));
});
// 受信: 他のクライアントの差分を適用する
socket.addEventListener('message', (e) => {
board.applyRemote(JSON.parse(e.data));
});type HistoryDelta =
| { type: 'note:create'; note: WemaNote }
| { type: 'note:update'; noteId: string; before: Partial<WemaNote>; after: Partial<WemaNote> }
| { type: 'note:delete'; note: WemaNote }
| { type: 'edge:create'; edge: WemaEdge }
| { type: 'edge:update'; edgeId: string; before: Partial<WemaEdge>; after: Partial<WemaEdge> }
| { type: 'edge:delete'; edge: WemaEdge };updateのbefore/afterには変わったキーだけが入るafterにキーがなくbeforeにあるものは「未設定に戻す」を表す。値がundefinedのキーは JSON にすると消えるため、applyRemote()はこの規則で復元する(接続線のcollapsedを解除したときなど)
- ドラッグとリサイズは、操作の途中では発火せず、終了時に 1 回だけ発火する(途中経過は
note:updateで受け取れる) - 1 回の操作の中で同じ付箋や接続線を何度も更新した場合、デルタは 1 件にまとまる(
beforeは操作前の値、afterは操作後の値)。元の値に戻ったキーは含まれず、何も変わらなかった操作では発火しない - Undo では元の差分を逆向きにしたもの(逆順、create ↔ delete、before ↔ after の入れ替え)、Redo では元の差分が流れる
applyRemote()による変更では発火しない。受け取った変更を送り返すことはない- 参照モード(viewOnly)中の移動と折り畳み、終了時の復元では発火せず、Undo 履歴にも積まれない。参照モード中でも、
updateNote()で変えたテキストや色のように終了時に戻らない変更は発火する
- readOnly / viewOnly 中でも適用する
- Undo 履歴には積まない。既存の Undo 履歴は消さない
- 対象がすでにない更新と削除、すでにある ID の作成、端点の付箋がない接続線の作成は、そのデルタだけを無視する
- 付箋を削除するとき、その付箋につながる接続線が残っていれば一緒に削除する
- 編集中(フォーカス中)の付箋は、表示中の内容を上書きしない
- 参照モード中に届いた付箋と接続線の作成、位置と折り畳みの変更は、モード終了時の復元先にも反映する
- 更新のデルタで変更できるのは
WemaNote/WemaEdgeのフィールドだけ。id/from/toと未知のキーは無視する - 値の型や範囲は検証しない。信頼できない相手から受け取ったデルタは、利用側で検証してから渡すこと。付箋の
textは描画時にサニタイズする
zIndex は各クライアントの表示状態として扱う。付箋をクリックして最前面に出したとき、note:update と history:commit は発火せず、Undo 履歴にも積まれない。同期する側でも zIndex は保存や配信の対象から外すことを想定している(note:create のデルタには作成時の zIndex が含まれる)。
付箋の作成・編集・移動・削除、接続線の操作をすべて禁止する。付箋の閲覧と選択は可能。
const board = new WemaBoard({ container, readOnly: true });
// または実行時に切り替え
board.setReadOnly(true);
board.setReadOnly(false);スタンドアロン版では 🔒 ボタン(Ctrl+L)で切替。状態はリロード後も保持される。
アンカー・リサイズハンドル・ポップアップなど編集 UI をすべて非表示にする。ロックモードより表示がクリーンで、ボードを「見せる」用途に向く。
const board = new WemaBoard({ container, viewOnly: true });
// または実行時に切り替え
board.setViewOnly(true);
board.setViewOnly(false);参照モード中も可能な操作(一時的・データは変更されない):
- 付箋のドラッグ移動 — モード終了時に元の位置に戻る
- 部分木の折り畳み/展開 — モード終了時に元の状態に戻る
この 2 つの操作は Undo 履歴に積まれず、history:commit も発火しない。参照モード中、undo() と redo() は何もしない。viewOnly: true で作成したボードは、終了時に作成時点の位置と折り畳み状態へ戻る。
同じ 2 つは、参照モードのままプログラムからも変えられる。位置は updateNote()、折り畳みは updateEdge(id, { collapsed }) で変える。どちらも利用者の操作と同じ扱いで、モード終了時に元へ戻り、Undo 履歴に積まれない。updateEdge() に渡した collapsed 以外のキーは、参照モードでは無視する。
スタンドアロン版では 👁 ボタン(Ctrl+Shift+L)で切替。状態はリロード後も保持される。
| 操作 | 通常 | ロック | 参照 |
|---|---|---|---|
| 付箋の閲覧 | ✓ | ✓ | ✓ |
| 付箋の移動 | ✓ | ✗ | ✓(一時的) |
| 付箋の編集 | ✓ | ✗ | ✗ |
| 付箋の追加・削除 | ✓ | ✗ | ✗ |
| 接続線の操作 | ✓ | ✗ | ✗ |
| 折り畳み/展開 | ✓ | ✗ | ✓(一時的) |
| Undo/Redo | ✓ | ✗ | ✗ |
| 表示位置の移動(パン)とズーム | ✓ | ✓ | ✓ |
interface WemaNote {
id: string;
x: number; y: number;
width: number; height: number;
text: string; // HTML文字列
color: string;
zIndex: number; // 重なり順。ローカルな表示状態(同期対象外)
autoSize?: boolean; // コンテンツに合わせてサイズ自動調整
foldable?: boolean; // 長い本文を畳んで表示する(3 行以上のとき、冒頭だけを見せる)
meta?: Readonly<Record<string, string>>; // 利用側のデータ。wema は中身を読まない
}
interface WemaEdge {
id: string;
from: string; to: string;
fromAnchor: 'top' | 'right' | 'bottom' | 'left' | 'auto';
toAnchor: 'top' | 'right' | 'bottom' | 'left' | 'auto';
style: 'arrow' | 'line' | 'dashed';
lineStyle?: 'solid' | 'dashed' | 'dotted';
arrowHead?: 'none' | 'start' | 'end' | 'both';
strokeWidth?: number;
arrowSize?: number;
routing?: 'curve' | 'polyline';
label?: string;
collapsed?: boolean; // true のとき接続先の部分木を非表示
}CSS変数でスタイルを調整できる:
.wema-board {
--wema-note-border-radius: 4px;
--wema-note-shadow: 0 2px 8px rgba(0,0,0,0.15);
--wema-note-font-size: 14px;
--wema-note-color-text: #333;
--wema-anchor-size: 12px;
--wema-anchor-color: #4A90D9;
--wema-edge-color: #555;
--wema-edge-width: 2px;
/* noteLinks のボタン(「つながっている付箋を、付箋の下に出す」) */
--wema-note-link-background: #fff;
--wema-note-link-color: #333;
--wema-note-link-border-color: rgba(0,0,0,0.15);
--wema-note-link-label-color: #666;
--wema-note-link-accent-color: #4A90D9;
}npm install # 依存インストール
npm run dev # 開発サーバー (HMR)
npm run build # ビルド (dist/)
npm test # テスト
npm run lint # 型チェック