ライブラリのビューで、ファイルアイコンクリックでドキュメントを開く

むか~しのSharePointはライブラリのファイルアイコンクリックでドキュメントが開けたものです。

tempsnip1

しかし、いつのころからか(2013あたり、たぶん)、アイコンクリックしてもうんともすんともいわなくなってしまいました・・・

2007とか2010とかからマイグレーションした現場では、この仕様かなり不評です。アイコンクリックで開く前提でファイル名をビューに表示してなかったりするし。

ついては、以下の記事など参考にしつつ、JSリンクでアイコンクリックでドキュメントを開かすコードを書いてみました。

https://sharepoint.stackexchange.com/questions/222599/type-icon-not-opening-into-the-document

以下のコードをOpenFileViaIcon.jsという名前でサイトのリソースファイルに保存し、ドキュメントをアイコンクリックで開かせたいビューのリストwebパーツでJSリンクに以下のパスを指定します。

~siteCollection/SiteAssets/OpenFileViaIcon.js


SP.SOD.executeFunc("clienttemplates.js", "SPClientTemplates", function() {
function getBaseHtml(ctx) {
return SPClientTemplates["_defaultTemplates"].Fields.default.all.all[ctx.CurrentFieldSchema.FieldType][ctx.BaseViewID](ctx);
}
function init() {
var overrideCtx = {};
overrideCtx.Templates = {};
overrideCtx.Templates.Fields = {
'DocIcon': {
'View': function CustomIcon(ctx, field, listItem, listSchema) {
var str = "";
var itemApp = ctx.CurrentItem["File_x0020_Type.mapapp"];
if (ctx.CurrentItem.FSObjType == '1'|| itemApp == '' && ctx.CurrentItem.File_x0020_Type != "pdf") {
str = (ComputedFieldWorker.DocIcon(ctx, field, listItem, listSchema));
} else if(itemApp != ""){
str = '<a href="' + ctx.CurrentItem.FileRef + '" onmousedown="return VerifyHref(this,event,\'' + listSchema.DefaultItemOpen + '\',\'' + listItem["HTML_x0020_File_x0020_Type.File_x0020_Type.mapcon"] + '\',\'' + listItem["serverurl.progid"] + '\')" target="_blank">' + ComputedFieldWorker.DocIcon(ctx, field, listItem, listSchema) + '</a>';
} else {
str = '<a href="' + ctx.CurrentItem.FileRef + '" onmousedown="return VerifyHref(this,event,\''+ listSchema.DefaultItemOpen + '\',\'\',\'\')" target="_blank">' + ComputedFieldWorker.DocIcon(ctx, field, listItem, listSchema) + '</a>'
}
return str;
}
}
};
SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideCtx);
}
RegisterModuleInit(SPClientTemplates.Utility.ReplaceUrlTokens("~siteCollection/SiteAssets/OpenFileViaIcon.js"), init);
init();
});

これでOfficeドキュメントやPDFは、アイコンクリックで別タブでファイルが開くようになります!

JSリンクなので、クラシックUI限定ですが・・・ご参考まで。

リストフォームに採番機能を実装する(PowerApps編)

前回に引き続きPowerAppsネタです。

このたびもSharePointあるある要望、カスタムのIDをアイテムに振りたい!というので採番機能を実装してみます。
採番のやり方はいろいろあると思いますが、今回は最後に登録されたアイテムのIDをインクリメントする方法をとりました。

あらかじめ、SharePointリストにはcustomIDという1行テキスト列を作成しておきます。
PowerAppsでフォームを開き、customIDのデータカードで、詳細設定のロックを解除します。
ユーザーが任意の値を入れられないよう、DisplayModeプロパティをDisplayMode.Viewに設定します。

次に、Defaultと、フォームの編集中にほかのユーザーが新しくアイテムを作ったケースに備え、Updateにもに以下の式を設定します。

If(SharePointForm1.Mode=New, 
Concatenate("Custom-",Text(First(SortByColumns(customList, "ID", Descending)).ID + 1, "000")),
ThisItem.customID)

これで、新規フォームを開くとcustomIDに以下のように最後のIDをインクリメントした値が表示されます。

※Text関数で、3桁になるよう0埋め表示にしています。

なお、最後に登録されたアイテムのIDの取り方ですが、First関数でなくLast関数やMax関数、あるいはIDを使わずCountRows関数などでもいいんではないか、という意見があると思います。
しかし、PowerAppsには委任問題というものがありまして・・・
かいつまんでいうと、委任とは関数による計算を直接データソース側でできるということと解釈してます。
では、委任されていない関数を使った場合どうなるかというと、最初の500件のデータだけローカルにダウンロードして、計算対象とするということのようです。
SharePointをデータソースとする場合、委任できる関数はSort、Filter、Search関連の関数だけなので、LastやMaxを使うと最初の500件しか使われないので正しくIDが取れないのですね。
そのため、IDの降順でソートして、最初にきたアイテムのIDを参照するようにしています。
委任については、以下Microsoftのドキュメントも参考にしてください。(何をいってるかよくわからん部分もありますが)
委任について

まあ、PowerAppsの機能は日々進化しているようなので、何か月かしたら委任できる関数も増えているかもしれないですけどね。
ともあれ、通常のSharePointリストフォームだと、最後に登録されたアイテムのIDを参照するのは骨が折れますが、PowerAppsだと関数一発でとれるのでちょっと感動しました!

新規フォームにログインユーザー名を表示(PowerApps編)

さて、SharePoint OnlineではPowerAppsを利用してフォームのカスタマイズができるようになりましたね。
PowerApps を使用した SharePointリストフォームのカスタマイズ
こうなるとInfoPathは完全に過去の遺物ですね・・・

というので、さっそくSharePointあるある要望、新規フォームのPeople Pickerにログインユーザーをデフォルト表示をPowerAppsで実装してみました。

まずはMicrosoft公式の以下の情報を元に試しました。
PowerAppsユーザーに関する情報を表示する
でも、User()関数をそのままPeople Pickerのデフォルト値に設定してみてもうまくいきません。
たぶん、データ型がテキストでないとだめなんでしょうね・・・

で、調べた結果下記スレッドを発見。
https://powerusers.microsoft.com/t5/General-Discussion/Set-current-user-as-default-value-of-Person-or-Group-field/td-p/78744
しかし、ここに書いてあるようにドロップダウンのデフォルト値やDataCardのUpdateに設定したりしてみたもののやはりうまくいかず・・・

いろいろ試した結果、DataCardのデフォルト値に以下の式を書いたところうまくいきました!

If(SharePointForm1.Mode = New,
{'@odata.type':"#Microsoft.Azure.Connectors.SharePoint.SPListExpandedUser",
Claims:Concatenate("i:0#.f|membership|",User().Email),DisplayName:User().FullName,
Email:User().Email},
ThisItem.Requestor)

ついでに、上のサンプルではDataCardの中にLabelを2つ設置し、People Pickerで選択したユーザのメールアドレスと部署を表示するようにしています。
設定方法ですが、事前にOffice365 Userへのデータ接続を追加しておき、LabelのTextに以下のような式を書くだけです。

Concatenate(
"Mail: ",
Office365ユーザー.UserProfile(DataCardValue11.Selected.Email).Mail
)

※DataCardValueのところは、お使いの環境に合わせて変更してください。
なお、Office365 Userに接続してユーザーのプロファイル情報をもってくるやり方は、以下Microsoftのドキュメントを参考にしました。
PowerApps から Office 365 ユーザーの接続に接続する

PowerAppsについてはまだまだ情報も少なく全貌をつかめてませんが、なんかいろいろできそうな予感がしてます。
また新たな発見があれば、記事にしたいと思います!

列の書式設定を試してたらIE11のバグに遭遇した話

こんにちは。
めずらしくSharePoint Onlineの話題でも。
モダンUIの案件管理リストで、ステータス列に赤青黄色の信号機を表示しようと列の書式設定をいろいろ試していたときのお話です。

手始めにMicrosoftの公式ドキュメントを見ていたら、
列の書式設定で SharePoint をカスタマイズする
以下git-hubレポジトリにほぼそのものなサンプルがあったので、さっそく拝借します。
Traffic Light (Red-Yellow-Green) Status Indicator
できたできた、と喜んでいたところ、IE11でみたときだけ上下センタリングが効かず上に張り付いちゃってます。

なんでだ・・・と頭を悩ましつつ、CSSを調べてみたところ、列の書式設定をすると自動的に「.sp-field-customFormatter」というクラスが当たるみたいなんですが、ここのalign-items: centerが効いていないようです。

さらに調べると、これIE11のバグの模様。
https://github.com/philipwalton/flexbugs/issues/231
display:flexの要素で、min-heightを指定するとalign-items: centerが効かなくなるみたいです・・・
Microsoftぅー!!!という感じですが、気を取り直して上記スレッドのworkaroundを試してみました。
min-heightより小さい値、ということでheightに10pxほど指定してみます。
おおー、うまくいきました!

以下、サンプルコードです。

{
"$schema": "http://columnformatting.sharepointpnp.com/columnFormattingSchema.json&quot;,
"debugMode": true,
"elmType": "div",
"style": {
"height": "10px"
},
"children": [
{
"elmType": "span",
"style": {
"width": "14px",
"height": "14px",
"border-radius": "14px",
"background-color": {
"operator": "?",
"operands": [
{
"operator": "==",
"operands": [
{
"operator": "toString()",
"operands": [
"@currentField"
]
},
"完了"
]
},
"#2dc937",
{
"operator": "?",
"operands": [
{
"operator": "==",
"operands": [
{
"operator": "toString()",
"operands": [
"@currentField"
]
},
"進行中"
]
},
"#e7b416",
{
"operator": "?",
"operands": [
{
"operator": "==",
"operands": [
{
"operator": "toString()",
"operands": [
"@currentField"
]
},
"未開始"
]
},
"#cc3232",
""
]
}
]
}
]
}
}
}
]
}

事情を知らない人からみたら意味不明のスタイル指定ですが、指摘されたらIE11で中央揃えにするオマジナイだと言ってあげてください。
(てゆーかIE11のバグを直してほしいですけどね!)

集計値のHTMLをJSリンクで修正する

ひさびさの投稿になっちゃいましたが生きてます。

昨年より以下のようにいろいろと話題になっておりますが、集計値列でHTMLを出力しビューの見栄えを整えるワザが使えなくなってしまいましたよね。

June 13th 2017 Microsoft blocked handling HTML markup in SharePoint calculated fields – how to get the same functionality back – SharePoint Stack Exchange

で、ついにこの波が我が職場にもやってまいりまして大騒ぎになっておりましたが、クライアントサイドでクイックに対応したいということでJSリンクを使って修正することにしました。
以下、サンプルコードです。
※ビュー上に複数の集計値列でHTMLを出力しているパターンとなります。

(function () {
var htmlColumnContext = {};
htmlColumnContext.Templates = {};
htmlColumnContext.Templates.Fields = {
"field1": {
"View": field1ViewTemplate
},
"field2": {
"View": field2ViewTemplate
},
"field3": {
"View": field3ViewTemplate
}
};
SPClientTemplates.TemplateManager.RegisterTemplateOverrides(htmlColumnContext);
})();
function field1ViewTemplate(ctx) {
var ret1 = ctx.CurrentItem["field1"];
return ret1;
}
function field2ViewTemplate(ctx) {
var ret2 = ctx.CurrentItem["field2"];
return ret2;
}
function field3ViewTemplate(ctx) {
var ret3 = ctx.CurrentItem["field3"];
return ret3;
}
view raw fixHTML.js hosted with ❤ by GitHub

こんないい加減なコードでいいのか・・・と思いますが、ちゃんと動くのですよ、これが。
これを、集計値列を表示しているビューにJSリンクとして設定してください。
JSリンクの設定方法は、以下にあります。
JSリンクを使ってビューにニコちゃんマークを表示してみた

ビューがいっぱいあると大変ですが、今すぐ!直さなければ!というときのご参考にしてください。

SharePointの入力フォームで列名の下にDescriptionをもってくる

半年ぶりの更新です(ひゃーーー)。
てゆーか最近SharePoint2007に携わっているので、ネタないんです・・・
はやく2016さわりたい~!!!

というわけで小ネタをちょっとだけ。

SharePointの列にはDescriptionを設定できます。入力例や注意事項を記載したいときに便利です。
pic20160927-1

でも、デフォルトのDescriptionの場合、入力欄の下に配置されるので見落とされがちなんですよね。
また、URLも貼れたりしますが、表示文言を自由に設定してハイパーリンク化はできません。

そこで、JavaScriptでDescriptionに書きたい内容を列名の下に持ってきてみました。
pic20160927-2

ソースコードです。
こちらをコンテンツエディタwebパーツでNewForm.aspxとEditForm.aspxに埋め込んでください。

<script src="https://code.jquery.com/jquery-1.12.4.min.js&quot; integrity="sha256-ZosEbRLbNQzLpnKIkEdrPv7lOy9C27hHQ+Xp8a4MxAQ=" crossorigin="anonymous"></script>
<script>
$(document).ready( function () {
$('h3:contains("Body")').parent().append('<p><a href="https://servername/sitename/Doclib/manual.pdf&quot; target="_blank" style="font-weight:normal; text-decoration: underline; color:blue">マニュアルへのリンク</a></p>');
});
</script>

コード内の”Body”のところはDescriptionを置きたい列名に適宜変更してくださいね。
ちょっとしたことですが、ユーザビリティーの向上につながる・・・かも?

SharePointファーム全体でADのセキュリティグループに割り当てられているアクセス権を洗い出すPowerShellスクリプト

題名、長かったですね。

SharePointファーム全体でADのセキュリティグループで付与されている権限の一覧を取りたい!取らねばならぬ!しかもアイテム単位まで!となったことがありまして、PowerShellなんかほとんどいじくったことなかったけどエイヤーと書いてみました。
ちなみに、これ動かした環境はMOSS2007 なので、2010、2013だと書き方のお作法が違うと思います。

[void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint")
Function UserSource([string]$userName)
{
if($userName.split("\").count -gt 1)
{
$userName.split("\")[0]
}
elseif ($userName.split(":").count -gt 1)
{
$userName.split(":")[0]
}
else
{
""
}
}
$Permissions=@()
$farm = [Microsoft.SharePoint.Administration.SPFarm]::Local
$farmWebServices = $farm.Services | where -FilterScript {$_.GetType() -eq [Microsoft.SharePoint.Administration.SPWebService]}
foreach ($farmWebService in $farmWebServices) {
foreach ($webApplication in $farmWebService.WebApplications) {
foreach ($webApplication in $SPWebApp) {
foreach ($site in $webApplication.Sites)
{
foreach ($web in $site.AllWebs)
{
Write-Host "Site Collection: ID:" $site.ID " - URL: " $web.Url " - rootweb" $web.IsRootweb
if ($web.HasUniqueRoleAssignments)
{
foreach ($RoleAssignment in $web.RoleAssignments)
{
$domain = UserSource($RoleAssignment.Member.LoginName)
if($domain -ne "")
{
if ($RoleAssignment.Member.IsDomainGroup)
{
if ($domain -ne "NT AUTHORITY")
{
$users = new-object psobject
$users | add-member noteproperty -name "ObjectType" -value "SPWeb"
$users | add-member noteproperty -name "ObjectTitle" -value $web.Title
$users | add-member noteproperty -name "Type" -value "direct"
$users | add-member noteproperty -name "URL" -value "$($web.url)/_layouts/user.aspx"
$users | add-member noteproperty -name "user" -value $RoleAssignment.Member.LoginName
$users | add-member noteproperty -name "Group" -value ""
$permlist = ""
$RoleAssignment.RoleDefinitionBindings | select-object name | ForEach-Object { $permlist += $_.name + ";" }
$users | add-member noteproperty -name "Permission" -value $permlist
write-host $users
$Permissions += $users
}
}
}
else
{
$allUsers = $Roleassignment.member.users
foreach($user in $AllUsers)
{
if ($user.IsDomainGroup)
{
$domain = UserSource($user.LoginName)
if ($domain -ne "NT AUTHORITY")
{
$users = new-object psobject
$users | add-member noteproperty -name "ObjectType" -value "SPWeb"
$users | add-member noteproperty -name "ObjectTitle" -value $web.Title
$users | add-member noteproperty -name "Type" -value "role"
$users | add-member noteproperty -name "URL" -value "$($web.url)/_layouts/user.aspx"
$users | add-member noteproperty -name "user" -value $user.LoginName
$users | add-member noteproperty -name "Group" -value $RoleAssignment.member.name
$permlist = ""
$RoleAssignment.RoleDefinitionBindings | select-object name | ForEach-Object { $permlist += $_.name + ";" }
$users | add-member noteproperty -name "Permission" -value $permlist
write-host $users
$Permissions += $users
}
}
}
}
}
}
foreach ($aList in $Web.lists)
{
$listType = @{$true="doclib";$false="list"}[$aList.BaseType -eq "DocumentLibrary"]
if ($aList.HasUniqueRoleAssignments)
{
foreach ($RoleAssignment in $aList.RoleAssignments)
{
$domain = UserSource($RoleAssignment.Member.LoginName)
if($domain -ne "")
{
if ($RoleAssignment.Member.IsDomainGroup)
{
if ($domain -ne "NT AUTHORITY")
{
$users = new-object psobject
$users | add-member noteproperty -name "ObjectType" -value "List"
$users | add-member noteproperty -name "ObjectTitle" -value $aList.Title
$users | add-member noteproperty -name "Type" -value "direct"
$users | add-member noteproperty -name "URL" -value "$($web.url)/_layouts/user.aspx?obj=$($aList.id),$($listType)&List=$($aList.id)"
$users | add-member noteproperty -name "user" -value $RoleAssignment.Member.LoginName
$users | add-member noteproperty -name "Group" -value ""
$permlist = ""
$RoleAssignment.RoleDefinitionBindings | select-object name | ForEach-Object { $permlist += $_.name + ";" }
$users | add-member noteproperty -name "Permission" -value $permlist
write-host $users
$Permissions += $users
}
}
}
else
{
$allUsers = $Roleassignment.member.users
foreach($user in $AllUsers)
{
if ($user.IsDomainGroup)
{
$domain = UserSource($user.LoginName)
if ($domain -ne "NT AUTHORITY")
{
$users = new-object psobject
$users | add-member noteproperty -name "ObjectType" -value "List"
$users | add-member noteproperty -name "ObjectTitle" -value $aList.Title
$users | add-member noteproperty -name "Type" -value "role"
$users | add-member noteproperty -name "URL" -value "$($web.url)/_layouts/user.aspx?obj=$($aList.id),$($listType)&List=$($aList.id)"
$users | add-member noteproperty -name "user" -value $user.LoginName
$users | add-member noteproperty -name "Group" -value $RoleAssignment.member.name
$permlist = ""
$RoleAssignment.RoleDefinitionBindings | select-object name | ForEach-Object { $permlist += $_.name + ";" }
$users | add-member noteproperty -name "Permission" -value $permlist
write-host $users
$Permissions += $users
}
}
}
}
}
} # end if $aList.HasUniqueRoleAssignments
foreach($folder in $aList.Folders)
{
if ($folder.HasUniqueRoleAssignments)
{
foreach ($RoleAssignment in $folder.RoleAssignments)
{
$domain = UserSource($RoleAssignment.Member.LoginName)
if($domain -ne "")
{
if ($RoleAssignment.Member.IsDomainGroup)
{
if ($doamin -ne "NT AUTHORITY")
{
$users = new-object psobject
$users | add-member noteproperty -name "ObjectType" -value "folder"
$users | add-member noteproperty -name "ObjectTitle" -value $folder.Name
$users | add-member noteproperty -name "Type" -value "direct"
$users | add-member noteproperty -name "URL" -value "$($web.url)/_layouts/user.aspx?obj=$($aList.id),$($folder.id),LISTITEM&List=$($aList.id)"
$users | add-member noteproperty -name "user" -value $RoleAssignment.Member.LoginName
$users | add-member noteproperty -name "Group" -value ""
$permlist = ""
$RoleAssignment.RoleDefinitionBindings | select-object name | ForEach-Object { $permlist += $_.name + ";" }
$users | add-member noteproperty -name "Permission" -value $permlist
write-host $users
$Permissions += $users
}
}
}
else
{
$allUsers = $Roleassignment.member.users
foreach($user in $AllUsers)
{
if ($user.IsDomainGroup)
{
$domain = UserSource($user.LoginName)
if ($domain -ne "NT AUTHORITY")
{
$users = new-object psobject
$users | add-member noteproperty -name "ObjectType" -value "folder"
$users | add-member noteproperty -name "ObjectTitle" -value $folder.Name
$users | add-member noteproperty -name "Type" -value "direct"
$users | add-member noteproperty -name "URL" -value "$($web.url)/_layouts/user.aspx?obj=$($aList.id),$($folder.id),LISTITEM&List=$($aList.id)"
$users | add-member noteproperty -name "user" -value $user.LoginName
$users | add-member noteproperty -name "Group" -value $RoleAssignment.member.name
$permlist = ""
$RoleAssignment.RoleDefinitionBindings | select-object name | ForEach-Object { $permlist += $_.name + ";" }
$users | add-member noteproperty -name "Permission" -value $permlist
write-host $users
$Permissions += $users
}
}
}
}
}
}
} # end foreach $folder
foreach ($anItem in $aList.Items)
{
if ($anItem.HasUniqueRoleAssignments)
{
# Write-host $anItem.URL
foreach ($RoleAssignment in $anItem.RoleAssignments)
{
$domain = UserSource($RoleAssignment.Member.LoginName)
if($domain -ne "")
{
if ($RoleAssignment.Member.IsDomainGroup)
{
if ($domain -ne "NT AUTHORITY")
{
$users = new-object psobject
$users | add-member noteproperty -name "ObjectType" -value "item"
$users | add-member noteproperty -name "ObjectTitle" -value $anItem.Name
$users | add-member noteproperty -name "Type" -value "direct"
$users | add-member noteproperty -name "URL" -value "$($web.url)/_layouts/user.aspx?obj=$($aList.id),$($anItem.id),LISTITEM&List=$($aList.id)"
$users | add-member noteproperty -name "user" -value $RoleAssignment.Member.LoginName
$users | add-member noteproperty -name "Group" -value ""
$permlist = ""
$RoleAssignment.RoleDefinitionBindings | select-object name | ForEach-Object { $permlist += $_.name + ";" }
$users | add-member noteproperty -name "Permission" -value $permlist
write-host $users
$Permissions += $users
}
}
}
else
{
$allUsers = $Roleassignment.member.users
foreach($user in $AllUsers)
{
if ($user.IsDomainGroup)
{
$domain = UserSource($user.LoginName)
if ($domain -ne "NT AUTHORITY")
{
$users = new-object psobject
$users | add-member noteproperty -name "ObjectType" -value "item"
$users | add-member noteproperty -name "ObjectTitle" -value $anItem.Name
$users | add-member noteproperty -name "Type" -value "role"
$users | add-member noteproperty -name "URL" -value "$($web.url)/_layouts/user.aspx?obj=$($aList.id),$($anItem.id),LISTITEM&List=$($aList.id)"
$users | add-member noteproperty -name "user" -value $user.LoginName
$users | add-member noteproperty -name "Group" -value $RoleAssignment.member.name
$permlist = ""
$RoleAssignment.RoleDefinitionBindings | select-object name | ForEach-Object { $permlist += $_.name + ";" }
$users | add-member noteproperty -name "Permission" -value $permlist
write-host $users
$Permissions += $users
}
}
}
}
}
} # end foreach item
} # end foreach list
} #end if $web.HasUniqueRoleAssignments
} # end foreach web
$site.Dispose()
} # end foreach site
# } # end foreach webapp
}
$permissions | Export-Csv -Path D:\Work\DomainGroups.csv -encoding UTF8

PowerShell初心者ゆえコードきたなくて済みません。
もっと簡単に書けるよ、というアドバイスがありましたら、ぜひコメントくださいませ。

ともあれPowerShellのパワーを思い知りました(シャレではない)。使いこなせればいろいろできそう。
2013バージョンなどもそのうち機会がありましたら書くかも、書きたいな。

SharePointのリストフォームに採番機能をJavaScriptで実装する

この世にはようかんぱんなるものがあることを知り気になって仕方ないのですが、そんなことはどうでもいいですね。

さて、だいぶ間が空いてしまいましたが久々の更新です!
いぜん、ページにビューカウンタをつける方法をポストしましたが、意外とよく読まれているようなので、気を良くして応用編(?)です。

SharePointのリストアイテムに、システム的に振られるIDではなく、独自ルールで採番したカスタムIDを振りたい!という要望はよくありますよね。
そこで、ページビューカウンタと同じやり方で、採番機能を実現してみました。
新規フォームで見るとこんな感じ。
20151217

実装方法です。
事前に「counter」という名前の採番用カスタムリストを同じサイト内に作成しておきます。
採番用カウンタは、Titleフィールドをそのまま使います。
新規フォーム作成時に、採番用リストから番号を取得し採番フィールドに表示、保存時のアクションでインクリメントした番号を採番用リストに返す仕組みです。
なので、新規フォームをキャンセルした場合は、インクリメントされません。

以下、ソースコードです。
こちらをNewForm.aspxにコンテンツエディタwebパーツで埋め込んでください。

<script src="//code.jquery.com/jquery-1.11.1.min.js" type="text/javascript"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery.SPServices/2014.01/jquery.SPServices.min.js" type="text/javascript"></script>
<script type="text/javascript">
var count;
var itemId;
$(document).ready( function () {
var numStr;
$().SPServices({
operation: "GetListItems",
async: false,
listName: "counter",
completefunc: function (xData, Status) {
$(xData.responseXML).SPFilterNode("z:row").each( function () {
itemId= $(this).attr("ows_ID");
count = Number($(this).attr("ows_Title"));
});
}
});
count++;
numStr = "CustomID - " + ("0000000" + count).slice(-8);
$("input[title='CustomID']").val(numStr).attr("readonly", "readonly").css("color", "#6a6a6a");
$(":text:not([readonly])").eq(0).focus();
});
function PreSaveAction(){
$().SPServices({
operation: "UpdateListItems",
async: false,
batchCmd: "Update",
listName: "counter",
valuepairs: [["Title", count]],
ID: itemId,
completefunc: function(xData, Status) {
}
});
return true;
}
</script>

SPServicesjQuery使ってます。

さらにちょっとした小技として、

21行目:カスタムIDの数値部分を桁固定0埋めで表示
23行目:採番フィールドを読み取り専用にする
24行目:採番フィールドがフォームの最初のフィールドでもフォーカスを当てない

などを盛り込んでおります。

なお、EditForm.aspxには以下を埋め込んでおきます。

<script src="//code.jquery.com/jquery-1.11.1.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function () {
$("input[title='CustomID']").attr("readonly", "readonly").css("color","6a6a6a");
$(":text:not([readonly])").eq(0).focus();
});
</script>

検証環境は2010ですが、2007、2013でも動くと思います。
参考にしていただけるとうれしいです!

コンテンツ検索 web パーツでホバーパネルを表示してみた

前回のポスト
コンテンツ検索webパーツの表示をカスタマイズする
のオマケです。

SharePoint2013では、検索結果のアイテムにカーソルをあてると詳細がホバーパネルで表示されます。
しかし、コンテンツ検索 web パーツではホバーパネルが表示されません。そこで、なんとか表示させるべくカスタマイズしてみました。

完成イメージはこちら。
20150828
ホバーパネルの表示テンプレートは、マスターページギャラリー>Display Templates>Searchフォルダの中にあります。
このうち、Item_xxx_HoverPanel.htmlというのがそれです。検索結果のファイルタイプによって、表示テンプレートが細かく分けられているのですね。
今回は、Item_Default_HoverPanel.htmlをダウンロードして使います。
ダウンロードしたItem_Default_HoverPanel.htmlを開き、中身を以下のように書き換えます。


<html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Blog Hover Panel</title>
<!–[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106603</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#SearchHoverPanel;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
<mso:ManagedPropertyMapping msdt:dt="string">'Title':'Title','AuthorOWSUSER':'AuthorOWSUSER','Path':'Path','Description':'Description','EditorOWSUSER':'EditorOWSUSER','LastModifiedTime':'LastModifiedTime','CollapsingStatus':'CollapsingStatus','DocId':'DocId','HitHighlightedSummary':'HitHighlightedSummary','HitHighlightedProperties':'HitHighlightedProperties','FileExtension':'FileExtension','ViewsLifeTime':'ViewsLifeTime','ParentLink':'ParentLink','FileType':'FileType','IsContainer':'IsContainer','SecondaryFileExtension':'SecondaryFileExtension','DisplayAuthor':'DisplayAuthor','DiscussionPost':'DiscussionPost','Created':'Created'</mso:ManagedPropertyMapping>
</mso:CustomDocumentProperties>
</xml><![endif]–>
</head>
<body>
<div id="Item_Blog_HoverPanel">
<!–#_
var postMaxLengthInChars = 500;
var i = 0;
var id = ctx.CurrentItem.id;
ctx.CurrentItem.csr_ShowViewLibrary = !Srch.U.isWebPage(ctx.CurrentItem.FileExtension);
if(ctx.CurrentItem.IsContainer)
{
ctx.CurrentItem.csr_FileType = Srch.Res.ct_Folder
}
ctx.currentItem_ShowChangedBySnippet = true;
var userPersonaId = id + "_peopleUserPersona";
var authorUsername = Srch.U.getUsernameFromAuthorField(ctx.CurrentItem.AuthorOWSUSER);
AddPostRenderCallback(ctx, function()
{
Srch.SSU.renderPersona(authorUsername, userPersonaId);
});
_#–>
<div class="ms-srch-hover-innerContainer ms-srch-hover-standardSize" id="_#= $htmlEncode(id + HP.ids.inner) =#_">
<div class="ms-srch-hover-arrowBorder" id="_#= $htmlEncode(id + HP.ids.arrowBorder) =#_"></div>
<div class="ms-srch-hover-arrow" id="_#= $htmlEncode(id + HP.ids.arrow) =#_"></div>
<div class="ms-srch-hover-content" id="_#= $htmlEncode(id + HP.ids.content) =#_" data-displaytemplate="BlogHoverPanel">
<div id="_#= $htmlEncode(id + HP.ids.header) =#_" class="ms-srch-hover-header">
_#= ctx.RenderHeader(ctx) =#_
</div>
<div id="_#= $htmlEncode(id + HP.ids.body) =#_" class="ms-srch-hover-body">
<div class="ms-srch-hover-post">
<div id="_#= $htmlEncode(userPersonaId) =#_" class="ms-srch-hover-postPersona"></div>
<div><p>
<h4>
<!–#_
var author = "";
if (!$isEmptyString(ctx.CurrentItem.AuthorOWSUSER))
{
author = Srch.U.getDisplayNameFromAuthorField(ctx.CurrentItem.AuthorOWSUSER);
}
_#–>
_#= $htmlEncode(author) =#_
</h4></p>
<div class="ms-srch-hover-text ms-srch-hover-postText">
_#= $htmlEncode(Srch.U.truncateEnd(ctx.CurrentItem.DiscussionPost, postMaxLengthInChars)) =#_
</div>
<div class="ms-metadata">
<!–#_
if(!$isNull(ctx.CurrentItem.Created))
{
var timeSincePostId = id + "_timeSincePost";
AddPostRenderCallback(ctx, function()
{
Srch.U.renderFriendlyTimeIntervalString(ctx.CurrentItem.Created, timeSincePostId);
});
_#–>
<span class="ms-srch-hover-dateMetadata" id="_#= $htmlEncode(timeSincePostId) =#_"></span>
<!–#_
}
_#–>
</div>
</div>
</div>
</div>
<div id="_#= $htmlEncode(id + HP.ids.actions) =#_" class="ms-srch-hover-actions">
_#= ctx.RenderFooter(ctx) =#_
</div>
</div>
</div>
</div>
</body>
</html>

書き換えたファイルを「Item_Blog_HoverPanel.html」とリネームして保存し、Searchフォルダにアップロードします。

次に、前回のポストで作成した「Item_BlogPost.html」をダウンロードし、以下のように書き換えます。


<html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>For Blog Post</title>
<!–[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:ManagedPropertyMapping msdt:dt="string">'Picture URL'{画像の URL}:'PublishingImage;PictureURL;PictureThumbnailURL','Link URL'{リンクの URL}:'Path','Line 1'{行 1}:'Title','Line 2'{行 2}:'DiscussionPost','Line 3'{行 3}:'Created','Line 4'{行 4}:'AuthorOWSUSER','Line 5'{行 5}:'LikesCount', 'SecondaryFileExtension','ContentTypeId'</mso:ManagedPropertyMapping>
<mso:MasterPageDescription msdt:dt="string">このアイテム表示テンプレートでは、100 x 100 の大きさでアイテムの画像が左側に表示されます。画像の右側には、タイトル、既定アイテムの説明、カスタムの管理プロパティ用の行が表示されます。</mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106603</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Content Web Parts;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties>
</xml><![endif]–>
</head>
<body>
<!–
Warning: Do not try to add HTML to this section. Only the contents of the first <div>
inside the <body> tag will be used while executing Display Template code. Any HTML that
you add to this section will NOT become part of your Display Template.
–>
<script>
$includeLanguageScript(this.url, "~sitecollection/_catalogs/masterpage/Display Templates/Language Files/{Locale}/CustomStrings.js");
</script>
<!–
Use the div below to author your Display Template. Here are some things to keep in mind:
* Surround any JavaScript logic as shown below using a "pound underscore" (#_ … _#) token
inside a comment.
* Use the values assigned to your variables using an "underscore pound equals"
(_#= … =#_) token.
–>
<div id="Item_BlogPost">
<!–#_
var id = ctx.ClientControl.get_nextUniqueId();
var itemId = id + Srch.U.Ids.item;
var hoverId = id + Srch.U.Ids.hover;
var hoverUrl = "~sitecollection/_catalogs/masterpage/Display Templates/Search/Item_Blog_HoverPanel.js";
$setResultItem(itemId, ctx.CurrentItem);
if(ctx.CurrentItem.IsContainer){
ctx.CurrentItem.csr_Icon = Srch.U.getFolderIconUrl();
}
ctx.currentItem_ShowHoverPanelCallback = Srch.U.getShowHoverPanelCallback(itemId, hoverId, hoverUrl);
ctx.currentItem_HideHoverPanelCallback = Srch.U.getHideHoverPanelCallback();
var encodedId = $htmlEncode(ctx.ClientControl.get_nextUniqueId() + "_BlogPost_");
var linkURL = $getItemValue(ctx, "Link URL");
linkURL.overrideValueRenderer($urlHtmlEncodeValueObject);
var line1 = $getItemValue(ctx, "Line 1");
var line2 = $getItemValue(ctx, "Line 2");
var line3 = $getItemValue(ctx, "Line 3");
var line4 = $getItemValue(ctx, "Line 4");
var line5 = $getItemValue(ctx, "Line 5");
var StrLine5 = 0;
if(!line5.isEmpty){
StrLine5 = line5.value;
}
var StrLine2 = line2.value;
if (StrLine2.length > 150){
StrLine2 = StrLine2.substr(0, 150) + "…";
}
var pictureURL = $getItemValue(ctx, "Picture URL");
var pictureId = encodedId + "picture";
var pictureMarkup = Srch.ContentBySearch.getPictureMarkup(pictureURL, 100, 100, ctx.CurrentItem, "cbs-picture3LinesImg", line1, pictureId);
line1.overrideValueRenderer($contentLineText);
line2.overrideValueRenderer($contentLineText);
line3.overrideValueRenderer($contentLineText);
line4.overrideValueRenderer($contentLineText);
line5.overrideValueRenderer($contentLineText);
var containerId = encodedId + "container";
var pictureLinkId = encodedId + "pictureLink";
var pictureContainerId = encodedId + "pictureContainer";
var dataContainerId = encodedId + "dataContainer";
var line1LinkId = encodedId + "line1Link";
var line1Id = encodedId + "line1";
var line2Id = encodedId + "line2";
var line3Id = encodedId + "line3";
var line4Id = encodedId + "line4";
var line5Id = encodedId + "line5";
var dataDisplayTemplateTitle = "BlogPost";
_#–>
<div class="cbs-picture3LinesContainer ms-srch-item" id="_#= $htmlEncode(itemId) =#_" onmouseover="_#= ctx.currentItem_ShowHoverPanelCallback =#_" onmouseout="_#= ctx.currentItem_HideHoverPanelCallback =#_" data-displaytemplate="_#= $htmlEncode(dataDisplayTemplateTitle) =#_">
<div id="_#= $htmlEncode(hoverId) =#_" class="ms-srch-hover-outerContainer"></div>
<div class="cbs-picture3LinesImageContainer" id="_#= pictureContainerId =#_">
<!–#_
if(!linkURL.isEmpty)
{
_#–>
<a class="cbs-pictureImgLink" href="_#= linkURL =#_" title="_#= $htmlEncode(line1.defaultValueRenderer(line1)) =#_" id="_#= pictureLinkId =#_">
<!–#_
}
_#–>
_#= pictureMarkup =#_
<!–#_
if(!linkURL.isEmpty)
{
_#–>
</a>
<!–#_
}
_#–>
</div>
<div class="cbs-picture3LinesDataContainer" id="_#= dataContainerId =#_">
<div style="overflow:hidden">
<a class="cbs-picture3LinesLine1Link" href="_#= linkURL =#_" title="_#= $htmlEncode(line1.defaultValueRenderer(line1)) =#_" id="_#= line1LinkId =#_" style="display:block">
<h4 class="cbs-picture3LinesLine1 ms-accentText2 ms-noWrap" id="_#= line1Id =#_"> _#= line1 =#_</h4>
</a>
<span class="cbs-picture3LinesLine4 ms-textSmall ms-noWrap" id="_#= line4Id =#_" style="float:right;">Posted By _#= line4 =#_</span>
</div>
<!–#_
if(!line2.isEmpty)
{
_#–>
<div class="cbs-picture3LinesLine2" style="height:auto;" id="_#= line2Id =#_" > _#= StrLine2 =#_ </div>
<!–#_
}
_#–>
<div style="overflow:hidden;">
<!–#_
if(!line3.isEmpty)
{
_#–>
<span class="cbs-picture3LinesLine3 ms-textSmall ms-noWrap" id="_#= line3Id =#_" style="float:left;">[Posted : _#= line3 =#_]</span>
<span class="cbs-picture3LinesLine5 ms-textSmall ms-noWrap" id="_#= line5Id =#_" style="float:right;">Likes Count : _#= StrLine5 =#_</span>
<!–#_
}
_#–>
</div>
<div>
<a class="cbs-button1" href="_#= linkURL =#_" style="float:right;">Read More</a>
</div>
</div>
</div>
</div>
</body>
</html>

書き換えたItem_BlogPost.htmlを、Content Web Partsフォルダにアップロードします。
以上です!
もし、カスタマイズが反映されない場合はコンテンツ検索webパーツの表示テンプレートで「For Blog Post」をえらびなおし、プロパティのマッピングで以下のように選択しなおしてみてください。
20150828-2

なお、カスタマイズにあたってはこちらの記事を参考にしました。
The SharepointWallah: Sharepoint 2013 Search – Adding a hover panel to a content search webpart display template.
C/D/H Talks Tech » Custom Hover Panel on the Content Search Web Part using a Custom Display Template

若干、無理やりな感じもありますが・・・参考になればうれしいです。

コンテンツ検索webパーツの表示をカスタマイズする

だいぶ前になりますが、コンテンツ検索webパーツでブログの新着を表示するやり方を紹介しました。
【2013】コンテンツ検索 webパーツでBlogの新着情報を表示する | Me & SharePoint.

今回の記事と、次回の記事
コンテンツ検索 web パーツでホバーパネルを表示してみた
はその続きとなります。

前回のままではタイトルとサムネイルだけでさびしいので、本文の一部、投稿日、それから「Read More」ボタンを表示してみたいと思います。
完成イメージです。
20150826

これを実現するには、「表示テンプレート」というものをカスタマイズします。
表示テンプレートのカスタマイズについては、MSのサポートチームのブログで詳しく紹介されていますので参考にしてください。
SharePoint 2013 お知らせアイテムを新着順に表示するコンテンツ検索 Web パーツを作成する – Japan SharePoint Support Team Blog – Site Home – TechNet Blogs.
SharePoint 2013 検索結果の表示を制御する表示テンプレート – Japan SharePoint Support Team Blog – Site Home – TechNet Blogs.

さて、マスターページギャラリーからDisplay Templates>Content Web Parts とフォルダをたどって、Item_Picture3Lines.htmlをダウンロードします。
ダウンロードしたテンプレートを「Item_BlogPost.html」とリネームします。
テンプレートを開き、以下のように書き換えます。

<html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882"> 
<head>
<title>For Blog Post</title>

<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:ManagedPropertyMapping msdt:dt="string">'Picture URL'{画像の URL}:'PublishingImage;PictureURL;PictureThumbnailURL','Link URL'{リンクの URL}:'Path','Line 1'{行 1}:'Title','Line 2'{行 2}:'DiscussionPost','Line 3'{行 3}:'Created', 'SecondaryFileExtension','ContentTypeId'</mso:ManagedPropertyMapping>
<mso:MasterPageDescription msdt:dt="string">このアイテム表示テンプレートでは、100 x 100 の大きさでアイテムの画像が左側に表示されます。画像の右側には、タイトル、既定アイテムの説明、カスタムの管理プロパティ用の行が表示されます。</mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106603</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Content Web Parts;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties>
</xml><![endif]-->
</head>

<body>

    <!--
            Warning: Do not try to add HTML to this section. Only the contents of the first <div>
            inside the <body> tag will be used while executing Display Template code. Any HTML that
            you add to this section will NOT become part of your Display Template.
    -->
    <script>
        $includeLanguageScript(this.url, "~sitecollection/_catalogs/masterpage/Display Templates/Language Files/{Locale}/CustomStrings.js");
    </script>

    <!--
        Use the div below to author your Display Template. Here are some things to keep in mind:
        * Surround any JavaScript logic as shown below using a "pound underscore" (#_ ... _#) token
        inside a comment.

        * Use the values assigned to your variables using an "underscore pound equals"
        (_#= ... =#_) token.
    -->

    <div id="Item_BlogPost">

<!--#_

var encodedId = $htmlEncode(ctx.ClientControl.get_nextUniqueId() + "_BlogPost_");

var linkURL = $getItemValue(ctx, "Link URL");
linkURL.overrideValueRenderer($urlHtmlEncodeValueObject);

var line1 = $getItemValue(ctx, "Line 1");
var line2 = $getItemValue(ctx, "Line 2");
var line3 = $getItemValue(ctx, "Line 3");

var StrLine2 = line2.value;
if (StrLine2.length > 150){
    StrLine2 = StrLine2.substr(0, 150) + "...";
}

var pictureURL = $getItemValue(ctx, "Picture URL");
var pictureId = encodedId + "picture";
var pictureMarkup = Srch.ContentBySearch.getPictureMarkup(pictureURL, 100, 100, ctx.CurrentItem, "cbs-picture3LinesImg", line1, pictureId);

line1.overrideValueRenderer($contentLineText);
line2.overrideValueRenderer($contentLineText);
line3.overrideValueRenderer($contentLineText);

var containerId = encodedId + "container";
var pictureLinkId = encodedId + "pictureLink";
var pictureContainerId = encodedId + "pictureContainer";
var dataContainerId = encodedId + "dataContainer";
var line1LinkId = encodedId + "line1Link";
var line1Id = encodedId + "line1";
var line2Id = encodedId + "line2";
var line3Id = encodedId + "line3";

var dataDisplayTemplateTitle = "BlogPost";

 _#-->
        <div class="cbs-picture3LinesContainer" id="_#= containerId =#_" data-displaytemplate="_#= $htmlEncode(dataDisplayTemplateTitle) =#_">
            <div class="cbs-picture3LinesImageContainer" id="_#= pictureContainerId =#_">
<!--#_
if(!linkURL.isEmpty)
{
_#-->
                <a class="cbs-pictureImgLink" href="_#= linkURL =#_" title="_#= $htmlEncode(line1.defaultValueRenderer(line1)) =#_" id="_#= pictureLinkId =#_">
<!--#_
}
_#-->
                    _#= pictureMarkup =#_
<!--#_
if(!linkURL.isEmpty)
{
_#-->
                </a>
<!--#_
}
_#-->
            </div>
            <div class="cbs-picture3LinesDataContainer" id="_#= dataContainerId =#_">
                <a class="cbs-picture3LinesLine1Link" href="_#= linkURL =#_" title="_#= $htmlEncode(line1.defaultValueRenderer(line1)) =#_" id="_#= line1LinkId =#_">
                    <h4 class="cbs-picture3LinesLine1 ms-accentText2 ms-noWrap" id="_#= line1Id =#_"> _#= line1 =#_</h4>
                </a>
<!--#_
if(!line2.isEmpty)
{
_#-->
                <div class="cbs-picture3LinesLine2" style="height:auto;" id="_#= line2Id =#_" > _#= StrLine2 =#_ </div>
<!--#_
}
_#-->
                <div style="overflow:hidden;">
<!--#_
if(!line3.isEmpty)
{
_#--> 
                    <span class="cbs-picture3LinesLine3 ms-textSmall ms-noWrap" id="_#= line3Id =#_" style="float:left;">[Posted : _#= line3 =#_ ]</span>
<!--#_
}
_#-->
                    <a class="cbs-button1" href="_#= linkURL =#_" style="float:right;">Read More</a>
                </div>
            </div>
        </div>
    </div>
</body>
</html>

書き換えたItem_BlogPost.htmlを、ダウンロード元のフォルダにアップロードし、タイトルに「For Blog Post」と入力して保存します。

コンテンツ検索webパーツを設置したページに戻り、webパーツを編集状態にします。
「表示テンプレート」セクションの「アイテム」で、先ほどテンプレートのタイトルに設定した「For Blog Post」を選択します。

20150826-2

もし、うまく内容が反映されない場合は「プロパティのマッピング」でデフォルトで入力されているプロパティを再度選択しなおしてから保存してみてください。

最後に、「Read More」のリンクをボタン状にし、全体の見栄えを整えるためのCSSをページに埋め込みます。
ページを編集状態にし、「スクリプトエディター」を挿入して、コードスニペットのに以下のコードを貼り付けて保存します。

<style>
.cbs-picture3LinesContainer {
    padding: 20px 0;
    margin: 5px;
}
.cbs-picture3LinesImageContainer {
    margin-top: 5px;
}
.cbs-button1{
    text-decoration: none;
    text-align: center;
    display: inline-block;
    background-color: #59b1eb;
    border: 2px solid #59b1eb;
    color: #fff;
    padding: 0 30px;
}
.cbs-button1:visited{
    color: #fff;
}
.cbs-button1:hover {
    background-color: #fff;
    border-color: #59b1eb;
    color: #59b1eb;
    text-decoration: none;
}
.cbs-button1::before,
.cbs-button1::after {
    position: absolute;
    z-index: -1;
    display: block;
    content: '';
}
.cbs-button1,
.cbs-button1::before,
.cbs-button1::after {
    box-sizing: border-box;
    transition: all .3s;
}
</style>

これでOKです!
おなじみのコンテンツクエリwebパーツよりは、xslスタイルシートをいじらなくて済む分カスタマイズのハードルは低いですよね。
これからどんどん使っていきたいと思います!