<template>
<div
ref="shell"
class="CompCssWAggridVue"
:changeParam="changeParam"
:changeFilterall="changeFilterall"
>
<div
:style="`width:${width}%; height:${height}px; transition:all 0.5s; opacity:${opacity};`"
@mouseleave="leaveTable"
>
<ag-grid-vue
ref="agv"
style="width:100%; height:100%;"
class="ag-theme-balham"
:columnDefs="columns"
:rowData="filterRows"
:rowStyle="getRowsStyle"
:rowSelection="'multiple'"
:gridOptions="gridOptions"
_enableRangeSelection="true"
_copyHeadersToClipboard="true"
_modules="AllModules"
@selectionChanged="agRowSelect"
@cellClicked="agCellClick"
@cellDoubleClicked="agCellDbClick"
@cellValueChanged="agCellChange"
@cellMouseOver="agCellMouseEnter"
@cellMouseOut="agCellMouseLeave"
@cellKeyDown="agCellKeyDown"
@rowDragEnter="agRowDragEnter"
@rowDragEnd="agRowDragEnd"
@rowDragMove="agRowDragMove"
@rowDragLeave="agRowDragLeave"
@rowDataUpdated="agRowDataUpdated"
@filterChanged="agFilterChanged"
@gridReady="agReady"
></ag-grid-vue>
</div>
</div>
</template>
<script>
import map from 'lodash-es/map.js'
import each from 'lodash-es/each.js'
import size from 'lodash-es/size.js'
import filter from 'lodash-es/filter.js'
import isEqual from 'lodash-es/isEqual.js'
import merge from 'lodash-es/merge.js'
import get from 'lodash-es/get.js'
import set from 'lodash-es/set.js'
import join from 'lodash-es/join.js'
import split from 'lodash-es/split.js'
import values from 'lodash-es/values.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import concat from 'lodash-es/concat.js'
import difference from 'lodash-es/difference.js'
import every from 'lodash-es/every.js'
import some from 'lodash-es/some.js'
import findIndex from 'lodash-es/findIndex.js'
import trim from 'lodash-es/trim.js'
import haskey from 'wsemi/src/haskey.mjs'
import arrHas from 'wsemi/src/arrHas.mjs'
import sep from 'wsemi/src/sep.mjs'
import iser from 'wsemi/src/iser.mjs'
import isobj from 'wsemi/src/isobj.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import isstr from 'wsemi/src/isstr.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isnum from 'wsemi/src/isnum.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import ispm from 'wsemi/src/ispm.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import isp0int from 'wsemi/src/isp0int.mjs'
import isnull from 'wsemi/src/isnull.mjs'
import isundefined from 'wsemi/src/isundefined.mjs'
import cdbl from 'wsemi/src/cdbl.mjs'
import cstr from 'wsemi/src/cstr.mjs'
import cint from 'wsemi/src/cint.mjs'
import binstr from 'wsemi/src/binstr.mjs'
import ltdtkeys2mat from 'wsemi/src/ltdtkeys2mat.mjs'
import downloadExcelFileFromData from 'wsemi/src/downloadExcelFileFromData.mjs'
import domShowInputAndGetFilesU8Arrs from 'wsemi/src/domShowInputAndGetFilesU8Arrs.mjs'
import getDataFromExcelFileU8Arr from 'wsemi/src/getDataFromExcelFileU8Arr.mjs'
import str2md5 from 'wsemi/src/str2md5.mjs'
import delay from 'wsemi/src/delay.mjs'
import replace from 'wsemi/src/replace.mjs'
import debounce from 'wsemi/src/debounce.mjs'
import domDetect from 'wsemi/src/domDetect.mjs'
import domTooltip from 'wsemi/src/domTooltip.mjs'
import * as agv from 'ag-grid-vue' //會再引用vue-class-component與vue-property-decorator, 已能被rollup打包進dist, 故不再需要由外部引入cdn
import 'ag-grid-community/styles/ag-grid.css'
import 'ag-grid-community/styles/ag-theme-balham.css'
// import { AllModules } from '@ag-grid-enterprise/all-modules' //ag-gird-enterprise雖可使用modules擴充支援剪貼簿貼上Excel range數據, 不過由Excel複製的數據會有換行字元, 此導致ag-grid解析多一列的空數據而覆蓋到原數據, 無法用
import getLangText from './getLangText.mjs'
//useAgGridVue, ag-grid-vue已打包進dist, 直接取用不再回退外部cdn
let useAgGridVue = get(agv, 'AgGridVue')
// console.log('useAgGridVue',useAgGridVue)
//tooltip, 通過key查msg避免特殊html符號無法顯示
let dtmsg = {}
window.ttWAggridVue = function(ele, kmsg) {
if (!isestr(kmsg)) {
return
}
let msg = dtmsg[kmsg]
domTooltip(ele, msg)
}
function parseText(contentPaste) {
contentPaste = trim(contentPaste)
contentPaste = replace(contentPaste, '\r\n', '\n')
contentPaste = replace(contentPaste, '\r', '\n')
let pRows = sep(contentPaste, '\n')
let pData = []
each(pRows, (v) => {
if (trim(v) !== '') {
let s = split(v, '\t')
pData.push(s)
}
})
return pData
}
//cvCellText, 供預設cellRenderer與CellSlotRenderer之fallback共用, 使同一儲存格值不論走哪條路徑顯示文字皆相同
//物件與陣列需字串化, 否則ag-grid會將回傳值視為DOM節點而appendChild拋錯, 且中斷animation frame迴圈導致整表未渲染格全空白
function cvCellText(v) {
//check
if (isnull(v) || isundefined(v)) {
return ''
}
if (isstr(v) || isnum(v)) {
return cstr(v)
}
//物件與陣列, 含循環參照時JSON.stringify會拋錯故退為空字串
try {
return JSON.stringify(v)
}
catch (err) {
return ''
}
}
//cvCellTextByParams, 該欄有kpCellFormat時優先用其經valueFormatter產生之valueFormatted, 否則用原值
//須以bCellFormat旗標判定而非valueFormatted是否存在, 因ag-grid對推斷為object之欄會自帶預設valueFormatter產生'[object Object]'
function cvCellTextByParams(params) {
let v = null
if (get(params, 'bCellFormat') === true) {
v = get(params, 'valueFormatted')
}
if (isnull(v) || isundefined(v)) {
v = get(params, 'value')
}
return cvCellText(v)
}
//方法之opt與其各鍵為額外傳入: 未給予(null、undefined)時使用預設; 有給予但非指定之值或型態時亦使用預設, 並以logUseDefault於console提示
//isGiven, 選項是否有給予(非null、undefined)
function isGiven(v) {
return !isnull(v) && !isundefined(v)
}
//logUseDefault, 選項有給予但非指定之值或型態時, 於console提示改用預設; 無效之值為字串與數值時顯示原值, 其餘顯示型態
function logUseDefault(name, key, v, def) {
let s = (isstr(v) || isnum(v)) ? `${v}` : Object.prototype.toString.call(v)
console.log(`[w-aggrid-vue] ${name}: invalid ${key}[${s}], use default ${key} ${def}`)
}
//pickRowsByKeys, 依keys由各列取值(無值為''), 回傳新列物件陣列; keys為空時回傳[]
//不使用wsemi之ltdtmapping: 其以isearr判斷, 恰1列且為空物件時回傳[], 2列以上才回傳空白列
function pickRowsByKeys(rows, keys) {
if (size(keys) === 0) {
return []
}
return map(rows, (row) => {
let r = {}
each(keys, (key) => {
r[key] = get(row, key, '')
})
return r
})
}
//isGridAvailable, 表格是否可用: 元件銷毀後getApi為null; 呼叫端經getApi銷毀ag-grid後api仍在但isDestroyed為true, 其方法皆已換成只印警告之函數
function isGridAvailable(api) {
return !!api && !(isfun(api.isDestroyed) && api.isDestroyed())
}
//getApiOrThrow, 需表格之公開方法取api之單一入口, 表格不可用時拋錯(async方法即reject); 內部延後執行之路徑改以isGridAvailable判斷後靜默略過
function getApiOrThrow(vo, name) {
let api = vo.getApi()
if (!isGridAvailable(api)) {
throw new Error(`[w-aggrid-vue] ${name}: grid is not available`)
}
return api
}
//getNodeRows, 依ag-grid目前之列序取各列原物件(node.data, 含不在opt.keys內之欄位, 與vo.rows之列為同一物件, 不複製), 供寫回opt.rows; 順序同getNowData
function getNodeRows(api) {
let rs = []
api.forEachNode((node) => {
rs.push(node.data)
})
return rs
}
//isCellEditable, 元件自寫之寫入路徑(不經ag-grid編輯器直接寫入資料列者, 目前為pasteText)判斷儲存格可否編輯之單一入口
//委由ag-grid之column.isCellEditable(node)求值, 語意同opt.kpCellEditable指給colDef.editable後ag-grid自身編輯入口(雙擊、F2、鍵入)之判斷:
//布林值直接採用; 函數以該儲存格之EditableCallbackParams(node、data、column、colDef、api、context)逐儲存格求值, 回傳值依真假; 其餘型態視為不可編輯
//函數拋錯時視為不可編輯並回傳false, 不使整批寫入失敗, 拋出之錯誤交onError通報(由呼叫端決定如何提示), 通報本身再拋錯亦不外傳
function isCellEditable(column, node, onError) {
try {
return !!column.isCellEditable(node)
}
catch (err) {
try {
if (isfun(onError)) {
onError(err)
}
}
catch (errOnError) {
//通報失敗不影響判斷結果
}
return false
}
}
//downloadCore, downloadData與downloadDisplayData共用之下載流程, 兩者僅欄位keys與來源列rows不同
//寫成模組層函數而非methods, 避免經ref外露為對外API; 不呼叫grid API, 使downloadData可於表格就緒前使用
//同步可偵測之失敗一律拋錯且不產檔; 寫檔為非同步, 其失敗只能以前綴記錄於console, 無法回報呼叫端
function downloadCore(vo, name, keys, rows, opt) {
//newError, 訊息加前綴供辨識來源, 呼叫端(如w-table-vue)可能只顯示固定訊息而不顯示錯誤內容
let newError = (msg) => {
return new Error(`[w-aggrid-vue] ${name}: ${msg}`)
}
//checkSync, 掛鉤僅支援同步回傳, 回傳Promise時先接手其rejection避免出現未處理之rejection, 再拋錯
let checkSync = (r, nameHook) => {
if (ispm(r)) {
r.catch(() => {})
throw newError(`${nameHook} must return synchronously, Promise is not supported`)
}
}
//opt
if (!isobj(opt)) {
if (isGiven(opt)) {
logUseDefault(name, 'opt', opt, '{}')
}
opt = {}
}
//spread
let {
funGetKeysHook,
funGetLtdtHook,
funGetMatHook,
useHead,
useFormat,
fileName,
sheetName,
} = opt
//hooks, 須為函數, 否則視為未給予(其後以isfun判斷是否呼叫)
each({ funGetKeysHook, funGetLtdtHook, funGetMatHook }, (fun, key) => {
if (!isfun(fun) && isGiven(fun)) {
logUseDefault(name, key, fun, 'null')
}
})
//useHead
if (!isbol(useHead)) {
if (isGiven(useHead)) {
logUseDefault(name, 'useHead', useHead, 'false')
}
useHead = false
}
//useFormat, 預設依kpCellFormat格式化使下載與畫面一致, 給false則下載原值
if (!isbol(useFormat)) {
if (isGiven(useFormat)) {
logUseDefault(name, 'useFormat', useFormat, 'true')
}
useFormat = true
}
//fileName, 無副檔名時補.xlsx(同ag-grid匯出), 否則瀏覽器會依無型別之Blob另給副檔名
if (!isestr(fileName)) {
if (isGiven(fileName)) {
logUseDefault(name, 'fileName', fileName, `'data.xlsx'`)
}
fileName = 'data.xlsx'
}
if (fileName.indexOf('.') < 0) {
fileName += '.xlsx'
}
//sheetName, 須符合Excel分頁名稱規則(同wsemi寫檔器hucre之validateSheetName: 字串原始值, 長度以UTF-16碼元計1至31, 不含[ ] : * ? / \, 首尾非單引號, 非History), 否則寫檔時才失敗且無法回報, 故改用預設'data'
//長度不可用lodash size(計Unicode碼位), 表情符號與擴充B區漢字各佔2個碼元; wsemi isestr對new String()亦為true, 故以typeof判斷
let bSheetName = typeof sheetName === 'string' && sheetName.length > 0 && sheetName.length <= 31 && !/[[\]:*?/\\]/.test(sheetName) && !sheetName.startsWith(`'`) && !sheetName.endsWith(`'`) && sheetName.toLowerCase() !== 'history'
if (!bSheetName) {
if (isGiven(sheetName)) {
logUseDefault(name, 'sheetName', sheetName, `'data'`)
}
sheetName = 'data'
}
//useKeys
let useKeys = cloneDeep(keys)
if (isfun(funGetKeysHook)) {
useKeys = funGetKeysHook(useKeys)
checkSync(useKeys, 'funGetKeysHook')
if (!isarr(useKeys)) {
throw newError('funGetKeysHook must return an array of keys')
}
if (!every(useKeys, (key) => isestr(key) || isnum(key))) {
throw newError('funGetKeysHook must return an array of non-empty strings')
}
useKeys = [...useKeys] //快照, 避免掛鉤保留回傳陣列之參照於後續改寫
}
//check
if (size(useKeys) === 0) {
throw newError('no downloadable keys')
}
//data, 由來源列依useKeys提取; 格式化函數之row為完整來源列且讀原值, 與畫面valueFormatter之params.data一致; 取出之值cloneDeep, 掛鉤改寫不影響表格數據
let data = map(rows, (row) => {
let r = {}
each(useKeys, (key) => {
let v = get(row, key)
if (useFormat) {
let funCellFormat = vo.kpCellFormat[key]
if (isfun(funCellFormat)) {
let vf = funCellFormat(v, key, row, null)
if (!isnull(vf) && !isundefined(vf)) {
v = vf //回傳值原樣使用, 如何回傳由呼叫端決定
}
}
}
if (isundefined(v)) {
v = '' //同pickRowsByKeys對無值之預設
}
r[key] = cloneDeep(v)
})
return r
})
//funGetLtdtHook, 收到格式化後之值, 可回傳空陣列(僅輸出表頭)
if (isfun(funGetLtdtHook)) {
data = funGetLtdtHook(data)
checkSync(data, 'funGetLtdtHook')
if (!isarr(data)) {
throw newError('funGetLtdtHook must return an array')
}
let ind = findIndex(data, (r) => {
return !iseobj(r)
})
if (ind >= 0) {
throw newError(`funGetLtdtHook must return an array of non-empty objects, invalid row[${ind}]`)
}
}
//dataOut, null轉空字串與畫面cvCellText一致, 否則ltdtkeys2mat會轉為字串'null'; 另建列物件不改動掛鉤回傳之物件
let dataOut = map(data, (r) => {
let t = { ...r }
each(useKeys, (key) => {
if (isnull(t[key])) {
t[key] = ''
}
})
return t
})
//ltdtkeys2mat
let mat = ltdtkeys2mat(dataOut, useKeys)
//heads, 一律產生新陣列避免與useKeys同址; useHead時取kpHead, 無對應者退回key(同kpHead預設)
let heads = map(useKeys, (key) => {
if (useHead && haskey(vo.kpHead, key)) {
return vo.kpHead[key]
}
return key
})
//concat
mat = concat([heads], mat)
//funGetMatHook, 須回傳二維陣列且至少一列非空
if (isfun(funGetMatHook)) {
mat = funGetMatHook(mat)
checkSync(mat, 'funGetMatHook')
if (!isarr(mat) || !every(mat, isarr)) {
throw newError('funGetMatHook must return a two-dimensional array')
}
if (!some(mat, (r) => {
return size(r) > 0
})) {
throw newError('funGetMatHook must return at least one non-empty row')
}
}
//check, Excel單一分頁之上限(含表頭列1048576列、每列16384欄), 以最終二維陣列判定(funGetLtdtHook、funGetMatHook可增減列與欄)
//hucre寫檔時才拒絕欄數且無法回報, 列數則不拒絕但產出之檔讀不回(含本套件之uploadData), 故事前同步拋錯
if (size(mat) > 1048576) {
throw newError('too many rows, Excel allows at most 1048576 rows including the header row')
}
let indWide = findIndex(mat, (r) => {
return size(r) > 16384
})
if (indWide >= 0) {
throw newError(`too many columns, Excel allows at most 16384 columns, invalid row[${indWide}]`)
}
//downloadExcelFileFromData, 非同步寫檔, 無法回報呼叫端; 寫檔器拒絕時wsemi以resolve回傳{ error }(例如funGetMatHook回傳寫檔器無法寫出之儲存格物件), 觸發下載時之例外則reject, 皆以前綴記錄於console
downloadExcelFileFromData(fileName, sheetName, mat)
.then((r) => {
if (haskey(r, 'error')) {
console.log(`[w-aggrid-vue] ${name}: can not write excel file`, r.error)
}
})
.catch((err) => {
console.log(`[w-aggrid-vue] ${name}: can not write excel file`, err)
})
return mat
}
//CellSlotRenderer, 用於cell-render與cell-tooltip slot的Vue組件
let CellSlotRenderer = {
render(h) {
let cellRenderSlotFn = this.params.cellRenderSlotFn
if (cellRenderSlotFn) {
let vnodes = cellRenderSlotFn({
value: this.params.value,
valueFormatted: this.params.bCellFormat === true ? this.params.valueFormatted : undefined, //僅該欄有kpCellFormat時給予, 避免ag-grid對object欄自帶之'[object Object]'流入slot
key: this.params.colDef.field,
row: this.params.data,
})
return h('span', vnodes)
}
return h('span', cvCellTextByParams(this.params))
},
mounted() {
let cellTooltipSlotFn = this.params.cellTooltipSlotFn
if (cellTooltipSlotFn) {
let vnodes = cellTooltipSlotFn({
value: this.params.value,
valueFormatted: this.params.bCellFormat === true ? this.params.valueFormatted : undefined, //僅該欄有kpCellFormat時給予, 避免ag-grid對object欄自帶之'[object Object]'流入slot
key: this.params.colDef.field,
row: this.params.data,
})
let VueCtor = this.$root.constructor
let vm = new VueCtor({
render: h => h('div', vnodes),
}).$mount()
let t = vm.$el.innerHTML
vm.$destroy()
t = cstr(t)
let kmsg = str2md5(t)
dtmsg[kmsg] = t
this.params.eGridCell.setAttribute('onmouseenter', `ttWAggridVue(this,'${kmsg}')`)
}
},
}
//HeaderSlotRenderer, 用於head-render與head-tooltip slot的Vue組件
let HeaderSlotRenderer = {
data() {
return {
currentSort: null,
filterActive: false,
sortOrder: '',
}
},
render(h) {
let headRenderSlotFn = this.params.headRenderSlotFn
let cJustifyContent = this.params.cJustifyContent || 'center'
let displayName = this.params.displayName
let key = this.params.column.colId
//header content
let headerContent
if (headRenderSlotFn) {
headerContent = headRenderSlotFn({
value: displayName,
key,
})
}
else {
headerContent = [displayName]
}
//sort icons
let sortChildren = []
if (this.currentSort === 'asc') {
sortChildren.push(
h('span', { class: 'ag-header-icon ag-sort-ascending-icon' }, [
h('span', { class: 'ag-icon ag-icon-asc' }),
])
)
}
else if (this.currentSort === 'desc') {
sortChildren.push(
h('span', { class: 'ag-header-icon ag-sort-descending-icon' }, [
h('span', { class: 'ag-icon ag-icon-desc' }),
])
)
}
//sort order, 多欄排序時之序號, 對齊預設headTemplate之eSortOrder
let sortOrderChildren = []
if (this.sortOrder !== '') {
sortOrderChildren.push(
h('span', { class: 'ag-header-icon ag-sort-order' }, this.sortOrder)
)
}
//filter icon, 對齊預設headTemplate之eFilter
//ag-grid僅對預設HeaderComp處理filter圖示(HeaderComp.setupFilterIcon->configureFilter), 自訂headerComponent須自行監聽column之filterChanged
let filterChildren = []
if (this.params.enableFilterIcon !== false) {
filterChildren.push(
h('span', {
class: 'ag-header-icon ag-header-label-icon ag-filter-icon' + (this.filterActive ? '' : ' ag-hidden'),
}, [
h('span', { class: 'ag-icon ag-icon-filter' }),
])
)
}
//menu button
//gridOptions.suppressMenuHide為true時ag-grid會為預設HeaderComp之eMenu加ag-header-menu-always-show使其常駐, 自訂headerComponent須自行加上
let cMenuAlwaysShow = ''
try {
if (this.params.api.getGridOption('suppressMenuHide')) {
cMenuAlwaysShow = ' ag-header-menu-always-show'
}
}
catch (err) {
}
let menuChildren = []
if (this.params.enableMenu) {
menuChildren.push(
h('span', {
class: 'ag-header-icon ag-header-cell-menu-button' + cMenuAlwaysShow,
on: {
click: (e) => {
e.stopPropagation()
this.params.showColumnMenu(e.target)
},
},
}, [
h('span', { class: 'ag-icon ag-icon-menu' }),
])
)
}
return h('div', {
class: 'ag-cell-label-container',
attrs: { role: 'presentation' },
on: { click: this.onSortClicked },
}, [
...menuChildren,
h('div', {
class: 'ag-header-cell-label',
style: { 'justify-content': cJustifyContent },
attrs: { role: 'presentation' },
}, [
h('span', {
class: 'ag-header-cell-text',
attrs: { role: 'columnheader' },
}, headerContent),
...sortOrderChildren,
...sortChildren,
...filterChildren,
]),
])
},
methods: {
onSortClicked(event) {
if (this.params.enableSorting !== false) {
this.params.progressSort(event.shiftKey)
}
},
onSortChanged() {
if (this.params.column) {
this.currentSort = this.params.column.getSort() || null
}
//sortOrder, 僅於超過1欄有排序時顯示序號, 與ag-grid之SortIndicatorComp.updateSortOrder一致
let sortOrder = ''
let ind = this.params.column ? this.params.column.getSortIndex() : null
if (isnum(ind) && ind >= 0) {
let n = 0
try {
let cs = this.params.api.getColumnState()
n = filter(cs, (v) => {
return isnum(get(v, 'sortIndex'))
}).length
}
catch (err) {
}
if (n > 1) {
sortOrder = cstr(ind + 1)
}
}
this.sortOrder = sortOrder
},
onFilterChanged() {
if (this.params.column) {
this.filterActive = this.params.column.isFilterActive()
}
},
},
mounted() {
//sort listener, 聽grid層級之sortChanged而非單欄之sortChanged
//單欄sortChanged於Column.setSort時派送, 此時SortController.updateSortIndex尚未指定各欄sortIndex, 且他欄排序變動時本欄不會收到單欄事件, 序號無法重算
//grid層級sortChanged於updateSortIndex之後派送, 與ag-grid之SortIndicatorComp.setupSort一致(其箭頭與序號皆聽grid層級事件)
try {
this.params.api.addEventListener('sortChanged', this.onSortChanged)
}
catch (err) {
}
this.onSortChanged()
//filter listener
if (this.params.column) {
this.params.column.addEventListener('filterChanged', this.onFilterChanged)
this.onFilterChanged()
}
//head tooltip
let headTooltipSlotFn = this.params.headTooltipSlotFn
if (headTooltipSlotFn) {
let displayName = this.params.displayName
let key = this.params.column.colId
let vnodes = headTooltipSlotFn({
value: displayName,
key,
})
let VueCtor = this.$root.constructor
let vm = new VueCtor({
render: h => h('div', vnodes),
}).$mount()
let t = vm.$el.innerHTML
vm.$destroy()
t = cstr(t)
let kmsg = str2md5(t)
dtmsg[kmsg] = t
this.$el.setAttribute('onmouseenter', `ttWAggridVue(this,'${kmsg}')`)
}
},
beforeDestroy() {
if (this.params.column) {
this.params.column.removeEventListener('filterChanged', this.onFilterChanged)
}
//grid整體destroy時api可能已失效, 故先檢查isDestroyed並try catch
try {
let api = this.params.api
if (api && !(isfun(api.isDestroyed) && api.isDestroyed())) {
api.removeEventListener('sortChanged', this.onSortChanged)
}
}
catch (err) {
}
},
}
/**
* @vue-prop {Object} opt 輸入資料設定物件
* @vue-prop {Array} opt.keys 輸入資料各欄位keys
* @vue-prop {Array} opt.rows 輸入資料列,各列為物件,內含各欄位keys之值,例[{},{},...,{}]
* @vue-prop {Object} [opt.kpHead={}] 輸入key對應head物件,預設各key值為本身key值
* @vue-prop {Number} [opt.defHeadMinWidth=null] 輸入cell預設最小寬度數字,預設為null
* @vue-prop {Number} [opt.defHeadMaxWidth=null] 輸入cell預設最大寬度數字,預設為null
* @vue-prop {Object} [opt.kpHeadWidth={}] 輸入key對應cell之寬度物件,預設各key值為undefined
* @vue-slot {Object} cell-render 輸入cell之渲染slot,slot props為{ value, key, row }
* @vue-slot {Object} cell-tooltip 輸入cell之tooltip渲染slot,slot props為{ value, key, row },slot內容於掛載時取靜態HTML快照,不支援事件綁定、子組件狀態與響應式更新
* @vue-slot {Object} head-render 輸入head之渲染slot,slot props為{ value, key }
* @vue-slot {Object} head-tooltip 輸入head之tooltip渲染slot,slot props為{ value, key },slot內容於掛載時取靜態HTML快照,不支援事件綁定、子組件狀態與響應式更新
* @vue-prop {String} [opt.defHeadAlignH='center'] 輸入head預設之左右對齊字串,預設為'center'
* @vue-prop {Object} [opt.kpHeadAlignH={}] 輸入key對應head之左右對齊字串物件,預設各key值為defHeadAlignH
* @vue-prop {Boolean} [opt.defHeadSort=true] 輸入head預設之是否允許排序布林值,預設為true
* @vue-prop {Object} [opt.kpHeadSort={}] 輸入key對應head之是否允許排序物件,預設各key值為defHeadSort
* @vue-prop {Function|String} [opt.defHeadSortMethod=null] 輸入head預設之排序方式函數或字串,若需自行定義則給予函數,若需使用內建的自動轉型判斷方式則給予'auto'字串,預設為null
* @vue-prop {Object} [opt.kpHeadSortMethod={}] 輸入key對應head之排序方式函數或字串,若需自行定義則給予函數,若需使用內建的自動轉型判斷方式則給予'auto'字串,預設各key值為defHeadSortMethod
* @vue-prop {Object} [opt.kpHeadFixLeft={}] 輸入key對應head之是否固定於左側物件,預設各key值為false
* @vue-prop {Boolean} [opt.defHeadFilter=true] 輸入head預設之是否允許過濾布林值,預設為true
* @vue-prop {Object} [opt.kpHeadFilter={}] 輸入key對應head之是否允許過濾物件,預設各key值為defHeadFilter
* @vue-prop {String} [opt.defHeadFilterType='num'] 輸入head預設過濾器字串,可選'num'、'text'、'time'、'set',預設為'num'
* @vue-prop {Object} [opt.kpHeadFilterType={}] 輸入key對應head之過濾器物件,可使用'num'、'text'、'time'、'set',預設各key值為'num'
* @vue-prop {Boolean} [opt.defHeadDrag=true] 輸入head預設之是否允許拖曳布林值,預設為true
* @vue-prop {Object} [opt.kpHeadDrag={}] 輸入key對應head之是否允許拖曳物件,預設各key值為defHeadDrag
* @vue-prop {Object} [opt.kpHeadCheckBox={}] 輸入key對應head與key的各列是否使用核選方塊物件,預設各key值為false
* @vue-prop {Object} [opt.kpHeadFocusHighlight={}] 輸入key對應key的各列於獲得焦點時是否高亮顯示物件,預設各key值為true
* @vue-prop {Object} [opt.kpHeadHide={}] 輸入key對應head是否隱藏物件,預設各key值為false
* @vue-prop {Object} [opt.kpRowStyle={}] 輸入key對應row style之物件,可設定各key欄之函數,函數給予cell值需回傳之row style,預設各key值為undefined
* @vue-prop {Object} [opt.kpRowDrag={}] 輸入key對應col之是否能拖曳排序物件,預設各key值為false;拖曳排序後依新列序更新opt.rows,各列保留全部欄位(含不在opt.keys內者,例如供kpCellEditable函數判斷之旗標);回呼rowDragEnd、rowDragLeave、rowDragChange收到之row為被拖曳之整列,rows為依opt.keys提取之新列序數據副本(同getNowData,就地修改不影響表格與opt.rows)
* @vue-prop {Function} [opt.genRowsPinnTop=null] 輸入產生置頂rows函數,輸入為表內全部數據,預設為null
* @vue-prop {Function} [opt.genRowsPinnBottom=null] 輸入產生置底rows函數,輸入為表內全部數據,預設為null
* @vue-prop {Object} [opt.kpColStyle={}] 輸入key對應col style之物件,可設定各key欄之col style,預設各key值為undefined
* @vue-prop {Object} [opt.kpColSpan={}] 輸入key對應col span之物件,可設定各key欄之col span,預設各key值為undefined
* @vue-prop {String} [opt.defCellAlignH='center'] 輸入cell預設之左右對齊字串,預設為'center'
* @vue-prop {Object} [opt.kpCellAlignH={}] 輸入key對應cell之左右對齊字串物件,預設各key值為defCellAlignH
* @vue-prop {Boolean} [opt.defCellEditable=false] 輸入cell預設之是否可編輯布林值,預設為false
* @vue-prop {Object} [opt.kpCellEditable={}] 輸入key對應cell之是否可編輯物件,各key值可為布林值或函數,預設各key值為defCellEditable;值原樣指給ag-grid之colDef.editable,故語意同ag-grid:布林值代表整欄是否可編輯,函數簽名為(params)=>boolean,params同ag-grid之EditableCallbackParams(含node、data、column、colDef、api、context),對每個儲存格逐列求值,回傳true代表該儲存格可編輯(可據以做到逐列唯讀),其他型態(非布林值非函數)視為不可編輯;ag-grid之雙擊、F2、鍵入編輯與元件之貼上(Ctrl+V、pasteText)皆依同一判斷,貼上時唯讀之儲存格略過不寫入(其餘儲存格照寫,欄位索引照常遞增),各儲存格之判斷皆於元件寫入前完成(同一次貼上先寫入之儲存格不影響後面儲存格之判斷),函數須無副作用;貼上時函數若拋錯,該儲存格視為不可編輯並略過(不使整批貼上失敗),同一次貼上同一欄於console以`[w-aggrid-vue] pasteText: kpCellEditable[key] threw, ...`說明一次;以鍵盤Ctrl+V自編輯器開啟中之儲存格貼上時,編輯器會先提交瀏覽器原生貼入之文字,函數看到的是提交後之資料
* @vue-prop {Object} [opt.kpCellFormat={}] 輸入key對應cell之值格式化函數物件,函數簽名為(value, key, row, params),回傳顯示字串,回傳值不經套件轉換直接使用,回傳null或undefined代表不格式化維持原值;所有原值(含null、undefined、空字串)皆原樣傳入,空值之呈現由呼叫端於函數內自行決定;作用於顯示與下載(下載時row同為該列完整數據、params為null),排序與過濾仍依原值;與cell-render slot可並用,slot props之value為原值、valueFormatted為格式化值,預設各key值為undefined
* @vue-prop {Object} [opt.kpConvertKeysWhenUploadData={}] 輸入上傳Excel檔案時將檔案表頭名稱轉為欄位key之對照物件,例如{ '製造商': 'make' },於uploadData之beforeUpload之前套用,轉換後與檔案既有表頭同名時依欄序後者覆蓋,預設{}
* @vue-prop {Function} [opt.rowsChange=()=>{}] 輸入rows change之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.rowClick=()=>{}] 輸入row click之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.rowDbClick=()=>{}] 輸入row double click之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.rowChange=()=>{}] 輸入row change之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.rowChecked=()=>{}] 輸入row checked之觸發事件,需使用kpHeadCheckBox開啟指定key的head與對應rows使用checkbox,預設為()=>{}
* @vue-prop {Function} [opt.rowMouseEnter=()=>{}] 輸入row mouseenter之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.rowMouseLeave=()=>{}] 輸入row mouseleave之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.cellClick=()=>{}] 輸入cell click之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.cellDbClick=()=>{}] 輸入cell double click之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.cellChange=()=>{}] 輸入cell change之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.cellMouseEnter=()=>{}] 輸入cell mouseenter之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.cellMouseLeave=()=>{}] 輸入cell mouseleave之觸發事件,預設為()=>{}
* @vue-prop {Function} [opt.filterChange=()=>{}] 輸入filter change之觸發事件,於掛載與filterall變更時(from為'filterall')、表頭過濾變更時(含setHeadFilter、clearHeadFilter、clearHeadFilterAll,from為'heads')各觸發1次,傳入{ from, filterall, flts, isFilter },flts為觸發後讀取當下之生效中過濾模型陣列(依opt.keys之順序,含隱藏欄),元件或ag-grid已銷毀後不觸發,預設為()=>{}
* @vue-prop {Boolean} [opt.autoFitColumn=false] 輸入當表格尺寸變更時自動調整欄寬布林值,預設false
* @vue-prop {String} [opt.language='en'] 輸入指定語系字串,可選'en'、'zh-tw'、'zh-cn',預設為'en'
* @vue-prop {Number} [height=300] 表格高度,單位為px,預設300
* @vue-prop {String} [filterall=''] 輸入對全表數據進行過濾之字串,預設為''
*/
export default {
components: {
AgGridVue: useAgGridVue,
CellSlotRenderer,
HeaderSlotRenderer,
},
props: {
opt: {
type: Object,
},
height: {
type: Number,
default: 300
},
filterall: {
type: String,
default: ''
},
},
data: function() {
let vo = this
return {
dbc: debounce(),
de: null,
isReady: false,
bWarnedRemovedKeys: false, //opt含2.0.56已移除之kp鍵時只警告一次
evPaste: null,
dataPasted: '',
width: 100,
opacity: 0,
autoFitColumn: false,
language: '',
rows: [],
keys: [],
columns: [],
kpHead: {},
defHeadMinWidth: null,
defHeadMaxWidth: null,
kpHeadWidth: {},
defHeadAlignH: null,
kpHeadAlignH: {},
defHeadSort: null,
kpHeadSort: {},
defHeadSortMethod: null,
kpHeadSortMethod: {},
kpHeadFixLeft: {},
defHeadFilter: null,
kpHeadFilter: {},
defHeadFilterType: null,
kpHeadFilterType: {},
defHeadDrag: null,
kpHeadDrag: {},
kpHeadCheckBox: {},
kpHeadFocusHighlight: {},
kpHeadHide: {},
kpRowStyle: {},
kpRowDrag: {},
kpColStyle: {},
kpColSpan: {},
defCellAlignH: null,
kpCellAlignH: {},
defCellEditable: null,
kpCellEditable: {},
kpCellFormat: {},
kpConvertKeysWhenUploadData: null,
tableClickEnable: false,
rowsChange: function() {},
rowChange: function() {},
rowChecked: function() {},
rowMouseEnter: function() {},
rowMouseLeave: function() {},
rowClick: function() {},
rowDbClick: function() {},
rowDragEnter: function() {},
rowDragEnd: function() {},
rowDragMove: function() {},
rowDragLeave: function() {},
rowDragChange: function() {},
cellClick: function() {},
cellDbClick: function() {},
cellChange: function() {},
cellMouseEnter: function() {},
cellMouseLeave: function() {},
filterChange: function() {},
//因ag-grid只提供mouseout沒有mouseleave, 導致會因為內元素而觸發mouseout, 得通過暫存機制產生mouseleave事件
vRowMouseEnter: null,
vRowMouseLeave: null,
eRowMouseLeaves: [],
vCellMouseEnter: null,
vCellMouseLeave: null,
eCellMouseLeaves: [],
iRowDragEnter: null,
genRowsPinnTop: null,
genRowsPinnBottom: null,
// AllModules,
gridOptions: {
animateRows: true,
suppressMenuHide: true, //預設為false, 代表標題右側的漢堡按鈕預先隱藏, 滑鼠移入時才顯示
// rowHoverHighlight: true,
suppressRowHoverHighlight: false, //ag-grid 25.3.0已由rowHoverHighlight改為suppressRowHoverHighlight
// suppressColumnHoverHighlight: false,
columnHoverHighlight: false, //ag-grid 26.2.0已由suppressColumnHoverHighlight改為columnHoverHighlight
// floatingFilter: true, //ag-grid 23.1.0已改為由column給予floatingFilter
rowDragManaged: true,
suppressRowClickSelection: true, //若開啟checkboxSelection
singleClickEdit: true, //單點即可變更
// stopEditingWhenGridLosesFocus: true,
stopEditingWhenCellsLoseFocus: true, //失去焦點則停止編輯, ag-grid 25.2.2改為stopEditingWhenCellsLoseFocus
// suppressCellFocus: true, //點擊因cell獲得焦點會出現邊框, 若關閉(suppressCellFocus=true)會導致全部cell都關閉, 另用賦予class處理
// localeText: null,
getLocaleText: vo.agGetLocaleText,
getRowStyle: vo.agGetRowStyle,
// debounceVerticalScrollbar: true,
pinnedTopRowData: [],
pinnedBottomRowData: [],
},
ag: null,
}
},
mounted: function() {
let vo = this
//監聽dom, 由隱藏恢復顯示時domDetect亦發出resize, 故顯示時亦會重算欄寬
vo.de = domDetect(() => {
return get(vo, '$refs.shell', null)
})
vo.de.on('resize', (s) => {
// console.log('resize', s)
if (vo.autoFitColumn) {
vo.fitColumns()
}
})
//paste
vo.evPaste = (e) => {
//console.log('paste', e)
//clipboardData
let clipboardData = event.clipboardData || window.clipboardData
//save dataPasted
vo.dataPasted = clipboardData.getData('Text')
}
window.addEventListener('paste', vo.evPaste)
},
beforeDestroy: function() {
//console.log('beforeMount')
let vo = this
//釋放監聽
vo.de.clear()
//unbindHeaderScrollSync
vo.unbindHeaderScrollSync()
//removeEventListener
window.removeEventListener('paste', vo.evPaste)
},
computed: {
changeParam: function() {
//console.log('computed changeParam')
let vo = this
//trigger
let opt = vo.opt
vo.__temp__ = { opt }
//changeOpt
vo.changeOpt()
return ''
},
changeFilterall: function() {
// console.log('computed changeFilterall')
let vo = this
//trigger
let filterall = vo.filterall
vo.__temp__ = { filterall }
//triggerFilterChange
vo.triggerFilterChange('filterall')
return ''
},
getRowsStyle: function(params) {
//console.log('computed getRowsStyle', params)
let vo = this
if (vo.tableClickEnable) {
return {
cursor: 'pointer'
}
}
return {}
},
filterRows: function() {
//console.log('computed filterRows')
let vo = this
//trigger
let rows = vo.rows
let filterall = vo.filterall
//check
if (filterall === '') {
return rows
}
//filter
let frs = filter(rows, function(v, k) {
let c = join(values(v), '')
return binstr(c, filterall)
})
return frs
},
},
methods: {
getGridOptions: function() {
//console.log('methods getGridOptions')
let vo = this
return vo.gridOptions
},
getApi: function() {
// console.log('getApi')
let vo = this
//api, ag-grid 31.1.0的gridOptions.api已改為須由組件ref取得
let api = get(vo, '$refs.agv.api', null)
return api
},
agGetLocaleText: function(params) {
// console.log('agGetLocaleText', params)
let vo = this
//getLangText
let t = getLangText(vo.language, params.key, params.defaultValue)
return t
},
agGetRowStyle: function(params) {
//console.log('agGetRowStyle', params) //由各row呼叫
let vo = this
//style
let style = {}
each(vo.kpRowStyle, function(v, k) {
let f = v
let d = get(params.data, k)
if (isfun(f)) {
style = merge(style, f(d))
}
})
return style
},
triggerRowDragChange: function(indFrom, indTo, row, rows, rowsFull) {
let vo = this
//check
if (indFrom !== indTo) {
vo.iRowDragEnter = indTo
//需使用set強制更新外部opt物件的rows並再同步更新至內部rows, 否則外面數據會沒更動
//寫回者為保留各列全部欄位之rowsFull(原列物件依新列序), 不用rows(getNowData之結果, 依opt.keys提取, 只含keys內之欄位, 僅供回呼), 否則不在keys內之欄位(例如供editable函數判斷之旗標)寫回後會消失
set(vo, `opt.rows`, rowsFull)
//rowDragChange
vo.rowDragChange(indFrom, indTo, row, rows)
// //rowsChange, rows是內部已更新數據, 直接用於回傳函數
// vo.rowsChange(rows) //agRowDataUpdated也會觸發並再觸發rowsChange, 故此處不觸發
}
},
agRowDragEnter: function(param) {
// console.log('methods agRowDragEnter', param)
let vo = this
//ind
let ind = get(param, 'node.rowIndex', '')
// console.log('agRowDragEnter ind', ind)
//iRowDragEnter
vo.iRowDragEnter = ind
//row
let row = get(param, 'node.data', null)
// //rows
// let rows = vo.getNowData()
// // console.log('agRowDragEnd rows', cloneDeep(rows))
//rowDragEnter
vo.rowDragEnter(ind, -1, row)
},
agRowDragEnd: function(param) {
// console.log('methods agRowDragEnd', param)
let vo = this
//ind
let ind = get(param, 'node.rowIndex', '')
// console.log('agRowDragEnd ind', ind)
//row
let row = get(param, 'node.data', null)
//rows
let rows = vo.getNowData()
// console.log('agRowDragEnd rows', cloneDeep(rows))
//rowsFull, 須於呼叫端回呼(rowDragEnd)之前取得, 回呼可能已銷毀表格
let rowsFull = getNodeRows(vo.getApi())
//rowDragEnd
vo.rowDragEnd(vo.iRowDragEnter, ind, row, rows)
//triggerRowDragChange
vo.triggerRowDragChange(vo.iRowDragEnter, ind, row, rows, rowsFull)
},
agRowDragMove: function(param) {
// console.log('methods agRowDragMove', param)
let vo = this
//ind
let ind = get(param, 'node.rowIndex', '')
// console.log('agRowDragMove ind', ind)
//row
let row = get(param, 'node.data', null)
// //rows
// let rows = vo.getNowData()
// // console.log('agRowDragMove rows', cloneDeep(rows))
//rowDragMove
vo.rowDragMove(vo.iRowDragEnter, ind, row)
},
agRowDragLeave: function(param) {
// console.log('methods agRowDragLeave', param)
//先move再leave至表格外, 拖曳排序依然有效, 但僅觸發DragLeave不觸發DragEnd, 得聯合處理
let vo = this
//ind
let ind = get(param, 'node.rowIndex', '')
// console.log('agRowDragLeave ind', ind)
//row
let row = get(param, 'node.data', null)
//rows
let rows = vo.getNowData()
// console.log('agRowDragLeave rows', cloneDeep(rows))
//rowsFull, 須於呼叫端回呼(rowDragLeave)之前取得, 回呼可能已銷毀表格
let rowsFull = getNodeRows(vo.getApi())
//rowDragLeave
vo.rowDragLeave(vo.iRowDragEnter, ind, row, rows)
//triggerRowDragChange
vo.triggerRowDragChange(vo.iRowDragEnter, ind, row, rows, rowsFull)
},
agRowDataUpdated: function(params) {
// console.log('agRowDataUpdated', params)
let vo = this
//check, 表格載入數據也會觸發agRowDataUpdated, 通過isReady判斷是否為載入後變更
if (!vo.isReady) {
return
}
// //rows
// let rows = vo.getNowData()
// console.log('agRowDataUpdated rows', cloneDeep(rows))
//rowsChange, 因為記憶體同址vo.rows會出現變更, 故直接用於回傳函數
vo.rowsChange(vo.rows)
},
agRowSelect: function(params) {
// console.log('agRowSelect', params)
let vo = this
//rn
let rn = params.api.getSelectedNodes() //params.api已是vo.getApi()
//rs
let rs = map(rn, (r) => {
return {
rowInd: cint(r.id),
rowIndShow: r.rowIndex,
data: r.data,
}
})
//rowChecked
vo.rowChecked(cloneDeep(rs))
},
agCellClick: function(param) {
//console.log('methods agCellClick', param)
let vo = this
//cellClick
vo.cellClick(param.colDef.field, param.data)
//rowClick
vo.rowClick(param.colDef.field, param.data)
},
agCellDbClick: function(param) {
//console.log('methods agCellDbClick', param)
let vo = this
//cellDbClick
vo.cellDbClick(param.colDef.field, param.data)
//rowDbClick
vo.rowDbClick(param.colDef.field, param.data)
},
agCellChange: function(param) {
// console.log('methods agCellChange', param)
let vo = this
//check值變化
if (param.oldValue !== param.newValue) {
//值變化因為記憶體同址vo.rows會出現變更, 故使用vo.rows
//refresh
vo.refresh()
//cellChange
vo.cellChange(param.colDef.field, param.data, vo.rows)
//rowChange
vo.rowChange(param.colDef.field, param.data, vo.rows)
//rowsChange, 因為記憶體同址vo.rows會出現變更, 故直接用於回傳函數
vo.rowsChange(vo.rows) //agRowDataUpdated不會觸發rowsChange, 故此處須觸發
//setRowsPinnBottom
vo.setRowsPinnBottom(vo.rows)
}
},
agFilterChanged: function(params) {
// console.log('methods agFilterChanged', params)
let vo = this
//triggerFilterChange
vo.triggerFilterChange('heads')
},
agReady: function(params) {
//console.log('methods agReady', params)
let vo = this
//ag
vo.ag = params
//bindHeaderScrollSync
vo.bindHeaderScrollSync()
if (vo.autoFitColumn) {
//fitColumns
vo.fitColumns(false)
}
//setRowsPinnTop
vo.setRowsPinnTop(vo.rows)
//setRowsPinnBottom
vo.setRowsPinnBottom(vo.rows)
//show
vo.opacity = 1
//isReady
vo.isReady = true
},
bindHeaderScrollSync: function() {
//console.log('methods bindHeaderScrollSync')
let vo = this
//標頭容器(.ag-header-viewport)為overflow:hidden但仍可被程式捲動, 如瀏覽器對只露出一部分之欄名做scrollIntoView或focus、Playwright之locator.click皆會將目標捲入可見, 而ag-grid僅單向由資料區(.ag-center-cols-viewport)同步標頭, 標頭自行捲動時資料區不動, 欄名便對到相鄰欄之資料, 故監聽標頭捲動並使兩者回到一致(規則帳本R21)
//unbindHeaderScrollSync
vo.unbindHeaderScrollSync()
//eHead, eBody, eFake, eFake為ag-grid之橫向捲軸元件(使用者拖曳捲軸時由它帶動資料區)
let eShell = get(vo, '$refs.shell', null)
let eHead = eShell ? eShell.querySelector('.ag-header-viewport') : null
let eBody = eShell ? eShell.querySelector('.ag-center-cols-viewport') : null
let eFake = eShell ? eShell.querySelector('.ag-body-horizontal-scroll-viewport') : null
if (!eHead || !eBody) {
return
}
//isSync, 容許1px: ag-grid由資料區同步標頭時會將位置取整數(GridBodyScrollFeature.onHScrollCommon之Math.round), 縮小容許值會與其取整互相追逐
let isSync = function() {
return Math.abs(eHead.scrollLeft - eBody.scrollLeft) < 1
}
//check, 於下一個畫面幀確認: ag-grid由資料區同步標頭或資料區仍在捲動(連續滾輪)時, 同幀內即會一致或資料區已動, 皆不介入; 資料區未動而仍不一致才使資料區跟上標頭(以標頭位置為準, 因目標已被有意捲入可見); 資料區捲動範圍小於標頭而到不了該位置時(標頭多出數px), 再隔一幀仍不一致則標頭還原為資料區位置
//跟上時資料區與橫向捲軸元件一併寫入: ag-grid同一時間只接受一個捲動來源(使用者拖曳捲軸後500ms內只認捲軸, 其餘認資料區), 被拒絕之寫入不會更新捲軸位置、頂底釘選列與欄虛擬化, 兩處皆寫則不論當下由誰掌控, ag-grid都會走完整同步
let idFrame = null
let check = function(leftBody, bAdopted) {
idFrame = requestAnimationFrame(() => {
idFrame = null
if (isSync() || eBody.scrollLeft !== leftBody) {
return
}
if (bAdopted) {
eHead.scrollLeft = eBody.scrollLeft
}
else {
let left = eHead.scrollLeft
eBody.scrollLeft = left
if (eFake) {
eFake.scrollLeft = left
}
check(eBody.scrollLeft, true)
}
})
}
//evScroll
let evScroll = () => {
if (idFrame !== null) {
cancelAnimationFrame(idFrame)
idFrame = null
}
if (isSync()) {
return
}
check(eBody.scrollLeft, false)
}
//addEventListener
eHead.addEventListener('scroll', evScroll, { passive: true })
//headerScrollOff
vo.headerScrollOff = () => {
eHead.removeEventListener('scroll', evScroll)
if (idFrame !== null) {
cancelAnimationFrame(idFrame)
idFrame = null
}
}
},
unbindHeaderScrollSync: function() {
//console.log('methods unbindHeaderScrollSync')
let vo = this
if (isfun(vo.headerScrollOff)) {
vo.headerScrollOff()
}
vo.headerScrollOff = null
},
leaveTable: function() {
//console.log('methods leaveTable')
let vo = this
//emitCellMouseLeaves
vo.emitCellMouseLeaves()
//emitRowMouseLeaves
vo.emitRowMouseLeaves()
//clear
vo.vRowMouseEnter = null
vo.vRowMouseLeave = null
vo.vCellMouseEnter = null
vo.vCellMouseLeave = null
},
emitCellMouseLeaves: function() {
//console.log('methods emitCellMouseLeaves')
let vo = this
//cellMouseLeave
each(vo.eCellMouseLeaves, (v) => {
vo.cellMouseLeave(cloneDeep(v.field), cloneDeep(v.data))
//console.log('cellMouseLeave', cloneDeep(v.field), cloneDeep(v.data))
})
//clear
vo.eCellMouseLeaves = []
},
emitRowMouseLeaves: function() {
//console.log('methods emitRowMouseLeaves')
let vo = this
//rowMouseLeave
each(vo.eRowMouseLeaves, (v) => {
vo.rowMouseLeave(cloneDeep(v.field), cloneDeep(v.data))
//console.log('rowMouseLeave', cloneDeep(v.field), cloneDeep(v.data))
})
//clear
vo.eRowMouseLeaves = []
},
agCellMouseEnter: function(param) {
//console.log('methods agCellMouseEnter', param)
let vo = this
//tvCellMouseEnter, tvRowMouseEnter
let tvCellMouseEnter = {
field: param.colDef.field,
data: param.data,
}
let tvRowMouseEnter = {
data: param.data, //紀錄資料只會比對data, data不一樣才觸發
}
//cellMouseEnter
if (!isEqual(vo.vCellMouseEnter, tvCellMouseEnter)) {
//emitCellMouseLeaves, 要觸發enter前先觸發leave, 避免cell leave偵測失效或過慢導致連發
vo.emitCellMouseLeaves()
//field, data
let field = cloneDeep(param.colDef.field)
let data = cloneDeep(param.data)
//update vCellMouseEnter
vo.vCellMouseEnter = tvCellMouseEnter
//cellMouseEnter
vo.cellMouseEnter(field, data)
//push, 儲存需離開事件資訊
vo.eCellMouseLeaves.push({
field,
data,
})
}
//rowMouseEnter
if (!isEqual(vo.vRowMouseEnter, tvRowMouseEnter)) {
//emitRowMouseLeaves, 要觸發enter前先觸發leave, 避免row leave偵測失效或過慢導致連發
vo.emitRowMouseLeaves()
//field, data
let field = cloneDeep(param.colDef.field)
let data = cloneDeep(param.data)
//update vRowMouseEnter
vo.vRowMouseEnter = tvRowMouseEnter
//rowMouseEnter
vo.rowMouseEnter(field, data) //滑鼠最初移入的欄位依然作為資料emit出去
//push, 儲存需離開事件資訊
vo.eRowMouseLeaves.push({
field,
data,
})
//update vRowMouseLeave, 因一進列可能在同列儲存格間移動, 故需先儲存更新列數據, 若移動到相鄰儲存格時, 則可判斷因數據相同故不用觸發rowLeave
vo.vRowMouseLeave = {
data: cloneDeep(param.data),
}
}
},
agCellMouseLeave: function(param) {
//console.log('methods agCellMouseLeave', param)
let vo = this
//check
if (param.event.target.contains(param.event.relatedTarget)) { //離開元素包含進入元素時則跳出, 因可能為父元素進入內元素而觸發
//console.log('進入元素包含離開元素時則跳出')
return
}
//role
let role = null
try {
role = param.event.target.getAttribute('role') //離開元素的role
}
catch (err) {
//console.log(err)
}
//check
if (role !== 'gridcell') { //離開元素的role不是gridcell則跳出, 因有可能是cell內元素觸發
//console.log('離開元素的role不是gridcell則跳出', param.event.target)
return
}
//tvCellMouseLeave
let tvCellMouseLeave = {
field: cloneDeep(param.colDef.field),
data: cloneDeep(param.data),
}
//tvRowMouseLeave
let tvRowMouseLeave = {
data: cloneDeep(param.data),
}
//emitCellMouseLeaves
if (!isEqual(vo.vCellMouseLeave, tvCellMouseLeave)) {
vo.vCellMouseLeave = tvCellMouseLeave
vo.emitCellMouseLeaves()
}
//emitRowMouseLeaves
if (!isEqual(vo.vRowMouseLeave, tvRowMouseLeave)) {
vo.vRowMouseLeave = tvRowMouseLeave
vo.emitRowMouseLeaves()
}
},
agCellKeyDown: function(param) {
//console.log('methods agCellKeyDown', param)
let vo = this
//check, 由剪貼簿貼上
if (!(param.event.ctrlKey && param.event.code === 'KeyV')) {
return
}
//console.log('param', param)
//showRowIndNow
let showRowIndNow = param.rowIndex
// console.log('showRowIndNow', showRowIndNow)
//showColKeyNow
let showColKeyNow = param.column.colId
// console.log('showColKeyNow', showColKeyNow)
//stopEditing, 停止cell的編輯狀態
vo.getApi().stopEditing()
//pasteText
vo.pasteText(vo.dataPasted, showRowIndNow, showColKeyNow)
},
pasteText: function(text, showRowIndNow = null, showColKeyNow = null) {
// console.log('methods pasteText', text, showRowIndNow, showColKeyNow)
let vo = this
//api, 表格不可用時拋錯
let api = getApiOrThrow(vo, 'pasteText')
//mShowRowInds, mShowNodes
let mShowRowInds = []
let mShowNodes = []
let mData = []
api.forEachNodeAfterFilterAndSort(function(node, k) {
//console.log('node', node)
mShowRowInds.push(node.id)
mShowNodes.push(node)
mData.push(node.data)
})
// console.log('mShowRowInds', mShowRowInds)
// console.log('mData', mData)
//check
if (size(mShowRowInds) === 0) {
//console.log('無有效之當前顯示數據')
return
}
//mShowColKeys
// let mShowColKeys = vo.gridOptions.columnApi.getAllGridColumns()
let mShowCols = api.getAllGridColumns() //gridOptions.columnApi已歸入api, 並須通過ref取得
let mShowColKeys = map(mShowCols, (v, k) => {
// console.log('v', v, v.getId)
return v.getId()
})
// console.log('mShowColKeys', mShowColKeys)
//check
if (size(mShowColKeys) === 0) {
//console.log('無有效之當前顯示數據')
return
}
//cs
// let cs = vo.gridOptions.columnApi.getColumnState()
let cs = api.getColumnState() //gridOptions.columnApi已歸入api, 並須通過ref取得
// console.log('cs', cs)
//kpHide, kpPinned
let kpHide = {}
let kpPinned = {}
each(cs, (v) => {
kpHide[v.colId] = v.hide
kpPinned[v.colId] = !isnull(v.pinned)
})
//console.log('kpHide', kpHide)
//console.log('kpPinned', kpPinned)
//check
if (kpHide[showColKeyNow]) {
//console.log('貼於禁止編輯的儲存格, 強制取消')
return
}
//pasteOnPinned
let pasteOnPinned = null
if (kpPinned[showColKeyNow]) {
pasteOnPinned = showColKeyNow
}
//console.log('pasteOnPinned', pasteOnPinned)
//kpColumn, 各欄之ag-grid Column物件, 供isCellEditable逐儲存格判斷; 寫入以colId為資料鍵, 故只收colId與field相同者
//opt.keys重複時ag-grid以<key>_1為第二欄之colId, 其讀取之field仍為<key>, 寫入colId會新增另一屬性或覆蓋他欄, 故該欄不貼上(同修前)
let kpColumn = {}
each(mShowCols, (v) => {
if (get(v.getColDef(), 'field') === v.getId()) {
kpColumn[v.getId()] = v
}
})
//console.log('kpColumn', kpColumn)
//onEditableError, editable函數拋錯時視為不可編輯, 同一次貼上同一欄只於console說明一次, 避免大量列時洗版
let kpEditableError = {}
let onEditableError = (colKey) => {
return (err) => {
if (!kpEditableError[colKey]) {
kpEditableError[colKey] = true
//msg, 拋出之值可能無法轉字串(無原型物件等), 退為物件型態字串
let msg = ''
try {
msg = String(get(err, 'message', err))
}
catch (errMsg) {
msg = Object.prototype.toString.call(err)
}
console.log(`[w-aggrid-vue] pasteText: kpCellEditable[${colKey}] threw, the cell is treated as not editable: ${msg}`)
}
}
}
//dataPaste
let dataPaste = parseText(text)
//console.log('dataPaste', dataPaste)
//default
if (isnull(showRowIndNow)) {
showRowIndNow = 0
}
if (isnull(showColKeyNow)) {
showColKeyNow = mShowColKeys[0]
}
//pasteRowMax, pasteColMax
let pasteRowMax = null
let pasteColMax = null
try {
pasteRowMax = size(dataPaste)
}
catch (err) {
//console.log('無法計算貼上數據列的總量', err)
return
}
try {
pasteColMax = size(dataPaste[0])
}
catch (err) {
//console.log('無法計算貼上數據行的總量', err)
return
}
//paste
let i = -1
// let rowsTemp = cloneDeep(vo.rows)
let rows = vo.rows //記憶體得用原始記憶體, 否則applyTransaction會無法找到原始數據物件進行更新
let updateRows = []
let writes = []
for (let ii = 0; ii < size(mShowColKeys); ii++) {
let v = mShowColKeys[ii]
//console.log('v', v)
//check
if (kpHide[v]) {
continue //數據貼到隱藏欄位故跳過
}
//check
if (pasteOnPinned) { //貼於固定欄位時
if (kpPinned[v]) {
if (pasteOnPinned === v) { //v !== pasteOnPinned
//console.log('已貼到固定欄位')
pasteOnPinned = true //已貼到固定欄位, 強制修改為true
}
else if (pasteOnPinned === true) {
//console.log('已貼過固定欄位, 允許貼至其他固定欄位')
}
else {
//console.log('雖然是貼到固定欄位, 但尚未貼到原本固定欄位, 強制取消')
continue
}
}
else {
//console.log('貼於固定欄位時, 數據只允許貼到該固定欄位上', pasteOnPinned, v)
continue //貼於固定欄位時, 數據只允許貼到該固定欄位上
}
}
else { //不是貼於固定欄位時
if (kpPinned[v]) {
//console.log('不是貼於固定欄位時, 數據貼到固定欄位則跳過', pasteOnPinned, v, kpPinned[v])
continue //不是貼於固定欄位時, 數據貼到固定欄位則跳過
}
}
//add i
if (v === showColKeyNow) {
i = 0
}
else if (i >= 0) { //當出現>=0就代表已經找到showColKey
i += 1
}
//modify data
if (i >= 0 && i < pasteColMax) {
let pasteColInd = i
let trueColKey = v
// console.log('pasteColInd', pasteColInd, 'trueColKey', trueColKey)
//column, onError
let column = kpColumn[trueColKey]
if (!column) {
continue //colId與field不同之欄(opt.keys重複)不貼上, i的增加還是得照常處理
}
let onError = onEditableError(trueColKey)
for (let j = 0; j < pasteRowMax; j++) {
let pasteRowInd = j
if (showRowIndNow + j < size(mShowRowInds)) {
let node = mShowNodes[showRowIndNow + j]
// console.log('pasteRowInd', pasteRowInd, 'node.id', node.id)
//check, 逐儲存格判斷可否編輯(editable為函數時依該列求值), 禁止編輯之儲存格故跳過, i的增加還是得照常處理, 也就是只有不能編輯儲存格的數據不變更
if (!isCellEditable(column, node, onError)) {
continue
}
// //pasteValue
// let pasteValue = dataPaste[pasteRowInd][pasteColInd]
// console.log('pasteValue', pasteValue)
// //trueValue
// let trueValue = rowsTemp[node.id][trueColKey]
// console.log('trueValue', trueValue)
//push, 先紀錄待寫入之儲存格, 全部儲存格判斷完才寫入, 使同一次貼上先寫入之儲存格不影響後面儲存格之可編輯判斷
writes.push([node, trueColKey, dataPaste[pasteRowInd][pasteColInd]])
}
}
}
}
//save, 並只紀錄需變更的rows; 寫入判斷可否編輯所依之那一列(node.data), 不以node.id索引vo.rows: filterall生效時rowData為過濾後之子集, node.id為子集索引, 與vo.rows索引不同; node.data與vo.rows之列為同一物件(applyTransaction亦依此比對)
each(writes, ([node, trueColKey, pasteValue]) => {
node.data[trueColKey] = pasteValue
updateRows.push(node.data)
})
//applyTransaction, 只變更必要資料加速, 注意記憶體得用原始記憶體, 也就是updateRows得使用原始vo.rows來給予欲變更之rows, 否則applyTransaction會無法找到原始數據物件進行更新
api.applyTransaction({ update: updateRows })
// console.log('applyTransaction', cloneDeep(updateRows))
//需使用set強制更新外部opt物件的rows並再同步更新至內部rows, 否則外面數據會沒更動
set(vo, `opt.rows`, rows)
// //rowsChange, rows是內部已更新數據, 直接用於回傳函數
// vo.rowsChange(rows) //agRowDataUpdated也會觸發並再觸發rowsChange, 故此處不觸發
},
refresh: async function() {
//console.log('methods refresh')
let vo = this
//delay
await delay(1)
//api, 延後執行期間可能因使用者切換組件而已銷毀, 表格不可用時略過
let api = vo.getApi()
if (!isGridAvailable(api)) {
return
}
//redrawRows, 因資料變更不會觸發getRowStyle, 給自行redrawRows
api.redrawRows()
},
keys2columns: function(keys) {
//console.log('methods keys2columns')
let vo = this
//cs
let cs = map(keys, function(key) {
let o = {}
//key
o.field = key
//text
o.headerName = vo.kpHead[key]
//sort
o.sortable = vo.kpHeadSort[key]
//comparator
let funHeadSortMethod = vo.kpHeadSortMethod[key]
if (isfun(funHeadSortMethod)) {
o.comparator = funHeadSortMethod
}
else if (funHeadSortMethod === 'auto') {
o.comparator = function(valueA, valueB, nodeA, nodeB, isInverted) {
//若值無效
let sA = iser(valueA)
let sB = iser(valueB)
if (sA && sB) {
return 0
}
if (sA) {
return -1
}
if (sB) {
return 1
}
//若值為數字
if (isnum(valueA) && isnum(valueB)) {
return cdbl(valueA) - cdbl(valueB)
}
//若值為字串, 先比長度越小越前面, 長度相同則用localeCompare比較
if (isestr(valueA) && isestr(valueB)) {
let lenA = size(valueA)
let lenB = size(valueB)
if (lenA !== lenB) {
return lenA - lenB
}
else {
return valueA.localeCompare(valueB)
}
}
return 0 //不比較
}
}
//filter, 欄位標題右邊的選單按鈕, 可使用數字過濾器'agNumberColumnFilter'或文字過濾器'agTextColumnFilter'
if (vo.kpHeadFilter[key]) {
let t = vo.kpHeadFilterType[key]
if (t === 'text') {
o.filter = 'agTextColumnFilter'
}
else if (t === 'time') {
o.filter = 'agDateColumnFilter'
}
else if (t === 'set') {
o.filter = 'agSetColumnFilter'
}
else { //其他皆為num
o.filter = 'agNumberColumnFilter'
}
}
else {
o.filter = false
}
//floatingFilter, 欄位標題下方的文字過濾輸入區, 因ag-grid 23.1.0已改為由column給予floatingFilter, 若全部column都false, 則標題下方查詢區就會自動清除騰出空間
o.floatingFilter = vo.kpHeadFilter[key]
//suppressMenu
//o.suppressMenu = true //關閉欄位標題右邊的選單按鈕
//filterParams
o.filterParams = {
// caseSensitive: true, //過濾區分大小寫, 會導致數字欄位無法被過濾
// applyButton: true, //彈出選單下方可加入使用按鈕
// clearButton: true, //彈出選單下方可加入取消按鈕
// filterParams: {
// comparator: function(ft, cv) {
// console.log('comparator', ft, cv)
// }
// }
}
//filter plus, 欄位標題下方加入文字過濾輸入框
// o.floatingFilterComponentParams = { suppressFilterButton: true }
o.suppressFloatingFilterButton = true //關閉文字過濾輸入框右邊按鈕, 需suppressFloatingFilterButton=true, 因ag-grid 31.1.0已廢棄floatingFilterComponentParams.suppressFilterButton, 須改用suppressFloatingFilterButton
//lockPosition, 若不拖曳而設定欄位lockPosition為true時, 該欄位會強制置左, 故不使用
//o.lockPosition = !vo.kpHeadDrag[key]
//suppressMovable, 指定欄位不能拖曳, 但會被其他欄位拖曳而改變位置
o.suppressMovable = !vo.kpHeadDrag[key]
//resizable
o.resizable = true
//editable
o.editable = vo.kpCellEditable[key]
// console.log(key, 'vo.kpCellEditable[key]', vo.kpCellEditable[key])
//row selection
if (vo.kpHeadCheckBox[key]) {
o.headerCheckboxSelection = true
o.headerCheckboxSelectionFilteredOnly = true //若有使用過濾filter, 點header的全選全取消核選方塊時, 僅針對過濾的rows來操作
o.checkboxSelection = true
}
//cell highlight when focused
if (!vo.kpHeadFocusHighlight[key]) {
o.cellClass = 'no-border'
}
//flex
// o.flex = 1
//text-align
let cTextAlign = vo.kpHeadAlignH[key]
//justify-content
let kpTA2JC = {
'left': 'flex-start',
'center': 'center',
'right': 'flex-end',
}
let cJustifyContent = kpTA2JC[cTextAlign]
//head-render, head-tooltip slot
let headRenderSlot = vo.$scopedSlots['head-render']
let headTooltipSlot = vo.$scopedSlots['head-tooltip']
if (headRenderSlot || headTooltipSlot) {
o.headerComponent = 'HeaderSlotRenderer'
o.headerComponentParams = {
headRenderSlotFn: headRenderSlot || null,
headTooltipSlotFn: headTooltipSlot || null,
cJustifyContent,
}
}
else {
//headTemplate, 基於ref=eText來尋覓與給予head字串
let headTemplate = `
<div class="ag-cell-label-container" role="presentation">
<span ref="eMenu" class="ag-header-icon ag-header-cell-menu-button"></span>
<div ref="eLabel" class="ag-header-cell-label" style="justify-content:${cJustifyContent};" role="presentation">
<span ref="eText" class="ag-header-cell-text" role="columnheader"></span>
<span ref="eSortOrder" class="ag-header-icon ag-sort-order ag-hidden" ></span>
<span ref="eSortAsc" class="ag-header-icon ag-sort-ascending-icon ag-hidden" ></span>
<span ref="eSortDesc" class="ag-header-icon ag-sort-descending-icon ag-hidden" ></span>
<span ref="eSortMixed" class="ag-header-icon ag-sort-mixed-icon ag-hidden"></span>
<span ref="eSortNone" class="ag-header-icon ag-sort-none-icon ag-hidden" ></span>
<span ref="eFilter" class="ag-header-icon ag-header-label-icon ag-filter-icon"></span>
</div>
</div>
`
o.headerComponentParams = {
template: headTemplate,
}
}
//funColSpan
let funColSpan = vo.kpColSpan[key]
//colSpan
if (isfun(funColSpan)) {
o.colSpan = function(params) {
// console.log('colSpan', params)
//row
let row = get(params, 'data', {})
//value
let value = get(row, key)
//funColSpan
let i = funColSpan(value, key, row, params)
//check
if (!isnum(i)) {
i = 1
}
//cint
i = cint(i)
//check
if (i < 1) {
i = 1
}
return i
}
}
//funCellFormat
let funCellFormat = vo.kpCellFormat[key]
//valueFormatter, 值的格式化(如小數位)於ag-grid之valueFormatter層處理, 使顯示與下載走同一字串(community版無剪貼簿模組, 不涉及複製); 排序與過濾仍依原值(ag-grid過濾器讀原值, 數值過濾須維持數值比較)
if (isfun(funCellFormat)) {
o.valueFormatter = function(params) {
// console.log('valueFormatter', params)
//row
let row = get(params, 'data', {})
//funCellFormat, 回傳值原樣交給ag-grid, 如何回傳由呼叫端決定; 回傳null或undefined時ag-grid之valueFormatted為null, renderer改用原值
return funCellFormat(params.value, key, row, params)
}
}
//cell-render, cell-tooltip slot
let cellRenderSlot = vo.$scopedSlots['cell-render']
let cellTooltipSlot = vo.$scopedSlots['cell-tooltip']
if (cellRenderSlot || cellTooltipSlot) {
o.cellRenderer = 'CellSlotRenderer'
o.cellRendererParams = {
cellRenderSlotFn: cellRenderSlot || null,
cellTooltipSlotFn: cellTooltipSlot || null,
bCellFormat: isfun(funCellFormat), //供cvCellTextByParams判定是否採用valueFormatted
}
}
else {
o.cellRenderer = function(params) {
return cvCellTextByParams(params)
}
o.cellRendererParams = {
bCellFormat: isfun(funCellFormat), //供cvCellTextByParams判定是否採用valueFormatted
}
}
//cellStyle
let cellStyle = {
'text-align': vo.kpCellAlignH[key] //add align
}
let funColStyle = vo.kpColStyle[key]
if (isfun(funColStyle)) {
cellStyle = merge(cellStyle, funColStyle()) //add col style
}
o.cellStyle = cellStyle
//fix
if (isbol(vo.kpHeadFixLeft[key])) {
//pinned
o.pinned = vo.kpHeadFixLeft[key] ? 'left' : ''
// //suppressSizeToFit, true代表禁止欄位被sizeColumnsToFit, 因有pinned也需能resize與自動調整寬度, 故取消此功能
// if (vo.kpHeadFixLeft[key]) {
// o.suppressSizeToFit = true
// }
}
//width, 內容物超過width不會自動撐開
if (isnum(vo.kpHeadWidth[key])) {
let w = cdbl(vo.kpHeadWidth[key])
o.width = w
}
//minWidth, ag-grid設定minWidth還是會自動被處理調整成寬度, 例如minWidth給150, 實際會依照內容長度轉成width如200, 而內容物超過轉換後的width不會自動撐開
if (isnum(vo.defHeadMinWidth)) {
o.minWidth = vo.defHeadMinWidth
}
//maxWidth
if (isnum(vo.defHeadMaxWidth)) {
o.maxWidth = vo.defHeadMaxWidth
}
//rowDrag
let rowDrag = false //default
if (isbol(vo.kpRowDrag[key])) {
rowDrag = vo.kpRowDrag[key]
}
o.rowDrag = rowDrag
//hide
let hide = false //default
if (isbol(vo.kpHeadHide[key])) {
hide = vo.kpHeadHide[key]
}
o.hide = hide
return o
})
return cs
},
changeOpt: function() {
//console.log('methods changeOpt')
let vo = this
//check, 未給予(null、undefined)時不提示; 有給予但非非空物件時提示表格不更新(R01、R03)
if (!iseobj(vo.opt)) {
if (isGiven(vo.opt)) {
console.log('[w-aggrid-vue] opt is invalid, it must be an object with keys and rows, the table is not updated')
}
return
}
//check removed keys, 2.0.56起改用slot, 呼叫端沿用舊文件傳入時提示; 僅警告不改行為, 每實例只警告一次
if (!vo.bWarnedRemovedKeys) {
let kpRemoved = {
kpCellRender: 'cell-render',
kpCellTooltip: 'cell-tooltip',
kpHeadRender: 'head-render',
kpHeadTooltip: 'head-tooltip',
}
each(kpRemoved, (slotName, k) => {
if (haskey(vo.opt, k)) {
vo.bWarnedRemovedKeys = true
console.warn(`[w-aggrid-vue] opt.${k} was removed in 2.0.56, use scoped slot '${slotName}' instead`)
}
})
}
//check keys
let tKeys = get(vo, 'opt.keys')
if (!isearr(tKeys)) {
console.log('[w-aggrid-vue] opt.keys is invalid, it must be a non-empty array of keys, the table is not updated')
return
}
//check rows
let tRows = get(vo, 'opt.rows')
if (!isarr(tRows)) {
console.log('[w-aggrid-vue] opt.rows is invalid, it must be an array, the table is not updated')
return
}
//setobj
function setobj(keys, def, kpobj) {
let o = {}
each(keys, function(key) {
let h = def(key)
if (isobj(kpobj)) {
if (haskey(kpobj, key)) {
h = kpobj[key]
}
}
o[key] = h
})
return o
}
//autoFitColumn, ag-grid僅有
let autoFitColumn = false
if (isbol(vo.opt.autoFitColumn)) {
autoFitColumn = vo.opt.autoFitColumn
}
vo.autoFitColumn = autoFitColumn
//keys
vo.keys = vo.opt.keys
//kpHead
vo.kpHead = setobj(vo.keys,
function(key) {
return key //預設使用key做head
},
vo.opt.kpHead
)
//defHeadAlignH
vo.defHeadAlignH = 'center'
if (arrHas(vo.opt.defHeadAlignH, ['left', 'center', 'right'])) {
vo.defHeadAlignH = vo.opt.defHeadAlignH
}
//kpHeadAlignH
vo.kpHeadAlignH = setobj(vo.keys,
function(key) {
return vo.defHeadAlignH
},
vo.opt.kpHeadAlignH
)
//defHeadSort
vo.defHeadSort = true
if (isbol(vo.opt.defHeadSort)) {
vo.defHeadSort = vo.opt.defHeadSort
}
//kpHeadSort
vo.kpHeadSort = setobj(vo.keys,
function(key) {
return vo.defHeadSort
},
vo.opt.kpHeadSort
)
//defHeadSortMethod
vo.defHeadSortMethod = null
if (isfun(vo.opt.defHeadSortMethod) || isestr(vo.opt.defHeadSortMethod)) {
vo.defHeadSortMethod = vo.opt.defHeadSortMethod
}
//kpHeadSortMethod
vo.kpHeadSortMethod = setobj(vo.keys,
function(key) {
return vo.defHeadSortMethod
},
vo.opt.kpHeadSortMethod
)
//kpHeadFixLeft
vo.kpHeadFixLeft = {}
if (iseobj(vo.opt.kpHeadFixLeft)) {
vo.kpHeadFixLeft = vo.opt.kpHeadFixLeft
}
//defHeadFilter
vo.defHeadFilter = true
if (isbol(vo.opt.defHeadFilter)) {
vo.defHeadFilter = vo.opt.defHeadFilter
}
//kpHeadFilter
vo.kpHeadFilter = setobj(vo.keys,
function(key) {
return vo.defHeadFilter
},
vo.opt.kpHeadFilter
)
//defHeadFilterType
vo.defHeadFilterType = 'num'
if (arrHas(vo.opt.defHeadFilterType, ['num', 'text', 'time', 'set'])) {
vo.defHeadFilterType = vo.opt.defHeadFilterType
}
//kpHeadFilterType
vo.kpHeadFilterType = setobj(vo.keys,
function(key) {
return vo.defHeadFilterType
},
vo.opt.kpHeadFilterType
)
//defHeadDrag
vo.defHeadDrag = true
if (isbol(vo.opt.defHeadDrag)) {
vo.defHeadDrag = vo.opt.defHeadDrag
}
//kpHeadDrag
vo.kpHeadDrag = setobj(vo.keys,
function(key) {
return vo.defHeadDrag
},
vo.opt.kpHeadDrag
)
//kpHeadCheckBox
vo.kpHeadCheckBox = setobj(vo.keys,
function(key) {
return false //每個col皆預設false
},
vo.opt.kpHeadCheckBox
)
//kpHeadFocusHighlight
vo.kpHeadFocusHighlight = setobj(vo.keys,
function(key) {
return true //每個col皆預設true
},
vo.opt.kpHeadFocusHighlight
)
//kpHeadHide
vo.kpHeadHide = {}
if (iseobj(vo.opt.kpHeadHide)) {
vo.kpHeadHide = vo.opt.kpHeadHide
}
//kpRowStyle
vo.kpRowStyle = {}
if (iseobj(vo.opt.kpRowStyle)) {
vo.kpRowStyle = vo.opt.kpRowStyle
}
//kpRowDrag
vo.kpRowDrag = {}
if (iseobj(vo.opt.kpRowDrag)) {
vo.kpRowDrag = vo.opt.kpRowDrag
}
//genRowsPinnTop
vo.genRowsPinnTop = null
if (isfun(vo.opt.genRowsPinnTop)) {
vo.genRowsPinnTop = vo.opt.genRowsPinnTop
}
//genRowsPinnBottom
vo.genRowsPinnBottom = null
if (isfun(vo.opt.genRowsPinnBottom)) {
vo.genRowsPinnBottom = vo.opt.genRowsPinnBottom
}
//kpColStyle
vo.kpColStyle = {}
if (iseobj(vo.opt.kpColStyle)) {
vo.kpColStyle = vo.opt.kpColStyle
}
//kpColSpan
vo.kpColSpan = {}
if (iseobj(vo.opt.kpColSpan)) {
vo.kpColSpan = vo.opt.kpColSpan
}
//kpCellFormat
vo.kpCellFormat = {}
if (iseobj(vo.opt.kpCellFormat)) {
vo.kpCellFormat = vo.opt.kpCellFormat
}
//defHeadMinWidth
vo.defHeadMinWidth = null
if (isnum(vo.opt.defHeadMinWidth)) {
vo.defHeadMinWidth = vo.opt.defHeadMinWidth
}
//defHeadMaxWidth
vo.defHeadMaxWidth = null
if (isnum(vo.opt.defHeadMaxWidth)) {
vo.defHeadMaxWidth = vo.opt.defHeadMaxWidth
}
//kpHeadWidth
vo.kpHeadWidth = {}
if (iseobj(vo.opt.kpHeadWidth)) {
vo.kpHeadWidth = vo.opt.kpHeadWidth
}
//defCellAlignH
vo.defCellAlignH = 'center'
if (arrHas(vo.opt.defCellAlignH, ['left', 'center', 'right'])) {
vo.defCellAlignH = vo.opt.defCellAlignH
}
//kpCellAlignH
vo.kpCellAlignH = setobj(vo.keys,
function(key) {
return vo.defCellAlignH
},
vo.opt.kpCellAlignH
)
//defCellEditable
vo.defCellEditable = false
if (isbol(vo.opt.defCellEditable)) {
vo.defCellEditable = vo.opt.defCellEditable
}
//kpCellEditable
vo.kpCellEditable = setobj(vo.keys,
function(key) {
return vo.defCellEditable
},
vo.opt.kpCellEditable
)
//tableClickEnable
vo.tableClickEnable = false
//rowsChange
vo.rowsChange = function() {}
if (isfun(vo.opt.rowsChange)) {
vo.rowsChange = vo.opt.rowsChange
}
//rowClick
vo.rowClick = function() {}
if (isfun(vo.opt.rowClick)) {
vo.rowClick = vo.opt.rowClick
vo.tableClickEnable = true
}
//rowDbClick
vo.rowDbClick = function() {}
if (isfun(vo.opt.rowDbClick)) {
vo.rowDbClick = vo.opt.rowDbClick
vo.tableClickEnable = true
}
//rowChange
vo.rowChange = function() {}
if (isfun(vo.opt.rowChange)) {
vo.rowChange = vo.opt.rowChange
}
//rowChecked
vo.rowChecked = function() {}
if (isfun(vo.opt.rowChecked)) {
vo.rowChecked = vo.opt.rowChecked
}
//rowMouseEnter
vo.rowMouseEnter = function() {}
if (isfun(vo.opt.rowMouseEnter)) {
vo.rowMouseEnter = vo.opt.rowMouseEnter
}
//rowMouseLeave
vo.rowMouseLeave = function() {}
if (isfun(vo.opt.rowMouseLeave)) {
vo.rowMouseLeave = vo.opt.rowMouseLeave
}
//rowDragEnter
vo.rowDragEnter = function() {}
if (isfun(vo.opt.rowDragEnter)) {
vo.rowDragEnter = vo.opt.rowDragEnter
}
//rowDragEnd
vo.rowDragEnd = function() {}
if (isfun(vo.opt.rowDragEnd)) {
vo.rowDragEnd = vo.opt.rowDragEnd
}
//rowDragMove
vo.rowDragMove = function() {}
if (isfun(vo.opt.rowDragMove)) {
vo.rowDragMove = vo.opt.rowDragMove
}
//rowDragLeave
vo.rowDragLeave = function() {}
if (isfun(vo.opt.rowDragLeave)) {
vo.rowDragLeave = vo.opt.rowDragLeave
}
//rowDragChange
vo.rowDragChange = function() {}
if (isfun(vo.opt.rowDragChange)) {
vo.rowDragChange = vo.opt.rowDragChange
}
//cellClick
vo.cellClick = function() {}
if (isfun(vo.opt.cellClick)) {
vo.cellClick = vo.opt.cellClick
vo.tableClickEnable = true
}
//cellDbClick
vo.cellDbClick = function() {}
if (isfun(vo.opt.cellDbClick)) {
vo.cellDbClick = vo.opt.cellDbClick
vo.tableClickEnable = true
}
//cellChange
vo.cellChange = function() {}
if (isfun(vo.opt.cellChange)) {
vo.cellChange = vo.opt.cellChange
}
//cellMouseEnter
vo.cellMouseEnter = function() {}
if (isfun(vo.opt.cellMouseEnter)) {
vo.cellMouseEnter = vo.opt.cellMouseEnter
}
//cellMouseLeave
vo.cellMouseLeave = function() {}
if (isfun(vo.opt.cellMouseLeave)) {
vo.cellMouseLeave = vo.opt.cellMouseLeave
}
//filterChange
vo.filterChange = function() {}
if (isfun(vo.opt.filterChange)) {
vo.filterChange = vo.opt.filterChange
}
//kpConvertKeysWhenUploadData
vo.kpConvertKeysWhenUploadData = {}
if (iseobj(vo.opt.kpConvertKeysWhenUploadData)) {
vo.kpConvertKeysWhenUploadData = vo.opt.kpConvertKeysWhenUploadData
}
//language
vo.language = 'en' //'en', 'zh-tw', 'zh-cn'
if (isestr(vo.opt.language)) {
vo.language = vo.opt.language
}
//columns, 需放在給予rows之前, 避免給予rows沒有column設定
vo.columns = vo.keys2columns(vo.keys)
//rows
vo.rows = vo.opt.rows
},
setRowsPinnTop: function(rows) {
// console.log('methods setRowsPinnTop', rows)
let vo = this
//check
if (isfun(vo.genRowsPinnTop)) {
//pinnedTopRowData
let pinnedTopRowData = vo.genRowsPinnTop(rows)
// console.log('pinnedTopRowData', pinnedTopRowData)
//api, 呼叫端之函數(含先前之cellChange等回呼)可能已銷毀表格, 不可用時略過
let api = vo.getApi()
if (!isGridAvailable(api)) {
return
}
//update
// vo.getApi().setPinnedTopRowData(pinnedBottomRowData)
api.setGridOption('pinnedTopRowData', pinnedTopRowData) //setPinnedTopRowData已統一改為setGridOption('pinnedTopRowData',newValue)
}
},
setRowsPinnBottom: function(rows) {
// console.log('methods setRowsPinnBottom', rows)
let vo = this
//check
if (isfun(vo.genRowsPinnBottom)) {
//pinnedBottomRowData
let pinnedBottomRowData = vo.genRowsPinnBottom(rows)
// console.log('pinnedBottomRowData', pinnedBottomRowData)
//api, 呼叫端之函數(含先前之cellChange等回呼)可能已銷毀表格, 不可用時略過
let api = vo.getApi()
if (!isGridAvailable(api)) {
return
}
//update
// vo.getApi().setPinnedBottomRowData(pinnedBottomRowData)
api.setGridOption('pinnedBottomRowData', pinnedBottomRowData) //setPinnedBottomRowData已統一改為setGridOption('pinnedBottomRowData',newValue)
}
},
showKeys: function(keys_new, opt = {}) {
// console.log('methods showKeys', keys_new, opt)
let vo = this
//check, keys_new為必要之資料: 須為陣列且元素為非空字串(欄位key), 否則拋錯且不改變欄位狀態; 未知之key由ag-grid略過, 空陣列代表全部隱藏
if (!isarr(keys_new) || !every(keys_new, isestr)) {
throw new Error('[w-aggrid-vue] showKeys: keys must be an array of non-empty strings')
}
//api, 表格不可用時拋錯
let api = getApiOrThrow(vo, 'showKeys')
//opt
if (!isobj(opt)) {
if (isGiven(opt)) {
logUseDefault('showKeys', 'opt', opt, '{}')
}
opt = {}
}
//applyOrder, 預設true代表依keys_new之順序調整欄位順序; false代表僅切換顯示與隱藏, 維持目前欄位順序(含使用者拖曳後之順序)
let applyOrder = get(opt, 'applyOrder', null)
if (!isbol(applyOrder)) {
if (isGiven(applyOrder)) {
logUseDefault('showKeys', 'applyOrder', applyOrder, 'true')
}
applyOrder = true
}
//keys_old
let keys_old = cloneDeep(vo.keys)
// console.log('keys_old', keys_old)
//keys_nouse
let keys_nouse = difference(keys_old, keys_new)
// console.log('keys_nouse', keys_nouse)
//csn, 僅給colId與hide, 不回灌getColumnState之width、sort、pinned等其他欄位狀態
let csn = []
each(keys_new, function(key) {
csn.push({ colId: key, hide: false })
})
each(keys_nouse, function(key) {
csn.push({ colId: key, hide: true }) //未顯示column改為隱藏
})
// console.log('csn', csn)
//applyColumnState
//applyOrder為true時依csn調整欄位順序; 被ag-grid固定(pinned)之欄位不論順序為何, 皆固定顯示於左側區
api.applyColumnState({ //gridOptions.columnApi已歸入api, 並須通過ref取得
state: csn,
applyOrder,
})
},
getFilters: async function() {
// console.log('methods getFilters')
let vo = this
//nextTick, 由computed(changeFilterall)於首次render中觸發時子組件ag-grid尚未建立, 須延至本次更新結束後才取api
await vo.$nextTick()
//api, 表格不可用時拋錯(即reject)
let api = getApiOrThrow(vo, 'getFilters')
//fm, 以同步之getFilterModel取各欄生效中之過濾模型(含隱藏欄); 不使用getColumnFilterInstance, 其對未啟用過濾之欄與不存在之key永不resolve
let fm = api.getFilterModel()
//flts, 依vo.keys之順序輸出(getFilterModel之鍵序為過濾器之建立順序), 以fm[key]取值(key含'.'時lodash get會解析為路徑)
let flts = []
each(vo.keys, (key) => {
let md = fm[key]
if (iseobj(md)) {
flts.push(md)
}
})
return flts
},
clearHeadFilterAll: async function() {
//console.log('methods clearHeadFilterAll')
let vo = this
//api, 表格不可用時拋錯(即reject)
let api = getApiOrThrow(vo, 'clearHeadFilterAll')
//bFiltered, 有生效中之過濾時, setFilterModel於模型改變後會自行觸發onFilterChanged
let bFiltered = iseobj(api.getFilterModel())
//reset all filters
api.setFilterModel(null)
//fire onFilterChanged, 無生效中之過濾時才自行觸發(呼叫後一律發出1次filterChange), 避免與setFilterModel重複觸發而發出2次
if (!bFiltered) {
api.onFilterChanged()
}
},
clearHeadFilter: async function(key) {
//console.log('methods clearHeadFilter', key)
let vo = this
//api, 表格不可用時拋錯(即reject)
let api = getApiOrThrow(vo, 'clearHeadFilter')
//setModel, 表格建立時無數據列者須待首批數據列載入後才完成(ag-grid型別推斷期間排隊)
// ft.setModel(null)
await api.setColumnFilterModel(key, null) //setModel改為setColumnFilterModel
//api, 等待期間表格可能已銷毀, 重新取得
api = getApiOrThrow(vo, 'clearHeadFilter')
//fire onFilterChanged
api.onFilterChanged()
},
setHeadFilter: async function(key, value, type = 'contains') {
//console.log('methods setHeadFilter', key, value, type)
let vo = this
//api, 表格不可用時拋錯(即reject)
let api = getApiOrThrow(vo, 'setHeadFilter')
// let tpsText = [
// 'empty',
// 'equals',
// 'notEqual',
// 'lessThan',
// 'lessThanOrEqual',
// 'greaterThan',
// 'greaterThanOrEqual',
// 'inRange',
// 'contains',
// 'notContains',
// 'startsWith',
// 'endsWith',
// 'blank',
// 'notBlank',
// ]
// let tpsNumber = [
// 'empty',
// 'equals',
// 'notEqual',
// 'lessThan',
// 'lessThanOrEqual',
// 'greaterThan',
// 'greaterThanOrEqual',
// 'inRange',
// 'contains',
// 'notContains',
// 'startsWith',
// 'endsWith',
// 'blank',
// 'notBlank',
// ]
//m
let m = {
type,
// filter: cstr(value), //ag-grid 23版有修改filter判斷式, 其內使用trim故限定value需為字串
filter: value, //ag-grid 31.1.0已修復
}
//setModel, 表格建立時無數據列者須待首批數據列載入後才完成(ag-grid型別推斷期間排隊)
// ft.setModel(m)
await api.setColumnFilterModel(key, m) //setModel改為setColumnFilterModel
//api, 等待期間表格可能已銷毀, 重新取得
api = getApiOrThrow(vo, 'setHeadFilter')
//fire onFilterChanged
api.onFilterChanged()
},
getDisplayDataKeys: function() {
//console.log('methods getDisplayDataKeys')
let vo = this
//api, 表格不可用時拋錯
let api = getApiOrThrow(vo, 'getDisplayDataKeys')
//cols, 顯示中之欄位且依畫面欄序(左固定、中間、右固定), 同ag-grid匯出之預設欄位; getColumnState之順序不反映固定欄(kpHeadFixLeft)之位置故不使用
let cols = api.getAllDisplayedColumns() //gridOptions.columnApi已歸入api, 並須通過ref取得
//show keys
let keys = map(cols, (col) => {
return col.getColId()
})
return keys
},
getDisplayData: function() {
//console.log('methods getDisplayData')
let vo = this
//api, 表格不可用時拋錯
let api = getApiOrThrow(vo, 'getDisplayData')
//rs
let rs = []
api.forEachNodeAfterFilterAndSort((node) => {
rs.push(cloneDeep(node.data))
})
//show keys
let keys = vo.getDisplayDataKeys()
//pickRowsByKeys, 全部欄位隱藏時keys為空而回傳[]
let data = pickRowsByKeys(rs, keys)
return data
},
getNowData: function() {
//console.log('methods getNowData')
let vo = this
//api, 表格不可用時拋錯
let api = getApiOrThrow(vo, 'getNowData')
//rs
let rs = []
api.forEachNode((node) => {
rs.push(cloneDeep(node.data))
})
//pickRowsByKeys
let data = pickRowsByKeys(rs, vo.keys)
return data
},
fitColumns: function(bDebounce = true) {
// console.log('methods fitColumns', bDebounce)
let vo = this
//fit
async function fit(w) {
// console.log('call fit', w)
try {
//width
vo.width = w
// //delay
// await delay(30)
//api, 去抖動或resize之後才執行, 表格可能已銷毀(例如於高頻初始化與解構時切換分頁), 不可用時略過
let api = vo.getApi()
if (!isGridAvailable(api)) {
return
}
//sizeColumnsToFit
api.sizeColumnsToFit()
}
catch (err) {
console.log('[w-aggrid-vue] fitColumns:', err)
}
}
//core
async function core() {
// console.log('call core')
//ag-grid 24.1.0已能正常fit至100%, 不會因微小差距而出現水平捲軸, 故關閉先縮小再還原機制, 但縮放第3次時有些欄位header會爆版, 故還是得使用先縮小再還原機制
// //先微縮小fit
// await fit(99.5)
// //delay
// await delay(30) //不能過短, 否則先縮小再還原機制會失效, header會爆版
//還原fit, ag-grid 27.3.0已能正常fit至100%
await fit(100)
}
//core
if (bDebounce) {
vo.dbc(() => {
core()
})
}
else {
core()
}
},
/**
* 下載表格顯示中之數據為Excel檔案,欄位為顯示中之欄位且依畫面欄序(含拖曳後之順序,固定於左右之欄依其畫面位置),數據為經表頭過濾、filterall與排序後之列,不含置頂置底列;須表格存在,元件或ag-grid銷毀後呼叫會拋錯
*
* 選項、處理順序、回傳值與拋錯皆同downloadData,訊息前綴改為'[w-aggrid-vue] downloadDisplayData: ',另於表格不存在(元件或ag-grid已銷毀)時拋'grid is not available';opt.funGetKeysHook加入隱藏欄時亦輸出其值
*
* @param {Object} [opt={}] 輸入設定物件,各選項同downloadData,預設{}
* @returns {Array} 回傳下載內容之二維陣列,同downloadData
* @throws {Error} 同downloadData,另於表格不存在時拋錯
*/
downloadDisplayData: function(opt = {}) {
//console.log('methods downloadDisplayData', opt)
let vo = this
//api, 表格不可用(元件或ag-grid已銷毀)時拋錯
let api = getApiOrThrow(vo, 'downloadDisplayData')
//keys, 顯示中之欄位且依畫面欄序
let keys = vo.getDisplayDataKeys()
//rows, 經表頭過濾、filterall與排序後之列, 於執行任何掛鉤前取定
let rows = []
api.forEachNodeAfterFilterAndSort((node) => {
rows.push(node.data)
})
return downloadCore(vo, 'downloadDisplayData', keys, rows, opt)
},
/**
* 下載表格全部數據為Excel檔案,欄位依opt.keys之順序且含隱藏欄,數據為opt.rows全部列(不受表頭過濾、filterall與排序影響),不含置頂置底列;不需表格就緒即可呼叫,惟opt變更後須待$nextTick再呼叫,否則取得變更前之數據
*
* 處理順序:欄位keys → opt.funGetKeysHook → 依keys由各列取值並依opt.kpCellFormat格式化 → opt.funGetLtdtHook → 轉為二維陣列並加上表頭列 → opt.funGetMatHook → 觸發下載;各掛鉤皆須同步回傳,不支援Promise
*
* @param {Object} [opt={}] 輸入設定物件,未給予或非物件時為{},各鍵未給予或非指定之值或型態時皆使用預設(掛鉤非函數時視為未給予),有給予但無效時於console提示,預設{}
* @param {Function} [opt.funGetKeysHook=null] 輸入欄位處理函數,輸入為欄位keys陣列之副本,可增刪或調整順序後回傳(可加入隱藏欄,數據列無該key時其值為空字串),須回傳由非空字串組成之陣列,回傳空陣列代表無可下載欄位而拋錯,預設null
* @param {Function} [opt.funGetLtdtHook=null] 輸入數據處理函數,輸入為依keys提取之各列物件陣列(值已依useFormat格式化且為副本,無數據列時為空陣列),須回傳物件陣列且各列皆為非空物件,回傳空陣列時僅輸出表頭列,本函數新增或修改之值不再格式化,預設null
* @param {Function} [opt.funGetMatHook=null] 輸入二維陣列處理函數,輸入之首列為表頭,須回傳二維陣列且至少一列非空,回傳之儲存格值原樣交給寫檔器(字串、數值、布林、Date、null可寫出,物件、陣列、NaN等多為空白格,帶value、formula、style等鍵之物件被寫檔器當成儲存格設定而格式不符時寫檔失敗並僅記錄於console),預設null
* @param {Boolean} [opt.useHead=false] 輸入表頭是否使用opt.kpHead之名稱布林值,false時使用key,opt.kpHead無對應之key仍使用key;下載檔欲上傳回表格時須另設opt.kpConvertKeysWhenUploadData為反向對照,且名稱須非空且不重複,預設false
* @param {Boolean} [opt.useFormat=true] 輸入是否依opt.kpCellFormat格式化各欄值布林值,true時與畫面一致(格式化函數之row為該列完整數據、params為null),false時下載原值;下載檔欲上傳回表格時宜給false,預設true
* @param {String} [opt.fileName='data.xlsx'] 輸入檔名字串,非字串或空字串時為'data.xlsx',無副檔名時自動補'.xlsx',預設'data.xlsx'
* @param {String} [opt.sheetName='data'] 輸入分頁名稱字串,須為字串且符合Excel規則(至多31字且以UTF-16碼元計,表情符號與擴充B區漢字各計2字;不含[ ] : * ? / \、首尾非單引號、非History),不符或非字串時為'data',預設'data'
* @returns {Array} 回傳下載內容之二維陣列(opt.funGetMatHook處理後),未經opt.funGetMatHook改寫之儲存格中null為空字串、布林值為'true'或'false'、物件與陣列為JSON字串
* @throws {Error} 下列情形同步拋錯且不產生檔案,訊息前綴為'[w-aggrid-vue] downloadData: ':掛鉤回傳Promise('<掛鉤名> must return synchronously, Promise is not supported')、opt.funGetKeysHook未回傳陣列或含非字串之key、無可下載欄位('no downloadable keys')、opt.funGetLtdtHook未回傳陣列或含非物件之列、opt.funGetMatHook未回傳二維陣列或全為空列、最終二維陣列超過Excel單一分頁之上限('too many rows, ...'為含表頭列超過1048576列,'too many columns, ..., invalid row[i]'為任一列超過16384欄);掛鉤與opt.kpCellFormat之函數自身拋錯時原樣拋出;觸發下載後之寫檔失敗僅以'[w-aggrid-vue] downloadData: can not write excel file'記錄於console,無法回報
*/
downloadData: function(opt = {}) {
//console.log('methods downloadData', opt)
let vo = this
//keys為opt.keys之順序且含隱藏欄, rows為全部列
return downloadCore(vo, 'downloadData', vo.keys, vo.rows, opt)
},
/**
* 開啟上傳視窗讀取Excel檔案,並以其數據取代或附加於表格數據
*
* 須於使用者操作(例如點擊)之事件處理函數內呼叫,否則瀏覽器不開啟選檔視窗,回傳之Promise亦不會結束。讀檔規則:讀取.xlsx之指定分頁,首列為表頭,表頭須與opt.keys完全相同(含大小寫與空白)或經表格之opt.kpConvertKeysWhenUploadData轉換,空表頭之欄略過,表頭重複或轉換後同名時後者覆蓋;整列皆無值之列略過,含空字串儲存格之列或只有無表頭欄有值之列讀為空白列;值一律讀為字串,日期儲存格讀為'YYYY-MM-DDTHH:mm:ss.SSS'(即Excel儲存格之年月日時分秒,不含時區,與執行環境時區無關),日期型之表頭亦同。寫入時只保留opt.keys之欄,缺欄之值為空字串;個別空白列照常寫入,全部列皆空白時拒絕;附加模式會重建既有之列物件。多次呼叫時依完成先後寫入,後完成者覆蓋。上傳屬程式化之整表資料取代或附加,不受opt.kpCellEditable約束(kpCellEditable只約束使用者之互動編輯,如雙擊、F2、鍵入與貼上),不可編輯之欄與列同樣會被寫入
*
* @param {Object} [opt={}] 輸入設定物件,未給予或非物件時為{},各鍵未給予或非指定之值或型態時皆使用預設,有給予但無效時於console提示,預設{}
* @param {Function} [opt.beforeUpload=null] 輸入寫入表格前之數據處理函數,輸入為讀取Excel所得(已依表格之opt.kpConvertKeysWhenUploadData轉換鍵值)之各列物件陣列,無數據列時為空陣列;須回傳處理後之數據陣列,或resolve為該陣列之Promise(含thenable),各列須為物件(可為空物件);欲中止上傳時拋錯或回傳rejected Promise,其錯誤原樣reject且保持參照;非函數時視為未給予,預設null
* @param {Number|String} [opt.parseSheetInd=0] 輸入讀取Excel檔案之第幾個分頁之非負整數,可為整數字串,依活頁簿分頁順序且含隱藏分頁,未給予或無效時為0,預設0
* @param {String} [opt.uploadMode='replace'] 輸入上傳模式字串,可選'replace'(取代表格數據)、'append'(附加於表格數據之後),未給予或無效時為'replace',預設'replace'
* @returns {Promise} 回傳Promise,resolve回傳上傳後之表格數據陣列,與opt.rows為同一陣列,resolve時表格已更新,opt.rowsChange於其後觸發;reject時本方法不寫入表格數據(beforeUpload自身對外之改動不回復),亦不觸發opt.rowsChange。reject之值:本方法產生者為錯誤物件{ msg, err },msg為下列代碼,err為說明(除'no file'、'can not read excel'與'can not read file'外皆以'[w-aggrid-vue] uploadData: '為前綴):使用者於選檔視窗取消時msg為'no file'、err為'file selection was canceled',此時未呼叫beforeUpload,呼叫端可依msg判斷為取消而不提示;選檔或讀取檔案失敗(例如檔案過大)時msg為'can not read file'、err為wsemi之錯誤物件;無法讀取Excel時msg為'can not read excel';指定之分頁不存在時msg為'can not get data from sheet';beforeUpload回傳值不合約定時msg為'invalid beforeUpload return';無可寫入之列或全部列於opt.keys之值皆為空時msg為'no data',呼叫端可視為無作為;無任一列有opt.keys之值時(例如首列非表頭、表頭與opt.keys不符、讀到隱藏分頁)msg為'no matching keys'。beforeUpload自身拋錯或reject時,其錯誤原樣reject且保持參照
*/
uploadData: async function(opt = {}) {
//console.log('methods uploadData', opt)
let vo = this
//newErr, 本方法產生之錯誤物件, err加前綴供辨識來源(呼叫端如w-table-vue可能只記錄錯誤內容), msg維持簡短代碼供呼叫端比對
let newErr = (msg, err) => {
return {
msg,
err: `[w-aggrid-vue] uploadData: ${err}`,
}
}
//以下開啟選檔視窗前之處理皆須同步執行, 不可有await, 否則瀏覽器之使用者啟動逾時而不開窗
//opt
if (!isobj(opt)) {
if (isGiven(opt)) {
logUseDefault('uploadData', 'opt', opt, '{}')
}
opt = {}
}
//params
let { beforeUpload, parseSheetInd, uploadMode } = opt
//beforeUpload, 須為函數, 否則視為未給予(不處理數據)
if (!isfun(beforeUpload)) {
if (isGiven(beforeUpload)) {
logUseDefault('uploadData', 'beforeUpload', beforeUpload, 'null')
}
beforeUpload = null
}
//uploadMode, 'replace'(取代)或'append'(附加), 否則為'replace'
if (uploadMode !== 'replace' && uploadMode !== 'append') {
if (isGiven(uploadMode)) {
logUseDefault('uploadData', 'uploadMode', uploadMode, `'replace'`)
}
uploadMode = 'replace'
}
//parseSheetInd, 非負整數(含整數字串, 例如select所綁定之'1'), 否則為0; useLimitSafe使Infinity亦視為無效
if (!isp0int(parseSheetInd, { useLimitSafe: true })) {
if (isGiven(parseSheetInd)) {
logUseDefault('uploadData', 'parseSheetInd', parseSheetInd, '0')
}
parseSheetInd = 0
}
parseSheetInd = cint(parseSheetInd)
// console.log('parseSheetInd', parseSheetInd)
//domShowInputAndGetFilesU8Arrs, 檔案過大或讀取失敗時wsemi以其錯誤物件reject, 包成{ msg, err }
let d = null
try {
d = await domShowInputAndGetFilesU8Arrs()
}
catch (err) {
return Promise.reject({
msg: 'can not read file',
err,
})
}
// console.log('d', d)
//check, 使用者取消選檔時wsemi以空陣列resolve(不視為錯誤), 若直接取d[0].u8a會拋TypeError, 故於此攔截並以msg='no file'標示, 呼叫端可依msg判斷為取消而不提示; 此時尚未呼叫beforeUpload亦未變動表格數據
if (!isearr(d)) {
return Promise.reject({
msg: 'no file',
err: 'file selection was canceled',
})
}
//取第一個檔案
let file = d[0]
//getDataFromExcelFileU8Arr
let r = await getDataFromExcelFileU8Arr(file.u8a, { fmt: 'ltdt' })
//check
if (!isarr(r)) {
return Promise.reject({
msg: 'can not read excel',
err: r.error,
})
}
//check, 指定之分頁不存在(parseSheetInd之型別已於開窗前檢查)
if (parseSheetInd >= size(r)) {
return Promise.reject(newErr('can not get data from sheet', `sheet[${parseSheetInd}] does not exist, the workbook has ${size(r)} sheet(s)`))
}
//rows, 取第parseSheetInd個分頁之數據
let rows = get(r, `${parseSheetInd}.data`)
if (!isarr(rows)) {
return Promise.reject(newErr('can not get data from sheet', `sheet[${parseSheetInd}] has no data`))
}
//kpConvertKeysWhenUploadData
if (iseobj(vo.kpConvertKeysWhenUploadData)) {
rows = map(rows, (row) => {
let t = {}
each(row, (v, k) => {
let rk = k
let rv = v
if (haskey(vo.kpConvertKeysWhenUploadData, k)) {
rk = vo.kpConvertKeysWhenUploadData[k]
}
t[rk] = rv
})
return t
})
}
//beforeUpload, 以await取值使同步陣列、Promise與thenable皆可; 自身拋錯或reject原樣傳出並保持參照(w-table-vue以參照比對其無有效數據之錯誤物件)
if (isfun(beforeUpload)) {
let rs = await beforeUpload(rows)
if (isundefined(rs)) {
return Promise.reject(newErr('invalid beforeUpload return', 'beforeUpload returned undefined, it must return an array of rows (missing return?)'))
}
if (!isarr(rs)) {
return Promise.reject(newErr('invalid beforeUpload return', 'beforeUpload must return an array, or a Promise resolving an array'))
}
let ind = findIndex(rs, (row) => {
return !isobj(row) //可為空物件, 讀檔器對只有無表頭欄有值之列亦產生空物件
})
if (ind >= 0) {
return Promise.reject(newErr('invalid beforeUpload return', `beforeUpload must return an array of objects, invalid row[${ind}]`))
}
rows = rs
}
//以下檢查須在beforeUpload之後(w-table-vue之包裝於無有效數據時先行reject其錯誤物件), 亦須在投影(pickRowsByKeys)之前(投影後各列皆有keys之鍵, 無從判斷原列有無值)
//keys
let keys = vo.keys
//check, 無可寫入之列
if (size(rows) === 0) {
return Promise.reject(newErr('no data', 'there is no row to upload'))
}
//check, 無任一列有keys之值(取值同pickRowsByKeys之get), 例如首列非表頭、表頭與keys不符、未設kpConvertKeysWhenUploadData或讀到隱藏分頁
let bMatch = some(rows, (row) => {
return some(keys, (key) => {
return !isundefined(get(row, key))
})
})
if (!bMatch) {
return Promise.reject(newErr('no matching keys', 'no value of opt.keys is found in the uploaded rows, the first row of the sheet must be the header whose names equal opt.keys or are converted by opt.kpConvertKeysWhenUploadData, and parseSheetInd counts hidden sheets'))
}
//check, 全部列於keys之值皆為空(''、null、undefined), 與w-table-vue判定無有效數據之準則相同; 個別空白列照常寫入
let bAllEmpty = every(rows, (row) => {
return every(keys, (key) => {
let v = get(row, key)
return v === '' || isnull(v) || isundefined(v)
})
})
if (bAllEmpty) {
return Promise.reject(newErr('no data', 'all rows are empty in opt.keys'))
}
//pickRowsByKeys
rows = pickRowsByKeys(rows, keys)
//uploadMode
if (uploadMode === 'replace') {
// rows=rows
}
else if (uploadMode === 'append') {
//rowsTemp
let rowsTemp = get(vo, 'opt.rows', [])
rowsTemp = cloneDeep(rowsTemp)
//push
each(rows, (v) => {
rowsTemp.push(v)
})
//save
rows = rowsTemp
// //setTimeout, 變更數據後頁面會先渲染, delay後才能調捲軸, 否則太快執行會被頁面渲染蓋掉
// setTimeout(() => {
// //getGridOptions
// let o = vo.getGridOptions()
// //ensureIndexVisible, scrollTo(index)
// o.api.ensureIndexVisible(size(rowsTemp) - 1, 'bottom') //捲到最下
// }, 1)
}
//需使用set強制更新外部opt物件的rows並再同步更新至內部rows, 否則外面數據會沒更動
set(vo, `opt.rows`, rows)
// //rowsChange, rows是內部已更新數據, 直接用於回傳函數
// vo.rowsChange(rows) //agRowDataUpdated也會觸發並再觸發rowsChange, 故此處不觸發
return rows
},
triggerFilterChange: async function(from) {
//console.log('methods triggerFilterChange', from)
let vo = this
//filterall
let filterall = vo.filterall
//flts, 為取得當下(觸發後之下一個更新週期)之過濾狀態
let flts = null
try {
flts = await vo.getFilters()
}
catch (err) {
//表格不可用(元件或ag-grid已銷毀)時不發事件; 其餘錯誤記錄後亦不發事件, 避免以錯誤之過濾狀態通知呼叫端
if (isGridAvailable(vo.getApi())) {
console.log('[w-aggrid-vue] triggerFilterChange:', err)
}
return
}
//check, 取得過濾狀態後至此之間表格可能已銷毀
if (!isGridAvailable(vo.getApi())) {
return
}
//isFilter
let isFilter = isestr(filterall) || size(flts) > 0
//filterChange
vo.filterChange({
from,
filterall,
flts,
isFilter,
// ev,
})
},
},
}
</script>
<style>
.CompCssWAggridVue .ag-theme-balham, .ag-header {
font-family: inherit;
}
.CompCssWAggridVue .ag-theme-balham .ag-header {
font-family: inherit;
}
.CompCssWAggridVue .ag-header-cell .ag-input-field-input, .ag-filter .ag-input-field-input {
transition: all 0.5s;
padding: 3px 8px;
box-shadow: inherit !important;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 5px;
}
.CompCssWAggridVue .ag-input-field-input:focus {
border: 1px solid #777;
}
/* kpHeadFocusHighlight為false之欄不顯示焦點框; ag-grid 31之焦點框規則為.ag-cell-focus:focus-within, 儲存格內子控制項(tabindex元素、input、button)取得焦點時儲存格只符合:focus-within不符合:focus, 故須兩者並列; 保留:focus以相容舊版ag-grid */
.CompCssWAggridVue .no-border.ag-cell:focus,
.CompCssWAggridVue .no-border.ag-cell:focus-within{
border-color: transparent !important;
outline: none;
}
</style>