20191129のJavaScriptに関する記事は26件です。

Twitterでのコード投稿の見栄えどうにかならんのと思った話

たぶん、長い投稿

きっかけ(こんな呟きを見かけた

出来たもの

キャプチャ.JPG

作成の過程で収穫物

  • Rails5.2での追加分(Active Record Storage含む
  • Twitter Login方法と仕組み、そのたTwitterあれこれ
  • JSの基礎(getElementByIdやsetAttribute、文字カウントなど
  • AWS S3の使い方
  • XSS対策

未だ残る改修すべき箇所

  • 検索結果画面のリダイレクトエラー(多分route.rbの書き順番由来
  • js辺りのエラー(動いてるけど、consoleではjs/mapのルーティングが何とか
  • スマホで
    タグ等を打つの面倒なので、なにか投稿補助ボタンでも
  • 本来の目的をよく考えたら、マークダウンの方は不要なのでは。
  • 作成要件

    Untitled.png

    • マークダウン投稿、シンタックスハイライト
      • gem: redcarpet, rouge(結局syntax-hightlightだけは反映されないまま
    • 投稿から画像生成
    • AWS S3にog:image用の画像を保存

    作成の流れ:予定

    1. rails new codr, git init, heroku create、Active Storage
    2. AWS S3あれこれ
    3. twitter登録、ログイン機能作成

    開発環境

    • vm : Linux Ubuntu (virtualbox + vagrant)
      • Ruby 2.5.1p57
      • Rails 5.2.3
      • Postgresql

    実作業: アプリ作成、諸準備

    rails new codr -d postgresql
    # DB設定等は割愛
    

    Gem

    今回は公開にまで至る予定なので、railsやdeviseの日本語化等も。が、想定ユーザはエンジニアだしと思い、殆ど英語になった。

    Gemfile
    gem 'mini_racer'   # uncomment
    gem 'rails-i18n'   # japanize
    
    # authetication
    gem 'devise' # login
    gem 'omniauth' # SNS login
    gem 'omniauth-twitter' # twitter login
    gem 'devise-i18n'  # japanize devise
    gem 'devise-i18n-views'
    
    gem 'redcarpet'   # for markdown
    gem 'rouge' # for syntax highlight
    
    gem 'meta-tags'
    
    gem 'aws-sdk-s3' # for aws s3
    

    kpumuk/meta-tags:Search Engine Optimization (SEO) for Ruby on Rails applications.は割愛。

    gitignore => rails.credentials.yml

    当初は.gitignoregem 'dotenv'等を使っていた。が、作成途中でRails5.2からのrails.credentials.ymlを知り、利用した。rails.credentials.ymlは暗号化されており、なお、復号化には/config/master.key`を利用。

    irb
    # editor setting
     EDITOR="vi" bin/rails credentials:edit
    # edit credentials.yml
    rails credentials:edit
    # show credential.yml
    rails credentials.yml:show
    
    # herokuにmaster.keyを環境変数として指定
    heroku config:set ENV_VAR="環境変数" --app "アプリ名"
    
    # 追加した変数を使用するには
    Rails.application.credentials.dig(:twitter, :API_Key)
    

    rails gあれこれ

    # devise
    # install devise
    rails g devise:install
    rails g devise User name:String
    
    # Add Admin column to User
    rails g migration AddAdminToUsers
    # add setting at /db/migrate/20191103141531_add_admin_to_users.rb
    add_column :users, :admin, :boolean, default: false
    
    # add views and controllers to modify devise
    rails g devise:controllers users
    rails g devise:views users
    
    # japanize
    # add at /config/application.rb
    config.i18n.default_locale = :ja
    => create /config/locale/devise.view.ja.yml
    
    # scaffold post
    rails g scaffold Post user:references name:string content:text date:datetime
    

    Active Record Associations関連付け

    /app/model/user.rb
    has_many :posts
    
    /app/model/post.rb
    belongs_to :user
    

    投稿関連

    マークダウン投稿

    基本:Redcarpet::Markdown.new(renderer, extensions = {}).render(@post.content)
    オプションやXSS対策等を追加したく、helperメソッドを作成した。

    app/helpers/posts_helper.rb
    Module PostsHelper
      require 'rouge/plugins/redcarpet'
      class RougeRedcarpetRenderer < Redcarpet::Render::HTML
        include Rouge::Plugins::Redcarpet
    
        def header(text, level)  # #や##等がh2、h3となるようにした。
          level += 1
          "<h#{level}>#{text}</h#{level}>"
        end
      end
    
      def markdown(text)
        render_options = {
          filter_html: true,  # do not allow any user-inputted HTML in the output.
          hard_wrap: true,
        }
    
        extensions = {
          autolink: true, # <>で囲まれていない時は、リンクとして認識しない
          fenced_code_blocks: true,   #  ```\n ```内をコード部分と見做す
          lax_spacing: true, 
          no_intra_emphasis: true,
          strikethrough: true,
          superscript: true,
          tables: false,  # テーブルを認識しない
          highlight: true,
          disable_indented_code_blocks: true,
          space_after_headers: false # #の後にスペースが無くても、h1等とする。
        }
        renderer = RougeRedcarpetRenderer.new(render_options)
        Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe
      end
    end
    

    html_safe => sanitize

    html_safeではXSS対策としては駄目と知った。名前詐欺である。
    sanitizeヘルパーを使用した。ホワイトリスト方式。要参照

    app/views/posts/index.html.erb
    # sanitize(html, options = {})
     <div id="capture" class="content">
        <%= sanitize(markdown(@post.content), tags: %w(div img h1 h2 h3 h4 h5 strong em a p pre code ), attributes: %w(class href)) %>
    </div>
    

    投稿内容のデータ化、AWSへの画像保存

    最初はTwitterAPIを利用して、投稿から作成、DBに直接保存した画像でTwitter投稿しようとした。だが、Herokuでは画像が保持されない事、TwitterAPIの変更などいろいろ面倒なことが発生したので、最終的には画像をAWS S3に保存し、og:imageに添付する形を取った。

    1. Webアプリ内で通常投稿
    2. showページ表示(同時にhtml2canvasでBase64としてデータ取得、hidden_fieldに格納
    3. Tweetボタン押す(Postされ、postモデル内でbase64をデコード
    4. Active Storageを通して、AWS S3に保存

    Active Storage

    Rail5.2からの機能で、今までのcarrievaveやpaperclip等を使わずに、クラウドストレージ等へのアップロードが容易になる。今回はAWS S3を使った。

    irb
    # set up
    rails active_storage:install
    # 今回は画像が紐づくPostテーブルが既にあるので、不要
    # rails g resource comment content:text
    rails db:migrate
    
    app/models/post.rb
    class Post < ApplicationRecord
    # 今回は1つの投稿につき、1枚の画像なので。複数なら => has_many_attached :prtscs
      has_one_attached :prtsc
    end
    
    app/config/enviroments/
    # ファイル保存先変更
    # development.rb
    config.active_storage.service = :local
    # production.rb
    config.active_storage.service = :amazon
    

    rails credentials:editでAWSアクセスキーとシークレットキーを追加。

    config/credentials.yml.enc
    aws:
      access_key_id: 
      secret_access_key: 
    
    config/storage.yml
    test:
      service: Disk
      root: <%= Rails.root.join("tmp/storage") %>
    
    local:
      service: Disk
      root: <%= Rails.root.join("storage") %>
    
    amazon:
      service: S3
      access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
      secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
      region: ap-northeast-1
      bucket: codr0
    
    Gemfile
    # gemが必要
    gem 'aws-sdk-s3', require: false
    # 今回は不要だったので、入れず。
    gem 'mini_magick'
    

    html2canvas

    参考:htmlを画像化する方法(html2canvasの使い方)
    jsはProgateレベルだったので、DOM操作は初めてで、なんか楽しかったぞ。

    1. Tweetボタン押下時に、画像をPostするためのフォーム、hidden_fieldを用意
    2. html2canvas.jsapp/assets/javascriptsディレクトリ配下に保存。
    3. html上に置くscriptコードを改修
    app/views/posts/show.html.erb
    <%= form_with(model: @post, local: true) do |form| %>
      <%= form.hidden_field :id, value: @post.id %>
      <%= form.hidden_field :prtsc, value: "" %> # idはpost_prtscになる。
      <%= form.submit "Post", class:"btn btn-outline-dark", id:"tweet", value:"tweet" %>
    <% end %>
    
    app/views/layouts/application.html.erb
    <script type="text/javascript">
      html2canvas(document.querySelector("#capture"),{scale:1, width:600}).then(canvas => {
        var base64 = canvas.toDataURL('image/jpeg', 1.0);
        document.getElementById('post_prtsc').setAttribute('value', base64);
     });
    </script>
    

    Base64デコード

    大学で画像処理していたとはいえ、 Base64とは?Blobとは?となり、良い機会だった。

    app/models/post.rb
    attr_accessor :img
    
    def parse_base64(img)
      if img.present?
        # data:image/jpeg;base64,/9j/4AAQSkZJRgABA・・・から/9j/4AA以降を選択取得
        content = img.split(',')[1]
        # 今回は、ユーザによる画像アップロード投稿ではなく、拡張子が決まっている
        filename = Time.zone.now.to_s + '.jpg'
        decoded_data = Base64.decode64(content)
        # String.IO.newにより、アプリ内に一時ファイルを作成しなくて済む
        prtsc.attach(io: StringIO.new(decoded_data), filename: filename)
      end
    end
    

    あとはposts_controllerで、paramsから受け取ったBase64データを上のparse_base64(img)で変換し、保存すれば完了。

    AWS S3

    AWS上での登録、設定、バケット作成等は割愛。

    Tweet button

    公式で生成されるTweetボタンのURLを利用し、押下時にwindow.openでTweet投稿ページを開くようにした。rubyonrailsで用意した変数をjsに渡すgem 'gon'も考えたが、見送った。

    app/views/layouts/application.html.erb
    <script>
      var base = 'https://twitter.com/intent/tweet?url=';
      var pageUrl = 'https://codr0.herokuapp.com/posts/' + document.getElementById('post_id').value;
      var option = '&button_hashtag=Codr0&ref_src=twsrc%5Etfw';
      var href = base + pageUrl + option;
      var twit = document.getElementById('tweet');
      twit.addEventListener('click', function() {
            window.open( href );
          });
    </script>
    

    og:imageに画像添付

    なお、headのmeta情報セットには、gem 'meta-tags'を使用。参照 : kpumuk/meta-tags

    service_url()とurl_for()

    基本的にはどちらも、ActiveStorageに保存したデータのUrlを取得するメソッドの様だ。
    どちらもセキュリティの為にリンクの有効期限が短いみたいだが、違いが分からなかった。今回はTweetボタン押下し、Tweetした際にog:imageとして表示されればいい。

    app/views/posts/show.html.erb
    # 画像がActive StorageでAWS S3に保存されて入れば
    <% if @post.prtsc.attached? %>
      <% set_meta_tags og:{image: @post.prtsc.service_url} %>
    <% end %>
    

    Twitterログイン

    TwitterDeveloperAccountが必要。割愛。

    なお、omniauthは脆弱性が見つかっており、githubの方でもアラートが来るのだが、パッチが無いのだが。クックパッドの人が対処してくれたので、感謝したい。

    app/models/user.rb
    # 参考ページと同じ基礎的な所は割愛する。
    class User < ApplicationRecord
      def self.from_omniauth(auth)
        find_or_create_by!(provider: auth['provider'], uid: auth['uid']) do |user|
          # 一部割愛
          user.username = auth['info']['nickname']
          # SNS登録時は、ダミーメールを登録
          user.email = User.dummy_email(auth)
        end
       end
    
      # SNS登録(providerが存在する)時は、パスワード要求をしない
      def password_required?
        super && provider.blank?
      end
    
      def self.new_with_session(params, session)
        if session['devise.user_attributes']
          new(session['devise.user_attributes']) do |user|
            user.attributes = params
          end
        else
          super
        end
      end
    
      private
    
      def self.dummy_email(auth)
        "#{auth.uid}-#{auth.provider}@example.com"
      end
    end
    

    Twitterのニックネームが取得できるようになったので、元からあるUserのnameテーブルは削除した。

    css

    今回はBootstrapを部分的に使用した。cssの優先順位など収穫があった。

    改修(加筆

    メディアクエリ

    想定ユーザは殆どスマホなのに、PCで作成し、CSSをPCの見た目でやってた。折角SCSSでやってるので、変数を利用した。

    app/assets/stylesheets/scaffold.scss
    # ディスプレイサイズが680pxまでなら。
    $tab: 680px; 
    @mixin tab {
      @media (max-width: ($tab)) {
        @content;
      }
    }
    
    // .box {
    //   @include tab {
    //     background-color: blue;
    //   };
    // }
    

    最後に

    gist等がコードスクショをog:imageで表示してくれたら全て済むんじゃと思った。
    因みにもう1段階先のWebアプリを考えてあるけど、たぶんjsの知識が足りないので、今は無理。
    転職活動中の無職です。働きたい。。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

p5.js向け 有用無用道具箱

■ はじめに

p5.js でスケッチを書く(描く?)のを何度か繰り返していると、なんだか同じようなコードを何度も書いてるなあ、と思う場面に遭遇します。そんなときは、再利用できそうな形でコードを書いて、また今度も使えるようにストックしておくと良いかもしれません。もし、そのストックを使う場面が再び訪れ、そのおかげで思いついたスケッチをスパッと作れたりすると、勝ったなガハハという気分になれます。1

ここでは、私がよく使ったり使わなかったりするいくつかの共通処理を、個別の記事に分けて紹介していきたいと思います。一つ一つはあまり大した内容ではありませんが……。

この記事はいったん目次だけですが、人によっていろいろ前提が違うと思うので、下の方にもろもろの前提知識(主に文法)についても書きました。

■ 目次

(仮)
(個別の記事はこれからです)

  • ランダム
  • イージング
  • タイマー
  • 配列要素の組み合わせ
  • 曲線
  • 図形のトリミング
  • ピクセル操作
  • キーボードで移動
  • 背景画像
  • 画面拡大

■ 環境

  • JavaScript
    • ES2015(後述)
  • p5.js
    • v0.10.2
    • 簡単のため、global mode(要するに普通のモード)を前提とします。
       参考: Global and instance mode (公式Wiki)
      instance mode がよく分からんという人でも大丈夫ですが、
      global mode がどう動くのかは知っといたほうがいいかも(補足を追記するかもしれません)
  • ブラウザ
    • たぶんどれでも大丈夫だとは思うけど、私は Chrome で確認しています。
    • 経験上 Edge はたまに怪しい

■ 注意事項

書こうとしている内容ですが、実際のところほとんどが

  • もっとうまくやってくれるライブラリが存在する
  • そもそも p5.js が向いていないかもしれない

のどちらかな気がします(ちゃんと調べてませんがたぶん)。

よって、主な対象読者は、寿命が無限であるなどの理由で長時間の無駄を許容できる仙人か、効率を度外視して過程を楽しむことを優先する変態か、もしくはその両方です。これはすなわち合理的経済人と対を成す理想的存在であり、現実のあなたにはそれぞれ個別の事情があることでしょう。(?)

■ 前提知識

ぜんぶ分かってないとダメとかではなくて(そんなに大層なコードは出てこない)、
初めて出くわしたときにギョッとしなければいいな、という諸々です。

ググるのが面倒な人のために例と説明も添えましたが、もっと優れた解説が多々あります。
しかも動作確認してないので、なんか違ったら後で直すかも。

JavaScript の基本文法

基礎的な概念やキーワードとその使い方

変数、関数(引数・戻り値)、データ型、演算子、値の比較、
ifforswitchnullundefined、……

オブジェクト型

これは p5.js のサンプルコードで(表面的には)あまり出てこないかも? と思ったので書きます。
var はこの連載では実際には使いません、後述の「ES2015」参照

object-example.js
var coordinates = {
  x: 20,
  y: 40
};

仮にこう書いたとき、coordinates.xcoordinates.yで値を参照できます。
xyのところは変数名と同様、好きな名前を使える。
参考: オブジェクトの基本 - JavaScript | MDN

三項演算子

条件次第で値を変えたいとき、ifよりシンプルに書けるやつです。

var flag = true;
var answer = flag ? "YES" : "NO";
// flag が true なら "YES", でなければ "NO" が結果として answer に代入される

参考: 条件 (三項) 演算子 - JavaScript | MDN

今回(たぶん)使わないもの

prototypeとかthisとかnewとかはたぶん出ない。2

p5.js の基本

setupdraw、座標システム、いろいろな図形の描画、frameCount、その他各種変数・関数、などなど。

何度か自分で書いた経験があって、
例えばまあ、こういった基本的なサンプルコードを見たときに、何をやってるのかすぐ理解できると良いです。
Coordinates | examples | p5.js

p5.js の関数その他についてはリファレンスを見よう!
reference | p5.js

3D(WebGL)とかはたぶんやらないと思います。

高階関数

引数が別の関数だったり、戻り値が別の関数だったりするような関数のことです。

※ 説明のため、いったん function宣言で例を書きますが、
  実際にはこの連載では後述の「アロー関数」で書きます。

引数が別の関数

:arrow_down: この例では引数 doSomething がそれですが、こういうのをコールバック関数と言います。
  参考: コールバック関数 - JavaScript | MDN

callback-function.js
// doSomethingは任意の関数
function runGivenFunction(doSomething) {
  doSomething(200);
}

// 使うとき: たとえばこのような関数があったとして、
function drawCircleAtMyPlace(size) {
  circle(100, 100, size); // p5.js の circle 関数
}

// こうすると、結果的に drawCircleAtMyPlace(200); が実行されるのと同じことになる
runGivenFunction(drawCircleAtMyPlace);

返り値が別の関数

:arrow_down: ちなみにこの例では、minusOneを外から参照することはできませんが、
  coolFunctionに入れた関数が存在する限りminusOneの宣言も内部的には残り続けています。
  こういうのをクロージャと言います。 参考: クロージャ - JavaScript | MDN

return-function.js
// これの返り値は、値を入れるとそれに-1を掛け算して返してくれる関数
function getCoolFunction() {
  var minusOne = -1;

  return function(value) {
    return minusOne * value;
  };
}

// 使うとき:
var coolFunction = getCoolFunction();
var result = coolFunction(100); // この結果は -100

// 私はあまりやらないけど、getCoolFunction()(100); などのように一度に書くことも可能

配列の処理

配列を使った操作のうち、上述のコールバック関数を使うものがいくつかあり、
その中でも forEach map filter あたりは使う可能性が高いです。3

const array = [10, 20, 30];

array.forEach(someFunc); // 各要素を引数としてsomeFuncを実行
array.map(someFunc);     // 各要素を引数としてsomeFuncを実行し、それらの結果を新しい配列で返す
array.filter(someFunc);  // 各要素を引数としてsomeFuncを実行し、結果trueの要素だけ新しい配列で返す

// 例えば
array.forEach(print); // print(10); print(20); print(30); が順に実行される
array.map(sq); // sq は p5.js の関数で、数値を二乗する。こうすると結果 [100, 400, 900] が返る

function greaterThan15(value) { return value > 15; } // 15より大きければ true を返す関数
array.filter(greaterThan15); // こうすると結果として、条件に合致した要素の配列 [20, 30] が返る

参考: Array - JavaScript | MDN

ES2015

JavaScript の文法(というか仕様)は年々拡張されており、
特に「ES5」以前と「ES2015」以降とで大きく様変わりしています。
参考: ES2015 (ES6) についてのまとめ

p5.js はそれなりに歴史があるので、ES5 以前の書き方のサンプルコードがたくさんあります。
その関係で、新たに書く場合でもあえて ES5 以前の書き方で書く、という人もいるかもしれません。
あと、ES2015 が各種ブラウザでちゃんと動作するようになったのは割と最近のことであるらしい。

そんな事情なので人によっては見慣れないかもですが、私の場合は ES2015 を取り入れて書きたいと思います。
中でもよく使うと思われるものを挙げます。

変数宣言

定数やオブジェクトや配列は基本的に const(再代入不可)、
あとで再び代入する予定の変数だけ let で宣言します。
ES2015より前は var しかありませんでした。

var とその他の厳密な違いとしては、スコープとかホイスティング(巻き上げ)とかがありますが、
そもそも var を使わなければあまり意識しないと思われます。

for...of ループ

こういうやつです。

const array = [10, 20, 30];

// 普通のfor
for (let i = 0; i < array.length; i += 1) print(array[i]);

// for...of(結果は上と同じ)
for (const element of array) print(element);

普通の forfor...of、あるいは先述の forEach、どれがいいのかは文脈次第。

参考: for...of - JavaScript | MDN

アロー関数

:arrow_down: 関数は function で定義できますが、

// [1] 引数2つの関数
function doSomethingCrazy(someString, someValue) {
  // ...
}

// [2] 引数1つで、計算した値を返す単純な関数
function calculateElegantly(value) {
  return value * 7777777;
}

// [3] 引数なしで、関数の返り値が別の新たな関数
function getCoolFunction() {
  return function(value) {
    return -value;
  };
}

:arrow_down: アロー関数という構文で、このようにも書ける。

// [1] 引数2つの関数
const doSomethingCrazy = (someString, someValue) => {
  // ...
};

// [2] このケースでは () も {} も return も省略できる
const calculateElegantly = value => value * 7777777;

// [3] 引数なしなら () => ... となる。
// value => -value の部分が返り値であり、function(value) { return -value; } に相当する
const getCoolFunction = () => value => -value;

短くすっきりしてて良いのですが、知らないと、慣れるのにちょっとかかるかもしれませんね。
参考: アロー関数 - JavaScript | MDN

あと、上述の var と似たような話(ホイスティング有無)が function にも言えますが、
基本的に function は使わず、変数でも関数でも const(たまに let )で宣言したいと思います。

分割代入

:arrow_down: こういうオブジェクトがあったとして、

const fantasticObject = {
  greatNumber: 1984,
  awesomeString: "WTF"
};

:arrow_down: こんな感じで中身を取り出したいとき、

const greatNumber = fantasticObject.greatNumber;
const awesomeString = fantasticObject.awesomeString;

:arrow_down: こうも書ける、ということ。

const { greatNumber, awesomeString } = fantasticObject;

取り出すときに別の名前にしたり、深いところから一気に取ったりもできます。
参考: 分割代入 - JavaScript - JavaScript | MDN

デフォルト引数

引数に何も渡さなかったときのデフォルト値を決めておくやつです。

const printName = (name = "no name") => print(name);

printName("John"); // print("John"); が実行される

printName(); // print("no name"); が実行される
printName(undefined); // 同上

参考: デフォルト引数 - JavaScript | MDN

今回(たぶん)使わないもの

単なる最近の私の好みですが、ひとまず表面的には class は使う機会が少ないと思います。

モジュール機能の import export も、普段は使うけどここでは使わない。

イテレーターやジェネレーター、yieldPromise、とかは私自身も分かってない。

とりあえず

このくらいでしょうか。
習うより慣れろでもいいと思います。

▶ 目次にもどる


  1. 二度と使わず死蔵してしまうことも多いわけですが、それも経験ということで。 

  2. prototypeとかthisとかが何なのか(どう動くのか)は知っておく価値がありますが、とりあえず今回のところは大丈夫だと思います。私も怪しいし。 

  3. 処理効率をキリキリ上げたいときはforEach系は遅いので避けると思いますが、今回そんな部分がネックになることはないので、気にしないこととします。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Riot v4 でCSSフレームワークを使うなら Semantic UI Riot はいかが?

Riot v4 がリリースされて随分経ってしまいましたが、ようやく Semantic UI Riotv4 に対応できました:rocket:
Riot.js Advent Calendar 2019の1日目で恐縮ですが宣伝させてください!
logo.png

Semantic UI Riot って何?

CSSフレームワークのSemantic UIに特化したRiotのコンポーネントセットです。
Semantic UIのJavaScriptやjQueryに依存することなく、Modulesが使えるようになっています。1
https://semantic-ui-riot.web.app

Semantic UI Riot が依存する Riot と Semantic UI のバージョン

Riotのバージョンに合わせて、1.xと2.xのどちらかをお使いください。2

Semantic UI Riot Riot Semantic UI
1.x.x 3.0.0 2.3.0
2.x.x 4.0.0 2.3.0

用意されているコンポーネントの一覧

Modules

Semantic UIのModulesの中から個人的によく使うものをコンポーネント化してあります。

  • Accordion
  • Checkbox
  • Dropdown
  • Modal
  • Popup
  • Progress
  • Rating
  • Tab

Addons

Semanti UIのModulesにはないもので、あったら便利そうなものを作りました。

  • Alert
    • ブラウザ標準のalert()の代わりに使えます。
  • Confirm
    • ブラウザ標準のconfirm()の代わりに使えます。
  • Datepicker
  • Pagination
  • Radio
    • Semantic UIではCheckboxに含まれているものを独立させました。
  • Table
    • 列でソートができます。
  • Toast
    • 一定時間経過したら消えるメッセージです。
  • Validation Error

コンポーネントの簡単な紹介

Checkbox

<su-checkbox name="checkbox1">
  Make my profile visible
</su-checkbox>

値のやりとりはchecked属性を使います。

this.$("[name='checkbox1']").checked = true

Dropdown

<su-dropdown name="dropdown1" items="{ dropdownItems }"></su-dropdown>

<script>
  export default {
    dropdownItems: [
      {
        label: 'Gender',
        value: null,
        default: true
      },
      {
        label: 'Male',
        value: 1
      },
      {
        label: 'Female',
        value: 2
      },
    ]
  }
</script>

items 属性でドロップダウンの中に表示する項目を設定します。

値のやり取りは value 属性を使いますが、value 属性の場合はgetAttribute , setAttribute じゃないとやり取りできないようです。

(Riot.jsで予約語として扱われているのが原因?と思っていますが、よくわからないので詳しい方教えてください。)

this.$("[name='dropdown1']").getAttribute('value')
this.$("[name='dropdown1']").setAttribute('value', value)

Modal

<su-modal modal="{ modal }" show="{ state.show }" onhide="{ onHide }">
    <p>Modal Content</p>
</su-modal>
<button class="ui button" onclick="{ showModal }">Show modal</button>

<script>
  export default {
    state: {
      show: false
    },
    modal: {
      header: 'Select a Photo',
      buttons: [{
        text: 'Ok',
        type: 'primary',
        icon: 'checkmark'
      }, {
        text: 'Cancel'
      }]
    },

    showModal() {
      this.update({ show: true })
    },
    onHide() {
      this.update({ show: false })
    }
  }
</script>

モーダルのコンテンツはタグのテキストに、それ以外のヘッダーやボタンの情報は modal 属性で設定して、 show 属性を true にするとモーダルが表示されます。

モーダルがクローズされると onhide コールバックが呼ばれるので、 show 属性の変数(ここでのstate.show) を false にするのをお忘れなく。

Other

他にも色々なコンポーネントを用意してあるので、興味があればのぞいてみてください。
https://semantic-ui-riot.web.app

インストール

ここまで読んで試してみたくなった方は是非インストールしてみてください!

お手軽に全部CDNから持ってくる方法

index.html
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css" />
  <script src="https://unpkg.com/riot@4.6.6/riot+compiler.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/semantic-ui-riot@2.1.0/dist/semantic-ui-riot.js"></script>
</head>

<body>
  <sample></sample>

  <script type="riot" data-src="./sample.riot"></script>
  <script>
    riot.compile().then(() => {
      riot.mount("sample");
    });
  </script>
</body>
</html>
sample.riot
<sample>
  <su-checkbox>Make my profile visible</su-checkbox>
</sample>

Webpackを使う方法

npm i -S semantic-ui-riot
index.js
import {component} from 'riot'
import 'semantic-ui-riot'
import Sample from './sample.riot'

component(Sample)(document.getElementById('app'))
webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.riot$/,
        exclude: /node_modules/,
        use: [{
          loader: '@riotjs/webpack-loader'
        }]
      }
    ]
  }
};
index.html
<!DOCTYPE html>
<html>
<head>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/semantic-ui@2.4.2/dist/semantic.min.css">
</head>
<body>
  <div id="app"></div>
  <script src="main.js"></script>
</body>
</html>
sample.riot
<sample>
  <su-checkbox>Make my profile visible</su-checkbox>
</sample>

Thanks!

Semantic UI Riot のアイコンは Riotユーザーでデザイナーの @nibushibu さんに作って頂きました!
どうもありがとうございます!


  1. Modulesを使いやすくするためのコンポーネントセットという位置付けなので、それ以外のElements, Collections, ViewsはSemantic UIを直接お使いください。 

  2. Riot v4対応版をリリースするにあたって、 v3対応版だった0.24.11.0.0にバージョンアップしました。破壊的な変更はなく、単なる気持ちの整理で上げただけなので、Riot v3をお使いの場合は是非とも1.x.xをお使いください。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

serverless-http で画像 ( image ) や pdf などの binary を返す

コードへの記述

serverless-http は環境変数 BINARY_CONTENT_TYPES見ているので下記のように書けば動くと思います。

process.env.BINARY_CONTENT_TYPES = ['application/pdf'].join(',')

const fs = require('fs')
const path = require('path')
const serverless = require('serverless-http')
const app = require('express')()
app.get('/file.pdf', (req, res, next) => {
  res.setHeader('Content-Type', 'application/pdf')
  fs.createReadStream(path.join(__dirname, 'file.pdf')).pipe(res)
})
exports.handler = serverless(app)

AWS Gateway の設定の変更も必要

AWS Gateway の設定の変更も必要ですのでこちらの記事を参考にしてください。

AWS Gateway の設定を Swagger でやっている場合トップレベルに下記を追加します

x-amazon-apigateway-binary-media-types:
  - '*/*'

serverless-http が内部でやってること

lambda はバイナリを返せません。
なので base64 エンコードして「エンコード済だよ」と AWS Gateway に伝えます。

こちらの記事が参考になります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

javascript 基本その2

昨日に続いて、javascriptの投稿です。
Node.jsについても投稿したいところですが、混乱してきそうなので
学んできたことを順番に投稿していこうと思います。

条件分岐

javascriptではif構文を使用します。

条件分岐
if (条件式1) {
  // 条件式1がtrueのときの処理
} else if (条件式2) {
  // 条件式1がfalseで条件式2がtrueのときの処理
} else {
  // 条件式1も条件式2もfalseのときの処理
}

// ()が条件、{}が処理内容を記述する形です。
条件分岐の例
let num = 60;

if (num % 15 == 0) {
  console.log(num + 'は3と5の倍数です');
} else if (num % 3 == 0) {
  console.log(num + 'は3の倍数です');
} else if (num % 5 == 0) {
  console.log(num + 'は5の倍数です');
} else {
  console.log(num + 'は3の倍数でも、5の倍数でもありません');
}

配列

Rubyと同様に配列が扱えます。

配列の例
let list = ['a', 'b', 'c', 'd', 'e'];
console.log(list);

//これで ["a", "b", "c", "d", "e"]が出力されます。

配列の要素を取得

配列は0から始まります。
基本左から欲しい要素-1で取得します。

let list = ['a', 'b', 'c', 'd', 'e'];
console.log(list[2]);

// これで 2: c が出力されます。

配列の要素数を取得する

Rubyと同様にlengthメソッドが利用できます。

let list = ['a', 'b', 'c', 'd', 'e'];
console.log(list.length);
// これで length: 5 が出力されます。

配列の要素を追加する

Rubyと同様にpushメソッドが利用できます。

let list = ['a', 'b', 'c', 'd', 'e'];
list.push('f');
console.log(list);
// これで ["a", "b", "c", "d", "e","f"] が出力されます。

配列の要素を削除する

popメソッドは配列の最後の要素を取り除きます
shiftメソッドは配列の先頭の要素を取り除きます。
Rubyでは取り除く要素数を指定できますが、JavaScriptではできません。

popメソッド
let list = ['a', 'b', 'c', 'd', 'e'];
list.pop();
// これで ["a", "b", "c", "d"]が出力されます。
shiftメソッド
let list = ['a', 'b', 'c', 'd', 'e'];
list.shift();
// これで ["b", "c", "d", "e"]が出力されます。

オブジェクト

オブジェクトはデータのまとまりのことです。
配列は順番でデータを管理しますが、オブジェクトは名前と値をセットにしてデータを管理します。

オブジェクトを作成するには波カッコ{}を用います。

作成
let obj = {}; 

オブジェクトはデータを名前と値をセットで管理します。
このセットをプロパティと言います。

let obj = { name: 'gimei' }; //これでプロパティが設定されました。

プロパティの値を取得

値を取得
let obj = { name: 'gimei', age: 25, address: 'tokyo' };
console.log(obj.name);

//これでgimeiが出力されます。

プロパティの値を変更

オブジェクトに対してプロパティ名を続けて記載することで値を取り出すことができます。

値を取得
let obj = { name: 'gimei', age: 25, address: 'tokyo' };
obj.name = 'gimei2';
console.log(obj.name);

//これでgimei2が出力されます。

繰り返し処理

繰り返しはfor文を使います。

繰り返し処理
for (初期値; 繰り返す回数の条件; 初期値への毎回の処理) {
  // 繰り返す処理の内容
}

//もう少し詳しく書くと

for (let i = 0; i < 繰り返す回数; i = i + 1) {
  // 繰り返す処理の内容
}
繰り返し処の例
num = 1;
for (let i = 0; i < 10; i += 1) {
  console.log(num + '回目の出力')
  num +=  1
}

これで下記のような出力がされます。
1回目の出力
2回目の出力
~
10回目の出力

関数を定義

Rubyでは def ~ end で囲むことで関数を定義しましたが
JavaScriptで関数を定義するにはfunction文を使います。

関数の定義
function 関数名(引数) {
  // 処理の内容
}
関数の定義の例
function sayHello(){
  console.log('hello');
}
//関数を一つ定義

function sayName(name){
  console.log(name);
}
//関数を二つ目定義

let myName = 'gimei'; 
//変数に値を設定
sayHello();
sayName(myName);
//下二行でそれぞれ関数を出力

//hello,gimeiが出力される。

関数の戻り値を明示

Rubyでは関数における最後の戻り値が関数の戻り値として処理されましたが
JavaScriptではreturnを用いて明示する必要があります。

戻り値
function calc(num1,num2){
  return num1*num2; //ここに注目、
}

let num1 = 3;
let num2 = 4;
console.log(calc(num1,num2));

//12と出力
悪い例
function calc(num1,num2){
  num1*num2; //returnがないので値を返さない。
}

let num1 = 3;
let num2 = 4;
console.log(calc(num1,num2));

//出力されず。

無名関数を定義

JavaScriptにおける関数の定義には2種類の方法があります。
関数宣言と呼ばれ、これまで学んで来た方法で、
無名関数と呼ばれ、コードの中に書き込む関数式と呼ばれる方法です。

関数定義
// 関数宣言
function hello(){
  console.log('hello');
}

// 関数式(無名関数)
let hello = function(){  //helloがletに定義され、functionにはなくなってます。
    console.log('hello');
}
//関数名が変数名に定義される形です。
無名関数
let hello = function(){
    console.log('hello');
}

hello();
//これでhelloが出力されます。

関数宣言と無名関数の違い

読み込まれる順番が違い
間違えると出力されません。

関数宣言は先に読み込まれるために、
このような事象が発生します。一方で、関数式であれば先に読み込まれることはありません。

関数宣言
hello();

function hello(){
    console.log('hello');
}
// helloとエラー無く出力
// 関数宣言は先に読み込まれるためです。
無名関数
hello();

let hello = function(){
    console.log('hello');
};
// hello is not a function...といった形でエラーが表示されます。
// 関数宣言がないためこうなります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【ポケモン】ゼロからはじめるダメージ計算ツールづくり【第1回:プロトタイプ編】

まえがき

ダイマックスちょーかっこよかった!

ポケモンシリーズの第8世代「ソード・シールド」が発売されました。
全作品プレイしている僕は当然のごとく今作も買いました。
メガシンカもZわざもないなんて…とバカにしていましたが、ダイマックスちょーかっこよかったです。
殿堂入りするまでノンストップでテンションあがりました。

さて、殿堂入りまでがチュートリアルといわれるポケモンシリーズですが、
僕もレート対戦がんばりたいと思っています。
そんななかで発生するのがこんな状況だと思います。


この相手にこのわざ撃って倒せるのか??
倒せないなら交代or別のわざ撃ったほうがいいなぁ~
具体的なダメージが分かれば戦略がたてられるのに…


こんなとき、使いたくなるのが「ダメージ計算ツール」ですよね。

でも、僕みたいな初心者からすると、いちいちステータス打ち込んでわざ打ち込んで…って
ダメージ計算するの、対戦中にやるには億劫だし慣れてないツールだと時間も足りないんですよね。

実際の対戦画面みたいに、わざのボタン押したらダメージ計算してくれるツールがあったらいいのにな~
と思いました。


よし!つくろう!

目標

  1. レート対戦(今作はランクバトル)初心者でも使いやすいダメージ計算ツールをゼロからつくること
  2. このツールで計算した結果が視覚的にわかりやすいこと
  3. エンジニアとしての知識・技術を身につけること
  4. SEとして名乗るのに具体的に見せることができる成果物をつくること
  5. このツールを使ってランクバトルに勝つこと

ほしい仕様

  1. 攻撃側、防御側のポケモンのステータスが計算できること
  2. 実際のバトルの状況に沿ってダメージ計算ができること
  3. 自分の使うポケモン・パーティー、流行っているポケモンの型が登録でき、なるべく少ない手順でそれが呼び出せること
  4. わざボタンを押すとダメージ計算の結果が表示されること
  5. ダメージ計算の結果が相手ポケモンのHPバーで表示されること(乱数による幅を含む)

使う技術、環境

HTML,JavaScript,CSS
Python(いったんやめた)

いままでプログラミングでつくったことがあるものは、PythonでExcelファイルを操作して業務効率化を図るコンソールアプリだけだったので、GUIのあるものをつくるのはこれがほぼはじめてです。
Python大好きエンジニアなので、はじめはPythonでつくろうと思っていたのですが、

  1. PythonのGUIライブラリよくわかんない(tkinterとか)
  2. 最近ポートフォリオサイトをgithub.ioでつくったのでHTMLに慣れてた
  3. ダメージ計算だけだったらとりあえずJavaScriptでもつくれそう

の理由でいったんHTML,JavaScriptをベースにプロトタイプをつくることにしました。
PythonをベースにHTML,CSSでGUIアプリをつくることができるEelというライブラリも試してみましたが、途中で3に気づき、複雑になってしまうことを避けたかったので、今回はHTML,JavaScriptを勉強しながらつくってみることにしました。

今回の進捗

数値を直接入力して使うタイプのダメージ計算ツールを避けるためにつくるのですが、まずはステータス計算、ダメージ計算の仕組みなども理解しながらつくる必要があると思ったので、よくあるシンプルなダメージ計算ツールを作成することにしました。

ここまでの成果物
proto.PNG
HTMLソース
JavaScriptソース

/* 味方側 */
function hcalc(){
    let hbs =Number(document.getElementById("hbs").value);
    let hiv =Number(document.getElementById("hiv").value);
    let hev =Number(document.getElementById("hev").value);
    let hresult = Math.floor((hbs+hiv/2+hev/8+60))
    document.getElementById("hresult").innerHTML = hresult;
}

function acalc(){
    let abs =Number(document.getElementById("abs").value);
    let aiv =Number(document.getElementById("aiv").value);
    let aev =Number(document.getElementById("aev").value);
    let anc =Number(document.getElementById("anc").value);
    aresult = Math.floor((abs+aiv/2+aev/8+5)*anc)
    document.getElementById("aresult").innerHTML = aresult;
    document.getElementById("adisp").innerHTML = aresult;
}

function bcalc(){
    let bbs =Number(document.getElementById("bbs").value);
    let biv =Number(document.getElementById("biv").value);
    let bev =Number(document.getElementById("bev").value);
    let bnc =Number(document.getElementById("bnc").value);
    let bresult = Math.floor((bbs+biv/2+bev/8+5)*bnc)
    document.getElementById("bresult").innerHTML = bresult;
}

function ccalc(){
    let cbs =Number(document.getElementById("cbs").value);
    let civ =Number(document.getElementById("civ").value);
    let cev =Number(document.getElementById("cev").value);
    let cnc =Number(document.getElementById("cnc").value);
    cresult = Math.floor((cbs+civ/2+cev/8+5)*cnc)
    document.getElementById("cresult").innerHTML = cresult;
    document.getElementById("cdisp").innerHTML = cresult;
}

function dcalc(){
    let dbs =Number(document.getElementById("dbs").value);
    let div =Number(document.getElementById("div").value);
    let dev =Number(document.getElementById("dev").value);
    let dnc =Number(document.getElementById("dnc").value);
    let dresult = Math.floor((dbs+div/2+dev/8+5)*dnc)
    document.getElementById("dresult").innerHTML = dresult;
}

function scalc(){
    let sbs =Number(document.getElementById("sbs").value);
    let siv =Number(document.getElementById("siv").value);
    let sev =Number(document.getElementById("sev").value);
    let snc =Number(document.getElementById("snc").value);
    let sresult = Math.floor((sbs+siv/2+sev/8+5)*snc)
    document.getElementById("sresult").innerHTML = sresult;
}


/* 相手側 */
function ehcalc(){
    let ehbs =Number(document.getElementById("ehbs").value);
    let ehiv =Number(document.getElementById("ehiv").value);
    let ehev =Number(document.getElementById("ehev").value);
    ehresult = Math.floor((ehbs+ehiv/2+ehev/8+60))
    document.getElementById("ehresult").innerHTML = ehresult;
    document.getElementById("ehdisp").innerHTML = ehresult;
}

function eacalc(){
    let eabs =Number(document.getElementById("eabs").value);
    let eaiv =Number(document.getElementById("eaiv").value);
    let eaev =Number(document.getElementById("eaev").value);
    let eanc =Number(document.getElementById("eanc").value);
    let earesult = Math.floor((eabs+eaiv/2+eaev/8+5)*eanc)
    document.getElementById("earesult").innerHTML = earesult;
}

function ebcalc(){
    let ebbs =Number(document.getElementById("ebbs").value);
    let ebiv =Number(document.getElementById("ebiv").value);
    let ebev =Number(document.getElementById("ebev").value);
    let ebnc =Number(document.getElementById("ebnc").value);
    ebresult = Math.floor((ebbs+ebiv/2+ebev/8+5)*ebnc)
    document.getElementById("ebresult").innerHTML = ebresult;
    document.getElementById("ebdisp").innerHTML = ebresult;
}

function eccalc(){
    let ecbs =Number(document.getElementById("ecbs").value);
    let eciv =Number(document.getElementById("eciv").value);
    let ecev =Number(document.getElementById("ecev").value);
    let ecnc =Number(document.getElementById("ecnc").value);
    let ecresult = Math.floor((ecbs+eciv/2+ecev/8+5)*ecnc)
    document.getElementById("ecresult").innerHTML = ecresult;
}

function edcalc(){
    let edbs =Number(document.getElementById("edbs").value);
    let ediv =Number(document.getElementById("ediv").value);
    let edev =Number(document.getElementById("edev").value);
    let ednc =Number(document.getElementById("ednc").value);
    edresult = Math.floor((edbs+ediv/2+edev/8+5)*ednc)
    document.getElementById("edresult").innerHTML = edresult;
    document.getElementById("eddisp").innerHTML = edresult;
}

function escalc(){
    let esbs =Number(document.getElementById("esbs").value);
    let esiv =Number(document.getElementById("esiv").value);
    let esev =Number(document.getElementById("esev").value);
    let esnc =Number(document.getElementById("esnc").value);
    let esresult = Math.floor((esbs+esiv/2+esev/8+5)*esnc)
    document.getElementById("esresult").innerHTML = esresult;
}

function damagecalc(){
    let aorc =Number(document.getElementById("aorc").value);
    let power =Number(document.getElementById("power").value);
    let match =Number(document.getElementById("match").value);
    let comp =Number(document.getElementById("comp").value);
    //let ehdresult =Number(document.getElementById("ehdisp").value);
    if (aorc == 0){
        //let adresult =Number(document.getElementById("adisp").value);
        //let ebdresult =Number(document.getElementById("ebdisp").value);
        temp = Math.floor(22*power*aresult/ebresult)
        temp = Math.floor(temp/50+2)
        mintemp = Math.floor(temp*0.85)
        temp = Math.floor(temp*comp)
        mintemp = Math.floor(mintemp*comp)
        temp = Math.ceil(temp*match-0.5)
        mintemp = Math.ceil(mintemp*match-0.5)
    }
    else{
        //let cdresult =Number(document.getElementById("cdisp").value);
        //let eddresult =Number(document.getElementById("eddisp").value);
        temp = Math.floor(22*power*cresult/edresult)
        temp = Math.floor(temp/50+2)
        mintemp = Math.floor(temp*0.85)
        temp = Math.floor(temp*comp)
        mintemp = Math.floor(mintemp*comp)
        temp = Math.ceil(temp*match-0.5)
        mintemp = Math.ceil(mintemp*match-0.5)
    }
    document.getElementById("mintemp").innerHTML = mintemp;
    document.getElementById("temp").innerHTML = temp;
    document.getElementById("mincurrentHP").innerHTML = ehresult-temp;
    document.getElementById("maxcurrentHP").innerHTML = ehresult-mintemp;
    document.getElementById("minrate").innerHTML = (ehresult-temp)/ehresult*100;
    document.getElementById("maxrate").innerHTML = (ehresult-mintemp)/ehresult*100;
}

基本的な計算ツールとしての要件を満たすものはつくれたかなと思います。
具体的には、ほしい仕様
1 は完璧に満たしている
2 はダメージ計算はできているが、どうぐや積み技によるランク上昇、天候などまだまだ足りない要素がある
3以降 はこれから
です。

反省点として

JavaScriptのコードが不必要に長いです。汚いです。

機能実装最優先でゴリ押しで書きましたが、
1. Javascript側の関数を1つにしてHTML側でonclick=の引数を指定することでステータスの計算に適用
2. JavaScript側で親関数を作って各ステータスを計算する子関数を作り、HTML側はいまのまま
のどちらかできれいなコードになる気がします。

このへんはまだオブジェクト指向な考え方が身についてないのですぐスマートな書き方にならないと思っています。
勉強します。

あと、随時コメントを書くクセをつけていかないとなと思っています。
いまは1人でつくっていますが、複数人で開発するときにコメントがないのは大問題ですよね。

課題

  1. ほしい仕様2の道具などの対応
  2. ほしい仕様3について、いきなりいろんなポケモンに対応するのは難しいので、いくつかのポケモンのプリセットボタンでダメージ計算のデモができるようにする
  3. ダメージ計算の結果をHPバーで表示する
  4. 機能はできてきたがデザインがダメ

見えている課題についてはこんなところです。

次回予告

次回はプリセットボタン、HPバーの実装について書きたいと思います。
また、今回は技術的なこと、苦労した点などについて書けていませんので、次回からはそのあたりも書いていきます。
(この記事についても追記したい)

さいごに

いっしょにつくりたい!という方絶賛募集中です!
デザイン強いよ!、WEBアプリ強いよ!、勉強したいよ!という方、コメントなどにてお待ちしております!

つぎ→ 次回:プリセットボタン・HPバー編

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

YYTypeScript#11「YouTubeの動画でTS入門者におすすめのやつを紹介する」「JSに詳しくないチームで、新規プロジェクトをTSで始めるに当たってのアドバイスを下さい」「React Hooksを使っていて、Reduxを導入すべきかどうか」「バックエンドにもっと関わるにはどうしたらいい?」

これは2019年11月29日に開催したTypeScriptイベントYYTypeScript#11のイベントレポートです。

YYTypeScriptは一言で「TypeScripterの部室」です。発表者の話を聞く「一方向的な勉強会」とは真逆で、TypeScriptについて、雑に・ゆるく・ワイワイ話しながらTypeScripter同士の交流を深める「双方向的な座談会」の形式になります。集まった人たちで「今日話たいこと」「聞きたいこと」をいくつか挙げていき、それをテーマに雑談していきます。

今回の配信動画

過去回の配信動画YouTubeプレイリスト「YYTypeScript」
前回YYTypeScript#10「TSを自分のスキルにしようと思ったキッカケは?」「publicは省略する派ですか?」「Auth0ってどうなの?」「GraphQLを使ってみての感想」「Javaを忘れてTSを書くべきか?」「テストって先にやります? あとにやります?」 - Qiita

雑談

YouTubeの動画でTS入門者におすすめのやつを紹介する

  • TS触ったことなくて、触ってみたいけど、いまいちちょっと難しそうだなと感じる人にオススメの動画。

TypeScript入門 #01:トランスパイルとTypeScript - YouTube

  • if文とかfor文の説明もしてくれているくらい初心者向け。
  • 明日から使ってみたいと思わせるような内容。
  • 見てみてどんなことが学べた?
    • TSで書いたらこうなるけど、JSだとこなる
    • TSの良さ
      • 型が使えて、堅牢にプログラミングできる
  • いっこ10分くらいでさくっとみれる
  • 日本語のやつ少ないとおもってたけど、あるんだね
  • ドットインストールとかにもあった
  • 動画じゃないけどTS Deepdiveよんでる
    • でも初心者向けじゃないから難しい
    • 動画を見てわかってからこれよんだほうがいいかも
  • まずざっくり理解するのが大事
  • TSの入門者向けの動画が気になる
  • 動画で勉強できるのっていいよね
  • 動画で勉強したことないけど、どんなところがいいの?
    • なれてきたら本のほうがいいとおもう
    • 動画のほうがヒトから説明されているのでわかりやすい
    • 作る過程がみれるから理解しやすい
  • ひらがなで学ぶJSを読んでいるけど、あんなかんじなのが動画になってるかんじ
  • 英語はいっぱいある
  • JSとかは膨大にある
  • O'reilly Safariに結構ある
    • 結構充実している

他の言語に入るときにどうやって勉強するかききたい

  • 前の言語で作ってたのと同じようなものを作ってみる
    • 同じものを他の言語でつくると、違いがわかる
    • PHPで簡単につくれそうなやつをTSでつくってみる。みたいな?
      • そうそう
  • 目の前のタスクを付け焼き刃でやっていてあんまり伸びなかったことを反省していて、
    • 最初は基礎をがっと入れた後に、興味があるところに行くのが大事だと思う。
    • 本だと体系的に学べる。
    • TodoMVC
      • いろんな言語で同じToDoアプリが作られていて面白い。
      • バニラのJSもあるし。
  • Scripting Languages I: Node.js, Python, PHP, Ruby - Hyperpolyglot
    • こういう言語比較サイトを見ながらやります
  • ドキュメントをななめ読みして、今使っている言語との違いを把握してから
    • アプリを作りながら続きを学んでいく。
    • 掘り下げたいときはSDKとかドキュメントを見に行く。
  • 新しく学んでいる言語でいままでなかった機能を試して、学習モチベーションを上げてく
    • 逆に、新しい言語で学んだことを元の知っていた言語で実現してみるなどもすると、使えるスキルの幅が広がる
  • ウェブを見ても分かるっちゃ分かるけど、書籍が出ていればそれ買って、概要を全体像をざっくり把握してからやってる。
    • 洋書も買う。
    • 点ではなく、面で把握する
    • 電子書籍ってばーっとめくるとか、並べて読むとかが難しいので、あえて紙の書籍を買ってる。
    • 私も書籍派だなぁ。とりあえず入門書を1冊やって、後はググるのがメイン。
  • この機能を作りたいっていうのがあるから新しい言語を使い始める
    • 作りたいもの主導
  • あんまり新しい言語を勉強したことはないが、Reactを1週間で学んだときのこと
    • いきなり本を読んでも分からないので、TodoMVCをgit cloneしてきてソースコードを呼んだ。
  • 模写も大事
  • 紙に書き写すペーパーコーディングが有効
  • 逆引きTips本を買うのもあり
  • プログラミング教室で学んだ。

・・・

逆にプログラミング教室ってどうなの?

  • 逆にプログラミング教室ってどうなの?が聞きいてみたい。ググるとだいたいアフィリでいいことしか書いてないし
    • あんまりいい噂は聞かないけど
    • 若かったときに「自分より詳しい人がいっぱいいるんだ!」と思って行ったら、そうでもなくてがっかりした。
  • 入ってみないと分からない、博打感あるよね。
    • 初心者だと疑いようもないし。
  • 選び方を知りたいです!
    • 全部無料カウンセリングしてもらって、
    • ウェブで評判をリサーチしました。
    • アフィリエイトが少ないところを選びました。
  • スクール行ってようが、本人の努力次第ってところは正直ありました。
    • それはあると思う
    • 受け身で頼り切ってるひとは、どんどん辞めていくイメージでした。
  • 得た知識だけで勝負するだけじゃだめで、その知識を元に、今持ってない知識を取りにいけることがエンジニアとして大事だと思う。
  • うちのスクールは特殊でほとんど自習
    • 宿題を出されて、それをやってくるかんじ。
    • 1日12時間勉強しろと言われた。
    • スパルタすぎる感はあった。
      • 実務に入ったら、スクールの5倍くらい大変だった
        • いきなり5,000行のコード直したりとか……
          • それは場所によりましねw

JSに詳しくないチームで、新規プロジェクトをTSで始めるに当たってのアドバイスを下さい

自分含め、ほぼみんなJSのスキルが無い状況で、TypeScriptでやるならどんなはじめかたをしたらいいか?

  • どんなチーム?
    • BackboneJSとCoffeeScriptで作られた既存のフロントエンドを改修することはできるが、バックエンドがメインなので、そこまで詳しくないというレベル
    • ES6以降もそこまで自信ない。
  • いつまでも古いスタックでいくわけにいかないので、TSなど新しいスタックにシフトしていきたい。
  • 社内案件なので、技術選定は小回りがきくプロジェクト。
    • JSのアプリケーションとしての難易度は引く。
  • 2人くらいのチーム。

・・・

  • 学習コスト?
    • 今からBackbone
    • TSはいれずにJSでいくって方針もありそうだと思いました。いきなり違う言語するより混乱しなそうかなと。
      • 社内案件なので、チャレンジするのもありかも。
  • JS/TSに限らず新技術入れるとほぼほぼギスギスする気がするので、まずメンタルを鍛える
    • 2人チームなのでギスギスはしなそうですね。
    • うちのチームはギスギスしたことあります。
      • TS導入派と反対派で対立したことが。
      • なんでTSを導入しようということになったんですか?
        • コード量が膨大で、バグが少なくなく、そのバグも減らしたくて。
      • 反対派の意見は?
        • 学習コストと外注先との関係もあったので。
      • どのくらいのタイミングで?
        • あと半年でプロジェクトが終わるというタイミングだった。
      • 結局よくよく話してTSは導入できた。
  • チームメンバーは今のスキルで満足していて、新しい技術を取り入れたいと思っていない可能性がある
  • 経営的にも新しい技術スタックに拘らず、古いスタックでもノウハウを貯めて高速化した方が利益率も維持でき、人材採用、外注先の幅も広がる(それだけ、買い叩くことが可能になる)
    • 同感です。リファクタリングだったり再実装は工数削減の経営戦略としてありですが、漠然としたモダンな技術スタックを採用したいってメリットがなかったりします。
  • TS導入で一番ハマっているところは型
    • 型はがんばらないという方針にしたら、うまくいくようになった。
    • 型を頑張らないってどういうこと?
      • anyで済ませちゃうとか
      • もっと工夫すれば型で表現できるけどしないとか
      • 普通のJS+型とか
    • 最初から100%、TSの機能を使おうとすると学習コストがすごく高くなる。
  • TSで行けそうかどうかやってみる
    • TSで1週間位やってみて、やっぱりTS無理!ってなったらトラインスパイル後のJSを使ってJSに戻ることが出来る

知っている人がいたら、React Hooksを使っていて、Reduxを導入すべきかどうか

  • どういう使い分けをするべきかなど
  • Hooks使っているならRedux要らないという意見もネットでちょいちょい見るが。

・・・

  • 分からないので、やってみます。
  • HooksはReduxと重複する部分としては、Contextに状態を持たせられるようになった。
    • 昔はpropsでバケツリレーしてたので、ReduxでContext的なことをしてた。
  • 依存の少ない UI の state は hooks でやってメインの State 管理は redux でやってます
  • hooks での redux-persist(永続化系) と redux-devtools (デバッグ)の代替方法がわからなくて移行できてなかったりしてます
  • コンポーネントステートの弱点はステートの階層がビューの構造に依存すること
  • 個人的にはStoreはMobXがめっちゃ使いやすかったので、ずっとこれ使ってます
  • どちらかではなく組み合わせるのも手だと思います。
    • collapse の開閉状態みたいなコンポーネントの外から参照しないような state なら内部でもって、ドメイン知識に関わるステートなら Redux みたいな

バックエンドにもっと関わるにはどうしたらいい?

仕事でフロントエンドだけに絞られているが、バックエンドにもっと関わりたい。

バックエンドにも興味があるが、どっちもやったら中途半端にならないかどうか心配でもある。

ちなみに、10年前に作ったプロダクトを5人くらいで保守している。

・・・

  • あと2ヶ月でプロジェクト終わるってことですが、今のプロジェクトで関わりたいのか、次のプロジェクトで関わりたいのか?
    • 会社の方針が見えないのでなんとも言えないですが、
    • もっとアピールしていったほうがいいかも。
  • BFF (backend-for-frontend)
    • microserviceを束ねてフロントエンド
  • 自分で学んでいくこともありだと思う
  • 転職するのもあり
  • ステップアップで考える
    • 遠い先でなく一歩先を見る
  • 着眼点
    • 変化の早いものでなく、変化しにくい領域を基礎をして学んでおく
    • ex.) BFFは2〜3年後には別の新しいアーキテクチャに置き換わっている可能性がある
  • 設計を学ぶ
    • ソフトウェアの凝集度を意識するといろいろと見えてくるかも

参加してよかったこと(参加者の感想)

  • TypeScriptに関わらず、新しい技術を導入する時の考えた方が参考になりました。
  • Typescriptを皮切りに実際他に色々な話が出来たため、有意義だった

YYTypeScriptは毎週やってます

YYTypeScriptについてワイワイ話したい方は、YYTypeScriptのイベント情報をチェックしてみて下さい。

以上、YYTypeScriptのレポートでした。次回もワイワイやっていきたいと思います! では、また来週!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[TypeScript�]配列にオブジェクトを格納する時の初期化について

備忘録として残す

共通部分

interface TestInterface {
    name: string,
    data: DataInterface[]
}

interface DataInterface {
    date: string,
    value: string
}

空配列を用意してpush()する

let test: TestInterface = {
    name: '',
    data: []
};

test.data.push({
    date: '20191129',
    value: 'いい肉の日'
});

スプレッド演算子を使用して格納する

let test: TestInterface = {} as TestInterface;

test.data = [...(test.data || []), {
    date: '20191129',
    value: 'いい肉の日'
}];

注意点

{} as interfaceで初期化を行うとundefinedが格納される。

スプレッド演算子の方が行数は少ないがスプレッド演算子に慣れていないと可読性は落ちる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

紙吹雪を飛ばしてみよう

Akashic Advent Calendar 2019 三日目の記事です。

ゲーム開発中、プレイ内容に応じて演出を加えたくなることがあります。この記事では、そういった演出に利用できそうな紙吹雪エンジン kamihubuki-js を Akashic Engine 上で実行する手順を解説します。

kamihubuki-js とは

kamihubuki-js は2Dの紙吹雪専用物理エンジンです。エンジン自体は描画機能を持っておらず、利用者が実装する必要があります。

confetti-anim.gif

動画は kamihubuki-js から転載。

kamihubuki-js の導入

kamihubuki-js は npmjs に publish されていません。GitHubからインストールします。

npm install blackspotbear/kamihubuki-js

Akashic Engine からの利用

kamihubuki-js 付属のサンプルの1つは Akashic Engine への組み込み例となっています。これを参考にします。1つ1つみていきましょう。

紙吹雪の生成

main.ts#L12-L45

main.ts#L12-L45
function createConfetti(x: number, y: number): kh.Confetti {
    const vx = 0;
    const vy = 200;
    const angle = g.game.random.get(0, 100) / 100 * Math.PI;

    const fins: kh.Fin[] = [
        {
            angle: g.game.random.get(-100, 100) / 100 * (Math.PI / 2),
            size: 30,
            armAngle: 0,
            armLength: 1
        },
        {
            angle: g.game.random.get(-100, 100) / 100 * (Math.PI / 2),
            size: 30,
            armAngle: g.game.random.get(50, 100) / 100 * Math.PI,
            armLength: 1
        }
    ];

    const co = new kh.Confetti({
        x: x, y: y,
        vx: vx, vy: vy,
        angle: angle,
        fins: fins,
        av: 0,
        M: 0.5,
        K: 0.02,
        I: 3,
        RD: 0.99
    });

    return co;
}

createConfetti() で kamihubuki-js の Confetti インスタンスを生成しています。 kamihubuki-js では1つの紙吹雪につき1つの Confetti インスタンスを用意します。

コンストラクタには初期位置(x、y)、初速度(vx, vy)、空気抵抗(K)などの値が渡されています。 しかしこれだけでは、紙吹雪が回転する様子を扱うことができません。 そこで kamihubuki-js では、質点にフィンを取り付けます。フィンが受ける風の力で質点が回転します。 fins がフィンのパラメータです。

fins パラメータによって、質点から任意の数の腕を伸ばし、その先にフィンを取り付けることができます。

physics-model.ja.png

図はkamihubuki-jsから転載。

フィンの取り付け方によって、紙吹雪はさまざまな運動をするようになります。

紙吹雪の描画

main.ts#L47-L97

main.ts#L47-L97
function createConfettiEntity(scene: g.Scene, co: kh.Confetti, noCollision: boolean): g.E {
    const w = 20;
    const h = 10;
    const w2 = w / 2;
    const h2 = h / 2;

    const rect = new g.FilledRect({
        scene: scene,
        width: w,
        height: h,
        x: co.x - w2,
        y: co.y - h2,
        cssColor: colors[colorIndex]
    });

    colorIndex = (colorIndex + 1) % colors.length;

    rect.update.add(() => {
        co.update(1 / g.game.fps, windX, windY, 0, 200);


        // collision detection and resolution.
        if (noCollision) {
            if (co.x < 0 || co.x > g.game.width || co.y > g.game.height) {
                rect.destroy();
            }
        } else {
            const k = 0.5;
            if (co.x < 0) {
                co.x = 0;
                co.vx = Math.abs(co.vx) * k;
            } else if (co.x > g.game.width) {
                co.x = g.game.width;
                co.vx = -Math.abs(co.vx) * k;
            }
            if (co.y < -g.game.height) {
                co.y = -g.game.height;
                co.vy = 0;
            } else if (co.y > g.game.height) {
                co.y = g.game.height;
                co.vy = -Math.abs(co.vy) * k;
            }
        }

        rect.x = co.x - w2;
        rect.y = co.y - h2;
        rect.angle = -co.angle / Math.PI * 180;
        rect.modified();
    });

    return rect;
}

createConfettiEntity() は紙吹雪を描画するインスタンスを生成する関数です。サンプルでは、紙吹雪の描画に g.FilledRect を利用しています。色の変更や画面端との衝突処理を省いて、大切なところだけ抜き出します。

    rect.update.add(() => {
        co.update(1 / g.game.fps, windX, windY, 0, 200);

        // 省略

        rect.x = co.x - w2;
        rect.y = co.y - h2;
        rect.angle = -co.angle / Math.PI * 180;
        rect.modified();
    });

rectupdate イベントで co.update() を実行するようにしています。これによって、毎フレーム紙吹雪の状態を更新しています。次に紙吹雪の位置と角度を rect に反映しています。 kamihubuki-js は角度をラジアンで扱っていますので、Akashicの角度の単位である度に変換していることに注意していください。

Akashic Engine 製コンテンツに紙吹雪を導入する手順は以上となります。

応用: 光沢のある紙吹雪

サンプルに少し手を加えて、光沢のある紙吹雪にしてみましょう。紙吹雪がある角度では白く光るようにします。

白く光る処理は、ここでは簡単に rect にもう1つ光沢のための g.FilledRect インスタンスを重ねることします。

    // const rect = new g.FilledRect... の後ろに以下を追加する。
    // 光沢を表現する g.FilledRect インスタンス。
    const specular = new g.FilledRect({
        scene: scene,
        // rect と同じサイズだとわずかに rect の輪郭が滲み出るので、少し大きくする。
        width: rect.width + 2,
        height: rect.height + 2,
        x: -1,
        y: -1,
        opacity: 0,
        cssColor: "white",
        // 加算合成を指定することで、光っている感じを出す。
        compositeOperation: g.CompositeOperation.Lighter
    });
    rect.append(specular);

次に specular の透明度を変更する処理を追加します。

    // co.update( ... ) の後ろに以下を追加する。
    let t = Math.abs((co.angle / Math.PI) % 2);
    if (t > 1) t = 2 - t;
    specular.opacity = Math.pow(t, 5);
    specular.modified();

co.angle から、角度に比例して 0 ~ 1 の間で変化する値 t を求めています。これを specular.opacity に与えると、角度に応じて specular が姿を現して光沢となります。 Math.pow(t, 5) としているのは t の変化に緩急をつけるためです。

次のグラフの黒い線、青い線、赤い線は順に Math.pow の第2引数が 1, 2, 5 の時の specular.opacity の変化を表しています。第2引数が大きな値であるほど、鋭く光沢が現れることがわかります。

pow.png

角度に応じて白く光るようになりました(わかりやすくするため、背景色を黒にしています)。

confetti-specular.gif

最後に

kamihubuki-js 付属のサンプルでは g.FilledRect で紙吹雪を描画していますが、桜の花びらや紅葉、雪のような画像を用意して物理量を調整すれば、四季の表現にも使えそうです。

このような演出には akashic-animation を用いることも1つの方法です。 akashic-animation は SpriteStudio で作成したアニメーションの再生を行うことことができます。また、ただアニメーションを再生するだけでなく、当たり判定やプログラムからアニメーションを上書きするといった機能もあります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

独学で居酒屋のホームページを作ってみた

結果

約半年間の勉強を経て、以下のようなホームページを制作しました。(ホームページ制作に費やした時間は3か月ほどです。)空いてる時間を見つけては少しずつ制作していったのでかなり時間がかかってしまいました。(笑)

焼き鳥 Dinning たんと | 野々市の焼き鳥居酒屋

[PC版サイト]
PC_1.png
PC_2.png

[スマホ版サイト]
res.png

サイトは、モバイルフレンドリー、SEO対策、ユーザーエージェント判定、jQueryによるアニメーションなどを考慮しました。
サイト制作には、HTML5、CSS3、Bootstrap4のみを学習した状態で取り掛かり、必要な技術や知識はその都度学びながら作業を進めていきました。

使用した言語やフレームワーク

・HTML5
・CSS3
・Bootstrap4
・jQuery

ホームページを作ろうと思ったきっかけ

半年ほど前にwebプログラミングの勉強を始め、徐々に勉強をしていく中で完全オリジナルのホームページを作りたいと思うようになったのがきっかけです。最初は、クラウドソーシングを用いて誰かのためにホームページを作ろうと考えていました。しかし、初心者である私は一つも案件を受注することができませんでした。そこでポートフォリオ作成のためにも現在自分がアルバイトをしている居酒屋のホームページを勝手に作ってみることにしました。(笑)
出来上がったホームページをオーナーに見せてみるとぜひ欲しいということで公開に至りました。

サイト公開までの制作手順

  1. サイトのデザイン、レイアウト、構造を大まかに決める
  2. HTML、CSSを記述し、大まかなサイトを作り上げる
  3. Bootstrap4を用いて余白やレイアウトを調節する
  4. レスポンシブ対応にするためにスマホ版のサイトを作成する(1~3を繰り返す)
  5. XAMPPを使用し動作確認
  6. 文書、写真の用意
  7. seo対策
  8. jQueryの実装
  9. レンタルサーバーを借りる(ロリポップ)
  10. ユーザーエージェント判定、CSSハックに対応する
  11. ホームページ公開

1. サイトのデザイン、レイアウト、構造を大まかに決める

正直自分はデザインやレイアウトを決めるのに一番頭を抱えました。デザインの勉強などもしたことがなかったので他の居酒屋ホームページを見まくりました。(笑)たくさんのホームページの中からいいなと思ったデザインを参考にしてサイトを制作しました。

2. HTML、CSSを記述し、大まかなサイトを作り上げる

HTML、CSSを記述し、大まかにサイトを作りました。文書や写真は仮のものを使用し、1で考案したデザインを形にしていきました。

3. Bootstrap4を用いて余白やレイアウトを調節する

CSSのフレームワークとしてBootstrap4を導入しました。制作したホームページの大半はBootstrap4を用いています。フレームワークによりかなりCSSの記述が楽になるのでCSSのフレームワークを学ぶことをお勧めします。自分の思い通りにCSSが反映されない時は、Chromeのディベロッパーツールを用いることで原因を突き止めていました。

4. レスポンシブ対応にするためにスマホ版のサイトを作成する(1~3を繰り返す)

サイトをレスポンシブ対応にする方法はいくつもありますが、自分はPC用サイトとスマホ版サイトを分けて作ることにしました。そのほうが、より自分の思い通りにレイアウトを調節できると感じたからです。手順としては、PC版と同じように1~3を行っていく形です。

5. XAMPPを使用し動作確認

ここまでのプログラミング反映の確認はChromeで行っていました(ディベロッパーツールを使うことでスマホ版のサイトも確認していた)。しかし、スマホ版のサイトを実機で確認がしてみたかったためサーバーが必要になりました。レンタルサーバーは費用がかかるため、無料でサーバー構築ができるXAMPPというソフトを使用しました。少し費用はかかりますが、圧倒的に楽で簡単なのは、レンタルサーバーを借りることです。

6. 文書、写真の用意

自分は、料理の魅力やオーナーの気持ちを伝えれるように文書を作成しました。写真は一部オーナーから頂き、足りない写真は自分のスマートフォンで営業時間外に撮影し編集しました。画像編集には、無料のソフトであるGIMPを使用しました。

7. seo対策

主なseo対策として、サイトに使用するワードについて気を付けました。(スモールワードとビッグワードをうまく使い分けることが大事。)また、HTMLファイル内にメタディスクリプションを記述したり、ファビコンの設定なども行いました。

8. jQueryの実装

サイトに動きを与えるためにjQueryを実装しました。サイトに動きを与えることでよりサイトが使いやすく見やすいものになっていきます。jQueryは、「スクロール幅によってトップに戻るボタンを出現させる」「ドロワーナビを表示させる」「ヘッダーとフッターのHTMLを別ファイルで管理する」といった箇所で実装しました。

9. レンタルサーバーを借りる(ロリポップ)

サイトが完成形に近づいてきたのでレンタルサーバーを借りました。レンタルサーバーはロリポップを使用しました。実践的にホームページをWeb上にアップロードしてみることでホームページの運用を体験しました。個人的にホームページをアップロードする瞬間が一番楽しかったです。(笑)

10. ユーザーエージェント判定、CSSハックに対応する

PC版のサイトとスマホ版のサイトをアクセスした端末によってうまく振り分けれるようにユーザーエージェント判定をjQueryで制御しました。
いざ、サイトを公開しようと思いアップロードしたところブラウザごとにレイアウトが乱れてしまうことが起きていたのでCSSハックにも対応させました。

11. ホームページ公開

自分の思うようなホームページが完成したので公開に至りました。今後は、「Google Search Console」や「Google Analytics」を用いてseo対策がうまく適応されているかどうかを確認しつつ、ホームページを運用していくつもりです。

感想

ホームページを作成してみて一番重要なのは、使用する写真とSEO対策であると感じました。より画質の良く、映える写真を利用することで見栄えのいいホームページになります。またそれを検索上位に表示させるSEO対策も必須です。
レイアウトが思い通りにいかないことが多く、試行錯誤を繰り返すことはとても大変でしたが、その分達成感はものすごくありました。現在HTMLやCSSなどを勉強していてアウトプットする方法を探している方は、オリジナルのホームページやブログを作成し、公開してみるといいと思います。

今後の展望

ホームページの作成当初は、出来上がったサイトを元にクラウドソーシングや近所のお店への直営業から案件を受注することを目標をしていましたが、ホームページが出来上がっていくにつれてより自分の技術を向上させたいと思うようになっていきました。
今後は、今回作成したホームページの運用とともにJavaScriptやそのフレームワークを中心に勉強していきたいと考えています。
ホームページのSEO対策についての改善案や、おすすめするJavaScriptのフレームワークなどありましたら教えてもらえると嬉しいです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JS】DOM操作が楽になる!dom_sen.js

はじめに

今回紹介するのはdom_sen.jsというライブラリです。
ポイントとしては、とても軽量で扱いやすいというところです。
dom_sen.jsは、そのままの通り、DOM操作が楽になります。
Jqueryほどではないけれど、簡単にDOM操作をしたいという方向けのライブラリとなります。

使い方

(こちらでは簡単に説明をしています。詳しく知りたい方はこちら:dom_sen.js)
まず、

<script src="https://tamralworld.com/library/dom_sen_v2.js"></script>

※ファイル名のv2はdom_sen.jsにある最新のバージョン名にしてください。2019/11/29時点ではV2です。

基本

まず、queryselectの省略をすることが可能になります。
こうなります。
document.querySelectorAll()
↓↓
qsa()

document.querySelector()
↓↓
qs()

次に、qsa()で指定した要素のHTMLを変更する場合です。
要素の中身を変更するにはこの二つの関数があります。
- qsa_html( CSSセレクタ , HTML)
- qsa_text( CSSセレクタ , テキスト)
※V2ではqsa_value( CSSセレクタ , value値)が使用可能になりました

Jqueryにしたら、この処理と同じです。
- $(CSSセレクタ).html(HTML);
- $(CSSセレクタ).text(テキスト);

また、V1ではCSSのdisplayのみを省略することができます。
ですが、v2では下のd_関数と同様のCSSを省略することが可能です。
記述例 : qsa_display( CSSセレクタ , CSS[display]の値 );

  • qsa_display()
  • qsa_opacity()
  • qsa_width()
  • qsa_height()
  • qsa_color()
  • qsa_background()
  • qsa_border()
  • qsa_border_radius()
  • qsa_top()
  • qsa_left()
  • qsa_bottom()
  • qsa_right()
  • qsa_position()
  • qsa_margin()
  • qsa_padding()
  • qsa_transition()
  • qsa_transform()
  • qsa_z_index()
  • qsa_box_sizing()
  • qsa_font_size()

また、このライブラリは、属性でも操作することができます。

属性により操作する方法

基本的に関数の構造は次のようになります。
d_○○( 属性ネーム , 属性ネームの値 , 値 )
HTML構造の例は次のようになります。

<div d_text="text1"></div>
<script>
d_html("text","text1","これはスクリプトで変更しました!");
//属性d_textがtext1の要素のHTMLを「これはスクリプトで変更しました!」に変更
</script>

d_○○はV1の時点でこの関数が存在します。
- d_html()
- d_text()
- d_value()
- d_display()
- d_opacity()
- d_width()
- d_height()
- d_color()
- d_background()
- d_border()
- d_border_radius()
- d_top()
- d_left()
- d_bottom()
- d_right()
- d_position()
- d_margin()
- d_padding()
- d_transition()
- d_transform()
- d_z_index()
- d_box_sizing()
- d_font_size()
※太字はV2で追加された関数です。
※関数はすべて置き換え用です。読み取りはできません。

最後に

V1とそれ以降のバージョンの変更点は太字で付け足しますのでよろしくお願いします。

このライブラリは比較的軽いので使いやすくWebの読み込みに影響しにくいのがポイントです。
Jqueryを使わずにDOM操作をしたいのならこのライブラリがいいでしょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【JavaScript 】アロー関数の書き方

アロー関数の書き方を学んだのでやってみました!

基本の関数

function func(text) {
    console.log(text);
}

やってみた
スクリーンショット 2019-11-29 9.59.45.png

アロー関数で書いてみた

const test = () => {
    console.log('アロー関数書き方');
}
test();
// console.logの結果が返ってくる

やってみた
スクリーンショット 2019-11-29 9.43.28.png

↑この書き方は省略できて…

省略形

const test2 = () => console.log('test2');
test2();

やってみた
スクリーンショット 2019-12-03 9.40.25.png

引数がある場合

const test3 = (text) => console.log(text); 
test3('test3引数');
//test3引数 が返ってくる

やってみた
スクリーンショット 2019-12-03 9.50.44.png


これと同じ動きをする省略しない書き方

function test4(text) {
    console.log(text);
}
test4(1111) //1111

やってみた
スクリーンショット 2019-12-08 3.56.17.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

cakephpのテンプレートにjavascript呼び出しのみのボタンを設置

test.ctp
<?php echo $this->Html->link('clickme', 'javascript:hoge();void(0)', ['class' => 'fuga']); ?>

// class無しの場合
<?php echo $this->Html->link('clickme', 'javascript:hoge();void(0)'); ?>


<script>
    function hoge() {
        alert('HOGE');
    }
</script>

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

jsのloop処理速度実験(GAS上)

When I experimented what was the fastest in the JavaScript loop, "for" was the fastest!

function getArray() {
  var array = []
  for(var i=1; i<=1000000; i++) {
    array.push(i)
  }
  return array
}

function test() {
  var array = getArray()
  testtest(array, forTest)

  testtest(array, forEachTest)

  testtest(array, mapTest)

  testtest(array, for2Test)

  testtest(array, whileTest)
}

function testtest(array, callback) {
  var start = new Date()
  callback(array)
  var end = new Date()
  var span_sec = (end - start)/1000
  Logger.log("処理時間は " + span_sec + " 秒でした" );
}


function forTest(array) {
  var l = array.length
  for(var i=0; i<l; i++) {
  }
}

function forEachTest(array) {
  array.forEach(function(i, e) {
  })
}

function mapTest(array) {
  array.map(function(e, i) {
  })
}

function for2Test(array) {
  for(var i in array) {
  }
}

function whileTest(array) {
  var i = array.length
  while(i > 0 ) {
    i--
  }
}

result

[19-11-29 15:20:48:459 JST] forTestの処理時間は 1.197 秒でした
[19-11-29 15:20:49:823 JST] forEachTestの処理時間は 1.364 秒でした
[19-11-29 15:20:51:461 JST] mapTestの処理時間は 1.637 秒でした
[19-11-29 15:20:52:912 JST] for2Testの処理時間は 1.451 秒でした
[19-11-29 15:20:54:404 JST] whileTestの処理時間は 1.491 秒でした
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

04. 元素記号

04. 元素記号

"Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."という文を単語に分解し,1, 5, 6, 7, 8, 9, 15, 16, 19番目の単語は先頭の1文字,それ以外の単語は先頭に2文字を取り出し,取り出した文字列から単語の位置(先頭から何番目の単語か)への連想配列(辞書型もしくはマップ型)を作成せよ.

Go

package main

import (
    "fmt"
    "strings"
)

func main() {
    var src = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can.";
    var dw = map[int]bool{1: true, 5: true, 6: true, 7: true, 8: true, 9: true, 15: true, 16: true, 19: true}; //  1文字の単語番号
    var res map[string]int = map[string]int{};

    //  単語に分割
    words := strings.Split(src, " ")
    for i, word := range words {
        idx := i + 1

        //  配列の exists的関数がなさそうなので Map を使用...
        if dw[idx] {
            //  1文字を map へ保存
            res[word[0:1]] = idx
        } else {
            //  2文字を map へ保存
            res[word[0:2]] = idx
        }
    }

    //  結果を表示
    fmt.Println(res)
}

python

# -*- coding: utf-8 -*-
src = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
dw = [1, 5, 6, 7, 8, 9, 15, 16, 19]  # 1文字の単語番号
res = {}

#   単語に分割
words = src.split(" ")
for i in range(len(words)):
    #   単語番号
    idx = i + 1

    #   1文字の単語番号の場合
    if idx in dw:
        #   1文字を map へ保存
        res[words[i][0:1]] = idx
    else:
        #   2文字を map へ保存
        res[words[i][0:2]] = idx

#   結果を表示
print(res)

Javascript

var src = "Hi He Lied Because Boron Could Not Oxidize Fluorine. New Nations Might Also Sign Peace Security Clause. Arthur King Can."
var dw = [1, 5, 6, 7, 8, 9, 15, 16, 19];    //  1文字の単語番号
var res = new Map();

//  単語を空白で分割し単語数処理
var words = src.split(' ');
for (var i = 0; i < words.length; i++) {
    //  単語番号
    var idx = i+1;

    //  1文字の単語指定の場合 (ES2017)
    if (dw.includes(i+1)) {
        //  1文字を map へ保存
        res.set(words[i].substring(0,1),idx);
    }
    else {
        //  2文字を map へ保存
        res.set(words[i].substring(0,2),idx);
    }
}

//  結果表示
console.log(res);

まとめ

if文で or を書くのがイマイチかと思い 1文字の単語番号をまとめてみた。
Goの配列などの存在チェックがイマイチ?。

次の「05. n-gram」 は問題のの意味がよくわからない、まずは問題の理解から初めて見る。

他の人のを確認したら Map の内容が [文字 => 単語番号] と逆だったため修正しました。

その他

もともと、C言語やPHPが長いので Goの行末の ";" 癖が治らない。w

トップ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Laravel Mixでwebpackを簡単に利用する」ってどういうこと?

はじめに

業務上活用しているLaravel Mixにてコンパイル関連で色々と模索していおり、
今更ですがLaravel Mixについて整理したことを備忘録としてまとめました。

まず、Laravel Mix公式を見てみると

以下公式サイトhttps://readouble.com/laravel/5.5/ja/mix.html より引用

Laravel Mixは多くの一般的なCSSとJavaScriptのプリプロセッサを使用し、Laravelアプリケーションために、構築過程をWebpackでスラスラと定義できるAPIを提供しています。シンプルなメソッドチェーンを使用しているため、アセットパイプラインを流暢に定義できます。例を見てください。

mix.js('resources/assets/js/app.js', 'public/js')
   .sass('resources/assets/sass/app.scss', 'public/css');

Webpackやアセットのコンパイルを始めようとして、混乱と圧倒を感じているならLaravel Mixを気に入ってもらえるでしょう。しかし、アプリケーションの開発時に必要だというわけではありません。どんなアセットパイプラインツールを使用してもかまいませんし、使わなくても良いのです。

なるほど、わからん。:grin:カタカナ語が使いたい年頃の人が書いた文書のようにになっていますが、ざっくり言うと「webpackというモジュールバンドラー(モジュールの束)を簡単に使えるようにしたラッパー」ということだと後になって理解しました。

以下でもうちょっとだけ詳しく見ていきます。

概要

①Laravel Mixとは、フロントエンドのアセット(JS,SASS等)をコンパイル、バンドルしてくれるツール
②webpack設定ファイルをより分かりやすく簡単に書けるように設定ファイルをラップしている
③lessやsass、babelなどよく使われるローダーが最初から用意されていて、デフォルトで利用することができる
⑤Laravelを使っていないアプリでも、コンパイル・バインディングのツールとして利用できる

使い方

Laravelをインストールした段階で、package.jsonとwebpack.mix.jsが用意されています。

・package.json
⇒Laravel Mix本体やその他必要なパッケージが記述済み。
package.jsonのscriptsにはwebpackを実行するためのスクリプトも記述されている。

・webpack.mix.js
⇒webpack設定ファイル(webpack.config.js)のラッパー。
ここにコンパイル対象ファイルやバンドル対象ファイルなどの設定を記述していく。

※LaravelではないアプリでLaravel Mixを使う場合でも、
package.jsonとwebpack.mix.jsの2ファイルを用意するだけであとは同じです。
Laravel Mix公式サイトのStand-Alone Projectを参考に。↓
https://laravel-mix.com/docs/5.0/installation

設定ファイルの記述

webpack.mix.jsに、設定を記述する。

sassファイルをコンパイルしたい場合

mix.sass('resources/assets/sass/app.scss', 'public/css');

cssファイルをバンドルしたい場合

mix.styles([
    'public/css/vendor/normalize.css',
    'public/css/vendor/videojs.css'
], 'public/css/all.css');

jsをコンパイルしたい場合

mix.js('resources/assets/js/app.js', 'public/js');

jsファイルをバンドルしたい場合

mix.scripts([
    'public/js/admin.js',
    'public/js/dashboard.js'
], 'public/js/all.js');

他にもいろいろ設定できることはある。

Laravel Mixの実行
npm run dev を実行すると、package.jsonに書いてあるスクリプトが実行され、
設定ファイルに記述したコンパイル、バンドルが実行される。

npm run production の場合は圧縮されたファイルが出力される。

知っておくと良い機能抜粋

もう少し具体的な機能について、知っておいたほうがいい主な機能を抜粋。

jsのコンパイル
上記の通り、jsファイルをコンパイルするには

mix.js('resources/assets/js/app.js', 'public/js');
と書く。

この処理では
・ES2015記法
・モジュール
・.vueファイルのコンパイル
・開発環境向けに圧縮
のコンパイルが実行される。

①babel実行
jsファイルをバンドルする設定として、mix.scripts()を紹介したが、
この代わりにmix.babel()を使うことができる。

これをすると、バンドルされたファイルはES5記法に変換された状態になる。

IEはES5までしか対応していないため、もしIEでも動作させる必要があるシステムの場合は
このmix.babel()機能を使ってES5記法に変換するほうが良い。

ES5, CoffeeScript, ES6の記法の違い

②キャッシュバスティング

mix.js('resources/assets/js/app.js', 'public/js')
   .version();

このように記述すると、
ファイル名の末尾に一意のハッシュ値が付与される。

これによって、コンパイルのたびにファイル名が変更されるため、CSSやJSなどがブラウザキャッシュに残って変更が反映されないことを防止できる。
このファイルを読み込むbladeファイル側では、

のようにmix()関数を使うことで、ハッシュ値のついたファイル名でも取得することができる。

おわりに

Laravel Mixにはオプションの指定で挙動を簡単に変えられるという特徴もあります。
そのあたりのカスタマイズについてのメモがたまってきたのでまた、整理していこうと思います。

とりあえず、CSS側のコンパイルが極端に遅い方は
"processCssUrls: false"を試してみましょう。
(画像PASSの書き換えオプションをOFFにする。デフォルトはON。)

例)

mix.sass('src/app.scss', 'dist/')
   .options({
      processCssUrls: false
   });

詳細はまた後ほどほどまとめます!

参考
https://readouble.com/laravel/5.5/ja/mix.html
https://laravel-mix.com/docs/5.0/quick-webpack-configuration

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptで例えば勤務時間を計算する場合

勤務時間を計算したい場合ってあると思います。
勤務時間計算用プログラムを過去に作ったので、Qiitaにも掲載したいと思います。

退勤、出勤時間を数値にして、それ同士を計算させる

一旦両者を数値の型にして計算します。
数値の型にする計算式は

ミリ秒/3600000 = 時間
(ミリ秒-h*3600000)/6000 = 分

これを出勤と退勤で計算して、その差を取ります。

退勤と、出勤時間が合計何時間、何分か計算する

  1. 退勤と出勤の差を計算する
  2. 差はミリ秒で取れてくるので、時、分へ変換する
  3. 時、分の和を計算する
var from = "08:00"
var to = "24:00"

var Y = new Date().getFullYear()
var M = new Date().getMonth()+1
var D = new Date().getDate()

var ymd = Y+"/"+M+"/"+D+"/"

var fromTime = new Date(ymd+" "+from).getTime()
var toTime = new Date(ymd+' '+to).getTime()
var Ms = toTime-fromTime

var h = ''
var m = ''

h = Ms/3600000
m = (Ms-h*3600000)/6000
var time = h+m;
console.log(time)

これで結果は16となります。
フォームとかで利用すると、計算できます。
ただ、24時間までしか計算ができないという制限があります。

image.png

働き方改革だし、24時間計算できれば大丈夫だよね…。

@shiracamus さんから

let from_time = "08:00"
let to_time = "24:00"

let [from_hour, from_minute] = from_time.split(':')
let [to_hour, to_minute] = to_time.split(':')

console.log(to_hour - from_hour + (to_minute - from_minute) / 60.0)

単純に計算すれば良いのではとご指摘を頂きました。
全くその通りでした。ありがとうございます!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

A-FRAME: 物理演算でボーリングっぽい動きを実現してみる8(ピンの物理的形状の中心)

A-Frameをつかって物理演算ができるようにしてみます。
A-Frame側の設定で、ピンの物理的形状とレンダリングされる形状の中心ずれを調整できるか検証します。

例1)複合シェイプのcylinderを利用してoffsetで位置を調整する

設定しました。
pin2.gifdemo
位置調整はできました。
ボールを転がしてみましょう。
pin2-1.gifdemo
物理的な形状にboxを指定していた時よりも、衝突時に下端の縁でまわるようになり、よりピンらしくなりました。
が、倒れても起き上がってきます。
しぶといですね。

例2)エンティティを入れ子にしてローカル座標で位置を調整する

設定しました。
pin1.gifdemo
こちらの方法でも位置調整はできました。
ボールを転がしてみましょう。
pin1-1.gifdemo
こちらも、衝突時に下端の縁でまわるようになり、よりピンらしくなりました。
そして倒れたら起き上がってこなくなりました!

まとめ

ピンの物理的形状とレンダリングされる形状の中心を揃える為に、2つの手法を試しました。
どちらの方法でも中心を揃える事ができましたが、エンティティを入れ子にする方法だとピンが倒れたままになりました。

普通のシェイプのcylinderと複合シェイプのcylinderで何かが違うようですが、今のところ違いの理由の見当はついていません。
ボーリングっぽい動きをするピンを目指すには、エンティティを入れ子にする方法が良さそうです。

今回はじめて複合シェイプを使いましたが、もっと色々できそうなので、どういったものかもう少し見ていきたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React入門】コンポーネントの条件付きRender (Conditional Rendering)

はじめに

Reactの公式ドキュメントの条件付きレンダー(Conditional Rendering)にトライ。

条件付きレンダー-React
https://ja.reactjs.org/docs/conditional-rendering.html

開発環境

  • Ubuntu18.04
  • yarn 1.17.3
  • create-react-app 3.1.2

create-react-appで準備

$ create-react-app try-conditional-rendering
$ cd try-conditional-rendering

で、./public/index.html./src/App.jsを編集。

./public/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />
    <title>Try Conditional Rendering</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
./src/App.js
import React from 'react';
import './App.css';

const App = () => {

}

export default App;

早速条件付きレンダーをやってみる。

まずは丸パクリにならないよう公式ドキュメントのマネをしてGreetInTheMorningGreetInTheAfternoon、そしてGreetInTheEveningというコンポーネントを作る。

./src/App.js
const GreetInTheMorning = () => {
  return <h1>おはようございます。</h1>
}

const GreetInTheAfternoon = () => {
  return <h1>こんにちは。</h1>
}

const GreetInTheEvening = () => {
  return <h1>こんばんは。</h1>
}

現在時刻によって上3つのコンポーネントのいずれかを表示したいとする。
本来ならちゃんとDateオブジェクトを使って...ってやるんだけど、今回は公式に沿ってGreetingコンポーネントを作ってAppコンポーネントからGreetingコンポーネントにnowHourという引数を渡してズルをする。

./src/App.js
const Greeting = (props) => {
  // 5時 ~ 12時までは"おはようございます。"を表示する
  if(5 <= props.nowHour && props.nowHour < 12) {
    return <GreetInTheMorning />
  }
  // 12時 ~ 18時までは"こんにちは。"を表示する
  else if(12 <= props.nowHour && props.nowHour < 18) {
    return <GreetInTheAfternoon />
  }
  // 18時 ~ 5時までは"こんばんは。"を表示する
  else if((18 <= props.nowHour && props.nowHour < 24)  || (0 <= props.nowHour && props.nowHour < 5) ) {
    return <GreetInTheEvening />
  }
  else {
    return <h1>Error.</h1>
  }
} 

./src/App.jsの完成形が下のコード。

./public/App.js
import React from 'react';
import './App.css';

const GreetInTheMorning = () => {
  return <h1>おはようございます。</h1>
}

const GreetInTheAfternoon = () => {
  return <h1>こんにちは。</h1>
}

const GreetInTheEvening = () => {
  return <h1>こんばんは。</h1>
}

const Greeting = (props) => {
  // 5時 ~ 12時までは"おはようございます。"を表示する
  if(5 <= props.nowHour && props.nowHour < 12) {
    return <GreetInTheMorning />
  }
  // 12時 ~ 18時までは"こんにちは。"を表示する
  else if(12 <= props.nowHour && props.nowHour < 18) {
    return <GreetInTheAfternoon />
  }
  // 18時 ~ 5時までは"こんばんは。"を表示する
  else if((18 <= props.nowHour && props.nowHour < 24)  || (0 <= props.nowHour && props.nowHour < 5) ) {
    return <GreetInTheEvening />
  }
  else {
    return <h1>Error.</h1>
  }
} 

// nowHour={}の中の数字を変えれば画面に表示される挨拶が変わる。
const App = () => {
  return <Greeting nowHour={13} />;
}

export default App;

おわりに

続きはまた書きます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【忘年会に向け】オールスター感謝祭的な四択クイズウェブシステムをNuxt.js+Socket.IOで実装してみた

経緯

  1. 我がチームが忘年会の幹事に任命される
  2. まじろう「出し物、、、なんかします?」
  3. 先輩「オールスター感謝祭・・・」
  4. 一同「は?」
  5. 先輩「オールスター感謝祭!!」

ということで、即興でnuxtによる四択クイズウェブシステムを作ったお話です。

ソースはgithubに設置しました。
https://github.com/majirou/allstar

また、開発にあたり以下の記事とソースを参考にさせていただきました。
オール◯ター感謝祭もどきアプリで社内イベントを乗り切る
私自身はVue派なので、Reactソースを眺めながら勉強になりました。

基盤となる技術

websocket

Slackなどのチャット系サービスなどに使われている技術とのことで、
nodejsでは、socketioというライブラリを用います。
wiki からの引用ですが、メリットはありそうです。

従来の手法に比べると、新たなコネクションを張ることがなくなる・HTTPコネクションとは異なる軽量プロトコルを使うなどの理由により通信ロスが減る、一つのコネクションで全てのデータ送受信が行えるため同一サーバに接続する他のアプリケーションへの影響が少ないなどのメリットがある

実装概要

  • フレームワークは Nuxt.js
  • Nuxt上のExpress(server/index.js)にて、上記の socketio によるコネクション確立と各イベント定義+もろもろ
  • クライアントは三種類を用意しました
    • admin: 問題やランキング表示をコントロール
    • client: ユーザーが解答などを行うUI(主にスマホ想定)
    • monitor: アナウンスやランキングを表示するための中央モニター
  • 音源は鳴らせる仕組みにしましたが、あくまで個人的なお遊びの範囲で行うため音楽ファイルそのものはgithubにあげていません。
    • 尚、CMから番組に戻った際の「でっで、でっででっでで、ででん!」というお馴染みの音は、荻野目洋子の「千年浪漫」という曲のラストです。

実装の肝

  • サーバー側は、「受信するイベント」をon(イベント名, 実行する関数)にて定義する。
  • クライアント側にイベントとデータを送るには、emit(イベント名, データ)にて実行する。
  • クライアント側でも、「受信するイベント」をon(イベント名, 実行する関数)にて定義する。
  • サーバー側へイベントとデータを送るには、emit(イベント名, データ)にて実行する。

と、双方向で「受け」と「送り」の「イベント名称」と「その処理」を定義していくことで、通信と処理が行えます。

クイズで必要となるイベント

  1. 司会「問題です」
  2. (サーバー) 「問題」イベントを送信
  3. (クライアント) 「問題」イベントを受信
  4. 司会「Ready Go」
  5. (サーバー) 「カウントダウン」イベントを送信
  6. (クライアント) 「カウントダウン」イベントを受信
  7. (クライアント) 解答をタップしたら、サーバーに解答内容を送る「解答」イベントを送信
  8. (サーバー) 「解答」イベントを受信 → 集計

に加え、

  • Answer Check → 解答数表示
  • 正解者はこちら → 解答表示、正解不正解の判定
  • 予選落ち → 解答が遅かった順ランキングの生成と表示
  • 早押しランキング → 上の逆
  • 全員スタンドアップ → 解答権を復活させる

などなどが必要で、加えて音源と連動させたり、不正防止や成績管理など考えねばならない機能があります。

サーバー

コネクション確立時on("connection")内にて、各種イベントを定義することで、該当するイベントに対するサーバー側の処理を実行します。
以下の場合、コネクション確立時に、readyGoイベントを受け取った時、コンソール上に書き出し、その後クライアントら接続対象にstartQuetionイベントを発行する処理となります。
当然、クライアント側にはstartQuetionを受けた時の処理を書く必要があります。

const socketio = require('socket.io')
const io = socketio.listen(server)

io.on('connection', socket => {
  socket.on('readyGo', text => { 
    console.log('readyGo', text )
    io.emit('startQuetion', text)
  })
})

クライアント

基本はサーバー同様ですが、クライアント側では、socketio-clientを使います。

以下の場合、マウント時に、socket.onにて各イベントの処理を定義しています。
startQuestionを受けたら、カウントダウンを開始=問題の始まりとなります。
逆に、クライアント側で解答をしたら、それをサーバー側に送るという処理は、socket.emit(イベント名, データ) という形で実施します。

import ioClient from "socket.io-client"

export default {
  data() {
    return {
      socket: ioClient('localhost'),
(中略)
  mounted() {
    this.socket.on("startQuetion", () => {
      // カウントダウン開始
      this.countDown()
(中略)
  methods: {
    answer(event) {
      this.socket.emit("answer", { answer: event.target.value })
    },

画面と機能の抜粋

基本的には、ユーザーがスマホ、モニターは中央の大画面、管理者はノートPCなどに映すことを想定しています。

ログイン画面

スクリーンショット 2019-11-28 1.33.16.png

回答者を識別するために、ログインを行わせます。
今回は、番号とアカウントを用意して、それらをサーバー側で生存フラグと共に管理します。
あくまで余興なので、ガチガチのセキュリティはしません。
スマホが基本対象機器なので、QRコードを用意して入力の手間などを省くようにしました。

忘年会なので、各席にQRコードを印字しておくなどの演出しようかと思います。

不正防止

一度ログインした場合、再ログインなどは認めない方向にしています。
localstorageに記憶させ、ログイン情報があれば前回のログイン者の名前などを表示し、問題解答画面を表示します。
ただし、ログアウト(/logout)はできるURLは用意しています。

問題解答画面 /client

スクリーンショット 2019-11-28 1.37.01.png

ユーザーのメインとなる画面です。

  • 「Ready Go」時に、タイマーはカウントダウン開始。
  • 「アンサーチェック」で回答数を表示
  • 「正解はこちら」で、正解不正解の判定
  • 四択のいずれかをタップ時に解答をサーバーに送る

などを、websocketイベントで処理をします。

管理画面 /admin

スクリーンショット 2019-11-28 1.39.47.png

スタンドアップ、問題の送信、音源などの制御を行います。
進行上操作しやすいように、一画面に収まるよう調整をした・・・つもりです。
ユーザー側のUIと同様に、アクションはWebsocketにて、サーバーに送られ処理されます。
基本的にトリガーとなるアクションが多いため、司会との阿吽の呼吸が試されます。

全体向けモニター /monitor

大画面に映す、ユーザー全体が見る画面です。基本的にはクライアントに準じますが、ログインなどは当然不要ですし、回答する機能などは省いています。

感想

Websocketでの通信は非常に軽く、通常のウェブページの遷移のような画面の切り替えが発生しないのは非常にありがたく、
また、スマホを何台か並べてテストした際、同一のタイミングで画面が切り替わることが、当然といえば当然なのですが、不思議な面白さがありました。

そして、思いつくがままに、部活動のようなノリで作り上げたので、仕事とは異なる面白さがありました。
実際に、忘年会でお披露目するまでデバッグと演出をチームで練り上げ、無事終わらせられるかを後ほどご報告できたらと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptのCookie操作を簡略化したライブラリ、pocket-cookieを作成しました

pocket-cookie

  • pocket-cookieはJavaScriptのCookie操作を簡略化したライブラリです。
  • リポジトリはこちら
  • ぶっちゃけ著者によるjs-cookieの再実装
  • tsで書いてるので、当然d.tsもパッケージに含まれています

なぜ作ったか

js-cookieがes5で書かれており、babelとか使っていなかったので、なんとなくbabel/typescriptで書き直しました

インストール

npm

$ npm install -S pocket-cookie

or yarn

$ yarn add pocket-cookie

import your source code

    import cookie from 'pocket-cookie'

基本的な使い方

  • pocket-cookieは、cookie取得時に、元々のデータ型 を予測して自動でキャストする機能を実装しています。
    import cookie from 'pocket-cookie'

    // json
    const json = {str:'foo', num:45 , obj:{ bool:true } }
    cookie.set('json', json)
    let result = cookie.getWithAutoCast('json')  
    // => result === {str:'foo', num:45 , obj:{ bool:true } }

    // array
    const array = [{ str : "foo" } , { str : "bar" }]
    cookie.set('array', array)
    result = cookie.getWithAutoCast('array')  
    // => result === [{ str : "foo" } , { str : "bar" }]

    // string
    const str = 'foo'
    cookie.set('str', str)
    result = cookie.getWithAutoCast('str')  
    // => result === 'foo'

    // number
    const num = 123.45
    cookie.set('num', num)
    result = cookie.getWithAutoCast('num')  
    // => result === 123.45

    // boolean
    const bool = true
    cookie.set('bool', bool)
    result = cookie.getWithAutoCast('bool')  
    // => result === true

    // null
    const nullValue = null
    cookie.set('null', nullValue)
    result = cookie.getWithAutoCast('null')  
    // => result === null

    // undefined
    const undefinedValue = undefined
    cookie.set('undefined', undefinedValue)
    result = cookie.getWithAutoCast('undefined')  
    // => result === undefined

API Reference

get

シンプルにcookieを取得して string を返すAPI

    import cookie from 'pocket-cookie'

    document.cookie = 'foo=bar'
    cookie.get('foo')  // =>  "bar"

    document.cookie = 'bar=123'
    cookie.get('bar')  // =>  "123"

    document.cookie = 'zero=0'
    cookie.get('zero')  // =>  "0"

    document.cookie = 'null=null'
    cookie.get('null')  // =>  "null"

    document.cookie = 'undefined=undefined'
    cookie.get('undefined')  // =>  "undefined"

    //存在しないcookieを指定された場合、nullを返します
    cookie.get('notExist')  // =>  null

set

cookieを新規に作成します

    import cookie from 'pocket-cookie'

    cookie.set('foo', 'bar')
    console.log(document.cookie)   // =>  "foo=bar"

cookie属性の指定

set()の第3引数でcookie属性の指定をすることができます

    import cookie from 'pocket-cookie'

    cookie.set('foo', 'bar', {
        expires: 365,   // expires is 365days
        path: '/index.html',
        domain: 'example.com',
        secure: true,
        maxAge: 200,  // max-age is 200days 
        samesite: 'strict',
    })
  • 注意! もしexpiresmaxAgeを同時に指定した場合、maxAgeが優先されます
  • cookie属性についての詳しい仕様は、MDNのこちらの記事をご参考ください

getWithAutoCast

cookieを取得し、自動で型変換を行うAPIです

  • support types
    • string number boolean Date Array JSON null undefined
  • 注意! number型への変換よりDate型への変換が優先されます
    • "20130208"の場合 => 2013年2月8日のDate型へ変換されます
    import cookie from 'pocket-cookie'

    document.cookie = 'foo=bar'
    const foo = cookie.getWithAutoCast('foo')  // => foo === "bar" and typeof foo === 'string'

    document.cookie = 'number=123.45'
    const number = cookie.getWithAutoCast('number')  // => number === 123.45 and typeof number === 'number'

    document.cookie = 'bool=true'
    const bool = cookie.getWithAutoCast('bool')  // => bool === true and typeof bool === 'boolean'

    // Dateへパース可能な日付フォーマットは、moment.jsの第2,第3引数がない場合でmoment.jsがパース可能な全てのフォーマットです
    // 詳細 => https://momentjs.com/docs/#/parsing/string/
    document.cookie = 'date=2013-02-08T09'
    const date = cookie.getWithAutoCast('date')  // => date.toUTCString() === "Fri, 08 Feb 2013 00:00:00 GMT" and (date instanceof Date) === true

    document.cookie = 'arr=[{ "str" : "foo" } , { "str" : "bar" }]'
    const arr = cookie.getWithAutoCast('arr')  // => arr === [{ str : "foo" } , { str : "bar" }] and Array.isArray(arr) === true

    document.cookie = 'obj={ "str" : "foo", "num" : 45 , "obj" : { "bool" : true } }'
    const obj = cookie.getWithAutoCast('obj')  // => obj === {str:'foo', num:45 , obj:{ bool:true } } and typeof obj === 'object'

    document.cookie = 'null=null'
    const nullCookie = cookie.getWithAutoCast('null')  // => nullCookie === null

    document.cookie = 'undefined=undefined'
    const undefinedCookie = cookie.getWithAutoCast('undefined')  // => undefinedCookie === undefined and typeof undefinedCookie === 'undefined'

    //not exist cookie return null
    cookie.getWithAutoCast('notExist')  // =>  null

getKeyValuePairs

全てのcookieを含むキーバリューペアを作成します

    import cookie from 'pocket-cookie'

    document.cookie = 'foo=bar'
    cookie.getKeyValuePairs()  // =>  [{ key: 'foo', value: 'bar' }]

    cookie.clearAll()

    document.cookie = 'foo=bar'
    document.cookie = 'baz=qux'
    cookie.getKeyValuePairs()  // =>  [{ key: 'foo', value: 'bar' },{ key: 'baz', value: 'qux' }]

    // return empty array on cookie is empty
    cookie.clearAll()
    cookie.getKeyValuePairs()  // =>  []

clear

指定されたkey名のcookieを削除します

    import cookie from 'pocket-cookie'

    cookie.set('foo','bar')
    cookie.clear('foo')
    cookie.get('foo')   // =>  null
  • page属性が指定されたcooikeを削除する場合、同じpage属性を指定してください
    import cookie from 'pocket-cookie'

    cookie.set('foo','bar',{path:'/index.html'})
    cookie.clear('foo')  // fail!
    cookie.get('foo')   // =>  'bar'
    cookie.clear('foo',{path:'/index.html'})  // removed!
    cookie.get('foo')   // =>  null

clearAll

全てのcookieを削除します

    import cookie from 'pocket-cookie'

    document.cookie = 'foo=bar'
    document.cookie = 'bar=123'
    cookie.clearAll()
    console.log(document.cookie)   // =>  ""

注意! HttpOnly属性とpath属性が不要されたcookieはこのメソッドでは削除できません

    import cookie from 'pocket-cookie'

   cookie.set('foo','bar',{path:'/index.html'})
   cookie.clearAll()
   console.log(document.cookie)   // =>  "foo=bar"
   // please use cookie.clear('foo' , {path:'/index.html'})

Encoding

本プロジェクトは RFC 6265 に準じています

License

  • MIT License

Authors

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

エンジニアにオススメしたいyoutuber

Able Programming

機械学習に必要なpythonのライブラリや、機械学習の手法について解説しています。初心者でも機械学習に触れられるようになっています。
Able Programming
スクリーンショット 2019-11-29 1.14.59.png

シリエン戦隊JUN TV

シリコンバレーの現役エンジニアで、シリコンバレーで働いているエンジニアならではの情報を載せています。
将来シリコンバレーで働きたい人や起業したい人にオススメです。
シリエン戦隊JUN TV

スクリーンショット 2019-11-29 1.15.05.png

KENTA / 雑食系エンジニアTV

エンジニア向けオンラインサロンを運営しているエンジニアさんのyoutubeチャンネル

KENTA / 雑食系エンジニアTV

スクリーンショット 2019-11-29 1.15.09.png

迫 佑樹

プログラミング教材販売などを行なっているエンジニアさんのyoutube
迫 佑樹

スクリーンショット 2019-11-29 1.15.15.png

たにぐち まことのともすたチャンネル

react,vue,aws,wordpressなどのコーディングについて解説しているyoutube
たにぐち まことのともすたチャンネル

スクリーンショット 2019-11-29 1.15.19.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

2019年11月の流行のWYSIWYGエディタ

はじめに

GithubにあるWYSIWYGタグでスターが多いWYSIWYG用のライブラリを調べてみます。

Quill

概要

Github
https://github.com/quilljs/quill

Demo
https://quilljs.com/
image.png

・テーブルの作成はできないようです(ver2.x用にテーブル追加のプラグインはある)
・クリップボードを経由して画像のアップロードが可能です。
highlight.jsを使用してコードブロックのハイライトが可能のようです

対象ブラウザ
image.png
IEは非推奨のようです。

ライセンス
BSD 3-clause

サンプル

1.3.7のサンプル

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Quill</title>
    <!-- highlight.js を使う場合はquillの前に参照する-->
    <link rel="stylesheet"
          href="http://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/styles/default.min.css">
    <script src="http://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/highlight.min.js"></script>

    <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
    <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>



  </head>
  <body>


<!-- Create the editor container -->
<div id="editor">
  <p>Hello World!</p>
</div>
<button id="btnContent">コンテンツ取得</button>
<button id="btnImage">イメージの挿入</button>
<button id="btnDisable">編集可能/不可能</button>

<script>
var Delta = Quill.import('delta');
const quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    syntax : true,              // Include syntax module
    // https://quilljs.com/docs/modules/toolbar/
    toolbar : [
      ['bold', 'italic', 'underline', 'strike'],
      [{ 'color': [] }, { 'background': [] }], 
      ['link', 'image'] ,
      ['code-block']
    ]
  }
});

document.getElementById('btnContent').addEventListener('click', function() {
  console.log(quill.getContents());
});

document.getElementById('btnImage').addEventListener('click', function() {
  // このあたりを工夫すればクリップボードからの画像貼り付け等ができそう・・
  console.log(quill.getSelection(true).index);
  quill.insertEmbed(quill.getSelection(true).index, 'image', 'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png');
});

let enableEditor = false;
document.getElementById('btnDisable').addEventListener('click', function() {
  quill.enable(enableEditor);
  enableEditor = !enableEditor;
  console.log(enableEditor);
});


/**
 * ペーストのイベント追加例
 */
quill.root.addEventListener("paste", function (t) {
  console.log('paste');
  console.log(t);
  return true;
} , false);


</script>
  </body>
</html>

拡張モジュール

画像の貼り付けについて

quill-image-drop-and-paste
https://github.com/chenjuneking/quill-image-drop-and-paste

image.png

quill-image-drop-and-pasteはどうも以下のように修正しないと動作しないようです。
export.ImageDropAndPasteを使用しているが、設定していないので替わりにImageDropAndPasteを設定する。

(function(){var exports={};
"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||false;n.configurable=true;if("value"in n)n.writable=true;Object.defineProperty(e,n.key,n)}}return function(t,a,n){if(a)e(t.prototype,a);if(n)e(t,n);return t}}();function _classCallCheck(e,t){if(!(e instanceof t)){throw new TypeError("Cannot call a class as a function")}}var ImageDropAndPaste=function(){function e(t){var a=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};_classCallCheck(this,e);this.quill=t;this.options=a;this.handleDrop=this.handleDrop.bind(this);this.handlePaste=this.handlePaste.bind(this);this.quill.root.addEventListener("drop",this.handleDrop,false);this.quill.root.addEventListener("paste",this.handlePaste,false)}_createClass(e,[{key:"handleDrop",value:function e(t){var a=this;t.preventDefault();if(t.dataTransfer&&t.dataTransfer.files&&t.dataTransfer.files.length){if(document.caretRangeFromPoint){var n=document.getSelection();var i=document.caretRangeFromPoint(t.clientX,t.clientY);if(n&&i){n.setBaseAndExtent(i.startContainer,i.startOffset,i.startContainer,i.startOffset)}}this.readFiles(t.dataTransfer.files,function(e,t){if(typeof a.options.handler==="function"){a.options.handler(e,t)}else{a.insert.call(a,e,t)}},t)}}},{key:"handlePaste",value:function e(t){var a=this;if(t.clipboardData&&t.clipboardData.items&&t.clipboardData.items.length){this.readFiles(t.clipboardData.items,function(e,t){if(typeof a.options.handler==="function"){a.options.handler(e,t)}else{a.insert(e,t)}},t)}}},{key:"readFiles",value:function e(t,a,n){[].forEach.call(t,function(e){var t=e.type;if(!t.match(/^image\/(gif|jpe?g|a?png|svg|webp|bmp)/i))return;n.preventDefault();var i=new FileReader;i.onload=function(e){a(e.target.result,t)};var r=e.getAsFile?e.getAsFile():e;if(r instanceof Blob)i.readAsDataURL(r)})}},{key:"insert",value:function e(t,a){var n=(this.quill.getSelection()||{}).index||this.quill.getLength();this.quill.insertEmbed(n,"image",t,"user")}}]);return e}();exports.default=ImageDropAndPaste;
window.Quill.register('modules/imageDropAndPaste',ImageDropAndPaste)})(); // export.ImageDropAndPaste->ImageDropAndPaste

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Quill</title>
    <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
    <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>

    <script src="quill-image-drop-and-paste-master/quill-image-drop-and-paste.min.js" type="text/javascript"></script>

  </head>
  <body>
<button id="btnContent">コンテンツ取得</button>
<br>
クリップボードからイメージをbase64で張り付けている。
<!-- Create the editor container -->
<div id="editor">
  <p>Hello World!</p>
</div>

<script>

const quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    imageDropAndPaste : true
  }
});
document.getElementById('btnContent').addEventListener('click', function() {
  console.log(quill.getContents());
});
</script>
  </body>
</html>

テーブル操作

quilljs-table

quilljs-table
https://github.com/dost/quilljs-table

image.png

サンプルを見る限り、テーブルの削除や列、行の削除がGUIからできそうにないです。
最終コミット日が2017年。

quilljs-table

quilljs-table
https://github.com/volser/quill-table-ui

quilljs v2.0.0-dev.3が必要になります。
最終更新日は2019年10月25日です。

テーブルの操作は以下のようなイメージになります。
image.png

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Quill</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/quill/2.0.0-dev.3/quill.min.js" type="text/javascript"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/quill/2.0.0-dev.3/quill.snow.min.css" rel="stylesheet">

    <script src="https://unpkg.com/quill-table-ui@1.0.5/dist/umd/index.js" type="text/javascript"></script>
    <link href="https://unpkg.com/quill-table-ui@1.0.5/dist/index.css" rel="stylesheet">

  </head>
  <body>
<button id="btnContent">コンテンツ取得</button>
<button id="btnTable">テーブル追加</button>
<br>
クリップボードからイメージをbase64で張り付けている。
<!-- Create the editor container -->
<div id="editor">
  <p>Hello World!</p>
</div>

<script>

Quill.register({
  'modules/tableUI': quillTableUI.default
}, true);

const quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    table: true,
    tableUI: true,
  }
});
document.getElementById('btnContent').addEventListener('click', function() {
  console.log(quill.getContents());
});
document.getElementById('btnTable').addEventListener('click', function() {
  let table = quill.getModule('table');
  console.log(table);
  table.insertTable(3, 3);
});


</script>
  </body>
</html>

メモ

2019/11/28時点の最終リリースはバージョン1.3.7です。
2.0の開発が進められていますが、そのマイルストーンは不透明なものとなっています。
https://github.com/quilljs/quill/issues/2435

moduleを実装することで拡張機能が作れる模様。

trix

Github
https://github.com/basecamp/trix

Demo
https://trix-editor.org/
image.png

対象ブラウザ
IE 11以降をサポートしているようです。
https://github.com/basecamp/trix/issues/173

ライセンス
MIT License

サンプル

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Trix</title>
    <link rel="stylesheet" type="text/css" href="trix.css">
    <script type="text/javascript" src="trix.js"></script>
  </head>
  <body>


<!-- Create the editor container -->
<trix-editor class="trix-content">サンプル</trix-editor>

<button id="btnContent">コンテンツ取得</button>
<button id="btnSave">セーブ</button>
<button id="btnLoad">ロード</button>

<script>
document.getElementById('btnContent').addEventListener('click', function() {
  var element =  document.querySelector("trix-editor");
  console.log(element.editor.getDocument());
  console.log(element.editor.getDocument().toString());
});

// エディタの内容はJSON化して保存と読み込みが可能
document.getElementById('btnSave').addEventListener('click', function() {
  var element =  document.querySelector("trix-editor");
  localStorage["editorState"] = JSON.stringify(element.editor);
});

document.getElementById('btnLoad').addEventListener('click', function() {
  var element =  document.querySelector("trix-editor");
  element.editor.loadJSON(JSON.parse(localStorage["editorState"]));
});

// イベントの確認
addEventListener("trix-attachment-add", function(event) {
  // 添付ファイルや画像を追加するとこのイベントが実行される
  // 以下のコードを参考にfileuploadとかができそう
  // https://trix-editor.org/js/attachments.js
  console.log('trix-attachment-add');
  console.log(event.attachment);
});
addEventListener("trix-attachment-remove", function(event) {
  // 添付ファイルや画像を削除するとこのイベントが実行される
  console.log('trix-attachment-remove');
  console.log(event.attachment);
});

addEventListener("trix-change", function(event) {
  // 内容が変化した場合実行
  console.log('trix-change');
  console.log(event);
});



</script>
  </body>
</html>

メモ

学習コストは低いと思われる。
※最低限の動作確認はtrix-editorタグを作ってtrix.jsを読み込むだけでいい。

テーブルをサポートする予定はない。
https://github.com/basecamp/trix/issues/539

コードブロックはあるが強調表示はサポートしていない。
image.png

拡張とかはできなさそう。

MediumEditor

medium.comインラインエディターツールバーのクローン

Github
https://github.com/yabwe/medium-editor

Demo
http : //yabwe.github.io/medium-editor/
image.png

画像の貼り付けやコードブロックはなさそう。

対象ブラウザ
image.png
IEをサポートしている

ドキュメント
https://github.com/yabwe/medium-editor/wiki

ライセンス
MIT

サンプル

単純な例

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Trix</title>
    <script src="http://cdn.jsdelivr.net/npm/medium-editor@latest/dist/js/medium-editor.min.js"></script>
    <link rel="stylesheet" href="http://cdn.jsdelivr.net/npm/medium-editor@latest/dist/css/medium-editor.min.css" type="text/css" media="screen" charset="utf-8">
  </head>
  <body>

<div class="editable"></div>
<button id="btnSave">セーブ</button>
<button id="btnLoad">ロード</button>
<script>
var editor = new MediumEditor('.editable', {
    placeholder: {
        text: 'テキストを入力してください',
        hideOnClick: true
    },
    toolbar: {
        /* These are the default options for the toolbar,
           if nothing is passed this is what is used */
        allowMultiParagraphSelection: true,
        buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],
        diffLeft: 0,
        diffTop: -10,
        firstButtonClass: 'medium-editor-button-first',
        lastButtonClass: 'medium-editor-button-last',
        relativeContainer: null,
        standardizeSelectionStart: false,
        static: false,
        /* options which only apply when static is true */
        align: 'center',
        sticky: false,
        updateOnEmptySelection: false
    }
});

document.getElementById('btnSave').addEventListener('click', function() {
  console.log(editor.getContent());
  localStorage["medium"] = editor.getContent();
});
document.getElementById('btnLoad').addEventListener('click', function() {
  console.log(editor.getContent());
  editor.setContent(localStorage["medium"]);
});
</script>
  </body>
</html>

MediumEditor Tables

テーブルの作成を行うプラグインです。
Jqueryに依存しています。

GitHub
https://github.com/yabwe/medium-editor-tables

demo
https://yabwe.github.io/medium-editor-tables/

tablemedium.gif

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Medium</title>
    <link rel="stylesheet" href="http://cdn.jsdelivr.net/npm/medium-editor@latest/dist/css/medium-editor.min.css" type="text/css" media="screen" charset="utf-8">

    <!-- medium-editor-tables.js が使用している -->
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>

    <script src="http://cdn.jsdelivr.net/npm/medium-editor@latest/dist/js/medium-editor.min.js"></script>
    <script type="text/javascript" src="lib/js/medium-editor-tables.js"></script>

    <link rel="stylesheet" href="lib/css/medium-editor-tables.css" />

  </head>
  <body>

    <div class="editable"></div>

<script>
  var editor = new MediumEditor('.editable', {
    toolbar: {
      buttons: [
        'bold',
        'italic',
        'table'
      ]
    },
    extensions: {
      table: new MediumEditorTable()
    }
  });
</script>
  </body>
</html>

jQuery insert plugin for MediumEditor

画像やYoutubeやTwitterなどの埋め込みが可能なプラグインです。
Jqueryに依存します。

Github
https://github.com/orthes/medium-editor-insert-plugin

demo
https://linkesch.com/medium-editor-insert-plugin/

tablemedium3.gif

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Medium</title>
    <link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/medium-editor-insert-plugin/2.5.0/css/medium-editor-insert-plugin-frontend.min.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/medium-editor-insert-plugin/2.5.0/css/medium-editor-insert-plugin.min.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/medium-editor/5.23.3/css/medium-editor.min.css" />


    <!-- medium-editor-tables.js が使用している -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.12/handlebars.runtime.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-sortable/0.9.13/jquery-sortable-min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/jquery.ui.widget@1.10.3/jquery.ui.widget.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.iframe-transport/1.0.1/jquery.iframe-transport.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/9.28.0/js/jquery.fileupload.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/medium-editor/5.23.3/js/medium-editor.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/medium-editor-insert-plugin/2.5.0/js/medium-editor-insert-plugin.min.js"></script>

  </head>
  <body>

    <div class="editable"></div>
<button id="btnSave">セーブ</button>
<button id="btnLoad">ロード</button>
<script>
  var editor = new MediumEditor('.editable', {
    toolbar: {
      buttons: [
        'bold',
        'italic',
        'table'
      ]
    }
  });
  $('.editable').mediumInsert({
      editor: editor
  });
document.getElementById('btnSave').addEventListener('click', function() {
  console.log(editor.getContent());
  localStorage["medium"] = editor.getContent();
});
document.getElementById('btnLoad').addEventListener('click', function() {
  console.log(editor.getContent());
  editor.setContent(localStorage["medium"]);
});
</script>
  </body>
</html>

メモ

Medium Editorのライブラリ自体はJavaScriptのみで外部のライブラリに依存していない。
しかし、その拡張機能がJQueryに依存している。

任意の拡張機能が作成可能。
https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/README.md

Pell

もっともサイズの小さいWYSIWYGライブラリで他のライブラリに依存しません。

GitHub
https://github.com/jaredreich/pell

Demo
https://jaredreich.com/pell/
画像はURL指定して表示。
Link等でダイアログを表示する際はブラウザのメッセージボックスを使用している

image.png

対象ブラウザ
image.png
かなり古いブラウザでも動作するようです。

ライセンス
MIT

メモ

軽量であるのが売り。
テーブル機能はなさそう。
また拡張機能等はなさそう。

Editor.js

GitHub
https://github.com/codex-team/editor.js

Demo
https://editorjs.io/

image.png

・表、画像のアップロードをサポートしている。
・ツールバーは表示されずに、必要な時にポップアップが出る

対象ブラウザ
image.png

IEは対象外の模様
※すくなくともデモサイトはIE11で動作しない

ドキュメント
https://github.com/codex-team/editor.js/tree/bcdfcdadbc444921aee62b38516329cda3c96a70/docs

ライセンス
Apache License 2.0
寄付を受け付けている
https://opencollective.com/editorjs

サンプル

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Editor.js example</title>
  <link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body>
  <div class="ce-example">
    <div class="ce-example__content _ce-example__content--small">
      <div id="editorjs"></div>

      <button id="saveButton">
        editor.save()
      </button>
      <button id="loadButton">
        editor.load()
      </button>
    </div>
  </div>

  <!-- Load Tools -->
  <!--
   You can upload Tools to your project's directory and use as in example below.
   Also you can load each Tool from CDN or use NPM/Yarn packages.
   Read more in Tool's README file. For example:
   https://github.com/editor-js/header#installation
   -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script><!-- Header -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"></script><!-- Image -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script><!-- Delimiter -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest"></script><!-- List -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest"></script><!-- Checklist -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script><!-- Quote -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script><!-- Code -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script><!-- Embed -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/table@latest"></script><!-- Table -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/link@latest"></script><!-- Link -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/warning@latest"></script><!-- Warning -->

  <script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@latest"></script><!-- Marker -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest"></script><!-- Inline Code -->

  <!-- Load Editor.js's Core -->
  <script src="./dist/editor.js"></script>

  <!-- Initialization -->
  <script>
    /**
     * To initialize the Editor, create a new instance with configuration object
     * @see docs/installation.md for mode details
     */
    var initObj = {
      /**
       * Wrapper of Editor
       */
      holder: 'editorjs',
      /**
       * Tools list
       */
      tools: {
        /**
         * Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
         */
        header: {
          class: Header,
          inlineToolbar: ['link'],
          config: {
            placeholder: 'Header'
          },
          shortcut: 'CMD+SHIFT+H'
        },
        /**
         * Or pass class directly without any configuration
         */
        image: {
          class: SimpleImage,
          inlineToolbar: ['link'],
        },
        list: {
          class: List,
          inlineToolbar: true,
          shortcut: 'CMD+SHIFT+L'
        },
        checklist: {
          class: Checklist,
          inlineToolbar: true,
        },
        quote: {
          class: Quote,
          inlineToolbar: true,
          config: {
            quotePlaceholder: 'Enter a quote',
            captionPlaceholder: 'Quote\'s author',
          },
          shortcut: 'CMD+SHIFT+O'
        },
        warning: Warning,
        marker: {
          class:  Marker,
          shortcut: 'CMD+SHIFT+M'
        },
        code: {
          class:  CodeTool,
          shortcut: 'CMD+SHIFT+C'
        },
        delimiter: Delimiter,
        inlineCode: {
          class: InlineCode,
          shortcut: 'CMD+SHIFT+C'
        },
        linkTool: LinkTool,
        embed: Embed,
        table: {
          class: Table,
          inlineToolbar: true,
          shortcut: 'CMD+ALT+T'
        },
      },
      /**
       * This Tool will be used as default
       */
      // initialBlock: 'paragraph',
      /**
       * Initial Editor data
       */
      data: {
      },
      onReady: function(){
      },
      onChange: function() {
        console.log('something changed');
      }
    };
    var editor = new EditorJS(initObj);
    /**
     * Saving example
     */
    const saveButton = document.getElementById('saveButton');
    const loadButton = document.getElementById('loadButton');

    saveButton.addEventListener('click', function () {
      editor.save().then((savedData) => {
        console.log(savedData);
        localStorage["editJs"] = JSON.stringify(savedData);
      });
    });
    loadButton.addEventListener('click', function () {
      let data = JSON.parse(localStorage["editJs"]);
      console.log(data);
      editor.render(data);
    });
  </script>
</body>
</html>

メモ

IEは動作しない
undo機能は2019/11/28時点では自前でやる必要があるっぽい。
https://github.com/codex-team/editor.js/issues/518

CKEditor5

Gitのスター順で並べるとでてきませんが、バージョンごとにリポジトリがことなるっぽいため、上位にでてこないだけで、累計で27.500.000+のダウンロードが行われているようです。

GitHub
https://github.com/ckeditor/ckeditor5

Demo
https://ckeditor.com/ckeditor-5/demo/

ライセンス
GNU General Public License Version 2 or later.

商用ライセンスがある。
https://ckeditor.com/pricing/#null

メモ

この中ではデモが一番、使い易かったので、金が豊富にあるならコレがよさそう。

まとめ

色々調べましたが、結局、一長一短ある感じがします。
その上で個人的にはコードのハイライトが簡単に使えそうなQuillか、学習コストの低そうなtrixがよさそうに見えます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Githubでスターが多いWYSIWYGエディタ(2019年11月)

はじめに

2019/11時点でGithubにあるWYSIWYGタグでスターが多いライブラリを調べてみます。

Quill

概要

Github
https://github.com/quilljs/quill

Demo
https://quilljs.com/
image.png

・テーブルの作成はできないようです(ver2.x用にテーブル追加のプラグインはある)
・クリップボードを経由して画像のアップロードが可能です。
highlight.jsを使用してコードブロックのハイライトが可能のようです

対象ブラウザ
image.png
IEは非推奨のようです。

ライセンス
BSD 3-clause

サンプル

1.3.7のサンプル

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Quill</title>
    <!-- highlight.js を使う場合はquillの前に参照する-->
    <link rel="stylesheet"
          href="http://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/styles/default.min.css">
    <script src="http://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.16.2/build/highlight.min.js"></script>

    <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
    <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>



  </head>
  <body>


<!-- Create the editor container -->
<div id="editor">
  <p>Hello World!</p>
</div>
<button id="btnContent">コンテンツ取得</button>
<button id="btnImage">イメージの挿入</button>
<button id="btnDisable">編集可能/不可能</button>

<script>
var Delta = Quill.import('delta');
const quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    syntax : true,              // Include syntax module
    // https://quilljs.com/docs/modules/toolbar/
    toolbar : [
      ['bold', 'italic', 'underline', 'strike'],
      [{ 'color': [] }, { 'background': [] }], 
      ['link', 'image'] ,
      ['code-block']
    ]
  }
});

document.getElementById('btnContent').addEventListener('click', function() {
  console.log(quill.getContents());
});

document.getElementById('btnImage').addEventListener('click', function() {
  // このあたりを工夫すればクリップボードからの画像貼り付け等ができそう・・
  console.log(quill.getSelection(true).index);
  quill.insertEmbed(quill.getSelection(true).index, 'image', 'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png');
});

let enableEditor = false;
document.getElementById('btnDisable').addEventListener('click', function() {
  quill.enable(enableEditor);
  enableEditor = !enableEditor;
  console.log(enableEditor);
});


/**
 * ペーストのイベント追加例
 */
quill.root.addEventListener("paste", function (t) {
  console.log('paste');
  console.log(t);
  return true;
} , false);


</script>
  </body>
</html>

拡張モジュール

画像の貼り付けについて

quill-image-drop-and-paste
https://github.com/chenjuneking/quill-image-drop-and-paste

image.png

quill-image-drop-and-pasteはどうも以下のように修正しないと動作しないようです。
export.ImageDropAndPasteを使用しているが、設定していないので替わりにImageDropAndPasteを設定する。

(function(){var exports={};
"use strict";Object.defineProperty(exports,"__esModule",{value:true});var _createClass=function(){function e(e,t){for(var a=0;a<t.length;a++){var n=t[a];n.enumerable=n.enumerable||false;n.configurable=true;if("value"in n)n.writable=true;Object.defineProperty(e,n.key,n)}}return function(t,a,n){if(a)e(t.prototype,a);if(n)e(t,n);return t}}();function _classCallCheck(e,t){if(!(e instanceof t)){throw new TypeError("Cannot call a class as a function")}}var ImageDropAndPaste=function(){function e(t){var a=arguments.length>1&&arguments[1]!==undefined?arguments[1]:{};_classCallCheck(this,e);this.quill=t;this.options=a;this.handleDrop=this.handleDrop.bind(this);this.handlePaste=this.handlePaste.bind(this);this.quill.root.addEventListener("drop",this.handleDrop,false);this.quill.root.addEventListener("paste",this.handlePaste,false)}_createClass(e,[{key:"handleDrop",value:function e(t){var a=this;t.preventDefault();if(t.dataTransfer&&t.dataTransfer.files&&t.dataTransfer.files.length){if(document.caretRangeFromPoint){var n=document.getSelection();var i=document.caretRangeFromPoint(t.clientX,t.clientY);if(n&&i){n.setBaseAndExtent(i.startContainer,i.startOffset,i.startContainer,i.startOffset)}}this.readFiles(t.dataTransfer.files,function(e,t){if(typeof a.options.handler==="function"){a.options.handler(e,t)}else{a.insert.call(a,e,t)}},t)}}},{key:"handlePaste",value:function e(t){var a=this;if(t.clipboardData&&t.clipboardData.items&&t.clipboardData.items.length){this.readFiles(t.clipboardData.items,function(e,t){if(typeof a.options.handler==="function"){a.options.handler(e,t)}else{a.insert(e,t)}},t)}}},{key:"readFiles",value:function e(t,a,n){[].forEach.call(t,function(e){var t=e.type;if(!t.match(/^image\/(gif|jpe?g|a?png|svg|webp|bmp)/i))return;n.preventDefault();var i=new FileReader;i.onload=function(e){a(e.target.result,t)};var r=e.getAsFile?e.getAsFile():e;if(r instanceof Blob)i.readAsDataURL(r)})}},{key:"insert",value:function e(t,a){var n=(this.quill.getSelection()||{}).index||this.quill.getLength();this.quill.insertEmbed(n,"image",t,"user")}}]);return e}();exports.default=ImageDropAndPaste;
window.Quill.register('modules/imageDropAndPaste',ImageDropAndPaste)})(); // export.ImageDropAndPaste->ImageDropAndPaste

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Quill</title>
    <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet">
    <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script>

    <script src="quill-image-drop-and-paste-master/quill-image-drop-and-paste.min.js" type="text/javascript"></script>

  </head>
  <body>
<button id="btnContent">コンテンツ取得</button>
<br>
クリップボードからイメージをbase64で張り付けている。
<!-- Create the editor container -->
<div id="editor">
  <p>Hello World!</p>
</div>

<script>

const quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    imageDropAndPaste : true
  }
});
document.getElementById('btnContent').addEventListener('click', function() {
  console.log(quill.getContents());
});
</script>
  </body>
</html>

テーブル操作

quilljs-table

quilljs-table
https://github.com/dost/quilljs-table

image.png

サンプルを見る限り、テーブルの削除や列、行の削除がGUIからできそうにないです。
最終コミット日が2017年。

quilljs-table

quilljs-table
https://github.com/volser/quill-table-ui

quilljs v2.0.0-dev.3が必要になります。
最終更新日は2019年10月25日です。

テーブルの操作は以下のようなイメージになります。
image.png

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Quill</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/quill/2.0.0-dev.3/quill.min.js" type="text/javascript"></script>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/quill/2.0.0-dev.3/quill.snow.min.css" rel="stylesheet">

    <script src="https://unpkg.com/quill-table-ui@1.0.5/dist/umd/index.js" type="text/javascript"></script>
    <link href="https://unpkg.com/quill-table-ui@1.0.5/dist/index.css" rel="stylesheet">

  </head>
  <body>
<button id="btnContent">コンテンツ取得</button>
<button id="btnTable">テーブル追加</button>
<br>
クリップボードからイメージをbase64で張り付けている。
<!-- Create the editor container -->
<div id="editor">
  <p>Hello World!</p>
</div>

<script>

Quill.register({
  'modules/tableUI': quillTableUI.default
}, true);

const quill = new Quill('#editor', {
  theme: 'snow',
  modules: {
    table: true,
    tableUI: true,
  }
});
document.getElementById('btnContent').addEventListener('click', function() {
  console.log(quill.getContents());
});
document.getElementById('btnTable').addEventListener('click', function() {
  let table = quill.getModule('table');
  console.log(table);
  table.insertTable(3, 3);
});


</script>
  </body>
</html>

メモ

2019/11/28時点の最終リリースはバージョン1.3.7です。
2.0の開発が進められていますが、そのマイルストーンは不透明なものとなっています。
https://github.com/quilljs/quill/issues/2435

moduleを実装することで拡張機能が作れる模様。

trix

Github
https://github.com/basecamp/trix

Demo
https://trix-editor.org/
image.png

対象ブラウザ
IE 11以降をサポートしているようです。
https://github.com/basecamp/trix/issues/173

ライセンス
MIT License

サンプル

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Trix</title>
    <link rel="stylesheet" type="text/css" href="trix.css">
    <script type="text/javascript" src="trix.js"></script>
  </head>
  <body>


<!-- Create the editor container -->
<trix-editor class="trix-content">サンプル</trix-editor>

<button id="btnContent">コンテンツ取得</button>
<button id="btnSave">セーブ</button>
<button id="btnLoad">ロード</button>

<script>
document.getElementById('btnContent').addEventListener('click', function() {
  var element =  document.querySelector("trix-editor");
  console.log(element.editor.getDocument());
  console.log(element.editor.getDocument().toString());
});

// エディタの内容はJSON化して保存と読み込みが可能
document.getElementById('btnSave').addEventListener('click', function() {
  var element =  document.querySelector("trix-editor");
  localStorage["editorState"] = JSON.stringify(element.editor);
});

document.getElementById('btnLoad').addEventListener('click', function() {
  var element =  document.querySelector("trix-editor");
  element.editor.loadJSON(JSON.parse(localStorage["editorState"]));
});

// イベントの確認
addEventListener("trix-attachment-add", function(event) {
  // 添付ファイルや画像を追加するとこのイベントが実行される
  // 以下のコードを参考にfileuploadとかができそう
  // https://trix-editor.org/js/attachments.js
  console.log('trix-attachment-add');
  console.log(event.attachment);
});
addEventListener("trix-attachment-remove", function(event) {
  // 添付ファイルや画像を削除するとこのイベントが実行される
  console.log('trix-attachment-remove');
  console.log(event.attachment);
});

addEventListener("trix-change", function(event) {
  // 内容が変化した場合実行
  console.log('trix-change');
  console.log(event);
});



</script>
  </body>
</html>

メモ

学習コストは低いと思われる。
※最低限の動作確認はtrix-editorタグを作ってtrix.jsを読み込むだけでいい。

テーブルをサポートする予定はない。
https://github.com/basecamp/trix/issues/539

コードブロックはあるが強調表示はサポートしていない。
image.png

拡張とかはできなさそう。

MediumEditor

medium.comインラインエディターツールバーのクローン

Github
https://github.com/yabwe/medium-editor

Demo
http : //yabwe.github.io/medium-editor/
image.png

画像の貼り付けやコードブロックはなさそう。

対象ブラウザ
image.png
IEをサポートしている

ドキュメント
https://github.com/yabwe/medium-editor/wiki

ライセンス
MIT

サンプル

単純な例

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Trix</title>
    <script src="http://cdn.jsdelivr.net/npm/medium-editor@latest/dist/js/medium-editor.min.js"></script>
    <link rel="stylesheet" href="http://cdn.jsdelivr.net/npm/medium-editor@latest/dist/css/medium-editor.min.css" type="text/css" media="screen" charset="utf-8">
  </head>
  <body>

<div class="editable"></div>
<button id="btnSave">セーブ</button>
<button id="btnLoad">ロード</button>
<script>
var editor = new MediumEditor('.editable', {
    placeholder: {
        text: 'テキストを入力してください',
        hideOnClick: true
    },
    toolbar: {
        /* These are the default options for the toolbar,
           if nothing is passed this is what is used */
        allowMultiParagraphSelection: true,
        buttons: ['bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote'],
        diffLeft: 0,
        diffTop: -10,
        firstButtonClass: 'medium-editor-button-first',
        lastButtonClass: 'medium-editor-button-last',
        relativeContainer: null,
        standardizeSelectionStart: false,
        static: false,
        /* options which only apply when static is true */
        align: 'center',
        sticky: false,
        updateOnEmptySelection: false
    }
});

document.getElementById('btnSave').addEventListener('click', function() {
  console.log(editor.getContent());
  localStorage["medium"] = editor.getContent();
});
document.getElementById('btnLoad').addEventListener('click', function() {
  console.log(editor.getContent());
  editor.setContent(localStorage["medium"]);
});
</script>
  </body>
</html>

MediumEditor Tables

テーブルの作成を行うプラグインです。
Jqueryに依存しています。

GitHub
https://github.com/yabwe/medium-editor-tables

demo
https://yabwe.github.io/medium-editor-tables/

tablemedium.gif

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Medium</title>
    <link rel="stylesheet" href="http://cdn.jsdelivr.net/npm/medium-editor@latest/dist/css/medium-editor.min.css" type="text/css" media="screen" charset="utf-8">

    <!-- medium-editor-tables.js が使用している -->
    <script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>

    <script src="http://cdn.jsdelivr.net/npm/medium-editor@latest/dist/js/medium-editor.min.js"></script>
    <script type="text/javascript" src="lib/js/medium-editor-tables.js"></script>

    <link rel="stylesheet" href="lib/css/medium-editor-tables.css" />

  </head>
  <body>

    <div class="editable"></div>

<script>
  var editor = new MediumEditor('.editable', {
    toolbar: {
      buttons: [
        'bold',
        'italic',
        'table'
      ]
    },
    extensions: {
      table: new MediumEditorTable()
    }
  });
</script>
  </body>
</html>

jQuery insert plugin for MediumEditor

画像やYoutubeやTwitterなどの埋め込みが可能なプラグインです。
Jqueryに依存します。

Github
https://github.com/orthes/medium-editor-insert-plugin

demo
https://linkesch.com/medium-editor-insert-plugin/

tablemedium3.gif

<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Test Medium</title>
    <link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/medium-editor-insert-plugin/2.5.0/css/medium-editor-insert-plugin-frontend.min.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/medium-editor-insert-plugin/2.5.0/css/medium-editor-insert-plugin.min.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/medium-editor/5.23.3/css/medium-editor.min.css" />


    <!-- medium-editor-tables.js が使用している -->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.12/handlebars.runtime.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-sortable/0.9.13/jquery-sortable-min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/jquery.ui.widget@1.10.3/jquery.ui.widget.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.iframe-transport/1.0.1/jquery.iframe-transport.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-file-upload/9.28.0/js/jquery.fileupload.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/medium-editor/5.23.3/js/medium-editor.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/medium-editor-insert-plugin/2.5.0/js/medium-editor-insert-plugin.min.js"></script>

  </head>
  <body>

    <div class="editable"></div>
<button id="btnSave">セーブ</button>
<button id="btnLoad">ロード</button>
<script>
  var editor = new MediumEditor('.editable', {
    toolbar: {
      buttons: [
        'bold',
        'italic',
        'table'
      ]
    }
  });
  $('.editable').mediumInsert({
      editor: editor
  });
document.getElementById('btnSave').addEventListener('click', function() {
  console.log(editor.getContent());
  localStorage["medium"] = editor.getContent();
});
document.getElementById('btnLoad').addEventListener('click', function() {
  console.log(editor.getContent());
  editor.setContent(localStorage["medium"]);
});
</script>
  </body>
</html>

メモ

Medium Editorのライブラリ自体はJavaScriptのみで外部のライブラリに依存していない。
しかし、その拡張機能がJQueryに依存している。

任意の拡張機能が作成可能。
https://github.com/yabwe/medium-editor/blob/master/src/js/extensions/README.md

Pell

もっともサイズの小さいWYSIWYGライブラリで他のライブラリに依存しません。

GitHub
https://github.com/jaredreich/pell

Demo
https://jaredreich.com/pell/
画像はURL指定して表示。
Link等でダイアログを表示する際はブラウザのメッセージボックスを使用している

image.png

対象ブラウザ
image.png
かなり古いブラウザでも動作するようです。

ライセンス
MIT

メモ

軽量であるのが売り。
テーブル機能はなさそう。
また拡張機能等はなさそう。

Editor.js

GitHub
https://github.com/codex-team/editor.js

Demo
https://editorjs.io/

image.png

・表、画像のアップロードをサポートしている。
・ツールバーは表示されずに、必要な時にポップアップが出る

対象ブラウザ
image.png

IEは対象外の模様
※すくなくともデモサイトはIE11で動作しない

ドキュメント
https://github.com/codex-team/editor.js/tree/bcdfcdadbc444921aee62b38516329cda3c96a70/docs

ライセンス
Apache License 2.0
寄付を受け付けている
https://opencollective.com/editorjs

サンプル

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Editor.js example</title>
  <link href="https://fonts.googleapis.com/css?family=PT+Mono" rel="stylesheet">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
</head>
<body>
  <div class="ce-example">
    <div class="ce-example__content _ce-example__content--small">
      <div id="editorjs"></div>

      <button id="saveButton">
        editor.save()
      </button>
      <button id="loadButton">
        editor.load()
      </button>
    </div>
  </div>

  <!-- Load Tools -->
  <!--
   You can upload Tools to your project's directory and use as in example below.
   Also you can load each Tool from CDN or use NPM/Yarn packages.
   Read more in Tool's README file. For example:
   https://github.com/editor-js/header#installation
   -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/header@latest"></script><!-- Header -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/simple-image@latest"></script><!-- Image -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/delimiter@latest"></script><!-- Delimiter -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/list@latest"></script><!-- List -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/checklist@latest"></script><!-- Checklist -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/quote@latest"></script><!-- Quote -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/code@latest"></script><!-- Code -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/embed@latest"></script><!-- Embed -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/table@latest"></script><!-- Table -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/link@latest"></script><!-- Link -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/warning@latest"></script><!-- Warning -->

  <script src="https://cdn.jsdelivr.net/npm/@editorjs/marker@latest"></script><!-- Marker -->
  <script src="https://cdn.jsdelivr.net/npm/@editorjs/inline-code@latest"></script><!-- Inline Code -->

  <!-- Load Editor.js's Core -->
  <script src="./dist/editor.js"></script>

  <!-- Initialization -->
  <script>
    /**
     * To initialize the Editor, create a new instance with configuration object
     * @see docs/installation.md for mode details
     */
    var initObj = {
      /**
       * Wrapper of Editor
       */
      holder: 'editorjs',
      /**
       * Tools list
       */
      tools: {
        /**
         * Each Tool is a Plugin. Pass them via 'class' option with necessary settings {@link docs/tools.md}
         */
        header: {
          class: Header,
          inlineToolbar: ['link'],
          config: {
            placeholder: 'Header'
          },
          shortcut: 'CMD+SHIFT+H'
        },
        /**
         * Or pass class directly without any configuration
         */
        image: {
          class: SimpleImage,
          inlineToolbar: ['link'],
        },
        list: {
          class: List,
          inlineToolbar: true,
          shortcut: 'CMD+SHIFT+L'
        },
        checklist: {
          class: Checklist,
          inlineToolbar: true,
        },
        quote: {
          class: Quote,
          inlineToolbar: true,
          config: {
            quotePlaceholder: 'Enter a quote',
            captionPlaceholder: 'Quote\'s author',
          },
          shortcut: 'CMD+SHIFT+O'
        },
        warning: Warning,
        marker: {
          class:  Marker,
          shortcut: 'CMD+SHIFT+M'
        },
        code: {
          class:  CodeTool,
          shortcut: 'CMD+SHIFT+C'
        },
        delimiter: Delimiter,
        inlineCode: {
          class: InlineCode,
          shortcut: 'CMD+SHIFT+C'
        },
        linkTool: LinkTool,
        embed: Embed,
        table: {
          class: Table,
          inlineToolbar: true,
          shortcut: 'CMD+ALT+T'
        },
      },
      /**
       * This Tool will be used as default
       */
      // initialBlock: 'paragraph',
      /**
       * Initial Editor data
       */
      data: {
      },
      onReady: function(){
      },
      onChange: function() {
        console.log('something changed');
      }
    };
    var editor = new EditorJS(initObj);
    /**
     * Saving example
     */
    const saveButton = document.getElementById('saveButton');
    const loadButton = document.getElementById('loadButton');

    saveButton.addEventListener('click', function () {
      editor.save().then((savedData) => {
        console.log(savedData);
        localStorage["editJs"] = JSON.stringify(savedData);
      });
    });
    loadButton.addEventListener('click', function () {
      let data = JSON.parse(localStorage["editJs"]);
      console.log(data);
      editor.render(data);
    });
  </script>
</body>
</html>

メモ

IEは動作しない
undo機能は2019/11/28時点では自前でやる必要があるっぽい。
https://github.com/codex-team/editor.js/issues/518

CKEditor5

Gitのスター順で並べると上位5位にでてきませんが、バージョンごとにプロジェクトが分かれているっぽいので累計すると、結構使われているように見えます。
公式ページを見ると累計で27.500.000+のダウンロードが行われているそうです。

GitHub
https://github.com/ckeditor/ckeditor5

Demo
https://ckeditor.com/ckeditor-5/demo/

ライセンス
GNU General Public License Version 2 or later.

商用ライセンスがある。
https://ckeditor.com/pricing/#null

メモ

この中ではデモが一番、使い易かったので、金が豊富にあるならコレがよさそう。

まとめ

色々調べましたが、結局、一長一短ある感じがします。
その上で個人的にはコードのハイライトが簡単に使えそうなQuillか、学習コストの低そうなtrixがよさそうに見えます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

nuxt generate したときに JavaScript ファイルを minify しない設定

確認バージョン: @nuxt/cli v2.10.2

nuxt generate コマンドで ./dist/_nuxt の下に出力される JavaScript は、デフォルトで minify されています。

これをやっているのは webpack です。 nuxt.config.js で minify しないように設定すれば、この設定が webpack まで渡るようになっています。

公式リファレンス

nuxt.config.js
export default {
  mode: 'universal',

//...

  build: {
    /*
    ** You can extend webpack config here
    */
    extend (config, ctx) {
    },
    optimization: {
      minimize: false
    }
  }
}

よく見たら

You can extend webpack config here

って思いっきりコメントに書いてあるわ!自分 webpack の設定をよく知らないんですが、もっと色々できそうですね。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

条件によって特定のプロパティがあったりなかったりするオブジェクトを簡単に書きたいなあと思った場合

例えば、is_flagtrue の時だけ、id というプロパティがあり、それ以外の場合は id プロパティが存在してはいけないオブジェクトをワンライナーで書きたいなあと思った場合。

const obj = {
  ...(is_flag ? { id:1111 } : {}),
  hoge: `hogehoge`,
  fuga: {
    fugafuga: `fugafuga`
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む