野良 Scrapbox アプリ - プロジェクトアクティビティ表示ダイアログ

またまた Scrapbox アプリの話。

社内で導入されている Scrapbox。隙間時間に閲覧・更新できて社内の活動も透明化されるということで、我々のように客先常駐が多い環境では導入効果高いです。

導入した管理者の人から、利用状況を把握するためページビュー(集計値) の推移を記録したいという要望がありました。

ページ一覧を取得する API で全ページのリストを取れば可能です。1回のリクエストで取得できる一覧には限りがあるので、繰り返し取得して最後に集計値をダイアログ表示するようにしています。Copy ボタンを押すとクリップボードに内容がコピーされます。

f:id:kondoumh:20190328212609p:plain

これは、僕の個人ページの情報ですが、会社のはすでに2,000ページ、80,000ビューを超えています。

Release v0.6.3 · kondoumh/sbe · GitHub

野良 Scrapbox アプリ - 見出し指定っぽいコンテキストメニュー

Scrapbox には見出し記法はなく、文字の大きさを [** text] というフォーマットで修飾する(* が多いとサイズが大きい) という仕様です。標準のメニューでは * 1個のパターンのみ Strong 記法として指定可能です。

長めの文章を書いていると、文字の大きさを見出し風に指定したい場合があるので、独自コンテキストメニューで4段階で指定できるようにしました。Scrapbox の場合タイトルの文字がだいたいレベル4ぐらいのようなので 1-4 で指定できれば OK ということで。

f:id:kondoumh:20190326004310g:plain

関連する機能として、見出しサイズを指定してプレースホルダーとして挿入するキーボードショートカット (⌘ + 1 など) も以前のバージョンで追加してました。

f:id:kondoumh:20190326005730g:plain

Release v0.6.2 · kondoumh/sbe · GitHub

野良 Scrapbox アプリ - ページ情報ダイアログを出せるようにしました

コンテキストメニュー Info からページ情報を表示するダイアログを出すようにしました。

f:id:kondoumh:20190309105249p:plain

ページを開く前に概要を把握できたらと思い、ページへのリンクに対してコンテキストメニューを表示しています。

タイトル、ページを作成したユーザ、共同編集ユーザ、ページ概要を表示します。

ページ概要は、本文の先頭5行ぐらいが入っている模様です。

ダイアログから直接ページを開くことも可能です。

f:id:kondoumh:20190309014844p:plain

Release v0.6.1 · kondoumh/sbe · GitHub

社内勉強会で Electron の話をしました

そもそも社内勉強会で喋るというのが初だったかも。

www.slideshare.net

趣旨としては、

  • Electron 熟成してきてるよ
  • クロスプラットフォーム開発大変だけど Electron はけっこう手軽
  • PWA の方が流行るかもしれないけどねー

という感じです

簡単なハンズオンもやりました。

  • WebView を使うサンプルを作ってみる
  • 独自の Menu を追加してみる
  • WebAPI からデータを取得してレンダリングしてみる

先日の Vue アプリの Electron 化もデモしました。

blog.kondoumh.com

Main process 経由で OS ネイティブのプログラムと Web 画面の相互運用ができることに興味を示した人がいました。

野良 Scrapbox アプリの fav 機能

Scrapbox の野良 Electron アプリにちょっとしたヒストリとブックマーク機能を追加しようと思いました。

最初はメニューに履歴を追加してピン止めするような UI を作ろうとしましたが、Electron では動的にメニューアイテムを追加削除する API が提供されてませんでした。

そこでツールバーに select-box を置いて動的に option 要素として追加するようにしてみました。

履歴とピン止めを同じリストで扱う UI の実装が複雑になるので、とりあえずピン止め用のリストを切り出しました。

そのうち履歴要らないって気持ちになってきました。Scrapbox 自体が履歴表示しますし、自前のページリスト画面もあるし。

ということで、fav 機能だけに絞りました。

着け外しの UI も実装、操作ともに面倒だし、fav リストに入れたページは最上位に出して、一定数の上限値を超えたものは古い方から消していくキューのような感じで、キューから消えちゃったものはまた検索して出せばよいと。

f:id:kondoumh:20190227055053p:plain

結果、ヒストリ的なピン止め機能といった UI に落ち着きました。

f:id:kondoumh:20190228055504g:plain

Release v0.6.0 · kondoumh/sbe · GitHub

Vue を使って Electron アプリを開発する

blog.kondoumh.com

Vue のことがちょっと分かった気になれました。すると、最近よく触っている Electron でのアプリ開発にも取り込みたくなります。

github.com

Scrapbox in Electron は WebView でサイトの画面を表示していますが、Electron では当然自前で画面を作ることもできます。その場合は Vue のようなフレームワークを適用するのが自然でしょう。

  • Web とアセットを共有したい
  • フレームワークのツールチェインや知識を流用したい

という意図があります。

electron-vue という Vue と Electron を使ったプロジェクトのボイラープレートがあります。

f:id:kondoumh:20190224130025p:plain

github.com

vue-cli が導入されていれば、以下のコマンドを叩いて、プロジェクト名などを入力するだけ。

$ vue init simulatedgreg/electron-vue my-project

Electron のインストールもお任せできます。

生成されたプロジェクトディレクトリに移動して

$ npm run dev

で、デフォルトの Electron アプリが起動します。

f:id:kondoumh:20190224125417p:plain

Vue 用のフォルダは src/renderer/components 配下になりますので、ここに Web アプリ用の Vue ファイルなどを配置します。

f:id:kondoumh:20190224140252p:plain

動きました。

f:id:kondoumh:20190224133624p:plain

Electron アプリとして作りこむためには、Renderer と Vue コンポーネント間の連携が必要ですが、そこをどう作りこむのか、Web とアセットの共有がどこまで可能かについてはやってみないと分からないところです。

2019/2/27 追記

Main Process のメニューから Renderer Process 経由で、ダイアグラムにノードを追加するコードはこんな感じになりました。

src/main/index.js

function createMenu () {
  console.log('createMenu')
  const template = [
    {
      label: 'Edit',
      submenu: [
        {
          label: 'add',
          click () {
            mainWindow.webContents.send('add', 'hoge')
          }
        }
      ]
    }
  ]

  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
}

src/renderer/components/JointDia.vue

<script>
  import joint from 'jointjs'

  export default {
    name: 'JointDia',
    mounted () {
      this.graph = new joint.dia.Graph()
         :
      this.$electron.ipcRenderer.on('add', (e, data) => {
        this.addNode(data)
      })
    },
</script>

this.$electron.ipcRenderer で IPC イベントを処理できます。

この例では、イベントを受けたいコンポーネントの mounted に直接ハンドラーを書きましたが、App.vue からディスパッチするようにすれば、サブコンポーネントは Web と同一にできそうな気がしました。

Vue と JointJS でトイ・ダイアグラムエディタ

Vue 人気ありますね。あまり真剣に触ったことがなく ToDo サンプルとかだとどうも分かった気になれない。ということで JointJS 使って CodePen で書いたトイ・ダイアグラムエディタを題材に構造を検討してみます。

blog.kondoumh.com

JointJS は Backbone ベースなので Vue にうまく混ぜられるのかなあと思いましたが、あっさり表示できました。data に graph オブジェクトを保持して mounted で joint.dia.Graph オブジェクトを設定すれば、methods 内のメソッドから graph オブジェクト経由でダイアグラムにノードを追加することができるようになりました。

JointDia.vue 抜粋

<template>
  <div>
    <div ref="myholder"></div>
    <input type="text" placeholder="new node name" v-model="nodeName" />
    <input type="button" v-on:click="addNode" value="add node" />
  </div>
</template>

<script>
  export default {
    mounted() {
      this.graph = new joint.dia.Graph
      let paper = new joint.dia.Paper({
        el: this.$refs.myholder,
        model: this.graph,
      }
    },
    data() {
      return {
        graph: {}
      }
    },
    methods : {
      addNode() {
        const rect = new joint.shapes.standard.Rectangle()
        rect.addTo(this.graph)
      }
    }
</script>

一通り動きましたが、ダイアグラム部分とノード登録の Form 部分が分かれていないので両者のデータ、メソッドが混然一体となっています。Vue らしく別コンポーネントにしたいところです。

Form 部分を InputForm.Vue に切り出しました。追加するノード名を nodeName というデータで保持し、ボタンのクリック時 $emit で親コンポーネント (App.vue) に通知するようにしました。

<template>
  <div>
    <div>
      <input type="text" placeholder="new node name" v-model.trim="nodeName" />
      <input type="button" v-on:click="add" value="add node" />
    </div>
  </div>
</template>

<script>
  export default {
    name: 'InputForm',
    data() {
      return {
          nodeName: '',
      }
    },
    methods: {
      add() {
        this.$emit('addNode', this.nodeName)
        this.nodeName = ''
      }
    }
  }
</script>

App.vue では InputForm と JointDia をコンポーネントとして利用しています。InputForm の v-on 属性で InputForm で emit したイベントハンドラーの関数 addNode を登録しています JointDia の v-bind: 属性で JointDia の nodeName プロパティと App.vue の data である nodeName をバインドしています。addNode 関数では、nodeName 受け取ったデータで書き換えます。これにより、JointDia コンポーネントの nodeName プロパティが書き換わります。

<template>
  <div id="app">
    <InputForm v-on:addNode="addNode" />
    <JointDia v-bind:nodeName="nodeName" />
  </div>
</template>

<script>
import JointDia from '@/components/JointDia'
import InputForm from '@/components/InputForm'

export default {
  name: 'App',
  components: {
    JointDia,
    InputForm
  },
  data() {
    return {
      nodeName: ''
    }
  },
  methods: {
    addNode(name) {
      this.nodeName = name
    }
  }
}
</script>

JointDia.vue の抜粋。プロパティ nodeName の変化を watch し変化時に Graph にノードを追加しています。

<template>
  <div>
    <div ref="myholder"></div>
  </div>
</template>

<script>
  import joint from 'jointjs'

  export default {
    name: 'JointDia',
    mounted(){
    },
    data() {
    },
    props: {
      nodeName: String,
    },
    watch: {
      nodeName: {
        handler (newVal, oldVal) {
          this.addNode(newVal)
        }
      }
    },
    methods: {
      addNode(name) {
      }
    }
  }
</script>

コンポーネントを分割すると、データ伝搬のコードが増えますが、コンポーネント内部は自身の責務に集中できてシンプルになる感じです。Vue らしいもっとすっきりした書き方があるかもしれませんが、ひとまずコードは CodeSandbox に置きました。

codesandbox.io

新規追加のノード名だけではなくダイアグラム内のノードのコレクションも管理する場合、JointJS のデータ構造とマッピングするためのコードを追加していく必要があります。