UNPKG

tym-table-editor

Version:

'tym-table-editor' is a simple table editor component.

1,253 lines (1,252 loc) 64.5 kB
import * as i0 from '@angular/core'; import { Component, Input, HostBinding, InjectionToken, Inject, NgModule } from '@angular/core'; /*! * tym-table-editor.js * Copyright (c) 2022 shinichi tayama * Released under the MIT license. * see https://opensource.org/licenses/MIT */ const AUTO = 'auto'; const SCROLL = 'scroll'; const num2 = (n) => ('00' + n).slice(-2); const FIRST_ELEMENT_CHILD = (elm) => elm?.firstElementChild; class TymTableEditorComponent { _elmRef; _renderer; static idnum = 0; _thisElm; // this table element row = 30; col = 20; defs = []; data = [['']]; menu = (event, row1, col1, row2, col2) => false; _opts = {}; set opts(opts) { this._opts = opts; this._setopt(); } _setopt() { const tableElm = FIRST_ELEMENT_CHILD(this._thisElm); const tbodyElm = FIRST_ELEMENT_CHILD(tableElm); setTimeout(() => { if (tbodyElm) { const [cl, opts] = [tbodyElm.classList, this._opts]; if (opts.whiteSpaceNoWrap) { cl.add('nowrap'); } else { cl.remove('nowrap'); } if (opts.noVerticalResize) { cl.add('novrsz'); } else { cl.remove('novrsz'); } } }); [this.thFont, this.thWidth, this.thBgColor, this.thBorder, this.thWidth1, this.thWidth2, this.tdShadow] = (this._opts.flatDesign) ? ['700 9pt/12pt system-ui', '1.40em', '#eee', 'solid 1px #666', '1px 1px 1px 0', '0 1px 1px 1px', 'none'] : ['700 8pt/10pt system-ui', '1.25em', '#ccc', 'outset 2px #eee', '2px', '2px', '.5px .5px 0px #000 inset']; } /** Host Binding style */ thFont; //700 8pt/10pt system-ui / 700 9pt/12pt system-ui thWidth; //1.25em / 1.40em thBgColor; //#ccc / #eee thBorder; //outset 2px #eee / solid 1px #666 thWidth1; //2px / 0 1px 1px 0 thWidth2; //2px / 0 1px 1px 0 tdShadow; //.5px .5px 0px #000 inset / none /** * コンストラクタ * * @memberof TymTreeViewComponent */ constructor(_elmRef, _renderer) { this._elmRef = _elmRef; this._renderer = _renderer; this._thisElm = this._elmRef.nativeElement; } ; /** * ビューを初期化した後の処理 */ ngAfterViewInit() { //--------------------------------------------------------------- // .. const thisElm = this._thisElm; const tableElm = FIRST_ELEMENT_CHILD(thisElm); const tbodyElm = FIRST_ELEMENT_CHILD(tableElm); const contentName = '_tymtableeditor-' + TymTableEditorComponent.idnum++; tableElm.setAttribute(contentName, ''); //--------------------------------------------------------------- // .. const maxrow = this.row; const maxcol = this.col; const nosels = { r1: -1, c1: -1, r2: -1, c2: -1 }; const editordefs = new Map(); /** 0:not move, 1:cell, 2:col, 3:row */ let mousemv = 0; let selects = { ...nosels }; let cpysels = { ...nosels }; //--------------------------------------------------------------- // .. let editElm = null; // edited td cell let crntElm = null; // current td cell //--------------------------------------------------------------- // .. const { overflowX, overflowY } = window.getComputedStyle(thisElm); const scrollElm = (!(overflowX == AUTO || overflowX == SCROLL || overflowY == AUTO || overflowY == SCROLL)) ? thisElm.parentElement : thisElm; //--------------------------------------------------------------- // .. const cell = (r, c) => tbodyElm.children[r]?.children[c]; const crntRange = () => { const [r, c] = cellRowCol(crntElm); return { r1: r, c1: c, r2: r, c2: c }; }; const cellRowCol = (td) => (td) ? [td.parentElement.rowIndex, td.cellIndex] : [1, 1]; //--------------------------------------------------------------- // create element const createElm = (name) => this._renderer.createElement(name); //--------------------------------------------------------------- // create th element const createTh = (tx) => { const th = createElm('th'); th.innerText = tx; return th; }; //--------------------------------------------------------------- // create td element const createTd = () => { const td = createElm('td'); td.tabIndex = -1; return td; }; //--------------------------------------------------------------- // get TYM_EDITOR_DEF const _getEditorDef = (th) => th.onprogress(new ProgressEvent('')); const getEditorDef = (td) => _getEditorDef(headTrElm.children[td.cellIndex]); //--------------------------------------------------------------- // get editFunc const getEditFunc = (td) => { const def = getEditorDef(td); return (def?.editfnc) ? (td, val) => def.editfnc(td, val, def.type) : undefined; }; //--------------------------------------------------------------- // get viewFunc const getViewFunc = (td) => { const def = getEditorDef(td); return (def?.viewfnc) ? (val) => def.viewfnc(val, def.type) : undefined; }; //--------------------------------------------------------------- // get align / background style const getDynamicStyle = (td) => { const def = getEditorDef(td); const style = '' + ((def?.align) ? `text-align:${def?.align};` : '') + ((def?.readonly) ? 'background-color:#f8f8f8;' : ''); return (style == '') ? '' : `table[${contentName}] td:nth-child(${td.cellIndex + 1}){${style}}`; }; //--------------------------------------------------------------- // get style texts const getStyleText = () => Array.from(headTrElm.children).map(th => getDynamicStyle(th)).join(''); //--------------------------------------------------------------- // create header th element const createRowTh = (col, def) => { const tx = num2(col); const th = createTh(tx); th.onprogress = (e) => { const _editordef = { ...def }; return _editordef; }; return th; }; //--------------------------------------------------------------- // .. const classlist = (elm) => elm.classList; const classrm = (elm, cls) => classlist(elm).remove(cls); const classadd = (elm, cls) => classlist(elm).add(cls); /**************************************************************** * set current element (crntElm, style) * @param elm 対象エレメント */ const setCurrent = (elm) => { const crn = 'crn'; if (crntElm) classrm(crntElm, crn); classadd(elm, crn); crntElm = elm; }; /**************************************************************** * get select range * @param _selects target RANGE * @returns { r1: number, c1: number, r2: number, c2: number } */ const range = (_selects = selects) => { let { r1, c1, r2, c2 } = (_selects.c2 < 0) ? (crntElm) ? crntRange() : nosels : _selects; [r1, r2] = r2 > r1 ? [r1, r2] : [r2, r1]; [c1, c2] = c2 > c1 ? [c1, c2] : [c2, c1]; return { r1: r1, c1: c1, r2: r2, c2: c2 }; }; /**************************************************************** * exec range function * @param fnc call back function * @param _selects target RANGE */ const execRange = (fnc, _selects = selects) => { const { r1, c1, r2, c2 } = range(_selects); if (c2 < 0) return; for (let _row = r1; _row <= r2; _row++) { for (let _col = c1; _col <= c2; _col++) { fnc(cell(_row, _col), _col == c2); } } }; //--------------------------------------------------------------- // set selection range functions const checkset = (max, n) => (-1 > n) ? -1 : (n > max) ? max : n; const setSelRange1stRowCol = (r1, c1) => [selects.r1, selects.c1] = [checkset(maxrow, r1), checkset(maxcol, c1)]; const setSelRangeLstRowCol = (r2, c2) => [selects.r2, selects.c2] = [checkset(maxrow, r2), checkset(maxcol, c2)]; const setSelRange1st = (td) => [selects.r1, selects.c1] = cellRowCol(td); const setSelRangeLst = (td) => [selects.r2, selects.c2] = cellRowCol(td); /**************************************************************** * clear selection range style * @param clear true:clear "selects" */ const clearSelRangeStyle = (clear) => { execRange(elm => classrm(elm, 'msel')); if (clear) selects = { ...nosels }; }; /**************************************************************** * set selection range style */ const drawSelRangeStyle = () => execRange(elm => classadd(elm, 'msel')); /**************************************************************** * set converted text to cell (if converted, save to dataset-val) * @param elm 対象エレメント * @param val 値 */ const setText = (elm, val) => { const viewfnc = getViewFunc(elm); elm.innerText = (viewfnc) ? viewfnc(elm.dataset.val = val) : val; }; /**************************************************************** * get real text (if converted, restore from dataset-val) * @param elm 対象エレメント * @returns 値 */ const text = (elm) => elm.dataset.val || elm.innerText; /**************************************************************** * set data to table * @param data データ */ const setData2Table = (data) => { const maxcol = data.reduce((a, b) => a.length > b.length ? a : b); setSelRange1stRowCol(1, 1); setSelRangeLstRowCol(data.length, maxcol.length); let r = 0, c = 0; execRange((elm, eol) => { const v = data[r][c] || ''; setText(elm, v); if (eol) c = 0, r++; else c++; }); }; /**************************************************************** * set data to range * @param data データ * @param row 行番号 * @param col 列番号 */ const setData2Range = (data, row, col) => { const maxcol = data.reduce((a, b) => a.length > b.length ? a : b); clearSelRangeStyle(); setSelRange1stRowCol(row, col); setSelRangeLstRowCol(row + data.length - 1, col + maxcol.length - 1); let hists = []; let r = 0, c = 0; execRange((elm, eol) => { const v = data[r][c] || ''; hists.push({ r: row + r, c: col + c, b: text(elm), a: v }); setText(elm, v); classadd(elm, 'msel'); if (eol) c = 0, r++; else c++; }); // 編集履歴に追加 addhists(hists); }; /**************************************************************** * set public function (setData) */ this.setData = (data, row1, col1) => { if (row1 && col1) { setData2Range(data, row1, col1); } else { clearAllData(); setData2Table(data); } }; /**************************************************************** * set public function (getData) */ this.getData = (rownum, colnum, row, col) => { const [r1, c1, r2, c2] = (row && col) ? [rownum, colnum, row, col] : [1, 1, rownum, colnum]; let cols = []; let data = []; for (let _row = r1; _row <= r2; _row++) { for (let _col = c1; _col <= c2; _col++) { const td = cell(_row, _col); cols.push((td) ? td.dataset.val || td.innerText : ''); } data.push(cols); cols = []; } return data; }; //--------------------------------------------------------------- // renumber row header const renumberRow = () => { for (let index = 1; index <= maxrow; index++) { const tr = tbodyElm.childNodes[index]; const th = FIRST_ELEMENT_CHILD(tr); th.innerText = num2(index); } }; //--------------------------------------------------------------- // renumber col header const renumberCol = () => { for (let index = 1; index <= maxcol; index++) { const th = headTrElm.childNodes[index]; th.innerText = num2(index); } }; //--------------------------------------------------------------- // create row const createrow = (row) => { const tr = createElm('tr'); tr.appendChild(createTh(num2(row))); for (let index = 1; index <= maxcol; index++) { tr.appendChild(createTd()); } return tr; }; //--------------------------------------------------------------- // view insert / remove row const viewInsertRemoveRow = (insRowNum, rmRowNum) => { clearSelRangeStyle(true); clearCpyRangeStyle(); const [insRow, rmRow] = [tbodyElm.childNodes[insRowNum], tbodyElm.childNodes[rmRowNum]]; tbodyElm.insertBefore(createrow(insRowNum), insRow); tbodyElm.removeChild(rmRow); if (crntElm && !crntElm?.isConnected) { crntElm = null; } renumberRow(); }; //--------------------------------------------------------------- // history insert / remove row const histInsertRemoveRow = (row, rmRowNum, ir) => { const trElm = tbodyElm.childNodes[rmRowNum]; let hists = [{ r: row, c: 0, b: '', a: ir }]; // c:0,a:i/r => insert/remove row for (let index = 1; index <= maxcol; index++) { const v = text(trElm.childNodes[index]); if (v != '') hists.push({ r: rmRowNum, c: index, b: v, a: '' }); } addhists(hists); }; //--------------------------------------------------------------- // insert row const insertRow = (row) => viewInsertRemoveRow(row, maxrow); /**************************************************************** * set public function (insertRow) */ this.insertRow = (row) => { histInsertRemoveRow(row, maxrow, 'i'); insertRow(row); }; //--------------------------------------------------------------- // remove row const removeRow = (row) => viewInsertRemoveRow(maxrow + 1, row); /**************************************************************** * set public function (removeRow) */ this.removeRow = (row) => { histInsertRemoveRow(row, row, 'r'); removeRow(row); }; //--------------------------------------------------------------- // view insert / remove col const viewInsertRemoveCol = (insColNum, rmColNum, def) => { clearSelRangeStyle(true); clearCpyRangeStyle(); const [insHeadCol, rmHeadCol] = [headTrElm.childNodes[insColNum], headTrElm.childNodes[rmColNum]]; headTrElm.insertBefore(createRowTh(insColNum, def), insHeadCol); headTrElm.removeChild(rmHeadCol); for (let index = 1; index <= maxrow; index++) { const tr = tbodyElm.childNodes[index]; const [insCol, rmCol] = [tr.childNodes[insColNum], tr.childNodes[rmColNum]]; tr.insertBefore(createTd(), insCol); tr.removeChild(rmCol); } if (crntElm && !crntElm?.isConnected) { crntElm = null; } styleElement.innerText = getStyleText(); renumberCol(); }; //--------------------------------------------------------------- // history insert col const histInsertRemoveCol = (col, rmColNum, ir) => { const th = headTrElm.children[col]; const def = _getEditorDef(th); let hists = [{ r: 0, c: col, b: '', a: ir, d: def }]; // r:0,a:i/r => insert/remove col for (let index = 1; index <= maxrow; index++) { const tr = tbodyElm.childNodes[index]; const v = text(tr.childNodes[rmColNum]); if (v != '') hists.push({ r: index, c: rmColNum, b: v, a: '' }); } addhists(hists); }; //--------------------------------------------------------------- // insert col const insertCol = (col, def) => viewInsertRemoveCol(col, maxcol, def); //--------------------------------------------------------------- // remove col const removeCol = (col) => viewInsertRemoveCol(maxcol + 1, col); /**************************************************************** * set public function (insertCol) */ this.insertCol = (col, def) => { histInsertRemoveCol(col, maxcol, 'i'); insertCol(col, def); }; /**************************************************************** * set public function (removeCol) */ this.removeCol = (col) => { histInsertRemoveCol(col, col, 'r'); removeCol(col); }; /**************************************************************** * set public function (copy) */ this.copy = () => elm2clipboard(); /**************************************************************** * set public function (paste) */ this.paste = () => clipboard2elm(); /**************************************************************** * set public function (delete) */ this.delete = () => deleteTexts(); /**************************************************************** * set public function (undo) */ this.undo = () => { undoredo(false); }; /**************************************************************** * set public function (redo) */ this.redo = () => { undoredo(true); }; /**************************************************************** * clear all data in table (history, select range/style, copy range/style) */ const clearAllData = () => { tbodyElm.childNodes.forEach(tr => { const _tr = tr; if (_tr.rowIndex > 0) tr.childNodes.forEach(cell => { const td = cell; if (td.cellIndex > 0) setText(td, ''); }); }); clearSelRangeStyle(true); clearCpyRangeStyle(); clearHistory(); }; //--------------------------------------------------------------- // clipboard data const clipboard = navigator.clipboard; let clipdata; let copydat = []; /**************************************************************** * clear copy range / style */ const clearCpyRangeStyle = () => { execRange(elm => classrm(elm, 'cpy'), cpysels); cpysels = { ...nosels }; }; /**************************************************************** * set copy range / style * @param range */ const setCpyRangeStyle = (range) => { cpysels = range; execRange(elm => classadd(elm, 'cpy'), cpysels); }; /**************************************************************** * clear copy data (clear clipboard data) */ const clearCpyData = async () => { copydat = []; try { await clipboard.writeText(''); } catch (err) { console.error('failed to writeText: ', err); } }; /**************************************************************** * クリップボードからの(\r\n,/\t区切りテキストの)貼り付け (use copydat) */ const clipboard2elm = async () => { if (!crntElm) return; let data = []; try { const text = await clipboard.readText(); if (clipdata != text) { clearCpyRangeStyle(); } const rows = text.split('\r\n'); rows.forEach(row => data.push(row.split('\t'))); } catch (err) { console.error('failed to readText: ', err); data = copydat; } const [r, c] = cellRowCol(crntElm); setData2Range(data, r, c); }; /**************************************************************** * クリップボードへの(\r\n,/\t区切りテキストの)設定 (set copydat) */ const elm2clipboard = async () => { clearCpyRangeStyle(); let cols = []; copydat = []; execRange((elm, eol) => { cols.push(text(elm)); if (eol) { copydat.push(cols); cols = []; } }); let rows = []; copydat.forEach(cols => rows.push(cols.join('\t'))); setCpyRangeStyle((selects.c2 >= 0) ? { ...selects } : crntRange()); try { clipdata = rows.join('\r\n'); await clipboard.writeText(rows.join('\r\n')); } catch (err) { console.error('failed to writeText: ', err); } }; /**************************************************************** * 選択範囲を消す */ const deleteTexts = () => { let hists = []; execRange(elm => { const [r, c] = cellRowCol(elm); hists.push({ r: r, c: c, b: text(elm), a: '' }); setText(elm, ''); }); // 編集履歴に追加 addhists(hists); }; //--------------------------------------------------------------- // prepare def(TYM_EDITOR_DEF) data this.defs.forEach(def => editordefs.set(def.col, def)); //--------------------------------------------------------------- // create : table - tbody - [1st tr:header - th] const headTrElm = tbodyElm.appendChild(createElm('tr')); { headTrElm.appendChild(createTh('')); // top&left headTrElm.lastElementChild.onprogress = () => { }; for (let index = 1; index <= maxcol; index++) { headTrElm.appendChild(createRowTh(index, editordefs.get(index))); // top } } //--------------------------------------------------------------- // create : table - tbody - [tr - td] { for (let index = 1; index <= maxrow; index++) { tbodyElm.appendChild(createrow(index)); } } //--------------------------------------------------------------- // create : style const styleElement = createElm('style'); thisElm.append(styleElement); styleElement.innerText = styleElement.innerText = getStyleText(); //--------------------------------------------------------------- // set cell data setData2Table(this.data || [['']]); //--------------------------------------------------------------- // set width headTrElm.childNodes.forEach(node => { const elm = node; if (elm.tagName == 'TH') { // #comment 除去 const realStyle = window.getComputedStyle(elm); elm.style.width = (elm.clientWidth > 200) ? '200px' : realStyle.width; } }); tableElm.style.width = 'fit-content'; //--------------------------------------------------------------- // set 1st cell clearSelRangeStyle(); setCurrent(cell(1, 1)); /**************************************************************** * mouse down event * @param e MouseEvent */ const event_mousedown = (e) => { let td = e.target; if (e.button == 2) { const { r1, c1, r2, c2 } = range(); const [r, c] = cellRowCol(td); if (r1 <= r && r <= r2 && c1 <= c && c <= c2) return; } if (e.detail == 1) { clearSelRangeStyle(); if (td.tagName == 'TH') { // 1st click header:th or row top:th element const [thRowIx, thColIx] = cellRowCol(td); const [crRowIx, crColIx] = (e.shiftKey) ? ((crntElm) ? cellRowCol(crntElm) : [1, 1]) : [thRowIx, thColIx]; const isHead = (thRowIx == 0); selects = (isHead) ? { r1: 1, c1: crColIx, r2: maxrow, c2: thColIx } : { r1: crRowIx, c1: 1, r2: thRowIx, c2: maxcol }; if (e.shiftKey) { mousemv = 0; drawSelRangeStyle(); } else { mousemv = (isHead) ? 2 : 3; setCurrent(cell(selects.r1, selects.c1)); } } else { // 1st click => change current if (e.shiftKey) { const [r, c] = cellRowCol(crntElm); setSelRange1stRowCol(r, c); setSelRangeLst(td); drawSelRangeStyle(); mousemv = 0; } else { setCurrent(td); setSelRange1st(td); mousemv = 1; } } } if (e.detail == 2) { if (td.tagName == 'TH') { // duble click => widen cell clearSelRangeStyle(); widen(td); drawSelRangeStyle(); } else { // duble click => change edit mode toEdit(td); e.preventDefault(); } } }; tableElm.addEventListener('mousedown', event_mousedown); /**************************************************************** * mouse move event * @param e MouseEvent */ const event_mousemove = (e) => { if (mousemv == 0) return; const td = e.target; let [r, c] = cellRowCol(td); if (mousemv == 1 && selects.r2 == r && selects.c2 == c) return; if (mousemv == 2) r = selects.r2; if (mousemv == 3) c = selects.c2; clearSelRangeStyle(); [selects.r2, selects.c2] = [r, c]; drawSelRangeStyle(); }; tableElm.addEventListener('mousemove', event_mousemove); /**************************************************************** * mouse leave event * @param e MouseEvent */ const event_mouseleave = (e) => { if (mousemv == 0) return; clearSelRangeStyle(true); mousemv = 0; }; tableElm.addEventListener('mouseleave', event_mouseleave); /**************************************************************** * mouse up event * @param e MouseEvent */ const event_mouseup = (e) => { const td = e.target; const [r, c] = cellRowCol(td); if (mousemv == 1 && selects.r1 == r && selects.c1 == c) { clearSelRangeStyle(true); } if (mousemv == 2 && selects.c1 == c) drawSelRangeStyle(); if (mousemv == 3 && selects.r1 == r) drawSelRangeStyle(); mousemv = 0; crntElm?.focus({ preventScroll: true }); }; tableElm.addEventListener('mouseup', event_mouseup); /**************************************************************** * contextmenu event * @param e MouseEvent */ const event_contextmenu = (e) => { const { r1, c1, r2, c2 } = range(); const ret = this.menu(e, r1, c1, r2, c2); if (ret) { e.preventDefault(); } return ret; }; tableElm.addEventListener('contextmenu', event_contextmenu); /**************************************************************** * keypress event * @param e KeyboardEvent */ const event_keypress = (e) => { const td = e.target; if (!editElm) { toEdit(td, e.key); } }; tableElm.addEventListener('keypress', event_keypress); let escapecnt = 0; const { offsetWidth: zzWidth, offsetHeight: zzHeight } = cell(0, 0); /**************************************************************** * key down event * @param e KeyboardEvent */ const event_keydown = (e) => { const thisCell = e.target; const [thisRowIx, thisColIx] = cellRowCol(thisCell); const rangeAll = { r1: 1, c1: 1, r2: maxrow, c2: maxcol }; //------------------------------------------------------------- /** 矢印によるフォーカスの移動 */ const arrow = (opt, rowix, colix) => { const td = cell(rowix, colix); { const ofLeft = td.offsetLeft - zzWidth; if (ofLeft < scrollElm.scrollLeft) scrollElm.scroll({ left: ofLeft }); } { const ofLeft = td.offsetLeft + td.clientWidth + scrollElm.clientLeft - scrollElm.clientWidth; if (ofLeft > scrollElm.scrollLeft) scrollElm.scroll({ left: ofLeft }); } { const ofTop = td.offsetTop - zzHeight; if (ofTop < scrollElm.scrollTop) scrollElm.scroll({ top: ofTop }); } td.blur(); td.focus(); setCurrent(td); e.preventDefault(); return td; }; //------------------------------------------------------------- /** 上下左右端用のスクロール指示情報取得 */ const getScroll = (row, col) => { let scroll = {}; const { r1, c1, r2, c2 } = rangeAll; if (col == c1) scroll.left = 0; if (col == c2) scroll.left = 9999; if (row == r1) scroll.top = 0; if (row == r2) scroll.top = 9999; return scroll; }; //------------------------------------------------------------- /** 矢印によるフォーカスの上下左右移動 */ const updownleftright = (isUpDown, isUpOrLeft, range = false) => { const b = (range && selects.c2 >= 0); const { r1, c1, r2, c2 } = b ? selects : rangeAll; let [rowIx, colIx] = [thisRowIx, thisColIx]; const [A, B, C, D, E] = (isUpDown) ? (isUpOrLeft) ? [(rowIx > r1), -1, 0, r2, (colIx > c1) ? colIx - 1 : c2] : [(rowIx < r2), +1, 0, r1, (colIx < c2) ? colIx + 1 : c1] : (isUpOrLeft) ? [(colIx > c1), 0, -1, (rowIx > r1) ? rowIx - 1 : r2, c2] : [(colIx < c2), 0, +1, (rowIx < r2) ? rowIx + 1 : r1, c1]; [rowIx, colIx] = A ? [rowIx + B, colIx + C] : ((b) ? [D, E] : [rowIx, colIx]); return arrow(getScroll(rowIx, colIx), rowIx, colIx); }; //------------------------------------------------------------- /** 矢印によるフォーカスの移動 */ const arrowmove = (isUpDown, isShift, dir) => { if (isShift) { clearSelRangeStyle(); if (selects.c1 < 0) { setSelRange1st(thisCell); } const _td = updownleftright(isUpDown, dir); setSelRangeLst(_td); drawSelRangeStyle(); } else { clearSelRangeStyle(true); updownleftright(isUpDown, dir); } }; //------------------------------------------------------------- // 編集モード時のキー処理 if (editElm) { switch (e.key) { case 'Tab': updownleftright(false, e.shiftKey, true); break; case 'Enter': updownleftright(true, e.shiftKey, true); break; case 'Escape': thisCell.innerText = beforeValue; thisCell.blur(); thisCell.focus(); e.preventDefault(); break; default: break; } } //------------------------------------------------------------- // 表示モード時のキー処理 else { escapecnt--; switch (e.key) { case 'ArrowDown': arrowmove(true, e.shiftKey, false); break; case 'ArrowUp': arrowmove(true, e.shiftKey, true); break; case 'ArrowRight': arrowmove(false, e.shiftKey, false); break; case 'ArrowLeft': arrowmove(false, e.shiftKey, true); break; case 'Home': if (e.ctrlKey) { arrow(getScroll(1, 1), 1, 1); } else { arrow(getScroll(thisRowIx, 1), thisRowIx, 1); } break; case 'End': if (e.ctrlKey) { arrow(getScroll(maxrow, maxcol), maxrow, maxcol); } else { arrow(getScroll(thisRowIx, maxcol), thisRowIx, maxcol); } break; case 'Tab': updownleftright(false, e.shiftKey, true); break; case 'Enter': updownleftright(true, e.shiftKey, true); break; case 'F2': toEdit(thisCell); e.preventDefault(); break; case 'Backspace': const newtd = toEdit(thisCell, ''); if (newtd) setText(newtd, ''); e.preventDefault(); break; case 'Delete': if (e.shiftKey) { // cut elm2clipboard(); } deleteTexts(); e.preventDefault(); break; case 'x': if (e.ctrlKey) { // cut elm2clipboard(); deleteTexts(); e.preventDefault(); } break; case 'c': if (e.ctrlKey) { // copy elm2clipboard(); e.preventDefault(); } break; case 'v': if (e.ctrlKey) { // paste clipboard2elm(); e.preventDefault(); } break; case 'Insert': if (e.ctrlKey) { // copy elm2clipboard(); e.preventDefault(); } if (e.shiftKey) { // paste clipboard2elm(); e.preventDefault(); } break; case 'z': case 'Z': if (e.ctrlKey) { // true(redo: Ctrl+Shift+z), false(undo: Ctrl+z) undoredo(e.shiftKey); e.preventDefault(); } break; case 'y': if (e.ctrlKey) { // redo: Ctrl+y undoredo(true); e.preventDefault(); } break; case 'Escape': if (escapecnt < 0) { escapecnt = 1; } else if (escapecnt >= 0) { clearCpyRangeStyle(); clearCpyData(); } break; } } }; tableElm.addEventListener('keydown', event_keydown); /**************************************************************** * Escapeキー戻値用 */ let beforeValue; /**************************************************************** * フォーカスアウトイベント処理,表示モードにする */ const editBlue = () => { if (editElm) { const thisValue = editElm.innerText; editElm.scrollLeft = 0; editElm.removeEventListener('blur', editBlue); editElm.removeAttribute('contentEditable'); const [r, c] = cellRowCol(editElm); setText(editElm, thisValue); if (thisValue != beforeValue && beforeValue != null) { const hist = { r: r, c: c, b: beforeValue, a: thisValue }; addhists([hist]); } if (this._opts.editModeAutoResize) { widen(headTrElm.children[editElm.cellIndex]); } } beforeValue = null; editElm = null; }; /**************************************************************** * 対象セルを編集モードにする * @param td 対象エレメント * @param val キーイベントによる値 * @returns 対象セルエレメント または 作り直したセルエレメント */ const toEdit = (td, val) => { const realStyle = window.getComputedStyle(td); if (realStyle.backgroundColor == 'rgb(248, 248, 248)') return null; clearCpyRangeStyle(); const [r, c] = cellRowCol(td); const editfnc = getEditFunc(td); beforeValue = text(td); if (editfnc) { // 編集機能あり editfnc(td, val || beforeValue).then(v => { if (v != null) { setText(td, v); addhists([{ r: r, c: c, b: beforeValue, a: v }]); } }); return td; } else { // 編集機能なし td.innerText = beforeValue; td.contentEditable = 'true'; const newtd = td.parentElement?.insertBefore(td.cloneNode(true), td); setCurrent(newtd); newtd.addEventListener('blur', editBlue); const [sel, rng] = [window.getSelection(), document.createRange()]; rng.selectNodeContents(newtd); sel?.removeAllRanges(); sel?.addRange(rng); td.remove(); editElm = newtd; return newtd; } }; //--------------------------------------------------------------- // .. let history = []; let history_pos = -1; let history_dwn = false; /**************************************************************** * 編集履歴:履歴クリア */ const clearHistory = () => { history.length = 0; history_pos = -1; history_dwn = false; }; /**************************************************************** * 編集履歴:履歴追加 * @param hists 履歴情報 */ const addhists = (hists) => { if ((history.length - 1 == history_pos && history_dwn) || history_pos == -1) { history.push(hists); history_pos++; } else { if (history_dwn) history_pos++; history.splice(history_pos, 99999, hists); } history_dwn = true; }; //--------------------------------------------------------------- // .. const undoredoRange = (r1, c1, r2, c2) => { if (r1 != r2 || c1 != c2) { setSelRange1stRowCol(r1, c1); setSelRangeLstRowCol(r2, c2); drawSelRangeStyle(); } const td = cell(r1, c1); td.focus(); setCurrent(td); }; /**************************************************************** * 編集履歴:アンドゥ&リドゥ処理 * @param redo true:リドゥ, false:アンドゥ */ const undoredo = (redo) => { let pos = history_pos; if ((pos < 0) || (!redo && pos <= 0 && !history_dwn) || (redo && history.length - 1 == pos && history_dwn)) return; if (redo != history_dwn) { history_dwn = redo; } else { pos += (redo ? history.length - 1 > pos : pos > 0) ? (redo ? 1 : -1) : 0; } const hist = history[pos]; history_pos = pos; const [v1, v2] = [hist[0], hist[hist.length - 1]]; let index; clearCpyRangeStyle(); clearSelRangeStyle(); if (v1.c == 0) { // c:0,a:r => remove row / c:0,a:i => insert row const { a, r } = v1; if (a == 'i') { if (redo) { insertRow(r); } else { removeRow(r); } } else if (a == 'r') { if (redo) { removeRow(r); } else { insertRow(r); } } index = (redo) ? hist.length : 1; undoredoRange(r, 1, r, maxcol); } else if (v1.r == 0) { // r:0,a:r => remove col / r:0,a:i => insert col const { a, c, d } = v1; if (a == 'i') { if (redo) { insertCol(c, d); } else { removeCol(c); } } else if (a == 'r') { if (redo) { removeCol(c); } else { insertCol(c, d); } } index = (redo) ? hist.length : 1; undoredoRange(1, c, maxrow, c); } else { index = 0; undoredoRange(v1.r, v1.c, v2.r, v2.c); } for (; index < hist.length; index++) { const { r, c, a, b } = hist[index]; setText(cell(r, c), (redo) ? a : b); } }; /**************************************************************** * カラムリサイズ * @param thElm 対象エレメント */ const widen = (thElm) => { const scrollLeft = scrollElm.scrollLeft; // スクロール状態を保持 const cntnrRect = FIRST_ELEMENT_CHILD(scrollElm).getBoundingClientRect(); const cellElms = tableElm.querySelectorAll(`tbody tr td:nth-child(${thElm.cellIndex + 1})`); if (cellElms.length <= 0) return; // padding, margin サイズを求める const realStyle = window.getComputedStyle(cellElms.item(0)); const padSize = '' + `${realStyle.paddingLeft} + ${realStyle.paddingRight} + ` + `${realStyle.marginLeft} + ${realStyle.marginRight}`; thElm.style.width = '4em'; // 一時的にセルのサイズを縮める tbodyElm.classList.add('widen'); // 全ての行をチェックし最大widthを求める (tbody > tr > td) let maxWidth = Array.from(cellElms).reduce((a, b) => a.scrollWidth > b.scrollWidth ? a : b).scrollWidth; thElm.style.width = `calc(${Math.min(maxWidth, (cntnrRect.width * .7))}px + ${padSize})`; tbodyElm.classList.remove('widen'); scrollElm.scrollLeft = scrollLeft; // スクロール状態を回復 }; } //----------------------------------------------------------------- // 公開関数 //----------------------------------------------------------------- /****************************************************************** * セルの文字列を取得する * @param rownum 取得する行数 * @param colnum 取得する列数 * @param fnc 取得用コールバック関数 */ getCells(rownum, colnum, fnc) { const tableElm = FIRST_ELEMENT_CHILD(this._thisElm); const tbodyElm = FIRST_ELEMENT_CHILD(tableElm); for (let _row = 1; _row <= rownum; _row++) { for (let _col = 1; _col <= colnum; _col++) { const td = tbodyElm.children[_row].children[_col]; fnc(_row, _col, td.innerText, colnum == _col); } } } /****************************************************************** * テーブルにデータを設定する * @param data 設定するデータ * @param row1 設定する開始行番号 * @param col1 設定する開始列番号 */ setData(data, row1, col1) { } /****************************************************************** * テーブルからデータを取得する * @param rownum 取得する行数 or 取得する開始行番号 * @param colnum 取得する列数 or 取得する開始列番号 * @param row 取得する終了行番号 * @param col 取得する終了列番号 * @returns data: any[][] */ getData(rownum, colnum, row, col) { return []; } /****************************************************************** * テーブルに行の挿入する * @param row 挿入する位置の行番号 */ insertRow(row) { } /****************************************************************** * テーブルから行を削除する * @param row 削除する位置の行番号 */ removeRow(row) { } /****************************************************************** * テーブルに列の挿入する * @param col 挿入する位置の列番号 * @param def 定義 */ insertCol(col, def) { } /****************************************************************** * テーブルから列を削除する * @param col 削除する位置の列番号 */ removeCol(col) { } /****************************************************************** * 選択範囲のセルをコピーする */ copy() { } /****************************************************************** * カレントセルから貼り付ける */ paste() { } /****************************************************************** * 選択範囲のテキストを消去する */ delete() { } /**********