kintone ポータルイベントが追加されたので、スペースイベント作ってみた。

# 2021/4 の[アップデート](https://kintone.cybozu.co.jp/update/main/2021-04.html)から[スペース表示イベント](https://developer.cybozu.io/hc/ja/articles/900006291023)が追加されているのでこちらを使用しましょう。







# :gear: 2019年7月アップデート
kintone 7月版から[ポータルイベント](https://developer.cybozu.io/hc/ja/articles/360028785452)と[ポータルの要素を取得する](https://developer.cybozu.io/hc/ja/articles/360028785552) JS API が追加されました。

詳細：https://developer.cybozu.io/hc/ja/articles/360028388291

# :gear: スペースのポータルには使えない…
![pose_ochikomu_businessman.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/31fb33bc-92a5-2aeb-07fa-3af7d4a0d6d9.png)

https://{subdomain}.cybozu.com/k/#/portal
に対して有効な JS API ですが、スペースのポータルでは発火しません。
![Image 1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/486c8802-0441-4553-e529-ea04c020bdd1.png)

スペースは部署やグループごとに作成しますが、すべてのスペースの構成が一緒なので、ぱっと見どのスペースかわからなくなったりします。
チームの特色を出すために独自のポータルが欲しくなります。
![Image 2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/975b8a65-4b60-2c03-d309-4ff900b71a3d.png)

# :gear: 本題
ポータルイベントはうれしい。
柔軟な kintone がさらに柔軟になり、
今まで kintone 界隈では下火だった React や Vue が使えるようになるかもしれないですね。
ただ、やっぱり…



**スペースのポータルをカスタマイズしたい。**

スペースカスタマイズしてやりましょう。
![niyakeru_takuramu_ayashii_man.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/a3640609-2e40-4797-97ef-1cdbca2a146d.png)

# :gear: コード
不格好ながらポータルイベントを作成しました。
コメントとかまだ途中ですが、コメント付ける元気は今のところないのでこのまま公開します。
GitHub: https://github.com/Naoto00/kintone-space-portal

```javascript:customize.js
(function() {
  'use strict';
  // スペースポータルのクラス名
  var CONFIG = {
    portalBodyId: 'contents-body-ocean', // ボディ
    ntfClass: 'gaia-argoui-page-space-show-left', // お知らせ
    threadClass: 'gaia-argoui-space-threadlistwidget', // スレッド
    appClass: 'gaia-argoui-space-applistwidget', // アプリ
    peopleClass: 'gaia-argoui-space-peoplelistwidget', // ピープル
    linkClass: 'gaia-argoui-space-relatedlinklistwidget', // 関連リンク
    spaceBodyClass: 'gaia-argoui-space-spacelayout-body', // スペースボディ
    rightBodyClass: 'gaia-argoui-page-space-show-right' // スペースの右側
  };

  window.SpaceCustomize = window.SpaceCustomize || {};

  /**
   * SpaceCustomize のインスタンス
   *
   * @param {Integer} spaceId
   * @param {String} domain
   */
  window.SpaceCustomize = function(spaceId, domain) {
    this.spaceId = spaceId;
    this.domain = domain + '.cybozu.com';
    this.spaceURL = 'https://' + this.domain + '/k/#/space/' + this.spaceId;
    this.setSpaceId = function(newID) {
      this.spaceId = newID;
    };
    this.getSpaceId = function() {
      return this.spaceId;
    };

    /**
     * イベントの定義
     *
     * @param {*} main
     * @param {*} deleteFlag
     */
    this.events = function(main, deleteFlag) {
      // ポータル body を取得
      var body = document.getElementById(CONFIG.portalBodyId);
      var self = this;
      // お知らせ、スレッド、アプリ、ピープルのウィジェットを監視
      var observer = new MutationObserver(function(MutationRecords) {
        // 各ウィジェット削除
        var $targetElm = document.getElementsByClassName(CONFIG.ntfClass);
        if (!$targetElm.length) return;
        if (deleteFlag) {
          self.deleteNtf();
          self.deleteThread();
          self.deleteApp();
          self.deletePeople();
        }
        // 関連リンクのみ描画タイミングが遅いので、別で監視
        var $bo = document.getElementsByClassName(CONFIG.rightBodyClass);
        observerLink.observe($bo[0], options);
        observer.disconnect();
      });
      // observer オプション定義
      var options = {
        childList: true
      };
      // 関連リンクのウィジェットを監視
      var observerLink = new MutationObserver(function(MutationRecord) {
        // 関連リンクウィジェット削除
        var $targetElm = document.getElementsByClassName(CONFIG.linkClass);
        if (!$targetElm.length) return;
        if (deleteFlag) {
          self.deleteLink();
        }
        // コールバック関数実行
        main();
        observerLink.disconnect();
      });

      // 対象のスペース URL で監視開始
      if (location.href === this.spaceURL) {
        observer.observe(body, options);
      }

      // ハッシュ値変更もバインド
      window.onhashchange = function() {
        if (location.href !== self.spaceURL) return;
        observer.observe(body, options);
      };
    };

    this.deleteNtf = function() {
      var $ntfDiv = document.getElementsByClassName(CONFIG.ntfClass);
      if ($ntfDiv.length) {
        $ntfDiv[0].parentNode.removeChild($ntfDiv[0]);
      }
    };
    this.getNtfWidget = function() {
      var $ntfDiv = document.getElementsByClassName(CONFIG.ntfClass);
      return $ntfDiv[0];
    };

    this.deleteThread = function() {
      var $threadDiv = document.getElementsByClassName(CONFIG.threadClass);
      if ($threadDiv.length) {
        $threadDiv[0].parentNode.removeChild($threadDiv[0]);
      }
    };
    this.getThreadWidget = function() {
      var $threadDiv = document.getElementsByClassName(CONFIG.threadClass);
      return $threadDiv[0];
    };

    this.deleteApp = function() {
      var $appDiv = document.getElementsByClassName(CONFIG.appClass);
      if ($appDiv.length) {
        $appDiv[0].parentNode.removeChild($appDiv[0]);
      }
    };
    this.getAppWidget = function() {
      var $appDiv = document.getElementsByClassName(CONFIG.appClass);
      return $appDiv[0];
    };

    this.deletePeople = function() {
      var $peopleDiv = document.getElementsByClassName(CONFIG.peopleClass);
      if ($peopleDiv.length) {
        $peopleDiv[0].parentNode.removeChild($peopleDiv[0]);
      }
    };
    this.getPeopleWidget = function() {
      var $peopleDiv = document.getElementsByClassName(CONFIG.peopleClass);
      return $peopleDiv[0];
    };

    this.deleteLink = function() {
      var $linkDiv = document.getElementsByClassName(CONFIG.linkClass);
      if ($linkDiv.length) {
        $linkDiv[0].parentNode.removeChild($linkDiv[0]);
      }
    };
    this.getLinkWidget = function() {
      var $linkDiv = document.getElementsByClassName(CONFIG.linkClass);
      return $linkDiv[0];
    };

    this.deleteAll = function() {
      this.deleteNtf();
      this.deleteThread();
      this.deleteApp();
      this.deletePeople();
      this.deleteLink();
    };

    this.getSpaceBody = function() {
      var $spaceBody = document.getElementsByClassName(CONFIG.spaceBodyClass);
      return $spaceBody[0];
    };
  };
})();
```

# :gear: 動かしてみた
とりあえず使ってみましょう。

```javascript:main.js
(function() {
  'use strict';
  // スペースID と サブドメイン
  var space = new SpaceCustomize(24, '{subdomain}');
  var domain = space.domain;
  var currentURL = location.href;

  // アプリの画面で読み込まないように変更
  if (!(currentURL.indexOf('https://' + domain + '/k/#/') === 0 || currentURL === 'https://' + domain + '/k/')) {
    return;
  }

  space.events(function() {
    console.log(space.getSpaceBody());
    alert('Hello Portal!');
  }, true);

})();
```
![Image 3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214670/45a3651f-23da-eb85-4aef-91a7486ec379.png)

**やったーーー！！！！！！**

# :gear: 使い方
### 導入
まずは `customize.js` を全体 JS に適用しましょう。
次に `main.js` 、実際にイベント内の処理を記述するファイルでは、以下のように宣言します。
`var space = new SpaceCustomize(24, '{subdomain}');`
スペース ID とサブドメイン名を入れてあげるとイベントが使えるようになります。
一応汎用的に作ったので、どのスペースでも使えるかと。
### イベント発火
`kintone.events.on()` みたいな感じで使います。
`space.events(callback, true)` でポータル描画後にコールバックが動きます。
第二引数は `boolean`で、`true` の場合、ポータルのお知らせなどのウィジェットがすべて消えます。
デフォルトは `false` です 

### ポータルの要素取得
`space.getSpaceBody()`
で ボディの Node が返ってきます。
お好きなポータルを作りましょう。

### 各ウィジェットの取得
ウィジェットそのままに、中身を DOM りたい人は以下使ってください。
・お知らせウィジェット
`space.getNtfWidget()`
・スレッドウィジェット
`space.getThreadWidget()`
・アプリウィジェット
`space.getAppWidget()`
・ピープルウィジェット
`space.getPeopleWidget()`
・関連リンクウィジェット
`space.getLinkWidget()`

# :gear: まとめ
これでスペースのカスタマイズが捗りますね。

### 大変だったこと
通知画面のカスタマイズで、kintone の要素は JS によって描画されるということは理解していたので、初めは順調でした。
ただ、関連リンクのウィジェットだけほかのウィジェットと描画タイミングが違い、イベント発火のタイミングが難しかったです。。。
（一応ウィジェット消す関数も用意していますが、使用するとこれのせいで若干要素が描画されてしまいます。
第二引数に `true` で全消ししてくれれば描画されることはないはずです。）
あと、前回は jQuery 使っていましたが、今回はネイティブの JS のみで実装しました。
これもなかなか面倒だった。。。


何かバグとかあったらコメントか [GitHub](https://github.com/Naoto00/kintone-space-portal) までお願いします。

ありがとうございました！