HandsonTableを使ってみる
入力系を一通り揃えたので、次は一覧
HandsonTableは引くほど高性能なTableをお手軽に作成してくれます
バージョン7以降はMITライセンスではなくなってしまったのMIT最後の6.2.2を使用します
なかなかいいお値段してます
NeGetでもGitHubでも取れますがバージョン指定が必要です
ほおっておくと最新が落ちてきます
ファイルにはバージョンが付きません
<link rel="stylesheet" href="@Url.Content("~/Content/handsontable/handsontable.full.min.css")" /> <script type="text/javascript" src="@Url.Content("~/Scripts/handsontable/handsontable.full.min.js")"></script>
CDNだと例えば
<link rel="stylesheet" href="https://js.cybozu.com/handsontable/6.2.2/handsontable.full.min.css"/> <script type="text/javascript" src="https://js.cybozu.com/handsontable/6.2.2/handsontable.full.min.js"></script>
定義ファイルは公式にありますが、力入りすぎてて使えません
NuGetで取れる古いものを使用します
とはいっても全面改修ですが
// Type definitions for Handsontable 0.24.3 // Project: https://handsontable.com/ // Definitions by: Handsoncode sp. z o.o. <http://handsoncode.net/> // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped\ interface Handsontable { addHook(key: string, callback: Function | any[]): void; addHookOnce(key: string, callback: Function | any[]): void; alter(action: string, index: number, amount?: number, source?: string, keepEmptyRows?: boolean): void; clear(): void; colOffset(): number; colToProp(col: number): string | number; countCols(): number; countEmptyCols(ending?: boolean): number; countEmptyRows(ending?: boolean): number; countRenderedCols(): number; countRenderedRows(): number; countRows(): number; countSourceRows(): number; countVisibleCols(): number; countVisibleRows(): number; deselectCell(): void; destroy(): void; destroyEditor(revertOriginal?: boolean): void; getActiveEditor(): Object; getCell(row: number, col: number, topmost?: boolean): Element; getCellEditor(row: number, col: number): Object; getCellMeta(row: number, col: number): Object; getCellRenderer(row: number, col: number): Function; getCellValidator(row: number, col: number): any; getColHeader(col?: number): any[] | string; getColWidth(col: number): number; getCoords(elem: Element): Object; getCopyableData(row: number, column: number): string; getCopyableText(startRow: number, startCol: number, endRow: number, endCol: number): string; getData(r?: number, c?: number, r2?: number, c2?: number): any[]; getDataAtCell(row: number, col: number): any; getDataAtCol(col: number): any[]; getDataAtProp(prop: string | number): any[]; getDataAtRow(row: number): any[]; getDataAtRowProp(row: number, prop: string): any; getDataType(rowFrom: number, columnFrom: number, rowTo: number, columnTo: number): string; getInstance(): any; getPlugin(pluginName: string): any; getRowHeader(row?: number): any[] | string; getRowHeight(row: number): number; getSchema(): Object; getSelected(): any[]; getSelectedRange(): any; getSettings(): Object; getSourceData(r?: number, c?: number, r2?: number, c2?: number): any[]; getSourceDataAtCell(row: number, column: number): any; getSourceDataAtCol(column: number): any[]; getSourceDataAtRow(row: number): any[] | Object; getValue(): any; hasColHeaders(): boolean; hasHook(key: string): boolean; hasRowHeaders(): boolean; isEmptyCol(col: number): boolean; isEmptyRow(row: number): boolean; isListening(): boolean; listen(): void; loadData(data: any[]): void; populateFromArray(row: number, col: number, input: any[], endRow?: number, endCol?: number, source?: string, method?: string, direction?: string, deltas?: any[]): any; propToCol(prop: string): number; removeCellMeta(row: number, col: number, key: string): void; removeHook(key: string, callback: Function): void; render(): void; rowOffset(): number; runHooks(key: string, p1?: any, p2?: any, p3?: any, p4?: any, p5?: any, p6?: any): any; selectCell(row: number, col: number, endRow?: number, endCol?: number, scrollToCell?: boolean, changeListener?: boolean): boolean; selectCellByProp(row: number, prop: string, endRow?: number, endProp?: string, scrollToCell?: boolean): boolean; setCellMeta(row: number, col: number, key: string, val: string): void; setCellMetaObject(row: number, col: number, prop: Object): void; setDataAtCell(row: number | any[], col: number, value: string, source?: string): void; setDataAtRowProp(row: number | any[], prop: string, value: string, source?: string): void; spliceCol(col: number, index: number, amount: number, elements?: any): void; spliceRow(row: number, index: number, amount: number, elements?: any): void; unlisten(): void; updateSettings(settings: Object, init: boolean): void; validateCells(callback: Function): void; // Add container(): any; scrollViewportTo(row: number, col: number, snapToBottom?: boolean, snapToRight?: boolean): boolean; getSourceDataArray(row?: number, col?: number, row2?: number, col2?: number): any[]; view: any; } declare namespace Handsontable { interface Options { allowEmpty?: boolean; allowInsertColumn?: boolean; allowInsertRow?: boolean; allowInvalid?: boolean; allowRemoveColumn?: boolean; allowRemoveRow?: boolean; autoColumnSize?: Object | boolean; autoComplete?: any[]; autoRowSize?: Object | boolean; autoWrapCol?: boolean; autoWrapRow?: boolean; bindRowsWithHeaders?: boolean | string; cell?: any[]; cells?: Function; checkedTemplate?: boolean | string; className?: string | any[]; colHeaders?: boolean | any[] | Function; collapsibleColumns?: boolean | any[]; columnHeaderHeight?: number | any[]; columns?: any[]; columnSorting?: boolean | Object; columnSummary?: Object; colWidths?: any[] | Function | number | string; commentedCellClassName?: string; comments?: boolean | any[]; contextMenu?: boolean | any[] | Object; contextMenuCopyPaste?: Object; copyable?: boolean; copyColsLimit?: number; copyPaste?: boolean; copyRowsLimit?: number; correctFormat?: boolean; currentColClassName?: string; currentRowClassName?: string; customBorders?: boolean | any[]; data?: any[] | Function; dataSchema?: Object; dateFormat?: string; debug?: boolean; defaultDate?: string; disableVisualSelection?: boolean | string | any[]; dropdownMenu?: boolean | Object | any[]; editor?: string | Function | boolean; enterBeginsEditing?: boolean; enterMoves?: Object | Function; fillHandle?: boolean | string | Object; filter?: boolean; filteringCaseSensitive?: boolean; filters?: boolean; fixedColumnsLeft?: number; fixedRowsBottom?: number; fixedRowsTop?: number; format?: string; fragmentSelection?: boolean | string; ganttChart?: Object; headerTooltips?: boolean | Object; height?: number | Function; hiddenColumns?: boolean | Object; hiddenRows?: boolean | Object; invalidCellClassName?: string; label?: Object; language?: string; manualColumnFreeze?: boolean; manualColumnMove?: boolean | any[]; manualColumnResize?: boolean | any[]; manualRowMove?: boolean | any[]; manualRowResize?: boolean | any[]; maxCols?: number; maxRows?: number; mergeCells?: boolean | any[]; minCols?: number; minRows?: number; minSpareCols?: number; minSpareRows?: number; multiSelect?: boolean; nestedHeaders?: any[]; noWordWrapClassName?: string; observeChanges?: boolean; observeDOMVisibility?: boolean; outsideClickDeselects?: boolean; pasteMode?: string; persistentState?: boolean; placeholder?: any; placeholderCellClassName?: string; preventOverflow?: string | boolean; readOnly?: boolean; readOnlyCellClassName?: string; renderAllRows?: boolean; renderer?: string | Function; rowHeaders?: boolean | any[] | Function; rowHeaderWidth?: number | any[]; rowHeights?: any[] | Function | number | string; search?: boolean; selectOptions?: any[]; skipColumnOnPaste?: boolean; sortByRelevance?: boolean; sortFunction?: Function; sortIndicator?: boolean; source?: any[] | Function; startCols?: number; startRows?: number; stretchH?: string; strict?: boolean; tableClassName?: string | any[]; tabMoves?: Object; title?: string; trimDropdown?: boolean; trimWhitespace?: boolean; type?: string; uncheckedTemplate?: boolean | string; undo?: boolean; validator?: Function | RegExp; viewportColumnRenderingOffset?: number | string; viewportRowRenderingOffset?: number | string; visibleRows?: number; width?: number | Function; wordWrap?: boolean; isEmptyCol?: (col: number) => boolean; isEmptyRow?: (row: number) => boolean; // Add selectionMode?: string; // hooks afterAutofillApplyValues?: (startArea: any[], entireArea: any[]) => void; afterCellMetaReset?: () => void; afterChange?: (changes: any[], source: string) => void; afterChangesObserved?: () => void; afterColumnMove?: (startColumn: number, endColumn: number) => void; afterColumnResize?: (currentColumn: number, newSize: number, isDoubleClick: boolean) => void; afterColumnSort?: (column: number, order: boolean) => void; afterContextMenuDefaultOptions?: (predefinedItems: any[]) => void; afterContextMenuHide?: (context: Object) => void; afterContextMenuShow?: (context: Object) => void; afterCopyLimit?: (selectedRows: number, selectedColumnds: number, copyRowsLimit: number, copyColumnsLimit: number) => void; afterCreateCol?: (index: number, amount: number) => void; afterCreateRow?: (index: number, amount: number) => void; afterDeselect?: () => void; afterDestroy?: () => void; afterDocumentKeyDown?: (event: Event) => void; afterFilter?: (formulasStack: any[]) => void; afterGetCellMeta?: (row: number, col: number, cellProperties: Object) => void; afterGetColHeader?: (col: number, TH: Element) => void; afterGetColumnHeaderRenderers?: (array: any[]) => void; afterGetRowHeader?: (row: number, TH: Element) => void; afterGetRowHeaderRenderers?: (array: any[]) => void; afterInit?: () => void; afterLoadData?: (firstTime: boolean) => void; afterMomentumScroll?: () => void; afterOnCellCornerMouseDown?: (event: Object) => void; afterOnCellMouseDown?: (event: Object, coords: Object, TD: Element) => void; afterOnCellMouseOver?: (event: Object, coords: Object, TD: Element) => void; afterRemoveCol?: (index: number, amount: number) => void; afterRemoveRow?: (index: number, amount: number) => void; afterRender?: (isForced: boolean) => void; afterRenderer?: (TD: Element, row: number, col: number, prop: string | number, value: string, cellProperties: Object) => void; afterRowMove?: (startRow: number, endRow: number) => void; afterRowResize?: (currentRow: number, newSize: number, isDoubleClick: boolean) => void; afterScrollHorizontally?: () => void; afterScrollVertically?: () => void; afterSelection?: (r: number, c: number, r2: number, c2: number) => void; afterSelectionByProp?: (r: number, p: string, r2: number, p2: string) => void; afterSelectionEnd?: (r: number, c: number, r2: number, c2: number) => void; afterSelectionEndByProp?: (r: number, p: string, r2: number, p2: string) => void; afterSetCellMeta?: (row: number, col: number, key: string, value: any) => void; afterUpdateSettings?: () => void; afterValidate?: (isValid: boolean, value: any, row: number, prop: string | number, source: string) => void | boolean; beforeAutofill?: (start: Object, end: Object, data: any[]) => void; beforeCellAlignment?: (stateBefore: any, range: any, type: string, alignmentClass: string) => void; beforeChange?: (changes: any[], source: string) => void; beforeChangeRender?: (changes: any[], source: string) => void; beforeColumnMove?: (startColumn: number, endColumn: number) => void; beforeColumnResize?: (currentColumn: number, newSize: number, isDoubleClick: boolean) => void; beforeColumnSort?: (column: number, order: boolean) => void; beforeDrawBorders?: (corners: any[], borderClassName: string) => void; beforeFilter?: (formulasStack: any[]) => void; beforeGetCellMeta?: (row: number, col: number, cellProperties: Object) => void; beforeInit?: () => void; beforeInitWalkontable?: (walkontableConfig: Object) => void; beforeKeyDown?: (event: Event) => void; beforeOnCellMouseDown?: (event: Event, coords: Object, TD: Element) => void; beforeRemoveCol?: (index: number, amount: number, logicalCols?: any[]) => void; beforeRemoveRow?: (index: number, amount: number, logicalRows?: any[]) => void; beforeRender?: (isForced: boolean) => void; beforeRenderer?: (TD: Element, row: number, col: number, prop: string | number, value: string, cellProperties: Object) => void; beforeRowMove?: (startRow: number, endRow: number) => void; beforeRowResize?: (currentRow: number, newSize: number, isDoubleClick: boolean) => any; beforeSetRangeEnd?: (coords: any[]) => void; beforeStretchingColumnWidth?: (stretchedWidth: number, column: number) => void; beforeTouchScroll?: () => void; beforeValidate?: (value: any, row: number, prop: string | number, source: string) => void; construct?: () => void; init?: () => void; modifyCol?: (col: number) => void; modifyColHeader?: (column: number) => void; modifyColWidth?: (width: number, col: number) => void; modifyCopyableRange?: (copyableRanges: any[]) => void; modifyRow?: (row: number) => void; modifyRowHeader?: (row: number) => void; modifyRowHeight?: (height: number, row: number) => void; persistentStateLoad?: (key: string, valuePlaceholder: Object) => void; persistentStateReset?: (key: string) => void; persistentStateSave?: (key: string, value: any) => void; unmodifyCol?: (col: number) => void; } } interface HandsontableStatic { new (element: Element, options: Handsontable.Options): Handsontable; editors: any; } declare var Handsontable: HandsontableStatic; declare module "handsontable" { export var Handsontable: { new (element: Element, options: Handsontable.Options): Handsontable; editors: any; }; }
オプションとメソッドの追加、グローバルは書き換えしてます
ヘッダコメントはそのままにしてあります
使うものは逐次追加の方向で
EXCELのような入力ではなくお手軽に高速テーブルを目指しますので入力系にはそれほど踏み込みません
ファイルを新たに追加して操作用のメソッドを幾つか作成します
prototypeとかやってみましたが駄目でした
よく解っていないようです
/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 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(); } }
セルの位置選択、設定系がほとんどです
viewは公式にはないオブジェクトなので反則かもしれません
表示内に目的のセルがある場合にスクロールしないようにするために使用しています
全画面表示させたいのでそのためのメソッドを追加します
/Src/cm.ts
namespace cm { .... /* * ページコンテンツサイズ取得 */ export function getPageContentSize(id?: string) { if (typeof id === "undefined") { id = getActivePageId(); } let page = $("#" + id); let content = page.children(".ui-content"); let w = content.width(); let h = $(window).height(); let padding = getPagePaddingValue(id); if (isNaN(padding.top) === false) { h -= padding.top; } if (isNaN(padding.bottom) === false) { h -= padding.bottom; } return [w, h]; } export function getPagePaddingValue(id?: string) { if (typeof id === "undefined") { id = getActivePageId(); } let page = $("#" + id); let top = parseInt(page.css("padding-top"), 0); let bottom = parseInt(page.css("padding-bottom"), 0); let left = parseInt(page.css("padding-left"), 0); let right = parseInt(page.css("padding-right"), 0); return { top: top, bottom: bottom, left: left, right: right }; } /* * 縦スクロールバー設定 */ export function hideBodyOverflowY(value: boolean) { let v = ""; if (value === true) { v = "hidden"; } $("html, body").css("overflow-y", v); getActivePage().css("overflow-y", v); } /* * 横スクロールバー設定 */ export function hideBodyOverflowX(value: boolean) { let v = ""; if (value === true) { v = "hidden"; } $("html, body").css("overflow-x", v); getActivePage().css("overflow-x", v); } .... }
コンテンツのサイズを取得するためにはページのpadding値を取得してwindow高さから引けばよいはずです
Bodyのスクロールは邪魔になるので無効化メソッドも用意します
tableのサイズを合わせるのにはresizeイベントを使用しますが、毎回やるのはシンドイので一定時間サイズ変更が止まっていたら実行できるように遅延タイマーを作成します
/Src/cm.ts
/* * 遅延タイマー */ class DelayTimer { private _timer = 0; private _millisec = 0; get millisec(): number { return this._millisec; } constructor(sec: number) { this._millisec = sec; } timeout(callback: Function, millisec?: number) { if (this._timer) { clearTimeout(this._timer); } if (typeof millisec === "undefined") { millisec = this._millisec; } this._timer = setTimeout(function () { callback(); }, millisec); } }
コンストラクタで遅延時間を指定しておき、resizeイベント内で使用します
こんな感じ
let _resizeDelay = new DelayTimer(500); // 500ms止まったら実行 $(window).on("resize", function (event: Event) { _resizeDelay.timeout(function () { // 処理実行 }); });
最後にCSS
この辺はお好みで
ゆったりしたデザインになっています
/Src/cm.css
/* handsontable */ .handsontable th { /* 列ヘッダ */ background-color: #eee; height: 30px; } div.handsontable tbody th.ht__highlight, /* 選択行ヘッダ */ div.handsontable thead th.ht__highlight { /* 選択列ヘッダ */ background-color: #ddd; color: #000; } div.handsontable tbody > tr > td.highlight, /* 選択セル */ div.handsontable tbody > tr > td.current-row { /* 選択行 */ background-color: rgba(51,136,204, 0.2); } .handsontable th, /* ヘッダ */ .handsontable td { /* セル */ vertical-align: middle; text-shadow: none; font-size: 14px; color: #000; } .handsontable .htDimmed { /* ReadOlnyセル */ color: #000; } .handsontable tbody > tr:nth-child(even) > td { /* 偶数列 */ background-color: #f8f8f8; } .handsontable tbody > tr:nth-child(odd) > td { /* 奇数列 */ background-color: transparent; } .handsontable .ht_clone_top tr:first-child th, .handsontable .ht_clone_top_left_corner tr:first-child th { border-top-width: 0; /* 上端部罫線不要 */ } .handsontable .ht_clone_left th:first-child, .handsontable .ht_clone_top_left_corner tr:first-child th { border-left-width: 0; /* 左端部罫線不要 */ }
準備完了