GitHubにあげちゃる
流行に乗ってGitHubにあげてみました
VisualStudiのソリューションをそのまま挙げてます
訳も分からやってますので変だったらゴメンナサイ
github.com
非VisualStudiの場合はhtml header内の"@Url.Content"を直接のパスに書き換えてください
ページ移動にはcm.changeLocationを読んでますのでローカルで動作させる場合はcm.jsに修正が必要です
GtiHub謹製GitHub Desktopを使ってみました
これが使いやすいかはわかりません
リポジトリは公開、プライベートとも無料で作成可能なようです
プライベート→公開はWebでしか出来ないようです
1.対象リポジトリからSettingsタブ選択
2.リストメニューからOptionsを選択
3.ページ下端のDanger Zoneで公開、プライベートの変更
ドロップダウン
jQuery Mobileにはドロップダウンは標準でもあるのですが、いろいろ痛い目に合わされるので、ボタンとポップアップで作ってみます
/* * ドロップダウン */ interface DropdownOptions { alignRight?: boolean; onSelected?: (selItem: Dropdown.Item) => void; // 選択された onClosed?: any; // 閉じられた } namespace Dropdown { "use strict"; export class Item { text: string; value: any; constructor(text?: string, value?: any) { this.text = text; this.value = value; } } /* * ドロップダウンリスト表示 */ export function open(selector: JQuery | string, items: Item[], options?: DropdownOptions) { let target = $(selector); let style = "min-height:1em;"; if (options && options.alignRight) { style += "text-align:right;"; } let content = ""; content += "<div"; content += " data-role='popup'"; content += " data-history='false'"; content += " data-dismissible='true'"; content += ">"; content += " <div role='main' class='ui-content'>"; content += " <ul data-role='listview' data-icon='false'>"; for (let i = 0; i < items.length; i++) { content += " <li idx=" + i + ">"; content += " <a href='#' style='" + style + "'>" + items[i].text + "</a>"; content += " </li >"; } content += " </ul>"; content += " </div>"; content += "</div>"; // ポップアップ作成 let box = $(content) .appendTo(cm.getActivePage()) .enhanceWithin() .popup(); // 選択 box.find("li").off("click").on("click", function (event: Event) { let idx = parseInt($(this).attr("idx"), 0); if (options && options.onSelected) { options.onSelected(items[idx]); } Popup.close(); }); // 表示 let pos = Popup.Pos.downLeft(target); // 位置取得 if (options && options.alignRight) { pos = Popup.Pos.downRight(target); } Popup.open(box, { transition: "none" , positionFixed: false , x1: pos.x1 , y1: pos.y1 , x2: pos.x2 , y2: pos.y2 , onClosed: function () { box.remove(); if (options && options.onClosed) { options.onClosed(); } } }); } }
ポップアップの表示には以前に作成したものを使用します
コールバックはオプションで取得します
固有のCSSはありません
使い方
$(document).on("click", "#ボタンID", function (event: Event) { try { // Pageをoverflow:hideでないと2重スクロール Dropdown.open( $(this) , [ { text: "選択1", value: 1 } , { text: "選択2", value: 2 } , { text: "選択3", value: 3 } , { text: "選択4", value: 4 } , { text: "選択5選択5", value: 5 } ] , { onSelected: (selItem: Dropdown.Item) => { if (selItem) { $(this).text(selItem.text).attr("value", selItem.value); } } , onClosed: () => { console.log("dropdown closed"); } , alignRight: false }); } catch (e) { cm.showErrMsg(e); } });
Dropdown.Itemで表示するアイテムを作成してDropdown.Openします
コールバックはアロー式でやってみました
アロー式を使用した場合、thisは呼び出し元のままになります
HandsonTableの編集の続き
以前にHandsonTableの編集でPopupを表示して・・・とやりましたが死角がありました
その1
Popup表示後のタブキー押下で制御がHandsonTableに戻ってしまい、裏でセル選択が移動します
対策としてキー入力禁止メソッドを作成しておきます
合わせてカレントセル保存復元メソッドも追加します
全景載せておきます
/Src/ht.ts
/// <reference path="../scripts/typings/jquery/jquery.d.ts" /> /// <reference path="../scripts/typings/jquerymobile/jquerymobile.d.ts" /> /// <reference path="../scripts/typings/handsontable/handsontable.d.ts"/> // 改変済 /* * Handsontable Helper */ namespace Ht { "use strict"; export namespace CurrentCell { let curRow = -1; let curCol = -1; export function save(ht: Handsontable, row: number, col: number) { curRow = -1; curCol = -1; if (row < 0) { curRow = getCurrentRow(ht); } if (col < 0) { curCol = getCurrentCol(ht); } } export function restor(ht: Handsontable, row: number, col: number) { let isRestor = false; if (curRow >= 0) { row = curRow; isRestor = true; } if (curCol >= 0) { col = curCol; isRestor = true; } if (isRestor) { curRow = -1; curCol = -1; ht.selectCell(row, col); } } } /* * キー入力禁止 */ export function setDisableKey(event: any) { // 漢字系 if ([28, 29, 241, 242, 243, 244].indexOf(event.keyCode) >= 0) { event.isImmediatePropagationEnabled = false; event.isImmediatePropagationStopped = function () { return true; }; } // ポップアップ表示中 // サブウィンドウ表示中 if (Popup.isActive() || SubWindow.isVisible()) { event.isImmediatePropagationEnabled = false; event.isImmediatePropagationStopped = function () { return true; }; } } /* * 親コンテナ取得 */ export function getContainerParent(ht: Handsontable): JQuery { return $(ht.container).parent("div"); } /* * 垂直スクロール位置取得 */ export function getVScrollPos(ht: Handsontable): number { return $(ht.container).children(".wtHolder").scrollTop(); } /* * 垂直スクロール位置設定 */ export function setVScrollPos(ht: Handsontable, value: number) { $(ht.container).children(".wtHolder").scrollTop(value); } /* * 水平スクロール位置取得 */ export function getHScrollPos(ht: Handsontable): number { return $(ht.container).children(".wtHolder").scrollLeft(); } /* * 水平スクロール位置設定 */ export function setHScrollPos(ht: Handsontable, value: number) { $(ht.container).children(".wtHolder").scrollLeft(value); } /* * 現在行列位置取得 */ export function getCurrentRowColPos(ht: Handsontable): number[] { let row = -1; let col = -1; if (ht) { let sel = ht.getSelected(); if (sel && sel.length > 0) { row = sel[0][0]; col = sel[0][1]; } } let vpos = getVScrollPos(ht); let hpos = getHScrollPos(ht); return [row, col, vpos, hpos]; } /* * 現在行列位置設定 */ export function setCurrentRowColPos(ht: Handsontable, pos: number[]) { if (!pos || !ht.countRows() || !ht.countCols()) { return; } let row = pos[0]; if (!row) { row = 0; } else { row = Math.max(row, 0); row = Math.min(row, ht.countRows() - 1); } let col = pos[1]; if (!col) { col = 0; } else { col = Math.max(col, 0); col = Math.min(col, ht.countCols() - 1); } let vpos = pos[2]; if (!vpos) { vpos = 0; } let hpos = pos[3]; if (!hpos) { hpos = 0; } ht.selectCell(row, col); setVScrollPos(ht, vpos); setHScrollPos(ht, hpos); } /* * 現在行取得 */ export function getCurrentRow(ht: Handsontable): number { let row = -1; if (ht) { let sel = ht.getSelected(); if (sel && sel.length > 0) { row = sel[0][0]; } } return row; } /* * 現在列取得 */ export function getCurrentCol(ht: Handsontable): number { let col = -1; if (ht) { let sel = ht.getSelected(); if (sel && sel.length > 0) { col = sel[0][1]; } } return col; } /* * 現在列取得 */ export function getCurrentProp(ht: Handsontable): string { let prop = ""; if (ht) { let sel = ht.getSelected(); if (sel && sel.length > 0) { prop = <string>ht.colToProp(sel[0][1]); } } return prop; } /* * 全面表示 */ export function setFullSize(ht: Handsontable) { if (ht) { let sz = cm.getPageContentSize(); ht.updateSettings({ width: sz[0] , height: sz[1] }, false); getContainerParent(ht).width(sz[0]).height(sz[1]); ht.render(); } } /* * 表示位置へスクロール */ export function setRowScroll(ht: Handsontable, row?: number): boolean { if (typeof row === "undefined") { row = getCurrentRow(ht); } let col = getCurrentCol(ht); // ビュー内に無ければスクロール { let first = ht.view.wt.wtScroll.getFirstVisibleRow(); if (row < first) { return ht.scrollViewportTo(row, col); } } { let last = ht.view.wt.wtScroll.getLastVisibleRow(); if (row > last) { return ht.scrollViewportTo(row, col, true); } } return false; } export function setColScroll(ht: Handsontable, col?: number): boolean { if (typeof col === "undefined") { col = getCurrentCol(ht); } let row = getCurrentRow(ht); // ビュー内に無ければスクロール { let first = ht.view.wt.wtScroll.getFirstVisibleColumn(); if (col < first) { return ht.scrollViewportTo(row, col); } } { let last = ht.view.wt.wtScroll.getLastVisibleColumn(); if (col > last) { return ht.scrollViewportTo(row, col, true); } } return false; } /* * 選択と表示位置へスクロール */ export function setRowSelectAndScroll(ht: Handsontable, row: number): boolean { let col = getCurrentCol(ht); if (ht.selectCell(row, col) === true) { return setRowScroll(ht, row); } else { return false; } } export function setColSelectAndScroll(ht: Handsontable, col: number): boolean { let row = getCurrentRow(ht); if (ht.selectCell(row, col) === true) { return setColScroll(ht, col); } else { return false; } } /* * データ初期化 */ export function initData(ht: Handsontable) { ht.loadData([]); ht.updateSettings({ maxRows: 0 }, false); } /* * カラムヘッダタイトル取得 */ export function getColumnTitle(ht: Handsontable, col: number | string): string { let colidx = 0; if (typeof col === "string") { colidx = ht.propToCol(<string>col); } else { colidx = col; } let hd = ht.getColHeader(colidx); return $(hd).text(); } }
HandsonTable作成時オプションの対象イベントで呼び出します
/Src/table.ts
/* * テーブル */ namespace HTable { /* * 作成 */ export function build() { _ht = new Handsontable($("#list_table")[0], { data: null , columns: getColumns() // 列設定 , colHeaders: getColHerders // 列見出し , rowHeaders: true // 行見出し , rowHeaderWidth: 30 // 行見出し幅 , manualColumnResize: true // 列幅変更 , multiSelect: false // 複数選択 , stretchH: "none" // 水平ストレッチ , autoColumnSize: false // 自動サイズ調整 , wordWrap: false // セル内折り返し , outsideClickDeselects: false // 選択を維持 , disableVisualSelection: "area" // 範囲選択不可 , selectionMode: "single" // 選択モード , startRows: 0 // データ無時の行数 , trimWhitespace: false // 前後の空白トリム , currentRowClassName: "current-row" // 選択列にクラス名付加 , rowHeights: function (row: number) { // 行高さ return 50; } , enterMoves: { row: 0, col: 0 } // Enterキー移動先 , autoWrapCol: false // 列移動ループ , autoWrapRow: false // 行移動ループ , fillHandle: false // 選択範囲を埋める , beforeOnCellMouseDown(event: Event, coords: any) { if (_ht) { Ht.CurrentCell.save(_ht, coords.row, coords.col); } } , afterSelection(row: number, col: number) { if (_ht) { Ht.CurrentCell.restor(_ht, row, col); } } , beforeKeyDown(event: any) { Ht.setDisableKey(event); // 無効なキー入力設定 } }); .setFullSize(_ht); // 全面表示 } .... }
漢字系潰しは他所にあったものをパクッてきました
その2
Android上でPopupの表示を維持できませんでした
出た瞬間に閉じられます
タイマー使って回避します
let CustomEditor = Handsontable.editors.TextEditor.prototype.extend(); { CustomEditor.prototype.beginEditing = function () { // 編集開始前 .... let td = this.TD; // ポップアップ表示 setTimeout(function () { Popup.open(dlg, { // dlgがPopup positionTo: td , focusSelector: ipt , onOpend: function () { ipt.NumberSpin({ digits: 0, slider: true }); // iptがPopup上のinput } }); }, 0); .... }; }
メッセージボックスを一捻り
スクロールするページでcm.MsgBoxを表示した時、ヘッダやフッタのボタンを基準にcm.MsgBallonを表示した時に「あ”」ってなったので、位置固定フラグを追加します
/Src/cm.ts
namespace cm{ .... /* * メッセージバルーン */ export class MsgBallon { onClosed(result: boolean) { ; }; constructor(message: string, positionTo: any, positionFixed: boolean, hasCancel?: boolean) { let scope = this; let content = ""; content += "<div"; content += " data-role='popup'"; content += " data-history='false'"; content += " data-overlay-theme='a'"; content += ">"; content += " <div role='main' class='ui-content'>"; content += " <p>" + message + "</p>"; content += " </div>"; content += " <div data-role='footer' style='text-align:right;padding:0.3em;'>"; content += " <a href='#' class='ui-btn ui-btn-inline ui-btn-active' cmd='apply'>OK</a>"; if (hasCancel) { content += " <a href='#' class='ui-btn ui-btn-inline' cmd='cancel'>キャンセル</a>"; } content += " </div>"; content += "</div>"; // 作成 let box = $(content) .appendTo(cm.getActivePage()) .enhanceWithin() .popup(); // ボタン押下 let result = false; box.find(".ui-btn").off("click").on("click", function (event: Event) { let cmd = $(this).attr("cmd"); switch (cmd) { case "apply": result = true; box.popup("close"); break; case "cancel": box.popup("close"); break; } }); // 表示 Popup.open(box, { transition: "pop" , positionTo: positionTo , positionFixed: positionFixed , arrow: true , closeButton: true , onClosed: function () { scope.onClosed(result); box.remove(); } }); } } export namespace MsgBallon { export function show(message: string, positionTo: any, positionFixed: boolean, hasCancel?: boolean) { return new MsgBallon(message, positionTo, positionFixed, hasCancel); } } /* * メッセージボックス */ export class MsgBox { onClosed(result: boolean) { ; }; constructor(message: string, title: string, hasCancel?: boolean) { let scope = this; let content = ""; content += "<div"; content += " data-role='popup'"; content += " data-history='false'"; content += " data-overlay-theme='b'"; content += " data-dismissible='false'"; content += ">"; content += " <div data-role='header'>"; content += " <h1>" + title + "</h1>"; content += " </div>"; content += " <div role='main' class='ui-content'>"; content += " <p style='text-align:center;'>" + message + "</p>"; content += " </div>"; content += " <div data-role='footer' style=text-align:center;padding:0.3em;>"; content += " <a href='#' class='ui-btn ui-btn-inline ui-btn-active' cmd='apply'>OK</a>"; if (hasCancel) { content += " <a href='#' class='ui-btn ui-btn-inline' cmd='cancel'>キャンセル</a>"; } content += " </div>"; content += "</div>"; // 作成 let box = $(content) .appendTo(cm.getActivePage()) .enhanceWithin() .popup(); // ボタン押下 let result = false; box.find(".ui-btn").off("click").on("click", function (event: Event) { let cmd = $(this).attr("cmd"); switch (cmd) { case "apply": result = true; box.popup("close"); break; case "cancel": box.popup("close"); break; } }); // 表示 Popup.open(box, { transition: "pop" , positionFixed: true , onClosed: function () { scope.onClosed(result); box.remove(); } }); } } export namespace MsgBox { export function show(message: string, title: string, hasCancel?: boolean) { return new MsgBox(message, title, hasCancel); } } .... }
ページのスクロールに付いていかなようにMsgBoxは常に位置固定、MsgBallonはパラメータ指定としました
ついでにPopupをボタンのドロップダウンメニューとして使えるように設定位置取得ヘルパーを追加
Popupオプションのx1,y1,x2,y2に与える数値を取得します
/Src/cm.ts
namespace Popup { .... export namespace Pos { export function downLeft(selector: JQuery | string, fixedParent: boolean) { return getPosition(selector, "down", "left", fixedParent); } export function downRight(selector: JQuery | string, fixedParent: boolean) { return getPosition(selector, "down", "right", fixedParent); } export function upLeft(selector: JQuery | string, fixedParent: boolean) { return getPosition(selector, "up", "left", fixedParent); } export function upRight(selector: JQuery | string, fixedParent: boolean) { return getPosition(selector, "up", "right", fixedParent); } export function rightDown(selector: JQuery | string, fixedParent: boolean) { return getPosition(selector, "right", "down", fixedParent); } export function rightUp(selector: JQuery | string, fixedParent: boolean) { return getPosition(selector, "right", "up", fixedParent); } export function leftDown(selector: JQuery | string, fixedParent: boolean) { return getPosition(selector, "left", "down", fixedParent); } export function leftUp(selector: JQuery | string, fixedParent: boolean) { return getPosition(selector, "left", "up", fixedParent); } function getPosition(selector: JQuery | string, location: string, align: string, fixedParent: boolean) { let target = $(selector); let offset = target.offset(); let x1: number = NaN; let y1: number = NaN; let x2: number = NaN; let y2: number = NaN; switch (location) { case "down": y1 = offset.top + target.outerHeight(); break; case "up": y2 = $(window).height() - offset.top; break; case "right": x1 = offset.left + target.outerWidth(); break; case "left": x2 = $(window).width() - offset.left; break; } switch (align) { case "left": x1 = offset.left; break; case "right": x2 = $(window).width() - offset.left - target.outerWidth(); break; case "down": y1 = offset.top; break; case "up": y2 = $(window).height() - offset.top - target.outerHeight(); break; } return { x1: x1, y1: y1, x2: x2, y2: y2 }; } } }
TryCatch
所謂例外処理です
が、特に特別な処理を行うつもりはありません
あくまで想定外の例外に対する備えなので、発生した時点でおしまいです
イベントドリブンではイベントの入り口に片っ端から仕掛けて、ありのままのエラーを表示すれば良いと思います
逆にメソッドなんかに仕掛けたり、例外先でリカバリなんてもことにすると軽く地獄を見られます
汎用的なエラー表示メソッドを作成します
./Src/cm.ts
namespace cm { ... /* * エラー表示 */ export function showErrMsg(e: any) { let msg: any; if (e.stack) { msg += e.stack + "\n"; } if (e.status) { msg += e.status + ":" + e.statusText + "\n"; } else if (e.statusText) { msg += e.statusText + "\n"; } if (e.responseText) { msg += e.responseText + "\n"; } if (e.message) { msg += e.message + "\n"; } if (!msg) { msg = e; } alert(msg); } ...
とりあえず身の回りにあるエラー関連のメンバーはこんな感じでした
取れる情報は全て表示を是としています
$(document).on("pagecontainershow", function (event: Event, ui: any) { try { ... } catch (e) { cm.showErrMsg(e); } }); $(document).on("click", "#popupMenu > li > .ui-btn", function (event: Event) { try { $(this).blur(); let cmd = $(this).attr("cmd"); ... } catch (e) { cm.showErrMsg(e); } });
こんな感じのワンパターンを一心不乱に追加し続けます
ボタン押下等には$(this).blur()を追加し、二度押し防止します
フォーカスが外れてしまいますが気にしません
まあ、この辺はお好みで
ブラウザのスクロール位置を復元してみる
シングルページ内でのスクロール位置の保持、復元してみます
シングルページで他ページへ移動後、元ページへ戻ってきてスクロール位置が復元されていないと"イラッ"とします
スクロール位置の保持と復元用クラスを作成してみます
/Src/cm.ts
class ScrollPos { private _value = 0; private _target: JQuery; constructor(selector?: string | JQuery) { // 対象無しはドキュメント対象 if (typeof selector !== "undefined") { this._target = $(selector); } } /* * スクロール位置取得 */ getScrollValue(): number { if (this._target) { return this._target.scrollTop(); } else { return document.documentElement.scrollTop || document.body.scrollTop; } } /* * スクロール実行 */ doScroll(value?: number) { if (typeof value === "undefined") { value = this._value; } if (this._target) { this._target.scrollTop(value); // 対象BOX位置 } else { $.mobile.silentScroll(value); // ドキュメント位置 } } /* * 設定値設定 */ setValue(value?: number) { if (typeof value === "undefined") { value = this.getScrollValue(); // 現在のスクロール位置 } this._value = value; } /* * 設定値参照 */ getValue(): number { return this._value; } }
- コンストラクタ 対象を指定、無い場合はdocument
- getScrollValue 対象の現在のスクロール位置を取得
- doScroll 指定位置、もしくは保持された位置へスクロール
- setValue スクロール位置を保持、引数がない場合は現在位置を保持
- getValue 保持されている値を取得
サンプルではHandsontableのスクロール位置保持と合わせて位置復元します
自ページ移動前に位置を保持しています
/Src/mobile.ts
namespace Page1 { .... let _pos = new ScrollPos(); .... $(document).on("pagecontainershow", function (event: Event, ui: any) { .... $(window).resize(); _pos.doScroll(); // スクロール位置復元 mq.update(); }); /* * ページ終了前 */ $(document).on("pagecontainerbeforehide", function (event: Event, ui: any) { if (cm.getPrevPageId(ui) !== "page1") { return; } _pos.setValue(); // スクロール位置保持 }); .... } namespace Page2 { .... let _pos: number[] = []; // 現在位置 .... /* * ページ表示前 */ $(document).on("pagecontainerbeforeshow", function (event: Event, ui: any) { if (cm.getToPageId(ui) !== "page2") { return; } if (_ht) { Ht.initData(_ht); _ht.render(); } }); /* * ページ表示 */ $(document).on("pagecontainershow", function (event: Event, ui: any) { .... Ht.setCurrentRowColPos(_ht, _pos); // 位置復元 _ht.render(); // 再描画 mq.update(); }); /* * ページ終了前 */ $(document).on("pagecontainerbeforehide", function (event: Event, ui: any) { if (cm.getPrevPageId(ui) !== "page2") { return; } if (_ht) { _pos = Ht.getCurrentRowColPos(_ht); } }); }
ロケーションを移動してみる
シングルページとはいっても、やはりアプリケーションは複数のHTMLで構成されます
ロケーション移動メソッドを作成します
/Src/cm.ts
namespace cm { "use strict"; /* * サイトのルート取得 */ let _siteRoot = ""; export function getSiteRoot(): string { if (!_siteRoot) { let scripts = document.getElementsByTagName("script"); let i = scripts.length; while (i--) { let match = scripts[i].src.match(/(^|.*\/)cm\.js*/); if (match) { _siteRoot = match[1] + "../"; // cm.jsの場所からルートを設定 break; } } let emt: HTMLAnchorElement = <HTMLAnchorElement>$("<a>").attr("href", _siteRoot).get(0); _siteRoot = emt.href; } return _siteRoot; } /* * ロケーション移動 */ export interface PostData { [index: string]: string | number; } export function changeLoacation(url: string, data?: PostData) { // オフライン if (isOffline() === true) { return; } if (!url) { url = ""; } url = getSiteRoot() + url; if (data) { // POST let f = $("<form/>", { "action": url, "method": "post", "target": "_self" }); for (let key in data) { if (key) { f.append($("<input/>", { "type": "hidden", "name": key, "value": data[key] })); } } f.appendTo(document.body); f.submit(); f.remove(); } else { // GET location.replace(url); } } .... }
トレイリングスラッシュ対策としてgetSiteRoot()内で"cm.js"の場所を基準にしてルートのURLを取り直しています
サーバー側の設定次第なのでしょうが、念のため
基準とするファイル名、フォルダ構成によっては修正が必要です
大抵の場合GETで間に合うのですが、POSTも用意しておきます
Key-Valueで値が渡された場合はFORMを作成してsubmit()でPOSTしています
(FORMはBodyに追加する必要があるかもしれません)
サンプルです
/Src/mobile.ts
/* * ツールバーボタン押下 */ $(document).on("click", "#page1_toolbar1 .ui-btn, #page1_toolbar2 .ui-btn", function (event: Event) { let cmd = $(this).attr("cmd"); switch (cmd) { case "home": cm.changeLoacation("home"); break; case "table": cm.changeLoacation("table"); break; } });
/Views/Mobile/Index.vbhtmlでツールバーにidを付け、適当なボタンにcmd属性を追加してやります