- 投稿日:2021-08-29T23:14:01+09:00
Puppeteer でスクレイピングを行う方法
前提条件 Yarn がインストールされている 作成手順 作業用ディレクトリを作成 コマンドライン上で作業用ディレクトリに移動 yarn init -y を実行して package.json を生成 yarn add puppeteer を実行。スクレイピングは Puppeteer で行う 次のような内容の index.js を作成。node index.js を実行すると「Example Domain」が出力される。これは <h1> 要素のテキストである const puppeteer = require("puppeteer"); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://example.com"); await page.waitForSelector("h1"); const elem = await page.$("h1"); const text = await elem.evaluate(el => el.textContent); console.log(text); await browser.close(); })(); 色々 await browser.newPage() によって Page オブジェクトが取得できる。これは、ブラウザでいう document に相当すると思う document.querySelector() は page.$() と似ている document.querySelectorAll() は page.$$() と似ている await page.$() によって要素のようなものが取得できる 要素のようなものは、そのままでは属性値もテキストも取得できない。取得するためには .evaluate() を利用する 要素のようなものを e と置いたとき、e.evaluate(el => el.textContent) でテキストを、e.evaluate(el => el.getAttribute("href")) で属性値を取得できる。これらの返す値は Promise である 便利な記述 await page.waitForSelector() : セレクタに一致する要素が現れるまで待機する await page.goto() : 特定のページに遷移する await page.waitForTimeout(1000) : 1秒間何もしない。スクレイピング対象のWebサイトに負荷をかけないために使う await page.evaluate(() => { /* ... */ }) : コールバック関数はブラウザのコンテキストで実行される。よってブラウザ上のあらゆる情報が取得できる。たとえば次のように書くと、ドキュメントの横幅と縦幅が取得できる const rect = await page.evaluate(() => { return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, }; }); page.$() や page.$$() は page.evaluate() で代用可能。たとえば最初のプログラムは次のように書ける。フロントエンジニアはこちらのほうが覚えることが少なくて楽かも。 const puppeteer = require("puppeteer"); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://example.com"); await page.waitForSelector("h1"); const text = await page.evaluate(() => { const h1 = document.querySelector("h1"); return h1.textContent; }); console.log(text); await browser.close(); })(); 色々その2 書き捨てのプログラムであればピュアな JavaScript でOK。VSCode を使えば十分に補完が効いてくれる TypeScript で書きたい場合は、作業用ディレクトリに移動後、以下のコマンド群を実行する。index.ts を編集すると自動で再実行してくれる(以下でインストールしているnodemonは22.9kスター、ts-nodeは9.1kスターを獲得しているのでどちらも信用できると思う) yarn init -y yarn add typescript ts-node nodemon touch index.ts yarn nodemon index.ts もしかすると足りないパッケージがあるかもしれない。手元の環境では、この3つのパッケージをインストールするだけで正常に実行できた。 Puppeteer の page.evaluate() に渡す関数内では document などを使うが、何も設定せずに TypeScript で document を参照すると「document は見つからない」と指摘される。yarn tsc --init で tsconfig.json を作成し、lib: [] の配列に "DOM" を追加することで解決する。TypeScript を使った Puppeteer のコードは次のようになる import puppeteer from "puppeteer"; (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto("https://example.com"); await page.waitForSelector("h1"); const text = await page.evaluate(() => { return document.querySelector("h1")?.textContent ?? null; }); console.log(text); await browser.close(); })(); まとめ まずはこの手順でスクレイピングプログラムを作成し、完成したらひとつの関数にまとめて、Cloud Functionで使ったりBigQueryに結果を保存したりすると良さそう。そのときのことを考えると、TypeScriptであらかじめ型付けしておいたほうが楽。 ということで、記事中にある以下のコマンド群はこれから頻繁に使っていくと思う。 yarn init -y yarn add typescript ts-node nodemon touch index.ts yarn nodemon index.ts
- 投稿日:2021-08-29T23:12:39+09:00
QR Codeをライブラリレスで
参考サイトの情報をもとに作ってみました。 See the Pen QR Code Data Structure by kob58im (@kob58im) on CodePen. 参考サイト 特集:あなたも描ける! JIS X0510準拠 QRコード®超入門 - OSSHC 岡崎高校スーパーサイエンス部 QRコードをつくってみる その4 - データの配置 ライブラリを使わずにC#でQRコードを生成してみる - Qiita QRコード - Wikipedia
- 投稿日:2021-08-29T23:05:31+09:00
jsでバーコードリーダー
ブラウザで動作するバーコードリーダーを実装した際の覚書 使用したライブラリ ZXing for JS 細かいことはドキュメントとか読んでね。 とりあえずインストールして # npm install npm i @zxing/library npm i @zxing/browser 表示用のhtml用意して <!-- カメラ描画用 --> <video id="player" controls autoplay playsinline style="width:320px; height:240px;" ></video> <!-- 読み込み結果表示 --> <input type="text" id="read_result"> リーダーを起動する import { BrowserMultiFormatOneDReader, BrowserQRCodeReader } from '@zxing/browser'; import { BrowserMultiFormatReader, NotFoundException, Result } from '@zxing/library'; const codeReader = new BrowserMultiFormatReader() codeReader.decodeFromConstraints({ audio: false, video: { width: {max: 640}, height: {max: 640}, focusMode: "continuous", // オートフォーカス有効にしたい facingMode: "environment", // 背面カメラ使う aspectRatio: 4/3, }, }, "player", (result, err) => { if (result) { // 読取結果を表示 const textbox = document.getElementById("read_result") textbox.value = result.text } }) </script> 最低限はこんな感じで。 余談 カメラが使えるかチェックする const is_camera_enabled = !!navigator.mediaDevices
- 投稿日:2021-08-29T22:31:42+09:00
Firebase Database NoSDK
エンドポイントの設定 base='https://osaka5-default-rtdb.asia-southeast1.firebasedatabase.app' Realtime Databaseを開き、赤枠内をbase変数に設定 Firebaseプロジェクト作成(80秒) Firebaseは無料で利用できます。クレカは登録してはいけません データを追加 curl curl $base/users.json -d '{"名前":"山田太郎","出身地":"沖縄"}' JavaScript await fetch(base+'/users.json', { method: 'POST', body: '{"名前":"山田花子","出身地":"北海道"}' }) Google Apps Script UrlFetchApp.fetch(base+'/users.json', { method: 'POST', payload: '{"名前":"JohnDoe","出身地":"米国"}' }) データを取得 curl curl $base/users.json JavaScript await fetch(base+'/users.json').then(r => r.json()) Google Apps Script JSON.parse(UrlFetchApp.fetch(base+'/users.json')) すべてのデータを取得 curl curl $base/.json JavaScript await fetch(base+'/.json').then(r => r.json()) Google Apps Script JSON.parse(UrlFetchApp.fetch(base+'/.json')) データを上書き curl curl $base/users/1.json -X PUT -d '{"名前":"山田太郎","出身地":"石垣島"}' JavaScript await fetch(base+'/users/2.json', { method: 'PUT', body: '{"名前":"山田花子","出身地":"択捉島"}' }) Google Apps Script UrlFetchApp.fetch(base+'/users/3.json', { method: 'PUT', payload: '{"名前":"JohnDoe","出身地":"カナダ"}' }) データを更新 curl curl $base/users/1.json -X PATCH -d '{"出身地":"沖ノ鳥島"}' JavaScript await fetch(base+'/users/2.json', { method: 'PATCH', body: '{"出身地":"弁天島"}' }) Google Apps Script UrlFetchApp.fetch(base+'/users/3.json', { method: 'PATCH', payload: '{"出身地":"ハワイ"}' }) データを削除 curl curl $base/users/1.json -X DELETE JavaScript await fetch(base+'/users/2.json', {method: 'DELETE'}) Google Apps Script UrlFetchApp.fetch(base+'/users/3.json', {method: 'DELETE'}) キャッシュ curl curl -i $base/users.json -H 'X-Firebase-ETag: true' ETagが付与される JavaScript await fetch(base+'/users.json', { headers: {'X-Firebase-ETag': true} }).then(r => r.json()) キャッシュによりデータ転送量が節約できる Google Apps Script JSON.parse(UrlFetchApp.fetch(base+'/users.json', { headers: {'X-Firebase-ETag': true} })) 上書き防止 curl curl $base/users.json -H 'X-Firebase-ETag: true' -H 'if-match: u/abcdZ64BrbTEOhyb2V5TWQM2o=' -iX PUT -d '{"名前":"山田太郎","出身地":"石垣島"}' 上記ETag: u/nNaTZ64BrbTEOhyb2V5TWQM2o=と異なるため上書きできない。データのコンフリクトを回避できる JavaScript await fetch(base+'/users.json', { method: 'PUT', headers: {'if-match': 'u/abcdZ64BrbTEOhyb2V5TWQM2o='}, body: '{"名前":"山田花子","出身地":"択捉島"}' }) Google Apps Script UrlFetchApp.fetch(base+'/users.json', { method: 'PUT', headers: {'if-match': 'u/abcdZ64BrbTEOhyb2V5TWQM2o='}, payload: '{"名前":"JohnDoe","出身地":"カナダ"}' }) クエリパラメータ プリティープリント プリティー curl $base/.json?print=pretty Not プリティー curl $base/.json 一階層目のみ取得(Not再帰的) curl $base/.json?shallow=true JSONP <script> function gotData(data) { console.log(data); } </script> <script src="https://osaka5-default-rtdb.asia-southeast1.firebasedatabase.app/.json?callback=gotData"></script> Download https://osaka5-default-rtdb.asia-southeast1.firebasedatabase.app/.json?download=db.json Content-Dispositionが付与され、ダウンロードできます Playground Access-Control-Allow-Origin: *のため、どこからでもAPIを実行可能です F12を押してQiita上やTampermonkeyでも実行できます。 See the Pen Firebase Realtime Database by John Doe (@04) on CodePen. 引用
- 投稿日:2021-08-29T20:11:14+09:00
Intersection Observerを使ってみる
Intersection Observerとは 【参考記事↓】 ・使ってみよう!Intersection Observer! ・JSでのスクロール連動エフェクトにはIntersection Observerが便利 実装の流れ ①監視したい要素を取得 ②オプションの設定 ③トリガーが交差したときの処理(今回はシンプルにクラスの付け外し) ④Intersection Observerを呼び出し ⑤IE11への対応 サンプル main.js //監視したい要素を取得 const scrollTrigger = document.querySelectorAll(".js-trigger"); const targetArray = Array.prototype.slice.call(scrollTrigger); //オプション設定 const scrollOption = { root: null, rootMargin: "-30% 0px", threshold: 0 }; //トリガーが交差したときの処理 function targets(entries) { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add("is-active"); } }); }; //Intersection Observerを呼び出し const scrollObserver = new IntersectionObserver(targets, scrollOption); targetArray.forEach((target) => { scrollObserver.observe(target); }); ①監視したい要素を取得 IE11でもforEachが動くように要素たちを明示的に配列としています。 main.js //監視したい要素を取得 const scrollTrigger = document.querySelectorAll(".js-trigger"); const targetArray = Array.prototype.slice.call(scrollTrigger); 【参考記事↓】 ・[JS]getElementByIdでは上手くいくのにgetElementsByClassNameでは上手くDOM操作が出来ないとき ②オプションの設定 ビューポート下辺から30%地点を交差地点に設定します main.js //オプション設定 const scrollOption = { root: null, rootMargin: "-30% 0px", threshold: 0 }; 【オプションについての参考記事↓】 ・使ってみよう!Intersection Observer! | ics.media ・JSでのスクロール連動エフェクトにはIntersection Observerが便利 ③トリガーが交差したときの処理 ターゲットとなる要素が交差してきたら.is-activeクラスを付与します。 ・entry.isIntersectingは要素が交差してきたらという意味。 ・監視要素はentry.targetでforEachの中での操作が可能。 main.js function targets(entries) { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add("is-active"); } }); }; ④Intersection Observerを呼び出し オプションとコールバック関数を渡してIntersection Observerを呼び出して、最初に取得した要素の一つ一つにforEachで監視マークを付けていきます。 main.js //Intersection Observerを呼び出し const scrollObserver = new IntersectionObserver(targets, scrollOption); targetArray.forEach((target) => { scrollObserver.observe(target); }); ⑤IE11への対応 polyfillを読み込ませる ・npmからimportして読み込ませる(IntersectionObserver polyfill) $ npm i intersection-observer ・CDNで読み込ませる <script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
- 投稿日:2021-08-29T20:03:54+09:00
【Handsontable】セル内に日時入力(DateTimePicker)の表示
はじめに 前回、weareoutman氏が作成した「ClockPicker」を使用して、セル内に時刻入力(ClockPicker)の表示を実現しました。 それならと日付と時刻の両方を満たす DateTimePicker に挑戦してみました。 今回は、バニラJavaScriptで作成されている DatetimePickerの「flatpickr」で実現したいと思います。 環境 HandsontableはMITライセンス版のバージョン 6.2.2を使用しています。 一応、有償版バージョン 8.3.2でも動作は確認しています。 CDN <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" /> <script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.js"></script> 仕様 今回は日時(yyyy/mm/dd hh:mi形式)とし、分は10分単位で増分するようにしています。 下記のオプションの内容を変更すれば、時間のみにすることや秒を追加することも可能です。 https://tr.you84815.space/flatpickr/configOptions.html this.dateTimeInput = flatpickr(this.dateTime, { dateFormat : 'Y/m/d H:i', enableTime : true, minuteIncrement: 10, locale : 'ja', clickOpens : false, monthSelectorType : 'static', 例えば、秒を追加したい場合、dateFormatに秒のSを追加し、秒入力の有効enableSeconds : true、増分をminuteIncrement: 1にします。どうも秒用の増分オプションは存在せず、分と秒の増分が共有で使用されてしまっています。その為、分を10分単位にすると秒も10秒単位になってしまいます。それでもよければ、minuteIncrement: 10のままでもいいです。 this.dateTimeInput = flatpickr(this.dateTime, { dateFormat : 'Y/m/d H:i:S', enableTime : true, enableSeconds : true, minuteIncrement: 1, locale : 'ja', clickOpens : false, monthSelectorType : 'static', 実装 See the Pen Handsontable datetimepicker by やじゅ (@yaju-the-encoder) on CodePen. ソースコード <!DOCTYPE html> <html lang="jp"> <body> <head> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css" /> <style> /* 日曜日:赤 */ .flatpickr-calendar .flatpickr-innerContainer .flatpickr-weekdays .flatpickr-weekday:nth-child(7n + 1), .flatpickr-calendar .flatpickr-innerContainer .flatpickr-days .flatpickr-day:not(.flatpickr-disabled):not(.prevMonthDay):not(.nextMonthDay):nth-child(7n + 1) { color: red; } /* 土曜日:青 */ .flatpickr-calendar .flatpickr-innerContainer .flatpickr-weekdays .flatpickr-weekday:nth-child(7), .flatpickr-calendar .flatpickr-innerContainer .flatpickr-days .flatpickr-day:not(.flatpickr-disabled):not(.prevMonthDay):not(.nextMonthDay):nth-child(7n) { color: blue; } .flatpickr-current-month { display: flex; justify-content: center; } .cur-year { order : 1; } .cur-month:before { content: '年 '; } .cur-month { order: 2; } .flatpickr-current-month span.cur-month { font-weight : 300; padding-top : 4px; } </style> </head> <div id="grid"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/handsontable/6.2.2/handsontable.full.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/flatpickr/dist/l10n/ja.js"></script> <script language="javascript" type="text/javascript"> (function(Handsontable){ 'use strict'; const DateTimeEditor = Handsontable.editors.BaseEditor.prototype.extend(); DateTimeEditor.prototype.init = function() { this.dateTime = document.createElement('input'); this.dateTime.setAttribute('type', 'text'); const that = this; this.dateTimeInput = flatpickr(this.dateTime, { dateFormat : 'Y/m/d H:i', enableTime : true, minuteIncrement: 10, locale : 'ja', clickOpens : false, monthSelectorType : 'static', onChange: function(selectedDates, dateStr, instance) { that.instance.setDataAtCell(that.row, that.col, dateStr); } }) }; DateTimeEditor.prototype.open = function () { if(this.dateTimeInput.isOpen) return; this.dateTimeInput.setDate(this.dateTime.value); this.dateTimeInput.open(); let rect = this.TD.getBoundingClientRect(); let offsetLeft = rect.left + window.pageXOffset || document.documentElement.scrollLeft; let offsetTop = rect.top + window.pageYOffset || document.documentElement.scrollTop; offsetTop += this.TD.offsetHeight; let pop = document.querySelector('div.flatpickr-calendar.animate.open.arrowTop.arrowLeft'); pop.style.top = offsetTop + "px"; pop.style.left = offsetLeft + "px"; event.stopPropagation(); }; DateTimeEditor.prototype.close = function () {}; DateTimeEditor.prototype.getValue = function(){ return this.dateTime.value ; }; DateTimeEditor.prototype.setValue = function(newValue){ this.dateTime.value = newValue; }; DateTimeEditor.prototype.focus = function () {}; Handsontable.editors.DateTimeEditor = DateTimeEditor; Handsontable.editors.registerEditor('datetime', DateTimeEditor); }(Handsontable)); let data = [['2021/08/27 15:30']]; let hot = new Handsontable(document.getElementById("grid"), { data: data, columns:[ { type: 'text', width: 200, renderer: 'autocomplete', editor: 'datetime', config: { enableTime : true, dateFormat : 'Y/m/d H:i', minuteIncrement : 1, locale : 'ja', clickOpens : false, monthSelectorType : 'static' } } ], colHeaders: ["日時"], manualColumnResize: true, contextMenu: { items:{ 'row_above': { name: '1行挿入' }, 'remove_row': { name: '1行削除', disabled: function(){ return hot.countRows() < 2; } }, "hsep": "---------", 'undo': { name: '戻る' }, }, }, }); </script> </body> </html> ポイント 脱jQuery flatpickr自体がバニラJavaScriptで作成されているので、jQueryを使用しない方法で修正しました。 一番ネックだったのは、offsetの書き換え処理で下記サイトを参考にしました。 イベント伝搬のキャンセル Open処理のところでイベント伝搬のキャンセル「event.stopPropagation()」処理を入れないと、カレンダー画面が表示された途端にフォーカス外と判断されてクローズ処理が動作してしまいます。 これに気が付くまであーでもないこーでもないとカレンダーが表示されるまでに一苦労してました。 初期日時セット セルに入力された内容でカレンダーの日時の状態にするためにセットしています。 this.dateTimeInput.setDate(this.dateTime.value); 最後に 日時入力の需要があるか分からない機能ですが、あるに越したことはない。 本当はもう少し汎用的にカラムオプションに日時設定を追加するようにしても良かったんだけど、だんだん面倒くさいなってしまった。 今回の成果としては、脱jQueryでカスタムエディターを作成できたことですね。
- 投稿日:2021-08-29T19:55:08+09:00
ANTLR4 を JavaScript から使ってみる
ANTLR4 で生成したパーサーを Java から使った経験があったので、同じように JavaScript から使おうとしたのですが、ハマってしまいました。何とか動かせたので手順を紹介します。 ANTLR4 のバージョンは 4.9.2 です。ドキュメントが不親切だと感じたのですが、ごく最近になって、生成されるコードが ESM 形式のモジュールに変更されていました。一方でランタイムは CommonJS 形式のままなので、現状はとても中途半端な状態です。将来、完全に ESM 形式に移行するなどの修正が行われれば、この文書の記述は役に立たなくなってしまう可能性があります。 前提 以下のインストールは済ませておいてください。 npm Java Visual Studio Code Install 'code' command in PATH も。 Chrome この手順で達成すること https://github.com/antlr/antlr4/blob/master/doc/javascript-target.md の手順に従って、以下の処理をする JavaScript プログラムを作成します。 ANTLR4 の文法をパース ANTLR でリスナーを使って左辺の名前をリストアップ ブラウザーに表示 JavaScript 用 lexer/parser の生成 一時保管用ディレクトリを作成し、ANTLR4 のパースで使う LexerAdaptor と文法ファイルをダウンロードし、ANTLR Tool (jar) で lexer/parser を生成します。 $ mkdir depot $ cd depot $ curl -sSLO https://github.com/antlr/grammars-v4/raw/master/antlr/antlr4/JavaScript/LexerAdaptor.js $ curl -sSLO https://github.com/antlr/grammars-v4/raw/master/antlr/antlr4/ANTLRv4Lexer.g4 $ curl -sSLO https://github.com/antlr/grammars-v4/raw/master/antlr/antlr4/ANTLRv4Parser.g4 $ curl -sSLO https://github.com/antlr/grammars-v4/raw/master/antlr/antlr4/LexBasic.g4 $ curl -sSLO https://www.antlr.org/download/antlr-4.9.2-complete.jar $ java -jar antlr-4.9.2-complete.jar -Dlanguage=JavaScript ANTLRv4Lexer.g4 ANTLRv4Parser.g4 $ ls ANTLRv4Lexer.g4 ANTLRv4Parser.g4 ANTLRv4ParserListener.js ANTLRv4Lexer.interp ANTLRv4Parser.interp LexBasic.g4 ANTLRv4Lexer.js ANTLRv4Parser.js LexerAdaptor.js ANTLRv4Lexer.tokens ANTLRv4Parser.tokens antlr-4.9.2-complete.jar Webpack 用の構成を作る webpack 実行用のディレクトリを作成します。最後に VS Code でこのディレクトリを開きます。 Webpack には詳しくないため、Getting Started ガイドを参考にしました。 $ cd .. $ mkdir webpack-antlr $ cd webpack-antlr $ npm init -y $ npm install webpack webpack-cli --save-dev $ code . package.json の書き換え 前述のガイドに従い、うっかり公開を防止するために package.json を書き換えます。さらに、念のため生成されたパーサーで使っている codepointat.js と fromcodepoint.js に合わせて、ライセンスを MIT にします。 package.json { "name": "webpack-antlr", "version": "1.0.0", "description": "", - "main": "index.js", + "private": true, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", - "license": "ISC", + "license": "MIT", "devDependencies": { "webpack": "^5.51.1", "webpack-cli": "^4.8.0" } } webpack.config.js の作成 webpack-antlr ディレクトリの直下に webpack.config.js を作成します。内容は以下のようにします。 webpack.config.js const path = require('path'); module.exports = { mode: 'development', entry: './src/index.js', devtool: 'eval-source-map', output: { filename: 'main.js', path: path.resolve(__dirname, 'dist'), }, resolve: { fallback: { fs: false } }, }; ガイドに記載のデフォルトから変更した箇所がいくつかあります。 resolve: { fallback: { fs: false } } を指定しています。これは javascript-target.md に記載されている修正です。これをしないと、Node.js のライブラリを読もうとしてエラーになってしまいます。 mode: 'development' を指定しています。Minify とかしてほしくなかったので。 devtool: 'eval-source-map' を指定しています。Webpack で一つにまとめられたファイルをデバッグ実行する際に、元のファイルとの位置の対応が必要なためです。Devtool で開発用に推奨されているものの中から選びました。 index.html の作成 webpack-antlr ディレクトリの直下に dist ディレクトリを作成し、index.html を作成します。内容は以下のようにします。 index.html <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>ANTLR-generated ANTLR parser running on browser JavaScript</title> </head> <body> <script src="main.js"></script> <textarea id="grammar-area" rows="10" cols="80"> // define a grammar called Hello grammar Hello; r : 'hello' ID; ID : [a-z]+ ; WS : [ \t\r\n]+ -> skip ; </textarea> <div> <button type="button" onclick="listUp()">List up rules</button> </div> <pre id="output-area"></pre> </body> </html> script タグは一つだけで、この後 webpack で作成するスクリプトを指定しています。 textarea には Hello.g4 の中身が入っています。 button がクリックされると、この後作成する listUp 関数を呼び出します。この関数は、grammar-area の中身をパースして、左辺をリストアップして、output-area に書き出します。 リストアップ処理を書いた index.js の作成 まず、webpack-antlr ディレクトリで以下を実行し、antlr4 runtime のインストールと、生成した lexer/parser の配置を行います。 $ npm install antlr4 $ mkdir src $ cp ../depot/*.js src/ 次に、src ディレクトリに index.js を作成します。中身は javascript-target.md を参考に、以下のようにします。 index.js import antlr4 from 'antlr4'; import ANTLRv4Lexer from './ANTLRv4Lexer.js'; import ANTLRv4Parser from './ANTLRv4Parser.js'; import ANTLRv4ParserListener from './ANTLRv4ParserListener.js'; function listUp() { const input = document.querySelector('#grammar-area').value; const chars = new antlr4.InputStream(input); const lexer = new ANTLRv4Lexer(chars); const tokens = new antlr4.CommonTokenStream(lexer); const parser = new ANTLRv4Parser(tokens); parser.buildParseTrees = true; const tree = parser.grammarSpec(); const lhsList = []; class MyListener extends ANTLRv4ParserListener { enterParserRuleSpec(ctx) { const ruleRefText = ctx.RULE_REF().getText(); lhsList.push(ruleRefText); } enterLexerRuleSpec(ctx) { const tokenRefText = ctx.TOKEN_REF().getText(); lhsList.push(tokenRefText); } } antlr4.tree.ParseTreeWalker.DEFAULT.walk(new MyListener(), tree); document.querySelector('#output-area').innerText = lhsList.join('\r\n'); } // make accessible from html window.listUp = listUp; VS Code の IntelliSense の効きは今ひとつです。執筆時点では、antlr4@4.9.2 を使っているにも関わらず VS Code で参照される型定義は 4.7 のもので、JSDoc は参照できません。これはランタイムだけでなく生成されたコードでも似たような状況で、例えば ctx に RULE_REF() があるのかなどは Java だと IDE の補完ですぐにわかるのですが、JavaScript ではソースコードを追う必要がありました。 上で new antlr4.InputStream(input); とした箇所は antlr4.CharStreams.fromString(input) でも良いかもしれません。CharStreams.js には、CharStreams の返す InputStream は Unicode の全範囲 (U+10FFFF まで) をサポートするのに対し、デフォルトの InputStream は U+FFFF までしかサポートしないとの記述があります。 最後の行は、HTML 側から作成した関数が見えるようにするためのものです。以下のような方法もあります。 listUp 関数を export し、webpack.config.js の libraryTarget: 'window' を使う。 listUp 関数を export し、webpack で application ではなく library を作成することで、onclick="MyLibrary.listUp()" のように参照する。 HTML には関数を書かず、addEventListener で click イベントに対応させる。この場合、script タグを button タグより後ろにする。 デバッグ実行できることの確認のために、document.querySelector('#output-area').innerText = lhsList.join('\r\n'); の行で F9 を押してブレークポイントを設定します。 webpack を実行して dist ディレクトリに必要な資材を揃える webpack-antlr ディレクトリで以下を実行します。 $ npx webpack --config webpack.config.js asset main.js 1.79 MiB [emitted] (name: main) runtime modules 670 bytes 3 modules modules by path ./node_modules/antlr4/src/antlr4/ 398 KiB modules by path ./node_modules/antlr4/src/antlr4/*.js 115 KiB 17 modules modules by path ./node_modules/antlr4/src/antlr4/atn/*.js 213 KiB 16 modules modules by path ./node_modules/antlr4/src/antlr4/error/*.js 43.7 KiB 5 modules modules by path ./node_modules/antlr4/src/antlr4/dfa/*.js 12.6 KiB 4 modules modules by path ./node_modules/antlr4/src/antlr4/tree/*.js 9.08 KiB 3 modules modules by path ./node_modules/antlr4/src/antlr4/polyfills/*.js 3.53 KiB 2 modules modules by path ./src/*.js 243 KiB ./src/index.js 1.03 KiB [built] [code generated] ./src/ANTLRv4Lexer.js 43.1 KiB [built] [code generated] ./src/ANTLRv4Parser.js 184 KiB [built] [code generated] ./src/ANTLRv4ParserListener.js 11.6 KiB [built] [code generated] ./src/LexerAdaptor.js 3.84 KiB [built] [code generated] fs (ignored) 15 bytes [built] [code generated] webpack 5.51.1 compiled successfully in 648 ms VS Code でデバッグする VS Code で index.html を開き、Activity Bar で Run view を開いて create a launch.json file のリンクをクリックし、Chrome を選択します。.vscode/launch.json にいい感じの設定ができると思います。 launch.json { // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "type": "pwa-chrome", "request": "launch", "name": "Open index.html", "file": "/path/to/webpack-antlr/dist/index.html" } ] } F5 を押してデバッグを開始すると Chrome が自動的に開きます。開いたら Chrome で F12 を押して DevTools を表示してください。 List up rules ボタンを押すと、VS Code にフォーカスが移り、設定したブレークポイントで停止しています。DevTools ではソースコードが読めないので、devtool: 'eval-source-map' の設定により非常にデバッグしやすくなっていることがわかります。lhsList にホバーすると今格納されている値が見えると思います。▶️ ボタンを押して実行を再開すると、ブラウザーにその内容が書き出されます。 まとめ javascript-target.md の手順で詳細が省略されている部分を具体的に説明しました。Webpack を使えば、CommonJS 形式と ESM 形式のモジュールが混在したコードをブラウザーで実行できることがわかりました。
- 投稿日:2021-08-29T19:44:29+09:00
用語集
refactoring リファクタリング とは、コンピュータプログラミングにおいて、プログラムの外部から見た動作を変えずにソースコードの内部構造を整理することである。 validation データのバリデーションといった場合、記述・入力されたデータが、あらかじめ規定された条件や仕様、形式などに適合しているかどうかを検証・確認することを表す。形式的な妥当性の検証であり、意味論的に適切な内容かどうかは判断しない。
- 投稿日:2021-08-29T18:45:05+09:00
アコーディオンの中にスムーススクロールのアンカーリンクを入れた時のIE11対応
業務上、なんやかんやとまだ使っているjQuery、そしてそれを利用したスムーススクロール。 基本的にjQueryを使ったスムーススクロールだと、以下のような記述をしていると思います。 $(function(){ $('a[href^="#"]').on('click', function(){ var href= $(this).attr('href'); var target = $(href == '#' || href == '' ? 'html' : href); var position = target.offset().top; $('html, body').animate({scrollTop:position}, 500, 'swing'); return false; }); }); 大体はこれで動きますし、offset()も普通に値が取得できると思います。 今回は jsでアコーディオン化した要素の中にアンカーリンクとアンカーリンク先がある アンカーリンクをクリックするとスムーススクロールで指定のアンカーに移動する この条件の時にIE11対応で躓いたので、その時の対応を忘れないためのものになります。 今も尚IE11対応してる人の助けにもなったら幸い。(今もIE11対応してる人どれだけいるの?という疑問はともかくとして) htmlの例としては以下のような感じです。 <dl class="accodion_wrap"> <dt class="accodion_btn">アコーディオン開閉ボタン</dt> <dd class="accodion_box"> <p>アコーディオンの中身</p> <ul> <li> <a href="#link1">アンカーリンク1</a> <a href="#link2">アンカーリンク2</a> <a href="#link3">アンカーリンク3</a> </li> </ul> <div id="link1"> アンカーリンク先1 </div> <div id="link2"> アンカーリンク先2 </div> <div id="link3"> アンカーリンク先3 </div> </dd> </dl> アコーディオンの中身が長く、アコーディオンの中にアンカーリンクを入れてほしいとの要望があったのでアンカーリンクを挿入したら、アコーディオン内のアンカーリンククリック時にIE11だけ何故かoffset()の値が取得できず、スムーススクロールが正常に動かなくなってしまいました。 似たような事象と対応例がないか調べてみたのですが見つからず、色々試した結果以下の対応で動作するようになりました。 html <dl class="accodion_wrap"> <dt class="accodion_btn">アコーディオン開閉ボタン</dt> <dd class="accodion_box"> <p>アコーディオンの中身</p> <ul> <li> <a href="#link1" class="js_accordion_anchor">アンカーリンク1</a> <a href="#link2" class="js_accordion_anchor">アンカーリンク2</a> <a href="#link3" class="js_accordion_anchor">アンカーリンク3</a> </li> </ul> <div id="link1"> アンカーリンク先1 </div> <div id="link2"> アンカーリンク先2 </div> <div id="link3"> アンカーリンク先3 </div> </dd> </dl> js $(function(){ $('.js_accordion_anchor').on('click', function(){ var href= $(this).attr('href'); var target = $('.accodion_box._is_open ' + href); var position = target.offset().top; $('html, body').animate({scrollTop:position}, 500, 'swing'); return false; }); }); jsの変数target部分の記述を、アコーディオンを開閉している(displayをblockとnoneで切り替えている)親要素に加え、アコーディオンを開く際に親要素にaddClass()で追加しているclass(上記記述の場合だと_is_open)を予め指定して記述することで、offset()の値が取得できるようになりました。 また、親要素の指定を入れる都合上、アコーディオン外にアンカーリンクがある場合、そのアンカーリンク+スムーススクロールが正常に動作できなくなってしまうので、アンカーリンクのイベント発火をclass指定にしています。 なお、この対応を入れる場合、アコーディオン外のスムーススクロール用記述に関しては以下のように記述を変えた方が良いです。 $(function(){ $('a[href^="#"]:not(".js_accordion_anchor")').on('click', function(){ var href= $(this).attr('href'); var target = $(href == '#' || href == '' ? 'html' : href); var position = target.offset().top; $('html, body').animate({scrollTop:position}, 500, 'swing'); return false; }); }); 色々試してみるうちにこの方法に辿り着き、問題解消できた訳ですが、この方法で何故有効になっているのか、理由というか理屈はわかっていなかったりします。 読み込み時に非表示状態(display: none;にしている)のアコーディオン部分に入っているから取得できないのか…? display: none;のとき要素のサイズが取得できない理屈と同様なのだろうか…? (でもアコーディオンを開いてリンクをクリックする訳で、その時にはdisplay: block;になっているのですが…) とは色々考えているのですが、ChromeとかEdgeではこの対応をしなくても普通に取得できているんですよね…。 このあたり、もしわかる方がいたら教えていただけると幸いです。 …それにしても、IE11対応から解放される日はいつになるのやら。 (以前ここに書いた記事も、IE11対応に泣かされた記事である事に気付いて頭を抱える)
- 投稿日:2021-08-29T17:07:00+09:00
【初学者向け】JavaScriptの分割代入について
今回は分割代入について解説していきます。 分割代入とは? 分割代入とは配列やオブジェクトから値を取り出して、変数に代入してくれるJavaScriptの機能です。 ☆通常の代入では一つの変数に対して一つの値を代入していくのに対して、 分割代入は複数の変数に、配列やオブジェクトの値を一気に流しこむイメージです。 使い方(①配列の場合 / ②オブジェクトの場合) ①配列の場合 ☆point: 左辺の変数は [ ] で囲うこと const arry = [10, 20, 30]; //分割代入で一気に代入 const [a, b, c] = arry; //確認 console.log(a); // 10 console.log(b); // 20 console.log(c); // 30 //一つずつ代入するとしたら...3回代入しなくてはいけない const a = arry[0]; const b = arry[1]; const c = arry[2]; ②オブジェクトの場合 ☆point: 左辺の変数は { } で囲うこと //まずはオブジェクトを用意 const obj = { name: "Tom", age: 87 } //一気に代入 const {name, age} = obj; //確認 console.log(name); //Tom console.log(age); //87 ////一つずつ代入するとしたら...3回代入しなくてはいけないし、ドット記法を使わなくてはいけない! const name = obj.name; cnnst age = obj.age; 注意点 オブジェクトを分割代入する際、変数名とプロパティは一致させる必要があります! 上の②をちょっと書き換えて確認します <定義の順を変えてみた場合> const obj = { name: "Tom", age: 87 } //ageとnameの順番をスイッチ const {age, name} = obj; //順番を変えても出る値は変わらず console.log(name); //Tom console.log(age); //87 <部分的に代入> const obj = { name: "Tom", age: 87 } //ageだけ定義(nameは代入しない) const {age} = obj; //確認 console.log(age); //87 このように変数名とプロパティは同じ名前のものが対応しているので注意する必要があります。 その他にもいろいろな使い方がありますのでぜひ活用してみてください。
- 投稿日:2021-08-29T14:28:21+09:00
Editorjsにメンション機能を実装する
前提 制約がないのであれば、ライブラリを使用する方が効率がいい。どうしても自作する必要がある人やメンションの実装をしてみたい人に向けた共有記事です。 vue-mention 環境 Editorjs v2.22.2 Vue 2.6.14 Vuetify 2.5.8 コンセプト MessangerやSlackのように「@」の入力に対してメニューを表示し、選択したユーザーをメンションする。 設計 Editorjsのreadyを使いDom生成を検知、Editorjsのsaveを使い編集を検知 Editorjsの一番外枠にMutationObserverをつけて、ブロックの変更を検知する。 動的に追加されるDomでメンション可能なものにさらにMutationObserverをつけて監視 行頭やスペース後の@に対してメニューを表示 表示メニューの選択に応じてアンカータグ(メンション)追加 実装物 ArticleEditor.vue <template> <div :id="uniqueId" /> </template> <script> import EditorJS from "@editorjs/editorjs"; import Header from "@editorjs/header"; import LinkTool from "@editorjs/link"; import ImageTool from "@editorjs/image"; import CheckList from "@editorjs/checklist"; import List from "@editorjs/list"; import Embed from "@editorjs/embed"; import Quote from "@editorjs/quote"; import Delimiter from "@editorjs/delimiter"; import Table from "@editorjs/table"; import gb from "../../../mixins/global/gb"; export default { name: "ArticleEditor", mixins: [gb], props: { article: { type: Object, default: () => ({}) }, readOnly: { type: Boolean, default: false } }, data: () => ({ editor: {}, uniqueId: `_${gb.methods.generateId()}` }), mounted() { this.setEditor(); }, methods: { setEditor() { this.editor = new EditorJS({ data: this.article, placeholder: "テキストを入力...", autofocus: true, readOnly: this.readOnly, minHeight: 0, onChange: function() { this.save(); }.bind(this), holder: this.uniqueId, tools: { header: { class: Header, shortcut: "CMD+SHIFT+H", config: { placeholder: "ヘッダー", levels: [1, 2, 3, 4], defaultLevel: 3 } }, checklist: { class: CheckList, inlineToolbar: true }, list: { class: List, inlineToolbar: true }, embed: { class: Embed, config: { services: { youtube: true, twitter: true } } }, quote: { class: Quote, inlineToolbar: true, shortcut: "CMD+SHIFT+O", config: { quotePlaceholder: "テキストを入力", captionPlaceholder: "キャプションを入力" } }, delimiter: Delimiter, table: { class: Table, inlineToolbar: true, config: { rows: 2, cols: 3 } } }, i18n: { messages: { ui: { blockTunes: { toggler: { "Click to tune": "クリックして調整", "or drag to move": "ドラッグして移動" } }, inlineToolbar: { converter: { "Convert to": "変換" } }, toolbar: { toolbox: { Add: "追加" } } }, toolNames: { Text: "テキスト", Heading: "タイトル", List: "リスト", Checklist: "チェックリスト", Quote: "引用", Delimiter: "直線", Table: "表", Link: "リンク", Bold: "太字", Italic: "斜体", Image: "画像" }, blockTunes: { deleteTune: { Delete: "削除" }, moveUpTune: { "Move up": "上に移動" }, moveDownTune: { "Move down": "下に移動" } } } }, onReady: () => { this.$emit("ready", this.uniqueId); } }); }, save() { this.editor .save() .then(data => { this.$emit("saved", data); }) .catch(err => { console.log(err); }); }, clear() { this.editor.clear(); }, render(body) { this.editor.render(body); } } }; </script> EditorComponent.vue <template> <div> <v-menu absolute :position-x="mentionMenu.x" :position-y="mentionMenu.y" :value="mentionMenu.show" rounded > <v-list dense max-height="100"> <v-list-item v-for="user in mentionableUsers" :key="user.id" dense @click="insertMention(user)" > <v-list-item-avatar> <v-img v-if="user.picture" :src="user.picture" /> <v-icon v-else>mdi-account</v-icon> </v-list-item-avatar> <v-list-item-title>{{ user.name }}</v-list-item-title> </v-list-item> <v-list-item v-if="!mentionableUsers.length"> <v-list-item-subtitle> メンション可能な人がいません。 </v-list-item-subtitle> </v-list-item> </v-list> </v-menu> <div style="max-height: 500px" class="overflow-y-auto"> <ArticleEditor ref="editor" :article="article" @saved="updateArticle" @ready="setBlockObserver" /> </div> </div> </template> import { getUsers } from "../../../../helpers/User"; import ArticleEditor from "../../../themes/editor/ArticleEditor"; import gb from "../../../../mixins/global/gb"; <script> export default { name: "TimelinePostEditor", components: { ArticleEditor }, mixins: [gb], props: { article: { type: Object, default: () => ({}) }, mentions: { type: Array, default: () => [] } }, data: () => ({ mentionMenu: { x: 0, y: 0, show: false, selectedParagraphIndex: null }, users: [], editorId: "" }), watch: { article(article) { const mentionUserIds = []; if (!article.blocks) return; article.blocks.forEach(block => { if (block.type === "paragraph") { const mentions = block.data.text.match(/<a(?: .+?)?>@.*?<\/a>/g); if (mentions) { mentions.map(m => { const id = m.match(/href="\d"/)[0]; if (id) { mentionUserIds.push(Number(id.match(/\d/)[0])); } }); } } }); this.updateMentions(mentionUserIds); } }, mounted() { getUsers().then(res => { this.users = res.data; }); }, computed: { mentionableUsers() { return this.users.filter(u => !this.mentions.includes(u.id)); } }, methods: { setBlockObserver(editorId) { this.editorId = editorId; const editorDiv = document.getElementById(editorId); const elm = editorDiv.getElementsByClassName("codex-editor__redactor")[0]; const observer = new MutationObserver( function() { this.setParagraphNodeObserver(); }.bind(this) ); const config = { childList: true }; observer.observe(elm, config); this.setParagraphNodeObserver(); }, setParagraphNodeObserver() { const paragraphNodeObserverConfig = { characterData: true, attributes: false, childList: false, subtree: true }; const editorDiv = document.getElementById(this.editorId); const paragraphNodes = editorDiv.getElementsByClassName("ce-paragraph"); Array.from(paragraphNodes).forEach((pn, pi) => { const paragraphNodeObserver = new MutationObserver( function(mutations) { mutations.forEach(r => { const selection = document.getSelection(); const regex = RegExp("(\\s| )@", "g"); const currentCaretPosition = selection.focusOffset; const initialMention = r.target.data.indexOf("@") === 0 && currentCaretPosition === 1; const contextMentions = [...r.target.data.matchAll(regex)]; const isMention = initialMention || contextMentions.filter(cm => { return cm.index + cm[0].length === currentCaretPosition; }).length; if (isMention) { const rangeRect = selection .getRangeAt(0) .getBoundingClientRect(); const { x, y } = rangeRect; this.mentionMenu.selectedParagraphIndex = pi; this.mentionMenu.x = x; this.mentionMenu.y = y; this.mentionMenu.show = true; } else { this.mentionMenu.selectedParagraphIndex = null; this.mentionMenu.x = 0; this.mentionMenu.y = 0; this.mentionMenu.show = false; } }); }.bind(this) ); paragraphNodeObserver.observe(pn, paragraphNodeObserverConfig); }); }, insertMention(user) { const editorDiv = document.getElementById(this.editorId); const paragraphNodes = editorDiv.getElementsByClassName("ce-paragraph"); Array.from(paragraphNodes).forEach((pn, pi) => { if (this.mentionMenu.selectedParagraphIndex === pi) { const selection = document.getSelection(); const currentCaretPosition = selection.focusOffset; if (currentCaretPosition === 1) { let text = pn.textContent; text = text.slice(1, text.length); text = `<a href="${user.id}"class="text-decoration-none font-weight-bold">@${user.name}</a>` + text; pn.innerHTML = text; this.addMentionedUser(user.id); } else { const node = document.getSelection(); let targetText = node.anchorNode.data ? node.anchorNode.data : ""; const regex = RegExp("(\\s| )@", "g"); let contextMentions = [...targetText.matchAll(regex)]; const targetPoint = contextMentions.find(cm => { return cm.index + cm[0].length === node.anchorOffset; }); if (targetPoint) { let before = targetText.slice(0, targetPoint.index); let after = targetText.slice( targetPoint.index + targetPoint[0].length, targetText.length ); const mention = before + `<a href="${user.id}" data-user-id="${user.id}" class="text-decoration-none font-weight-bold">@${user.name}</a>` + after; if (pn.innerHTML.match(targetText)) { pn.innerHTML = pn.innerHTML.replace(targetText, mention); } else { const escapedText = this.escapeHtml(targetText); //$nbsp const replacedText = escapedText.replace( String.fromCharCode(160), " " ); if (pn.innerHTML.match(replacedText) !== null) { pn.innerHTML = pn.innerHTML.replace(replacedText, mention); } } } this.addMentionedUser(user.id); } } }); }, addMentionedUser(userId) { const mentions = [...this.mentions]; !this.mentions.find(id => id === userId) && mentions.push(userId); this.updateMentions(mentions); }, updateMentions(mentions) { this.$emit("updateMentions", mentions); }, updateArticle(article) { this.$emit("updateArticle", article); }, clear() { this.$refs.editor.clear(); } } }; </script> Editorjsでreadyとチェンジイベントを利用する。 初期化時にonReadyイベントを設定 holderのidを動的に設定しているのは、一画面で複数のエディターを利用するケースがあるため。 ArticleEditor.vue <script> //省略 this.editor = new EditorJS({ data: this.article, placeholder: "テキストを入力...", autofocus: true, readOnly: this.readOnly, minHeight: 0, onChange: function() { this.save(); //ブロックの追加や、テキストの編集などに応じて saveし、最新のオブジェクトを共有 }.bind(this), holder: this.uniqueId, tools: {}, i18n: {}, onReady: () => { this.$emit("ready", this.uniqueId); //準備完了の検知とholder divのid共有 } }); //省略 save() { this.editor .save() .then(data => { this.$emit("saved", data); }) .catch(err => { console.log(err); }); }, </script> MutationObserverの活用 Editorjsがいい感じに追加してくれるブロックに対して、それぞれMutationObserverをつけ、「@」の検知とメニューの表示をする <script> setBlockObserver(editorId) { this.editorId = editorId; const editorDiv = document.getElementById(editorId); //holder div const elm = editorDiv.getElementsByClassName("codex-editor__redactor")[0]; //ブロックが追加されていくdiv const observer = new MutationObserver( function() { this.setParagraphNodeObserver(); //ブロックが追加されたら子ノードに対してMutation Observer付けるというコールバック }.bind(this) ); //要素の追加削除の変更のみ検知する設定 const config = { childList: true }; observer.observe(elm, config); //監視開始 this.setParagraphNodeObserver(); //Editorjsが初期化時に追加するParagraphブロック対応用に手動で読んでいる }, </script> 追加されたDomがParagraphブロックなのか調べ、「@」を検知するObserverを設置 <script> setParagraphNodeObserver() { const paragraphNodeObserverConfig = { characterData: true, //textContentの変更を見る attributes: false, //属性の変更は見ない childList: false, //子要素の追加削除は見ない subtree: true, //サブツリーまで見る }; const editorDiv = document.getElementById(this.editorId);//holder const paragraphNodes = editorDiv.getElementsByClassName("ce-paragraph"); //paragraph dom Array.from(paragraphNodes).forEach((pn, pi) => { const paragraphNodeObserver = new MutationObserver( function(mutations) { mutations.forEach(r => { const selection = document.getSelection(); //caretの取得 const regex = RegExp("(\\s| )@", "g"); //半角・全角スペースの後ろに@があるか const currentCaretPosition = selection.focusOffset; //caretの位置(textContent上での位置になるため、innerHtmlの位置ではない)取得 const initialMention = r.target.data.indexOf("@") === 0 && currentCaretPosition === 1; //行の先頭にあるかのチェック const contextMentions = [...r.target.data.matchAll(regex)]; //文中に正規表現にマッチする@があるか const isMention = initialMention || contextMentions.filter(cm => { return cm.index + cm[0].length === currentCaretPosition; //caretのある位置の@がメンションであるかのチェック }).length; if (isMention) { const rangeRect = selection .getRangeAt(0) .getBoundingClientRect(); //caretの画面上の座標取得 const { x, y } = rangeRect; //メンションメニューの表示 this.mentionMenu.selectedParagraphIndex = pi; //何番目のパラグラフなのか一時保存 this.mentionMenu.x = x; this.mentionMenu.y = y; this.mentionMenu.show = true; } else { this.mentionMenu.selectedParagraphIndex = null; this.mentionMenu.x = 0; this.mentionMenu.y = 0; this.mentionMenu.show = false; } }); }.bind(this) ); paragraphNodeObserver.observe(pn, paragraphNodeObserverConfig); //監視コールバックとオプションの定義 }); }, </script> メンションメニューからメンションを追加する メンションメニューに関してはVuetifyにほぼお任せ、座標と表示可否を管理するだけ。対象のユーザーがクリックされた時にクリックイベントからメンション追加メソッドを呼ぶ。 <script> insertMention(user) { const editorDiv = document.getElementById(this.editorId); //holder div const paragraphNodes = editorDiv.getElementsByClassName("ce-paragraph"); //paragraph div Array.from(paragraphNodes).forEach((pn, pi) => { //indexで対象パラグラフ特定 if (this.mentionMenu.selectedParagraphIndex === pi) { const selection = document.getSelection(); //caretの取得 const currentCaretPosition = selection.focusOffset; //caret位置の取得 if (currentCaretPosition === 1) { let text = pn.textContent; //textContent取得 text = text.slice(1, text.length); text = `<a href="${user.id}"class="text-decoration-none font-weight-bold">@${user.name}</a>` + text; pn.innerHTML = text; //メンションタグ追加 this.addMentionedUser(user.id); //サーバーサイドに送るように別途保存 } else { const node = document.getSelection(); let targetText = node.anchorNode.data ? node.anchorNode.data : ""; //caretのある位置のtextContentを取得(対象パラグラフの中に他にもアンカータグやイタリックなどが含まれている場合には、キャレットのある位置からタグが見つかる位置までをanchorNodeとして取得してくれる) const regex = RegExp("(\\s| )@", "g"); let contextMentions = [...targetText.matchAll(regex)]; const targetPoint = contextMentions.find(cm => { return cm.index + cm[0].length === node.anchorOffset; }); if (targetPoint) { let before = targetText.slice(0, targetPoint.index); let after = targetText.slice( targetPoint.index + targetPoint[0].length, targetText.length ); const mention = before + `<a href="${user.id}" data-user-id="${user.id}" class="text-decoration-none font-weight-bold">@${user.name}</a>` + after; if (pn.innerHTML.match(targetText)) { pn.innerHTML = pn.innerHTML.replace(targetText, mention); } else { //特殊文字やtextContentとinnerHtmlで差異が出る特殊文字などを変換した上で、置換する const escapedText = this.escapeHtml(targetText); //$nbsp const replacedText = escapedText.replace( String.fromCharCode(160), " " ); if (pn.innerHTML.match(replacedText) !== null) { pn.innerHTML = pn.innerHTML.replace(replacedText, mention); } } } this.addMentionedUser(user.id); } } }); }, </script>
- 投稿日:2021-08-29T14:28:07+09:00
【kintone】サブテーブルの行を自動追加・削除する
やりたいこと 数値フィールドに入力された値を元にサブテーブルの行を追加したり削除したりしたい フィールドとフィールドコード タイプ フィールド名 フィールドコード 数値 テーブルの行数 rowCount テーブル テーブル table 文字列(1行) 文字列(1行) string コード sample.js (function() { "use strict"; // rowCountフィールドが変更されたときに動かす const events = ["app.record.create.change.rowCount", "app.record.edit.change.rowCount"]; kintone.events.on(events, function(event) { const record = event.record; const rowCount = record.rowCount.value; const tableLength = record.table.value.length; if (rowCount > tableLength) { // 行を増やす処理 for (let i = 0; i < (rowCount - tableLength); i++) { record.table.value.push({ "value": { "string": { //追加した行数を確認するためにvalueに数字を設置セットする "value": i + 1, "type": "SINGLE_LINE_TEXT" } } }); } } else if (rowCount < tableLength) { // 行を減らす処理 record.table.value.splice(rowCount, tableLength - rowCount); } return event; }); })(); できた
- 投稿日:2021-08-29T14:24:14+09:00
SORACOM RS-LTECO2 で計測したCO2値を kintone で確認する
概要 ちょっと前に SORACOM RS-LTECO2 を購入して、以下を参考に自宅の仕事場のCO2濃度や室温などのデータを SORACOM Lagoon で可視化までやっていました。 SORACOM レシピ:IoTで、CO2と温湿度を計測し換気促進 https://soracom.jp/recipes_index/9972/ とはいえ、確認のため SORACOM Lagoon をいちいち起動するのは手間で、普段使いしている kintone で CO2濃度や室温などのデータを確認できるように可視化してみました。 SORACOM RS-LTECO2 について SORACOM RS-LTECO2 は SORACOM さんが委託販売しているラトックシステム株式会社のCO2や温度、湿度が計測できるセンサ機器です。白でおしゃれなコンパクトな筐体にLEDで現在のCO2濃度を知らせる機能があり、SORACOM の SIM を差し込むことで簡単な設定で SORACOM Lagoon を利用して可視化が可能です。 僅かなサイズのバイナリーデータを指定間隔でLTE-M通信でバイナリーデータで送信するため、通信料も気にせずリーズナブルに利用できます。またUSB給電で、USBバッテリーなどと一緒に持ち運びして色々な場所で計測可能です。SORACOMのサイト、以下のメーカサイトのwifi版製品とともに消費電力のデータが無かったため計算はできませんでしたが、USBバッテリーでそれなりに長い時間運用可能ではと推測しています。 wifi版製品 RS-WFCO2 https://www.ratocsystems.com/products/subpage/wfco2.html SORACOM の設定 先に紹介した「SORACOM レシピ:IoTで、CO2と温湿度を計測し換気促進」を参考に、SORACOM Harvest Data の設定 まで行ってください。但し、SORACOM Harvest Dataの有効化 は以降のSORACOM Lagoon で可視化までやらずに、kintone のみで確認する場合は必須ではありません。 kintone の準備 kintone の複数スレッドを持つ適当なスペースに以下の項目を持つアプリを追加します。「お知らせ」を利用しますので、必ず複数スレッドを持つスペースでアプリを作成します。 フィールド名 種類 フィールドコード その他 計測日時 日時 dateTime CO2 数値 co2 少数以下0 温度 数値 temp 少数以下1 湿度 数値 humid 少数以下1 計測間隔 数値 interval 少数以下0 フォーム以外にはAPIトークンの発行を行います。その他は適宜以下のような今日のCO2を表示するグラフを作成しておくと便利でしょう。 APIトークンは次の AWS Lambda で SORACOM RS-LTECO2 より受け取ったデータを kintone 追加するために使用します。 以上で、一旦 kintone の準備は完了です。 AWS Lambda の準備 AWS 側の事前準備として、アカウントが必要になります。こちらの設定は以前 SORACOM GPS マルチユニット で説明した内容と同じですので、以下を参照ください。 AWS アカウントの追加 https://qiita.com/yukataoka/items/c07b78f5151c29ac8858#aws-%E3%82%A2%E3%82%AB%E3%82%A6%E3%83%B3%E3%83%88%E3%81%AE%E8%BF%BD%E5%8A%A0 ユーザの追加に成功したら、アクセスキーIDと、シークレットアクセスキーは以降の設定で必要になりますので、控えておきます。 AWS Lambda で以下のように関数を作成します。 「一から作成」を選択し、適当な「関数名」を設定し、「ランタイム」(言語)は最新の Node.js を選択して関数を作成します。 関数のARNは以降の設定で必要になりますので、控えておきます。 コードは以下になります。 index.js 'use strict'; const request = require('request-promise'); const moment = require("moment"); const Url = "https://" + process.env['subdomain'] + ".cybozu.com/k/v1/record.json"; const AppId = process.env['appid']; const Token = process.env['token']; exports.handler = async function(event, context, callback) { console.log('Function Start.'); // 取得したデータをkintone用のJSONに編集 const dateTime = moment().format("YYYY-MM-DDTHH:mm:ssZ"); const json = { "dateTime" : { "value" : dateTime }, "co2" : { "value" : event.co2 }, "temp" : { "value" : event.temp }, "humid" : { "value" : event.humid }, "interval" : { "value" : event.interval }, }; // kintoneのアプリにデータを追加 await PostKintoneRecode(request, Url, AppId, Token, json); console.log('Function Stop.'); }; // kintoneにデータを追加 async function PostKintoneRecode(request, url, appId, token, json) { try { const options = { url: url, method: 'POST', headers: { 'Content-type': 'application/json', 'X-Cybozu-API-Token': token }, json: { app : appId, record: json }, }; await request(options); return true; } catch (err) { console.error(JSON.stringify(err)); return false; } } event の引数で受け取った CO2 などの値をAPIで追加するだけで、簡単に kintone にデータを追加できます。 AWS Lambda のコードにはは、以下の node.js のパッケージ環境 request-promise と moment を追加する必要があります。 https://www.npmjs.com/package/request-promise https://www.npmjs.com/package/moment request-promise は現在非推奨になっています。本格的に利用する場合は request-promise を使わず Aysnc/Await を使って HTTP リクエストを使って実装する方が良いでしょう。 Lambda で node.js のパッケージを使う説明は割愛しますが、以下などを参考に設定すると良いでしょう。 Lambda の Node.js でもっといろんなパッケージを使いたいとき https://tech-lab.sios.jp/archives/9017 AWS Lambda Layersでnode_modulesを使う https://xp-cloud.jp/blog/2019/01/12/4630/ その他の設定ではタイムアウトがデフォルトでは短すぎるので、15秒程度に値を変更しておくと良いでしょう。 kintone の環境が変わってもコードを変更しなくて済むように、process.env['キー'] で環境変数から設定値を取得するようにしています。 キー 値 appid 先に追加した kintone のアプリID subdomain kintone のサブドメイン xxxx.cybozu.com の xxxx の部分 token 先に追加した kintone アプリのAPIトークン 以上の設定が完了したら、SORACOM のコンソール画面で SORACOM Funk の設定を行います。 SORACOM Funk の設定 SORACOM のコンソール画面で、先の「SORACOM の設定」SIMグループで作成したSIMグループの設定を変更します。 「SORACOM Air for セルラー設定」のバイナリパーサーが以下のように設定されているか確認します。 co2:0:uint:16 temp:2:int:16:/10 humid:4:uint:16:/10 interval:6:uint:8 「SORACOM Air for セルラー設定」で、以下のように設定します。 関数のARMは先に控えたものを利用します。認証情報の設定については、先に控えたアクセスキーIDと、シークレットアクセスキーを使って以下を参照に行います。 SORACOM コンソールで SORACOM Funkの設定 https://qiita.com/yukataoka/items/c07b78f5151c29ac8858#sim%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97%E3%81%AE%E8%BF%BD%E5%8A%A0%E3%81%A8%E8%A8%AD%E5%AE%9A 以上を完了すると、kintone のアプリにデータが追加されるようになります。 kintone のスペースの「お知らせ」に表示 kintoneアプリを開かないとCO2などのデータを確認できないのは面倒ですので、kintone の JavaScript カスタマイズでよく利用するスペースの「お知らせ」にデータを表示できるようにします。但し、この作業には kintone の管理者権限が必要です。 kinotne のカスタマイズではスペース表示イベントを取得できるようになりましたので、このイベントのタイミングでスペースの「お知らせ」に最新のCO2等のデータを表示するようにカスタマイズします。 スペース表示イベント https://developer.cybozu.io/hc/ja/articles/900006291023 スペースの「お知らせ」に何かを表示させるためには、HTML の DOM 構造を解析して、データを表示したい部分の id か class に表示したい内容を追加します。但し、kintone の仕様変更があった場合は影響を受ける可能性が高く推奨できる方法ではありません。ですが他に手がなく現状ではこの方法を使って実装しています。とはいえ、全ての DOM 構造を解析するのは大変なので、今回は以下を参考に kintone-space-events.js を利用させていただきました。 kintone ポータルイベントが追加されたので、スペースイベント作ってみた。 https://qiita.com/Naoto00/items/a87312d5168de63543e5 後は、日付処理に Moment.js の後継的なライブラリにあたる Luxon を利用しています。 Luxon を使って kintone の日付や日時フィールドのフォーマットをカスタマイズする https://developer.cybozu.io/hc/ja/articles/900000985463 kintoneTop1.js (function() { "use strict"; // スペースの表示イベント処理 const space = new SpaceCustomize(8, 'kinotoeのサブドメイン'); kintone.events.on('space.portal.show', function(event) { funcEnvSetInfomation(); return event; }); // スペースのお知らせに最新の環境計測情報を表示 function funcEnvSetInfomation(){ // LTE-M CO2センサーサンプル(SORACOM)アプリから環境情報を取得しスペースのお知らせに表示 let params = { 'app': 'kintoneアプリのID', 'query': ' order by dateTime desc limit 1 ', }; // 最新の環境計測情報をアプリから取得 kintone.api(kintone.api.url('/k/v1/records', true), 'GET', params, function(resp) { if (resp.records[0] !== null) { // 取得したデータから表示内容を編集 const dt = luxon.DateTime.fromSQL(resp.records[0]['dateTime'].value.replace('T', ' ').replace('Z', '')); let infoText = "<span style=\"font-weight:bold;font-size:120%;\">"+dt.plus({hours: 9}).toFormat('yyyy年MM月dd日 HH時mm分')+" 現在のオフィスの環境</span><br />"; infoText += "CO2濃度:"+resp.records[0]['co2'].value+" ppm(1000ppmを超えたら換気を!)<br />"; infoText += "温度:"+resp.records[0]['temp'].value+" ℃(28℃を超えたら熱中症に注意!)<br />"; infoText += "湿度:"+resp.records[0]['humid'].value+" %<br />"; infoText += "次回更新:"+resp.records[0]['interval'].value+" 分後 <a href=\"JavaScript:window.location.reload();\">表示更新</a><br />"; // スペースのお知らせに情報を表示 let info = space.getNtfWidget(); let elmText = info.getElementsByClassName("gaia-argoui-widget-menu"); elmText[0].style.display ="block"; elmText[0].style.marginTop ="1em"; elmText[0].style.marginLeft ="1em"; let newElm = document.createElement("div"); newElm.innerHTML = infoText; elmText[0].appendChild(newElm); } }); } })(); 通常ならアプリの設定画面で JavaScript プログラムをアップロードしますが、スペースのお知らせをカスタマイズする場合は、以下の kinotne のシステム管理から行います。 カスタマイズ表示の下の「JavaScript / CSSでカスタマイズ」を開きます。 以下の画面でライブラリィの追加と、ソースコードファイルをアップロードします。 以上で以下のように、スペースの「お知らせ」にCO2などの計測結果を表示できるようになります。 グラフなどはお知らせの標準機能で編集して出力できますので、以下のように組み合わせると SORACOM Lagoon の表示に近づけることができます。 おまけ(Windows アプリで通知) 普段PCに向かって仕事している間はCO2の上昇に気づきづらいため、Windowsフォームアプリケーション(.NET Freamwork)で以下のような通知する簡単なアプリを作成してみました。 興味ある方は以下にコードを公開しますので、お試しください。 設定メモ ・C# Windowsフォームアプリケーション(.NET Freamwork)のプロジェクト追加 ・ConfigurationManager 参照追加 「ソリューションエクスプローラー」→「参照」右クリック→「参照の追加」 アセンブリ:フレームワーク内にある「System.Configuration」にチェックを入れて「OK」。 ・NuGet で Newtonsoft.Json 13.0.1 を追加 https://www.nuget.org/packages/Newtonsoft.Json/ 設定ファイル App.config <?xml version="1.0" encoding="utf-8" ?> <configuration> <appSettings> <add key="kintoneDomain" value="kintoneサブドメイン名" /> <add key="kintoneToken" value="kintoneトークン" /> <add key="kintoneAppId" value="kintoneアプリID" /> <add key="logFile" value="c:\work\AlertFromKintone\Error.log" /> </appSettings> <startup> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" /> </startup> </configuration> フォーム Form1.cs using System; using System.Configuration; using System.IO; using System.Windows.Forms; namespace AlertFromKintone { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { label1.Text = ""; GetKintoneData(); timer1.Interval = 10 * 60 * 1000; // 10分毎に確認 timer1.Start(); } private void timer1_Tick(object sender, EventArgs e) { GetKintoneData(); } private void GetKintoneData() { DateTime dt = DateTime.Now; var dtNow = dt.ToString("yyyy/MM/dd HH:mm"); dt = dt.AddMinutes(-11); var kintoneDt = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, 0); var queryDate = kintoneDt.AddHours(-9).ToString("yyyy-MM-ddTHH:mm:ssZ"); var doQuery = " dateTime > \"" + queryDate + "\" order by dateTime desc"; var appId = ConfigurationManager.AppSettings["kintoneAppId"]; try { var json = KintoneTools.GetJsonData(doQuery, appId); if (json != null && json.totalCount > 0) { var co2 = (int)json.records[0].co2.value; if (co2 > 2500) { MessageBox.Show("室内のC02が " + co2.ToString() + "ppm です!\n危険ですので即座に換気してください!", "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error); } else if (co2 > 2000) { MessageBox.Show("室内のC02が " + co2.ToString() + "ppm です!\n体調不良を避けるため換気してください!", "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error); } else if (co2 > 1200) { MessageBox.Show("室内のC02が " + co2.ToString() + "ppm です!\n快適な環境にするため換気しましょう!", "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error); } var temp = (float)json.records[0].temp.value; if (temp > 38) { MessageBox.Show("室内の気温が " + temp.ToString() + "℃ です!\n即座に冷房するか部屋から出てください!", "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error); } else if (temp > 34) { MessageBox.Show("室内の気温が " + temp.ToString() + "℃ です!\n冷房してください!", "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error); } else if (temp > 30) { MessageBox.Show("室内の気温が " + temp.ToString() + "℃ です!\n冷房しましょう!", "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Error); } label1.Text = dtNow + " 確認 CO2 " + co2.ToString() + "ppm 気温 " + temp.ToString() + "℃ 計測時刻 UTC " + json.records[0].dateTime.value; } else { MessageBox.Show("kintone から最新のデータが取得できません!", "環境計測値警告", MessageBoxButtons.OK, MessageBoxIcon.Question); } } catch (Exception ex) { File.AppendAllText(ConfigurationManager.AppSettings["logFile"], dt.ToString("yyyy/MM/dd HH:mm:ss") + " " + ex.Message); } } } } kintoneからデータを取得 KintoneTools.cs using Newtonsoft.Json; using System; using System.Configuration; using System.IO; using System.Net; using System.Text; namespace AlertFromKintone { public static class KintoneTools { private static readonly string domain = ConfigurationManager.AppSettings["kintoneDomain"]; private static readonly string apiUrl = "https://" + domain + ".cybozu.com/k/v1/records.json"; private static readonly string apiSinglUrl = "https://" + domain + ".cybozu.com/k/v1/record.json"; private static readonly string apiToken = ConfigurationManager.AppSettings["kintoneToken"]; private static readonly string logFilePath = ConfigurationManager.AppSettings["logFile"]; public static dynamic GetJsonData(string query, string appId) { var respons = GetRespons(query, appId); if (respons == null || respons.Contains("<!DOCTYPE HTML>")) { return null; } return JsonConvert.DeserializeObject(respons); } private static string GetRespons(string query, string appId) { ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; string url; if (query == "") { url = apiUrl + Uri.EscapeUriString("?app=" + appId + "&totalCount=true"); } else { url = apiUrl + Uri.EscapeUriString("?app=" + appId + "&query=" + query + "&totalCount=true"); } var request = WebRequest.Create(url); var enc = Encoding.GetEncoding("UTF-8"); request.Headers.Add("X-Cybozu-API-Token:" + apiToken); try { using (var respons = request.GetResponse()) { using (var stream = respons.GetResponseStream()) { var sr = new StreamReader(stream, enc); string text = sr.ReadToEnd(); sr.Close(); stream.Close(); return text; } } } catch (WebException) { DateTime dt = DateTime.Now; File.AppendAllText(logFilePath, dt.ToString("yyyy/MM/dd HH:mm:ss") + " " + "WebException. GET data = " + url + "\r\n"); return null; } } } } 参考情報 LTE-M CO2センサー RS-LTECO2 スターターキット https://soracom.jp/store/12112/ ** wifi版製品 RS-WFCO2 ** https://www.ratocsystems.com/products/subpage/wfco2.html SORACOM レシピ:IoTで、CO2と温湿度を計測し換気促進 https://soracom.jp/recipes_index/9972/ SORACOM GPS マルチユニット のデータを kintone に保存する https://qiita.com/yukataoka/items/c07b78f5151c29ac8858 request-promise https://www.npmjs.com/package/request-promise moment https://www.npmjs.com/package/moment Lambda の Node.js でもっといろんなパッケージを使いたいとき https://tech-lab.sios.jp/archives/9017 AWS Lambda Layersでnode_modulesを使う https://xp-cloud.jp/blog/2019/01/12/4630/ スペース表示イベント https://developer.cybozu.io/hc/ja/articles/900006291023 kintone ポータルイベントが追加されたので、スペースイベント作ってみた。 https://qiita.com/Naoto00/items/a87312d5168de63543e5 Luxon を使って kintone の日付や日時フィールドのフォーマットをカスタマイズする https://developer.cybozu.io/hc/ja/articles/900000985463 NuGet で Newtonsoft.Json 13.0.1 を追加 https://www.nuget.org/packages/Newtonsoft.Json/
- 投稿日:2021-08-29T12:28:19+09:00
Flutter経験者がReactで温泉ランキングを作ったその1(API取得まで)
React歴 もともとwebの知識(html/css,javascript)とFlutterでのアプリ開発経験があるので、行けるのではないかと思い挑戦しました。 つまり、Reactは初です。 必要な知識 html/cssとFlutterが理解できれば大丈夫です。 完成図 検索フォーム(今回は昨日少なめな検索フォームです)があり、下に内容がでるという感じです。 参考にした動画 こちらの動画を参考にさせていただきました。 https://www.youtube.com/watch?v=U9T6YkEDkMo&t=2344s 環境構築 こちらを参考にしました。 https://qiita.com/rspmharada7645/items/25c496aee87973bcc7a5 すごく簡単でした。 App.js こちらがApp.jsですね。 Flutterでいうところのmain.dartいう認識でよさそう。 App.js import React from 'react'; const App = () => { return ( <div id="root"> </div> ); } export default App; index.js Reactに必要なモノをimportしています。 ちなみに{useState, useEffect}は今のところ気にしなくてもいいです。 後で使うメソッドのようなものをimportしています。 index.js import React, {useState, useEffect} from 'react'; import ReactDOM from 'react-dom'; import './index.css'; APIを探してみる じゃらんwebサービスが使えないみたいですね。。 なので今回はこちら。 アプリID発行からログインすると新規アプリが作成できます。 アプリIDは後で利用します。 今回は以上です。 次回は実際にコードを書いていきます。
- 投稿日:2021-08-29T12:21:20+09:00
Typescript入門におすすめのユーデミー講座5選。JS未経験者も可
タイトルの通り、初心者にオススメのTypescriptのユーデミー動画のまとめです。 Javascriptの経験がある方が望ましいですが、JS未経験者も想定読者としています。 Firebase + React Hooks(TypeScript)によるWebアプリ開発 おすすめ度:★★★★★ ・Firebase + React Hooks(TypeScript)によるWebアプリ開発 この講座で学べる内容 ・FirebaseとReact Hooks連携 ・TypeScriptによるReact 開発 ・FirebaseのCloud storage / Cloud firestore / Authorization / Hosting ・Material UI 超TypeScript入門 完全パック(2021) おすすめ度:★★★★ ・超TypeScript入門 完全パック(2021) この講座で学べる内容 ・TypeScriptの基礎的な型と応用的な型、コンバイラの設定方法、クラスやインターフェースの使い方 ・ジェネリクスや、デコレーター、モジュール、Webpack、React、Node jsを使った実践的な開発方法 ・オブジェクト指向プログラミングを使った、健康管理アプリの作成とTypeScriptでのJavaScriptライブラリーの使い方 ハンズオンで学ぶTypeScript - JavaScript エンジニアのためのTypeScript徹底入門 おすすめ度:★★★★ ・ハンズオンで学ぶTypeScript - JavaScript エンジニアのためのTypeScript徹底入門 この講座で学べる内容 ・TypeScriptの開発環境の構築方法、TypeScriptの型定義の読解方法及び定義の方法 ・TypeScriptの体系的な言語仕様:TypeScriptを支える全プリミティブ型から複雑な型制約を実現するUtility TypesとConditional Typesまで ・TypeScriptでReactアプリケーションを実装する事例 ・TypeScriptで起こり得る諸問題とそれらの解決方法 [JIRA編]React Hooks/TypeScript + Django REST APIで作るオリジナルJIRA おすすめ度:★★★★ ・[JIRA編]React Hooks/TypeScript + Django REST APIで作るオリジナルJIRA この講座で学べる内容 ・React Hooks ・TypeScript ・Redux ToolKit ・Django REST Framework 【世界で7万人が受講】Understanding TypeScript - 2020年最新版 おすすめ度:★★★★ ・【世界で7万人が受講】Understanding TypeScript - 2020年最新版 この講座で学べる内容 ・TypeScriptの使い方と機能(型、ES6サポート、クラス、ESモジュール、インターフェースなど)とTypeScriptプロジェクトで必要となるその他の知識 ・TypeScriptとは何か? ・TypeScriptのメリットとその理由 ・TypeScriptの全般的な機能を実際のプロジェクトで適用する方法 ・ReactJS や NodeJS / Express のアプリケーションでTypeScriptを利用する方法 関連リンク ・Java初心者に超オススメ出来るUdemy動画講座7選まとめ。最速マスター特化
- 投稿日:2021-08-29T11:49:29+09:00
JavaScript for..in, for..of チートシート
Array Object × for...in for...of 配列も、オブジェクトも、for 文で回せるのが JavaScript のいいところなんだけど、in と of の2種類があって、どっちがどっちなのか忘れちゃうので、自分のためのチートシートを作った。 for Array Object in index key of value SyntaxError Array + in -> index for ( const i in [ 111, 222, 333 ] ) // Array + in -> index { console.log( i ) // '0' '1' '2' } ここでの注意点は、なんとiには数値(number)ではなく数字(string)がくるところ(なんでやねん!まぁ、気にせず計算はできちゃうんだけどね) Array + of -> value for ( const v of [ 111, 222, 333 ] ) // Array + of -> value { console.log( v ) // 111 222 333 } index value 両方を得たい場合はこうするしかなくて const ary = [ 111, 222, 333 ] for ( const i in ary ) { console.log( i, ary[i] ) // 0 111 1 222 2 333 } であれば手っ取り早いのは .forEach() なんだけど、 [ 111, 222, 333 ].forEach( (v,i)=> // value, index の順番 { console.log( i, v ) // 0 111 1 222 2 333 } ) でも、.forEach() 使うくらいなら、 .map() を使うかなぁ。 なんというか、戻り値を使わないのがもったいなくて。貧乏性? console.log( [ 111, 222, 333 ].map( (v,i)=>[i,v] ) ) // [ [ 0, 111 ], [ 1, 222 ], [ 2, 333 ] ] Object + in -> key for ( const k in { a: 111, b: 222, c: 333 } ) // Object + in -> key { console.log( k ) // a b c } Object + of -> SyntaxError 実行できません for ( const v of { a: 111, b: 222, c: 333 } ) // Object + of -> SyntaxError { ^^ console.log( v ) // } 両方ほしいときは、まぁ、こうするかな。 const obj = { a: 111, b: 222, c: 333 } for ( const k in obj ) { console.log( k, obj[k] ) // a 111 b 222 c 333 } もしくは、 const obj = { a: 111, b: 222, c: 333 } Object.keys( obj ).forEach( k=> // [ 'a', 'b', 'c' ] { console.log( k, obj[k] ) // a 111 b 222 c 333 } ) 以下、参考
- 投稿日:2021-08-29T10:43:21+09:00
ニコニコ動画風コメント+アンケートアプリをnode.jsとElectronで作ってみた
はじめに 作成の動機はプレゼンやセミナーがWeb会議にシフトし、会場の空気感を感じない、発表時一人で話してる気がすることから、インタラクティブにしたいと思ったことです。 この手のアプリではCommentScreenがあるけど、無料版だと人数やアンケート回数の制限があったり、有料版の社内利用は決裁取得やらが面倒。 ネットで探してみると似たようなものを開発されてる記事があったので自分なりにカスタマイズして開発してみました。 Javascriptやnode.jsを扱うのは初めてでしたが、部署内のプレゼンや会議を盛り上げるツールができました。 本アプリの作成、記事執筆にあたり非常に参考にさせていただきました。ありがとうございます。heroku + electron でニコニコメソッドを作ってみました。その2(ルーム分け機能追加) 本記事ではアプリの紹介と利用方法を説明します。後日に別の記事で実装しながら自分が理解した内容と参考元アプリから改造して工夫した点を説明したいと思います。 アプリの動作イメージ コメント投稿画面(コントローラー)とコメント表示画面 アンケート画面 とりあえず使いたい方(64bit WindowsPC限定です) GitHubのhttps://github.com/mochiokun/nicomatsu-display/releases/tag/installer_win10_v2.0.0よりzipファイルをダウンロードして解凍してください。 「nicomatsu-display Setup 2.0.0.exe」を起動。 接続画面が表示されたら「nicomatsu」を入力。(デモサイトへの接続用) ルーム入力画面が表示されたら好きな名前を入力。 コントローラー画面とアンケート画面が表示される。 コントローラー画面や画面下部のQRコードを読み込んだスマートフォンで操作すると、PC画面にコメントが流れたりスタンプが表示されたり、アンケート結果に反映されます。 画面共有機能がついたWeb会議アプリ(Zoom、Teams、Webexなど)で、画面全体を共有すると共有資料の上にコメントやスタンプを重ねて共有可能です。 デモサイトではなく自分で使いたい方(Heroku利用で説明します。所要時間約10分) デモサイトはHerokuの無償枠で構築してるため、月間500時間までしか起動しません。またURLが公開されているので、ルーム名が被ってしまうと混線して他のプレゼン向けのコメントが流れてくる可能性もあります。 無料(2021年8月現在)で誰でも簡単に自分専用のアプリを構築する方法を説明します。 ※本説明は手段の一例です。Heroku以外のサーバーでもnode.jsが動作可能、かつインターネット接続されていればアプリケーションサーバーとして利用可能です。 今回の内容はGitHubのnicomatsu-serverのREADMEに記載している内容を少し易しめに説明しています。Git操作やサーバ構築など慣れている方には退屈な内容かと思います。 事前準備 GitHubのアカウント登録 HerokuへのデプロイをGitHub経由で説明するため。HerokuCLIでデプロイ可能な場合は不要。 Herokuのアカウント登録 無償で月500時間起動まで利用可能。30分アクセスがないとスリープして時間を消費しないので、1日16時間以上使わないのなら困ることはないはず。(クレジットカードを登録すれば月750時間まで無償枠が拡大されます。) アプリケーションサーバー構築 サーバー構築といっても全部ブラウザから操作するだけで、コマンド実行したり黒いターミナル画面とか出てこないので安心してください。 ソースコードを取得 GitHubのnicomatsu-server にアクセスして画面右上のForkボタンを選択。スマートフォンからのアクセスではForkボタンが表示されないのでPCでアクセスしてください。 (gitコマンド操作なしでの構築のためにフォークを利用している。認証機能を使いたい場合はフォークではなく、プライベートリポジトリを作成して利用を推奨する) 自身のアカウントにnicomatsu-serverがコピーされます。 Herokuでのアプリ作成 Herokuへログインしてダッシュボードを表示。 右上のNewを押してCreate new appを選択。 好きなアプリ名を入力してCreate appを選択。この名前がURLになる。 真ん中くらいにあるDeployment methodでGitHubを選択。 GitHubの認証情報を入力して接続する。 repo-nameにnicomatsu-severを入力してsearch。検索結果が下に表示されるのでconnectを選択。 画面を下にスクロールするとManual deployのエリアがあるので、Deploy Branchを選択。 画面上にログが出てきてしばらくすると、デプロイ成功した旨が表示される。 以上でアプリケーションサーバの構築は完了です。 クライアントアプリの使い方 Windows10ユーザーの方 本稿の「とりあえず使いたい方」に記載しているインストーラを取得していただき、接続画面でHerokuでのアプリ作成時に決定したアプリ名を入力してっ接続すれば利用可能です。 Macユーザの方 GitHubのnicomatsu-displayhttps://github.com/mochiokun/nicomatsu-displayにアクセスしてREADMEに従って操作をお願いします。(こちらはターミナル使います。) 最後に このアプリおよびHerokuでの構築に関して、セキュリティ面などの細かいことは気にしてません。重要な機密情報をコメントに書いてしまったりしないよう利用者へ呼びかけるよう気をつけてください。 気持ちセキュリティを高めたい場合は、nicomatsu-serverのTipsを参考に認証をONにするなどして活用ください。 スタンプ画像の変更・追加方法もTipsに記載してますので、盛り上がるスタンプなど追加してイベント事にご利用ください。 次回以降アプリの中身を紹介していく予定です。
- 投稿日:2021-08-29T08:03:41+09:00
【Ruby on Rails】Rate.jsを使ったときに星が増えるバグの解消法(jQuery)
対象者 レビュー評価を実装している方 星が増える不具合を実装している方 目的 星が増える不具合を解消して、星を5個で収める 実際の手順と実例 1.結論(解決策) scriptタグに$('').empty();を追加すればOK! 下記例です div.rateの部分は人によって違うと思います。 <script> $(document).on("turbolinks:load", function() { $('div.rate').empty(); #これを追加しました $('div.rate').raty({ size: 14, starOff: '<%= asset_path('star-off.png') %>', starOn : '<%= asset_path('star-on.png') %>', starHalf: '<%= asset_path('star-half.png') %>', half: true, readOnly: true, }); }); </script> 2.原因 星の表示されてるページから 他のページへ遷移 その後、再度星が表示されているページへ この間に星が削除されていない(保存されたまま??)のが原因みたいです。 3..emptyメソッドについて 下記記事によると jQueryオブジェクトで指定した要素の内容(子孫要素やテキストなど)を削除します。jQueryオブジェクトで指定した要素は削除しません。 とありました。 つまり、 ページ再読み込み時に まず子要素である"div.rate"を.emptyで削除 その後、.rateで星を表示 となって星5個の表示が保たれています。 参照 emptyメソッド - 初心者向けのjQuery入門講座 投稿者コメント 今回は「どのような流れでそうなったか」という点に重きを置いて記事を書いてみました。次回以降の投稿でもどのような手順でそうなったかを曖昧にさせないように理解しながら書いていきたいと思います。 星が増えるとなんか笑っちゃいますよね。笑 My Profile プログラミング学習歴3ヶ月目のアカウントです! プログラミングスクールで学んだ内容や自分が躓いた箇所等のアウトプットの為に発信しています。 また、プログラミング初学者の方にわかりやすく、簡潔にまとめて情報共有できればと考えています。 もし、投稿した記事の中に誤り等ございましたら、コメント欄でご教授いただけると幸いです。