【Javascript】select要素の開いた/閉じたイベントを実装した話
はじめに
前回の記事の続き
前回の話は、セレクトボックスを選択した時(値が決まった時)にモーダルを表示するという機能を作成したけれども、
onchangeイベントを使っていたため、同じ値を選択したときにモーダルが表示されない問題点があった。
調べてみてもあまりコレだ!とくる内容のサイト・記事が見つからなかった
なので、セレクトボックスに「開かれた」、「閉じた」、「値が変わらなかった」という独自イベントを作成した話
実装
動作するイベントを調べた結果、「開くとき」に発生するイベントは「click」で「閉じるとき」に発生するイベントは「click blur keyup」ということがわかった(Chromeで調査)
今回はjQueryとAngularJSを使って実装
HTML
<div ng-app="App" ng-controller="Sample"> <select ng-model="Sample.selectValue" ng-select-event expanded="Sample.onOpened()" unexpanded="Sample.onClosed()" nonchanged="Sample.onNonChanged()" ng-change="Sample.onChanged()"> <option value="1">1</option> <option value="2">2</option> <option value="3">3</option> </select> {{Sample.selectValue}} </div>
Javascript(jQuery、AngularJS)
(function(window, jQuery, angular) { 'use strict'; var module = angular.module('App', []); module.controller('Sample', [ '$scope', function($scope) { $scope.Sample = this; this.selectValue = '1'; this.onOpened = function() { window.console.log('おーぷん'); }; this.onClosed = function() { window.console.log('しまった'); }; this.onChanged = function() { window.console.log('変わった'); }; this.onNonChanged = function() { window.console.log('変わってない'); }; } ]); /** * 普通のselect要素では、「リストボックスが開かれた」、「リストボックスが閉じた」、 * 「値が変わらなかった」等のイベントが無いため作成 * * 以下のイベントを発生させる * ・リストボックスが開かれた * ・リストボックスが閉じた * ・値が変わらなかった */ module.directive('ngSelectEvent', [ '$timeout', function($timeout) { return { restrict: 'A', scope: { expanded: '&', unexpanded: '&', nonchanged: '&' }, link: function(scope, element, atts, ctrl) { var jQueryElement = jQuery(element); var WATCH_EXPAND_EVENTS = 'click'; var WATCH_UNEXPAND_EVENTS = 'click blur keyup'; jQueryElement.data('expanded', false); var __expanding = function() { jQueryElement.off(WATCH_EXPAND_EVENTS, __expanding); jQueryElement.data('expanded', true); jQueryElement.data('oldvalue', jQueryElement.val()); jQueryElement.on(WATCH_UNEXPAND_EVENTS, __unexpanding); (scope.expanded)(); }; var __unexpanding = function() { jQueryElement.off(WATCH_UNEXPAND_EVENTS, __unexpanding); jQueryElement.data('expanded', false); jQueryElement.on(WATCH_EXPAND_EVENTS, __expanding); (scope.unexpanded)(); var oldValue = jQueryElement.data('oldvalue'); $timeout(function() { var newValue = jQueryElement.val(); if (oldValue == newValue) { (scope.nonchanged)(); } }, 0); }; jQueryElement.on(WATCH_EXPAND_EVENTS, __expanding); } }; } ]); })(window, window.jQuery, window.angular);
今後の自分のためにも、AngularJSを使用しないソースコードも作ろうと思ったけど面倒くさくなったでござる
問題点?
見直しでjsfiddleで動作確認したところ、「ESC」キーを押したときの挙動がイマイチかも
「ESC」キー押してもイベントが走るように、「keyup」イベントを監視するようにしているけれども、2回押さないと反応してくれない・・・
他のキー(数字の1とか)を押した後だと、1回でイベントが呼ばれる感じ
keyupイベントは別処理にして、押されたキーのチェックをしたほうが良いかもしれない
【Javascript】ある要素のすべてのイベントの発生タイミングを調べる
経緯
セレクトボックスを選択した時(値が決まった時)にモーダルを表示するという機能を作成した
この時、セレクトボックスのonchangeイベントに関数を登録し、内部で値をチェック後、モーダルを表示する動きをしていた
この時の問題点は、同じ値を選択したときにモーダルが表示されないこと
onchangeイベントが発生しないため仕方ない・・・
チェックするイベントを増やせば良いと思ったものの、「選択中」とか「変わらなかった」とか分かりやすいイベントが無い
今回はセレクトボックスだったので、セレクトボックスに「開かれた」、「閉じた」、「値が変わらなかった」という独自イベントを追加したくなった
なので、セレクトボックスが「開かれた」・「閉じた」ときにどんなイベントが発生しているかチェックする処理を他のサイトを参考にしながら作成した
実装
以下のサイトを参考に作成
(function(window, jQuery) { 'use strict'; function getAllEvents(element) { var result = []; for(var key in element) { if (key.indexOf('on') === 0) { var text = key.substr(2, key.length) ; result.push(text); } } return result.join(' '); } window.console.log(jQuery('#test').context); window.console.log(getAllEvents(jQuery('#test')[0])); jQuery('#test').bind(getAllEvents(jQuery('#test')[0]), function(e) { window.console.log(e.type); }); })(window, window.jQuery);
↓実際のコード
セレクトボックスに「開かれた」、「閉じた」、「値が変わらなかった」という独自イベントを追加した話は次に書こうと思う
【Python】運行情報をSlack API を使って通知するデーモンを作ってみた
前にPythonでYahoo路線情報から運行情報をスクレイピングで取得するアプリを作った
なので今回は運行情報が変わったときに、Slack APIで通知するデーモンを作ることにした
ざっくり機能まとめ
- 10分間隔でYahoo路線情報にアクセスして、スクレイピング
- 情報を監視する路線は複数対応可能
- 以下の状態でSlackに通知する
- トラブルを検知したとき
- トラブルが無くなったとき
- 情報が更新されたとき(掲載日時が変わったとき)
Pythonスクリプトの作成
普段Pythonを触らないので、命名規則やら標準関数・クラスに苦しみながら作ってみた
まずは、Yahoo路線情報からスクレイピングする機能を作成
#!/ust/bin/env python # -*- coding: utf-8 -*- from abc import ABCMeta, abstractmethod from bs4 import BeautifulSoup from datetime import datetime from urllib import request from my_util import logger, dict_to_str class Scraper(metaclass=ABCMeta): def __init__(self, url): self._url = url @abstractmethod def get_data(self): pass def __make_proxy_url(self): pass class JRService(Scraper): def __init__(self, url): super().__init__(url) def get_data(self): html = request.urlopen(self._url).read() soup = BeautifulSoup(html, 'lxml') # 路線情報 route_element = soup.find('div', {'id': 'main'}).find('div', {'class': 'mainWrp'}).find('div', {'class': 'labelLarge'}) route_name = route_element.find('h1', {'class': 'title'}).find(text=True, recursive=False) update_time_str = route_element.find('span', {'class': 'subText'}).find(text=True, recursive=False) update_time = self.__get_update_datetime(update_time_str) # 運行情報 status_element = soup.find('div', {'id': 'mdServiceStatus'}) information = status_element.find('p').find(text=True, recursive=False) posting_element = status_element.find('p').find('span') posting_data = self.__get_posting_datetime(posting_element) message = information + posting_data['datetime_str'] trouble_class = status_element.find('dd', {'class': 'trouble'}) in_trouble = False if trouble_class is not None: in_trouble = True data = { 'route_name': route_name, 'update_datetime': update_time, 'posting_datetime': posting_data['datetime'], 'message': message, 'in_trouble': in_trouble } logger.info(dict_to_str(data)) return data @staticmethod def __get_update_datetime(update_time_str): year = datetime.today().year convert_str = '%d年%s' % (year, update_time_str) update_time = datetime.strptime(convert_str, '%Y年%m月%d日 %H時%M分更新') return update_time @staticmethod def __get_posting_datetime(element): posting_date_str = '' posting_date = None if element is not None: posting_date_str = element.find(text=True, recursive=False) year = datetime.today().year convert_str = '%d年%s' % (year, posting_date_str) posting_date = datetime.strptime(convert_str, '%Y年(%m月%d日 %H時%M分掲載)') return { 'datetime_str': posting_date_str, 'datetime': posting_date }
次に周期的にスクレイピング処理を実行し、情報が変わっていたら、通知する処理を作成
#!/ust/bin/env python # -*- coding: utf-8 -*- from configparser import ConfigParser from datetime import datetime from scraping import JRService from threading import Timer from time import sleep from sys import exit from my_util import logger import requests config = ConfigParser() config.read('./.env') thread_time_min = config.getint('settings', 'watch_interval_min') thread_time_sec = thread_time_min * 60 services = config.items('jr_services') def push_slack_chat(name, message): url = config.get('settings', 'post_url') token = config.get('settings', 'token') channel = config.get('settings', 'channel') data = { 'token': token, 'channel': channel, 'username': name, 'text': message } requests.post(url, data=data) def scrape_service(): try: for key, url in services: scraper = JRService(url) data = scraper.get_data() last_posting_datetime_str = config.get('posting_datetime', key) last_posting_datetime = datetime.strptime(last_posting_datetime_str, '%Y-%m-%d %H:%M') last_in_trouble = config.getboolean('in_trouble', key) if data['in_trouble'] and (data['posting_datetime'] <= last_posting_datetime): continue elif (data['in_trouble'] and not last_in_trouble)\ or (last_in_trouble and not data['in_trouble'])\ or data['in_trouble'] and (data['posting_datetime'] > last_posting_datetime): push_slack_chat(data['route_name'], data['message']) in_trouble_str = 'True' if data['in_trouble'] else 'False' config.set('in_trouble', key, in_trouble_str) if data['in_trouble']: posting_datetime_str = data['posting_datetime'].strftime('%Y-%m-%d %H:%M') config.set('posting_datetime', key, posting_datetime_str) config.write(open('./.env', 'w')) except Exception as e: logger.error(e.with_traceback()) finally: thread = Timer(thread_time_sec, scrape_service) thread.start() if __name__ == '__main__': scrape_service() try: while True: sleep(0.1) except KeyboardInterrupt: exit(0)
全体的に日時の文字列をdatetime型に変換する方法がなんか冗長な気がする・・・
↓参考にならないかもしれないソースコードはコチラ
supervisorの設定
作成したPythonスクリプトをデーモン化するため、supervisorの設定を追記
ここで使用しているsupervisorのバージョンは2.1
ちなみに、Pythonスクリプトの配置場所は「/usr/local/bin/Train-Service-Information」
[program:train_service_information] command=bash -c '. /etc/profile; cd /usr/local/bin/Train-Service-Information; python -u main.py' autostart=true autorestart=true startsecs=10 startretries=3 log_stdout=true log_stderr=true logfile=/var/log/supervisor/train_service_information.log
commandで「. /etc/profile」や「cd /usr/local/bin/Train-Service-Information」をしているのは、環境変数がなかったり、カレントがsupervisor2.1で設定出来ないため
あと「python -u main.py」の【-u】オプションは、スクリプトのデバッグログが全然出力されないのを防ぐため、バッファリングを無効化するオプション
↓参考サイト
pythonで標準出力のバッファリングを無効にする - blog.mouten.info
実行結果
ちゃんと遅延が発生した時と遅延が収まった時にSlackに通知することが出来た。
次の機能について
Yahoo路線情報は情報の更新に大きめなタイムラグがある
ツイッターのつぶやきを解析して通知する機能を入れたほうがリアルタイムな感じになるかも