JQuery: Bootstrap popover のcontent htmlの手動セットとcloseボタン設置


Railsで作ったデータベースで、同姓同名のデータがある場合に、その一覧を Bootstrap の popover で表示するようにしたかった(下図のような感じ)。

fig01
姓名のフィールドに入力があった時点で、JQueryのchangeイベントを受けて、検索するアクションをたたいて、getJSONで受け取って表示すればよい。

BootstrapページのpopoverのサンプルはButtonにメッセージを設定して、クリックなどで表示させるものだが、自分で検索の都度にcontentを設定したいし、また、popoverを消すためのcloseボタンも設置したい。どうするか?

まずは、表示したい場所にdivを書く。

<div id="popoverSameNames"></div>

これに対して、popover で content を設定して、show すればよい。

popover.({options}) の中でcontentを指定することもできるが、それだと、再検索したときに表示が切り替わらないことが判明。

こちらにヒントがあった
Bootstrap popover content cannot changed dynamically
書き換えるたびに以下のようにすればよいようだ。

$("#popoverSameNames").data('bs.popover').options.content = samenames_html;

また、closeボタンのつけ方は以下にあった。
How to add a close button to Bootstrap Popover

最終的に以下のようなコードにした。

※getJSONで受け取った同姓同名の情報から samenames_html を作って↓
if (samenames_html.length > 0) {
  $("#popoverSameNames").popover({ 
    title: '同姓同名があります.<span class="close">&times;</span>',
    template: '<div class="popover" role="tooltip" style="width:500px;font-size:80%;"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"><div class="data-content"></div></div></div>',
    html: true
  }).on('shown.bs.popover', function(e){
    var popover = $(this);
    $(this).parent().find('div.popover .close').on('click', function(e){
      popover.popover('hide');
    });
  });
  $("#popoverSameNames").data('bs.popover').options.content = samenames_html;
  $("#popoverSameNames").popover('show');
} else {
  $("#popoverSameNames").data('bs.popover').options.content = "";
  $("#popoverSameNames").popover('hide');
}

これでとりあえずうまく動いている。

以上

Rails+JQuery.validation: Radio Button の errorPlacement


複数ページのフォームでバリデーションを行うケースがあり、railsのモデルのバリデーションが使えないように思ったので、jquery-validation-rails を使うことにした。

しかし、デフォルトのエラー表示の位置が気に入らない。
例えば、下の[     ]が入力フィールドであるとして

姓[     ] 名[     ]

これらを必須項目にすると、姓の入力がない場合、

姓[     ] 姓を入力してください 名[     ]

のように「姓」の後ろにエラーが表示され「名」が離れてしまう。

そこでerrorPlacement を使う。例えば、以下のようにすれば、「姓」も「名」もエラーメッセージを後ろに集めることができる。これは Jquery Validation plug-in custom error placement からの受け売り。

姓<%= f.text_field :family_name %> 名<%= f.text_field :first_name %>
<span id="invalid-registration_family_name"></span> <!-- <=ここでエラー表示 -->
<span id="invalid-registration_first_name"></span>
$("form").validate({
  rules: { ..... 略 ..... },
  messages: { ..... 略 ..... },
  errorPlacement: function(error, element) {
      error.insertAfter('#invalid-' + element.attr('id'));
  }
});

ここからが本題。

ラジオボタンなんかはどうすればいいのだろう。ラジオボタンでは、elementのIDがみんな違っていて、それら全てを span で書かなければならないだろうか?スゴイたくさんの選択肢がある場合に困る。

elementのIDではなくname を使えれば問題ないはずだ。以下のように書き直してみる。

error.insertAfter('#invalid-' + element.attr('id'));
↓
error.insertAfter('#invalid-' + element.attr('name'));

しかし、rails の form_for 要素では element name は例えば name=”registration[xxxx]” のようになっていて、この [ ] 記号がIDとしてはどうもまずいらしい。どうする?

結論↓

いろんなやり方がある。element name を使って、その[]を例えば _ に文字置換するとか。
僕は以下のようにしてみました。正規表現でregistration[]を取り除いてデータのアイテム名だけにしている。
動いてはいるが問題ないだろうか。

以下から選択してください。<span id="invalid-category"></span> <!-- <=ここでエラー表示 -->
<%= f.radio_button :category, '010', {} %><%= f.label :category, "ソフトウェア開発",'010'  %>
<%= f.radio_button :category, '020', {} %><%= f.label :category, "プロバイダ",'020' %>
<%= f.radio_button :category, '030', {} %><%= f.label :category, "製造",'030' %>
$("form").validate({
  rules: { ..... 略 ..... },
  messages: { ..... 略 ..... },
  errorPlacement: function(error, element) {
    var element_name = element.attr('name');
    var field_name = element_name.match(/.+\[(.+)\]/);
    error.insertAfter('#invalid-' + field_name[1]);
  }
});

Rails: 1つの form_for でコンテンツとは別にFile Upload


1つの form_for の中で、添付ファイルをアップロードもやりたいのだけれど、その file_field は別に form_for のコンテンツの項目でもアソシエーションでも何でもないということがあり、以下のように書いたが、うまく行かなかった。

↓View

<%= form_for @note do |f| %>
  <%= f.text_field :title %>
  <%= f.text_field :body %>
  <%= file_field_tag :attachment %>
<% end %>

↓Controller

def create
  @note = Note.new(note_params)
  respond_to do |format|
    if @note.save
      if params[:attachment]
        attachment = params[:attachment] # <= ここでファイル情報を持つオブジェクトが取得できない
        filename = attachment.original_filename # <= したがってoriginal_filenameも取得できない
        ..... 略 ......
      end
      ..... 略 ......
    end
  end
end

def note_params
  params.require(:note).permit(:title, :body)
end

上記で、attachment = params[:attachment] は、ファイル情報を持つオブジェクトが取得できない。ファイル名の文字列だけが取得されている。

attachment を form_for の傘下に含めれば、ファイル情報を持つオブジェクトが取得できる。つまり、以下のようにviewを書けばよいことはわかったが……。

<%= form_for @note do |f| %>
  <%= f.text_field :title %>
  <%= f.text_field :body %>
  <%= f.file_field :attachment %> # <= f. を付けた file_field
<% end %>

そうすると、Strong Parameter も変更か。permitを記述するのはいいことだ……などと考える。

def note_params
  params.require(:note).permit(:title, :body, :attachment)
end

いやいや、これではNoteモデルにない項目を追加することになるし、@note = Note.new(note_params) も当然エラーになってしまう。どうする?

とりあえず、以下のようにしてStrong Paramaterを別にして回避したが、これでいいのだろうか。

↓結論(今のところの)

def create
  @note = Note.new(note_params)
  respond_to do |format|
    if @note.save
      if attachment_params[:attachment]
        attachment = attachment_params[:attachment] # <= ファイル情報を持つオブジェクトが取得できた!
        filename = attachment.original_filename # <= したがってoriginal_filenameも取得OK
        ..... 略 ......
      end
      ..... 略 ......
    end
  end
end

def note_params
  params.require(:note).permit(:title, :body)
end
def attachment_params # <= コレ追加
  params.require(:note).permit(:attachment)
end

Rails4: ActiveRecord polymorphicのjoins検索?(追記あり)


polymorphicをjoinsで記述する方法が分からず、ほとんどSQLで書いたことのメモです。

例えば動物と植物のデータテーブルがあって、研究報告書のテーブルを共有して使うような構造。

class Report < ActiveRecord::Base
  belongs_to :reportable, polymorphic: true
end
class Animal < ActiveRecord::Base
  has_many :report, as: :reportable, dependent: :destroy
end
class Plant < ActiveRecord::Base
  has_many :report, as: :reportable, dependent: :destroy
end

動物のコアラと植物のユーカリのレポートを検索して一覧に表示したい。

ここでの前提は、何らかの検索の結果としてのReportのコレクション reports が手元にあり、ここから検索を追加記述しなければならないということ。(つまり AnimalやPlan側からの開始でReportを抽出できない)
joinsで検索を追加できるだろうか?

調べたが分からなかったので、とりあえず期待を込めてこうだったらいいなで書いてみる。

koala_reports = repors.joins(:animals).where(:animals => {:specie => KOALA})
eucalyptus_reports = repors.joins(:plants).where(:plants => {:classification => EUCALYPTUS})
result = koala_reports | eucalyptus_reports

残念、やっぱりダメで1〜2行目で report は animalを持っていない、plants を持っていないという意味でエラーになる。ではreportabelだろうかとヤマカンをはる(多分かなり疲れていた)。

koala_reports = repors.joins(:reportabel).where(:animals => {:specie => KOALA})
eucalyptus_reports = repors.joins(:reportabel).where(:plants => {:classification => EUCALYPTUS})
result = koala_reports | eucalyptus_reports

当然、これも間違い。
仕方ないので、SQLでベタに書くことにした。

koala_reports = repors.joins("JOIN animals ON animais.id = reports.reportable_id AND reports.reportable_type = 'Animal' AND animals.specie = #{KOALA}")
eucalyptus_reports = repors.joins("JOIN plants ON plants.id = reports.reportable_id AND reports.reportable_type = 'Plant' AND plants.classification = #{EUCALYPTUS}")
result = koala_reports | eucalyptus_reports

なにかよい記述方法があるのだろうか。


【追記】

多分、repors.animals と repors.plants でアクセスできるようにモデルを書きかえた上で(前提が変わるか?)、以下で行けるのでは? Arelを使う。2015/03/04

result = repors.includes(:animals, :plants)
.where(Animal.arel_table[:specie].eq(KOALA)
.or(Plant.arel_table[:classification].eq(EUCALYPTUS)))
.references(:animals, :plants)

SwiftのextensionでUITextFieldにInset余白を追加(メモ)


UITextFieldにInset 10ピクセルの余白を追加する。
Objective-CではCategoryで鷹揚にやっていたことが、お手軽に書ける感じになった。

extension UITextField {
    func textRectForBounds(bounds: CGRect) -> CGRect {
        return CGRectInset(bounds, 10, 10)
    }
    func editingRectForBounds(bounds: CGRect) -> CGRect {
        return CGRectInset(bounds, 10, 10)
    }
}

iOSの軽いアプリならクラスのファイルの冒頭にでも書いてもいいかも。

Swiftで半角ASCII文字判定


前にもObjective-Cで同じ課題について書いたが、かなり昔の話しで正規表現を使うやり方だった。今回は NSCharacterSet を使う。

今回やりたいのは文字列の1文字目が半角文字か日本語漢字なのかの判定。

結論的には以下。titleStrの1文字目が半角文字かどうかを unicodeScalarsを使い、valueで判定。

let hankaku = NSCharacterSet(range: NSMakeRange(0x20, 0x7e))
let titleUniStr = titleStr.unicodeScalars
let initialChar = titleUniStr[titleUniStr.startIndex]
if hankaku.longCharacterIsMember(initialChar.value) {
    println("半角")
} else {
    println("全角")
}

なお、iOSには文字列の判定に使える以下のようなセットが予め用意されているが、これらは全角判定に使えない。なぜなら、漢字も下記の letters だし、全角の数字も下記の digits に含まれるからだ。

let letters = NSCharacterSet.letterCharacterSet()
let digits = NSCharacterSet.decimalDigitCharacterSet()
let alphanum = NSCharacterSet.alphanumericCharacterSet()

以上

SwiftでQuickDialogを使ってFontPickerを作る


QPickerElementを使って、フォントの種類とサイズを選べるFontPickerを作る。

このとき PickerView にはフォントの種類に応じてフォント名をそのフォントの書体で表示するようにしたい。
これをやるにはQPickeElementが pickerView のハンドルでやっている
– (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row forComponent:(NSInteger)component
ではなく、UILabelを使って
– (UIView*)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
で表示しなければならないのでObjective-Cのカテゴリを使ってQPickerElementを拡張する(これは後述)。

とりあえず、FontPicker用のQPickerElementの準備。
QPickerElement は今のところ Extras の中に入っているので、cocoapod にQuickDialog/Extrasも追加。
Podsの Profile

pod 'QuickDialog'
pod 'QuickDialog/Extras'

SWiftで使うために myAppxxxxxxx-Bridging-Header.h に以下を追加。
後に述べるQPickerElementの拡張のためのヘッダも併記。

#import <QuickDialog/QuickDialog.h>
#import <QuickDialog/QPickerElement.h>
#import "QPickerTableViewCellExtension.h"

QPickerElementさえ使えれば、FontPickerを作るのはそう難しくはない。

var fontNames: [String] = []
var fontSizes: [String] = []
for family in UIFont.familyNames() as [String] {
    for fontname in UIFont.fontNamesForFamilyName(family) as [String] {
        fontNames.append(fontname)
    }
}
fontNames.sort{  $0 < $1 }
for fontsize in  Array<Int>(8...50) as [Int] {
    fontSizes.append(String(fontsize))
}
let fontPickerSection = QSection()
let fontPickerElement = QPickerElement(title: "Font Picker", items: [fontNames, fontSizes], value: fontValue)
fontPickerElement.key = "fontPicker12345"
fontPickerSection.addElement(fontPickerElement)
root.addSection(fontPickerSection)

値の取得は、rootからほかのElementなどとまとめてとってもよい。

let dict = NSMutableDictionary()
root.fetchValueIntoObject(dict)

しかし、onValueChanged で、その都度取って NSUserDefaults などに保存する方が好み。

fontPickerElement.onValueChanged = { ( el: QRootElement!) in
    var dict = NSMutableDictionary()
    fontPickerElement.fetchValueIntoObject(dict)
    .... dict を NSUserDefaultなどに格納 ....
}

最後に、PickerViewでフォント名の表示をそのフォントで表示する拡張のコード。これはSwiftではなく Objective-Cで書く。
拡張するのは QPickerElementではなく QPickerTableViewCell の方である。
QPickerTableViewCellExtension.h を追加する。

#import <Foundation/Foundation.h>
#import "QPickerTableViewCell.h"
#import "QPickerElement.h"

@interface QPickerTableViewCell(QPickerTableViewCellExtension)

@property (nonatomic, strong) UIPickerView *pickerView;
@property (nonatomic, readonly) QPickerElement *pickerElement;

- (UIView*)pickerView;

@end

QPickerTableViewCellExtension.m を追加する。

#import "QPickerTableViewCellExtension.h"

@implementation QPickerTableViewCell(QPickerTableViewCellExtension)

@dynamic pickerView;
@dynamic pickerElement;

// add following for font picker by eiji
- (UIView*)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
    UILabel* fontlabel = (UILabel*)view;
    if (!fontlabel) {
        fontlabel =  [[UILabel alloc] initWithFrame:CGRectZero];
        fontlabel.adjustsFontSizeToFitWidth = YES;
        fontlabel.backgroundColor = [UIColor clearColor];
    }
    [fontlabel setFrame:CGRectMake(0.0f, 0.0f, ([pickerView rowSizeForComponent:component].width - 8.0f),
                                   [pickerView rowSizeForComponent:component].height)];
    if (component == 0) {
        NSString* fontname = [[[self.pickerElement.items objectAtIndex:(NSUInteger) component] objectAtIndex:(NSUInteger) row] description];
        fontlabel.text = fontname;
        fontlabel.font = [UIFont fontWithName:fontname size:[UIFont labelFontSize]];
        fontlabel.textAlignment = NSTextAlignmentLeft;
    } else {
        NSString* fontsize = [[[self.pickerElement.items objectAtIndex:(NSUInteger) component] objectAtIndex:(NSUInteger) row] description];
        fontlabel.text = fontsize;
        fontlabel.font = [UIFont boldSystemFontOfSize:15];
        fontlabel.textAlignment = NSTextAlignmentRight;
    }
    return fontlabel;
    
}

これでフォント名の書体で表示されるPickerができる。