@evotecit/canopyx 0.1.3 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/CanopyX.Html/Assets/canopyx-calendar-edit.js +114 -0
  2. package/CanopyX.Html/Assets/canopyx-calendar-ics.js +87 -0
  3. package/CanopyX.Html/Assets/canopyx-calendar-recurrence.js +36 -0
  4. package/CanopyX.Html/Assets/canopyx-calendar.css +279 -0
  5. package/CanopyX.Html/Assets/canopyx-calendar.js +307 -0
  6. package/CanopyX.Html/Assets/canopyx-cell-range.css +38 -0
  7. package/CanopyX.Html/Assets/canopyx-cell-range.js +141 -0
  8. package/CanopyX.Html/Assets/canopyx-column-groups.css +18 -0
  9. package/CanopyX.Html/Assets/canopyx-column-groups.js +51 -0
  10. package/CanopyX.Html/Assets/canopyx-data.js +14 -0
  11. package/CanopyX.Html/Assets/canopyx-detail-rows.css +18 -0
  12. package/CanopyX.Html/Assets/canopyx-detail-rows.js +51 -0
  13. package/CanopyX.Html/Assets/canopyx-editing.css +44 -0
  14. package/CanopyX.Html/Assets/canopyx-editing.js +134 -0
  15. package/CanopyX.Html/Assets/canopyx-enhance.js +126 -0
  16. package/CanopyX.Html/Assets/canopyx-export.js +18 -0
  17. package/CanopyX.Html/Assets/canopyx-facets.css +86 -0
  18. package/CanopyX.Html/Assets/canopyx-facets.js +65 -0
  19. package/CanopyX.Html/Assets/canopyx-filter-editor.css +15 -18
  20. package/CanopyX.Html/Assets/canopyx-filter-editor.js +23 -17
  21. package/CanopyX.Html/Assets/canopyx-format-editor.css +77 -0
  22. package/CanopyX.Html/Assets/canopyx-format-editor.js +30 -0
  23. package/CanopyX.Html/Assets/canopyx-formatting.css +40 -0
  24. package/CanopyX.Html/Assets/canopyx-formatting.js +101 -0
  25. package/CanopyX.Html/Assets/canopyx-grid-module.mjs +352 -246
  26. package/CanopyX.Html/Assets/canopyx-grid.css +139 -117
  27. package/CanopyX.Html/Assets/canopyx-grid.js +352 -246
  28. package/CanopyX.Html/Assets/canopyx-grouping.css +39 -0
  29. package/CanopyX.Html/Assets/canopyx-grouping.js +85 -0
  30. package/CanopyX.Html/Assets/canopyx-highlight.css +34 -0
  31. package/CanopyX.Html/Assets/canopyx-highlight.js +80 -0
  32. package/CanopyX.Html/Assets/canopyx-history.js +13 -0
  33. package/CanopyX.Html/Assets/canopyx-interactive.css +18 -6
  34. package/CanopyX.Html/Assets/canopyx-interactive.js +179 -100
  35. package/CanopyX.Html/Assets/canopyx-labels-de.js +304 -0
  36. package/CanopyX.Html/Assets/canopyx-labels-es.js +304 -0
  37. package/CanopyX.Html/Assets/canopyx-labels-fr.js +529 -0
  38. package/CanopyX.Html/Assets/canopyx-labels-it.js +529 -0
  39. package/CanopyX.Html/Assets/canopyx-labels-pl.js +304 -0
  40. package/CanopyX.Html/Assets/canopyx-layouts.css +48 -0
  41. package/CanopyX.Html/Assets/canopyx-layouts.js +39 -0
  42. package/CanopyX.Html/Assets/canopyx-link.css +19 -0
  43. package/CanopyX.Html/Assets/canopyx-link.js +38 -0
  44. package/CanopyX.Html/Assets/canopyx-links.js +14 -0
  45. package/CanopyX.Html/Assets/canopyx-live.css +24 -0
  46. package/CanopyX.Html/Assets/canopyx-live.js +88 -0
  47. package/CanopyX.Html/Assets/canopyx-module.mjs +180 -100
  48. package/CanopyX.Html/Assets/canopyx-pager.css +10 -0
  49. package/CanopyX.Html/Assets/canopyx-pager.js +14 -0
  50. package/CanopyX.Html/Assets/canopyx-record-panel.css +111 -0
  51. package/CanopyX.Html/Assets/canopyx-record-panel.js +178 -0
  52. package/CanopyX.Html/Assets/canopyx-reporting-module.mjs +406 -342
  53. package/CanopyX.Html/Assets/canopyx-reporting.css +140 -135
  54. package/CanopyX.Html/Assets/canopyx-reporting.js +406 -342
  55. package/CanopyX.Html/Assets/canopyx-search.js +32 -0
  56. package/CanopyX.Html/Assets/canopyx-timeline.css +80 -0
  57. package/CanopyX.Html/Assets/canopyx-timeline.js +88 -0
  58. package/CanopyX.Html/Assets/canopyx-tree-tools.css +103 -0
  59. package/CanopyX.Html/Assets/canopyx-tree-tools.js +92 -0
  60. package/CanopyX.Html/Assets/canopyx.css +17 -5
  61. package/CanopyX.Html/Assets/canopyx.js +175 -96
  62. package/JavaScript/calendar.d.ts +362 -0
  63. package/JavaScript/cell-ranges.d.ts +76 -0
  64. package/JavaScript/column-groups.d.ts +32 -0
  65. package/JavaScript/editing.d.ts +257 -0
  66. package/JavaScript/enhance.d.ts +202 -0
  67. package/JavaScript/facets.d.ts +58 -0
  68. package/JavaScript/formatting.d.ts +88 -0
  69. package/JavaScript/grid.d.ts +409 -56
  70. package/JavaScript/hosts.d.ts +177 -2
  71. package/JavaScript/index.d.ts +53 -4
  72. package/JavaScript/live.d.ts +115 -0
  73. package/JavaScript/locales/de.mjs +304 -0
  74. package/JavaScript/locales/es.mjs +304 -0
  75. package/JavaScript/locales/fr.d.ts +6 -0
  76. package/JavaScript/locales/fr.mjs +527 -0
  77. package/JavaScript/locales/it.d.ts +6 -0
  78. package/JavaScript/locales/it.mjs +527 -0
  79. package/JavaScript/locales/pl.mjs +304 -0
  80. package/JavaScript/plugins.d.ts +113 -0
  81. package/JavaScript/protocol.d.ts +139 -6
  82. package/JavaScript/records.d.ts +136 -0
  83. package/README.md +61 -9
  84. package/README.nuget.md +1 -1
  85. package/package.json +99 -3
@@ -0,0 +1,134 @@
1
+ (function(global){"use strict";const installed=new WeakSet();const part={version:"01965774e28f70bf",requires:["filter-editor"],install(context){if(installed.has(context.DataGrid.prototype))return;const{DataGrid,h,setText,fill,gridButton,gridDecorateButton,GRID_ICONS,cellValue,cellText,isCellObject,gridFreeze,gridMomentFromParts,numberFormat,gridQuery,foldText,requireIdentifier,checkDocument,validateGridRows,inlineGridSource,PROTOCOL,boundedPattern,validateGridFacetPage,RECORD_IMPL}=context;const EDIT_LABELS={gridEditCell:"Edit {column}",gridEditSave:"Save",gridEditCancel:"Cancel",gridEditDiscard:"Discard",gridEditUndo:"Undo",gridEditRedo:"Redo",gridEditEdited:"Edited",gridEditChanges:"Unsaved changes: {count}",gridEditErrors:"Errors: {count}",gridEditSaving:"Saving…",gridEditSaved:"Saved rows: {count}",gridEditFailed:"Rows not saved: {count}",gridEditRequired:"{column} is required.",gridEditNumber:"{column} must be a number.",gridEditDate:"{column} must be a valid date.",gridEditMin:"{column} must be at least {min}.",gridEditMax:"{column} must be at most {max}.",gridEditStep:"{column} must be a multiple of {step}.",gridEditMinLength:"{column} needs at least {min} characters.",gridEditMaxLength:"{column} allows at most {max} characters.",gridEditPattern:"{column} does not have the expected format.",gridEditChoice:"Choose {column} from the list.",gridEditConflict:"Someone else changed this row. Reload it and try again.",gridEditRejected:"Not saved: {error}",gridEditNotSaved:"Not saved.",gridEditNoMatches:"No matches",gridEditSearching:"Searching…",gridEditUndone:"Undone: {column}",gridEditRedone:"Redone: {column}",gridEditNone:"(none)"};const editCopy=value=>gridFreeze(JSON.parse(JSON.stringify(value===undefined?null:value)));const editSame=(left,right)=>left===right||left==null&&right==null;const editScalar=value=>value===null||typeof value==="string"||typeof value==="boolean"||Number.isFinite(value);const editBad=(id,problem)=>{throw new Error("CanopyX: column "+id+" "+problem+".")};function editOptions(list,id){if(list===undefined)return null;if(!Array.isArray(list)||list.length>1000)editBad(id,"needs an array of at most 1000 editor options");return list.map(item=>{const option=item!==null&&typeof item==="object"?item:{value:item};if(!["string","number","boolean"].includes(typeof option.value)||option.label!==undefined&&typeof option.label!=="string")
2
+ editBad(id,"has an invalid editor option");return Object.freeze({value:option.value,label:option.label??String(option.value)})})}
3
+ function editSpec(column,editors,columns){const id=column.id,raw=column.editor===undefined?{}:typeof column.editor==="string"?{type:column.editor}:column.editor;const rules=column.validation===undefined?{}:column.validation,object=value=>value&&typeof value==="object"&&!Array.isArray(value);if(!object(raw)||!object(rules))editBad(id,"has an invalid editor or validation");const type=raw.type??column.kind;if(typeof type!=="string"||!Object.hasOwn(editors,type))editBad(id,"names an editor nobody registered");const fits={number:["number"],text:["text"],toggle:["text"],date:["datetime","text"],datetime:["datetime","text"],time:["datetime","text"]}[type];if(fits&&!fits.includes(column.kind))editBad(id,"cannot use a "+type+" editor for its kind");const bound=value=>value===undefined||Number.isFinite(value)||typeof value==="string"&&!Number.isNaN(Date.parse(value));const count=value=>value===undefined||Number.isInteger(value)&&value>=0&&value<=1000000;if(raw.step!==undefined&&!(Number.isFinite(raw.step)&&raw.step>0)||![raw.min,raw.max,rules.min,rules.max].every(bound)||
4
+ !count(rules.minLength)||!count(rules.maxLength)||rules.required!==undefined&&typeof rules.required!=="boolean"||
5
+ [rules.pattern,rules.message,raw.placeholder].some(text=>text!==undefined&&typeof text!=="string"))editBad(id,"has invalid editor or validation settings");let pattern=null;if(rules.pattern!==undefined)try{pattern=boundedPattern(rules.pattern,true,true)}catch(error){editBad(id,"has an invalid or unsafe pattern")}
6
+ const lookup=raw.lookup??(type==="lookup"?{}:null);if(lookup&&(!object(lookup)||lookup.column!==undefined&&!columns.some(item=>item.id===lookup.column)||
7
+ lookup.limit!==undefined&&!(Number.isInteger(lookup.limit)&&lookup.limit>=1&&lookup.limit<=50)||
8
+ lookup.minLength!==undefined&&!(Number.isInteger(lookup.minLength)&&lookup.minLength>=0&&lookup.minLength<=16)))editBad(id,"has invalid lookup settings");return Object.freeze({id,column,type,editor:gridFreeze({...raw,type}),options:editOptions(raw.options,id),lookup:lookup&&Object.freeze({column:lookup.column??id,limit:lookup.limit??20,minLength:lookup.minLength??1}),pattern,rules:Object.freeze({required:!!rules.required,min:rules.min??raw.min,max:rules.max??raw.max,step:raw.step,minLength:rules.minLength,maxLength:rules.maxLength,message:rules.message})})}
9
+ function editCell(original,value,text){if(!isCellObject(original)&&text===undefined)return value;const cell=isCellObject(original)?{...original}:{};delete cell.text;delete cell.searchText;delete cell.sort;delete cell.secondary;cell.value=value;if(text!==undefined)cell.text=text;return cell}
10
+ Object.assign(DataGrid.prototype,{startEditing(){if(this.edit||this.destroyed)return;const options=this.opts.editing??{},view=this.view.editing??{},object=value=>Object(value)===value;const mode=options.mode??view.mode??"cell",drawer=options.drawer??view.drawer??"phone";if(!object(options)||!object(view)||["isEditable","validateRow","commit"].some(name=>options[name]!=null&&typeof options[name]!=="function")||
11
+ ["validators","lookups","editors","formatters"].some(name=>options[name]!=null&&!object(options[name]))||
12
+ !["cell","row"].includes(mode)||!["phone","always","never"].includes(drawer))throw new Error("CanopyX: invalid editing settings.");const editors=Object.assign({},EDIT_EDITORS,...this.plugins.map(item=>item.editors||{}),options.editors);for(const[name,editor]of Object.entries(editors))if(!editor||typeof editor.create!=="function")
13
+ throw new Error("CanopyX: the editor "+name+" needs a create function.");const labels={};for(const[name,text]of Object.entries(EDIT_LABELS))labels[name]=this.labels[name]||text;for(const name of Object.keys(this.labels))if(/^gridEdit[A-Z]/.test(name) && name !== "gridEditAsText" && !Object.hasOwn(EDIT_LABELS, name))
14
+ throw new Error("CanopyX: there is no label '"+name+"'.");const specs=new Map(this.columns.filter(column=>column.editable).map(column=>[column.id,editSpec(column,editors,this.columns)]));const limit=options.undoLimit??100;if(!Number.isInteger(limit)||limit<0||limit>10000)throw new Error("CanopyX: editing.undoLimit must be an integer from 0 to 10000.");this.edit={options,mode,drawer,labels,editors,specs,limit,changes:new Map(),inserted:new Map(),undo:[],redo:[],session:null,group:null,checks:new Map(),messages:new Map(),sequence:0,bar:options.editBar??view.editBar??true};this.editReadOnly??=!!(options.readOnly??view.readOnly);this.buildEditing();this.version++;this.scheduleRender()},canEditCell(record,id){const spec=this.edit?.specs.get(id),test=this.edit?.options.isEditable;return!!spec&&!this.editReadOnly&&!!record&&(!test||test(record,id)!==false)},editRecord(id){for(const row of this.rowCache.values())if(row.key===id)return row.h;const change=this.edit.changes.get(id)?.row??this.edit.inserted.get(id);if(change||this.view.source.kind!=="inline")return change;const row=this.view.rows.find(item=>item.id===id);return row&&JSON.parse(JSON.stringify(row))},editApply(id,spec,value,source,record=this.editRecord(id),checked){if(!record)throw new Error("CanopyX: the record "+id+" is not loaded.");const state=this.edit,cells=record.cells,previous=cellValue(cells[spec.id]);if(editSame(previous,value))return true;const detail={id,column:spec.id,value,previous,row:editCopy(record),source};if(!checked?.prepared&&!this.emit("cellValueChanging",detail))return false;const entry=state.changes.get(id)||{id,original:Object.create(null),values:Object.create(null),errors:Object.create(null),error:null};if(!entry.inserted&&!Object.hasOwn(entry.original,spec.id))entry.original[spec.id]=cells[spec.id];const original=entry.original[spec.id];if(entry.inserted){cells[spec.id]=editCell(cells[spec.id],value,this.editText(spec,value,record));entry.values[spec.id]=value}else if(editSame(cellValue(original),value)){if(original===undefined)delete cells[spec.id];else cells[spec.id]=original;delete entry.original[spec.id];delete entry.values[spec.id];delete entry.errors[spec.id];state.checks.delete(id+"\u0000"+spec.id);entry.checked?.delete(spec.id)}else{cells[spec.id]=editCell(original,value,this.editText(spec,value,record));entry.values[spec.id]=value}
15
+ entry.row=record;entry.error=entry.rowError=null;entry.rowErrors=Object.create(null);entry.committed=false;if(entry.status!=="saving")entry.status=undefined;if(Object.keys(entry.values).length||entry.status==="saving")state.changes.set(id,entry);else state.changes.delete(id);if(source!=="undo"&&source!=="redo"){const step={id,column:spec.id,before:previous,after:value};if(state.group)state.group.push(step);else if(state.limit){state.undo.push([step]);if(state.undo.length>state.limit)state.undo.shift();state.redo.length=0}}
16
+ if(Object.hasOwn(entry.values,spec.id))this.editCheck(id,spec,checked);this.editChanged();this.emit("cellValueChanged",{...detail,row:editCopy(record)});return true},editText(spec,value,record){const formatters=this.edit.options.formatters,format=spec&&formatters&&Object.hasOwn(formatters,spec.id)?formatters[spec.id]:null;if(typeof format==="function"&&value!=null)return String(format(value,editCopy(record)));const option=spec&&(spec.options||this.edit.seen?.get(spec.id))?.find(item=>item.value===value);return option&&option.label!==String(value)?option.label:undefined},editGroup(run){const state=this.edit,outer=state.group,steps=state.group=outer||[];try{return run()}finally{state.group=outer;if(!outer&&steps.length&&state.limit){state.undo.push(steps);if(state.undo.length>state.limit)state.undo.shift();state.redo.length=0}}},editChanged(){if(this.formatStarted){this.formatVersion++;this.formatStatsKey=undefined}
17
+ this.version++;this.scheduleRender();this.syncEditBar()},getChanges(){return this.editEntries().map(entry=>{const change=entry.inserted?{op:"insert",id:entry.id,values:entry.values}:{id:entry.id,values:entry.values,original:Object.create(null)};if(!entry.inserted)for(const id of Object.keys(entry.values))change.original[id]=cellValue(entry.original[id]);const errors=editErrors(entry),error=entry.error||entry.rowError;if(Object.keys(errors).length)change.errors=errors;if(error)change.error=error;if(entry.status)change.status=entry.status;return editCopy(change)})},hasChanges(){return this.editEntries().length>0},editEntries(){return[...this.edit.changes.values()].filter(entry=>Object.keys(entry.values).length)},async setCellValue(id,column,value){requireIdentifier(id,"edited row ID");const spec=this.edit.specs.get(column),record=this.editRecord(id);if(!spec||!record)throw new Error("CanopyX: setCellValue needs a loaded record and an editable column.");if(!editScalar(value))throw new Error("CanopyX: a cell value is a string, a finite number, a boolean or null.");if(this.edit.session?.id===id&&this.edit.session.fields.has(column))this.editClose(false);if(!this.editApply(id,spec,value,"api",record))return editCopy({applied:false,valid:true});await this.edit.checks.get(id+"\u0000"+column)?.promise;const error=editErrors(this.edit.changes.get(id)||{errors:{}})[column]||null;return editCopy({applied:true,valid:!error,...(error?{error}:{})})},revertChanges(ids){if(ids!==undefined&&!Array.isArray(ids))throw new Error("CanopyX: revertChanges takes an array of row IDs.");this.cancelEdit();this.editGroup(()=>{for(const id of ids??[...this.edit.changes.keys()]){const entry=this.edit.changes.get(id);if(entry?.inserted){this.editRemoveInserted(id,true);continue}
18
+ if(entry)for(const column of Object.keys(entry.values))
19
+ this.editApply(id,this.edit.specs.get(column),cellValue(entry.original[column]),"revert",entry.row)}})},canUndo(){return this.edit.undo.length>0},canRedo(){return this.edit.redo.length>0},undo(){return this.editHistory(this.edit.undo,this.edit.redo,"before","undo")},redo(){return this.editHistory(this.edit.redo,this.edit.undo,"after","redo")},editHistory(from,to,key,source){if(!from.length||this.destroyed)return false;if(key==="before"&&from[from.length-1].some(step=>step.insert&&this.edit.changes.get(step.id)?.status==="saving"))return false;this.cancelEdit();const steps=from.pop(),order=key==="before"?[...steps].reverse():steps;for(const step of order){if(step.insert){if(key==="before")this.editRemoveInserted(step.id);else this.editInsert(step.insert,source);continue}
20
+ const record=this.editRecord(step.id),spec=this.edit.specs.get(step.column);if(record&&spec)this.editApply(step.id,spec,step[key],source,record)}
21
+ to.push(steps);const last=this.edit.specs.get(order.findLast(step=>!step.insert)?.column);this.editAnnounce(fill(this.edit.labels[source==="undo"?"gridEditUndone":"gridEditRedone"],{column:last?last.column.title:""}));this.syncEditBar();return true},setReadOnly(value){if(typeof value!=="boolean")throw new Error("CanopyX: setReadOnly takes a boolean.");if(value)this.cancelEdit();this.editReadOnly=value;this.version++;this.scheduleRender();this.syncEditBar()}});const EDIT_STORE_PAGE=DataGrid.prototype.storePage;DataGrid.prototype.storePage=function(page){const changes=this.edit?.changes;if(changes?.size)for(const item of page.items){const entry=changes.get(item.id);if(entry)for(const[column,value]of Object.entries(entry.values)){item.cells[column]=editCell(item.cells[column],value,this.editText(this.edit.specs.get(column),value,item))}}
22
+ const result=EDIT_STORE_PAGE.call(this,page);if(changes?.size)for(const row of this.pages.get(page.offset)?.rows||[])if(changes.has(row.key))changes.get(row.key).row=row.h;const session=this.edit?.session;if(session&&this.rowCache.get(session.wrapper._ri)!==session.wrapper)this.editClose(true,true);return result};const editPad=(number,width=2)=>String(number).padStart(width,"0");function editInput(context,type="text"){const input=h("input","cx-editor");input.type=type;input.autocomplete="off";if(typeof context.editor.placeholder==="string")input.placeholder=context.editor.placeholder;return input}
23
+ const EDIT_NUMBER_LOCALES=new Map();function editParseNumber(text,locale){let format=EDIT_NUMBER_LOCALES.get(locale);if(!format){let number;try{number=new Intl.NumberFormat(locale||undefined)}catch(error){number=new Intl.NumberFormat()}
24
+ const digits=new Map();for(let digit=0;digit<10;digit++)digits.set(number.formatToParts(digit).find(part=>part.type==="integer").value,String(digit));format={parts:number.formatToParts(-12345.6),digits};if(EDIT_NUMBER_LOCALES.size>=32)EDIT_NUMBER_LOCALES.delete(EDIT_NUMBER_LOCALES.keys().next().value);EDIT_NUMBER_LOCALES.set(locale,format)}
25
+ const parts=format.parts;const group=parts.find(part=>part.type==="group")?.value??",",decimal=parts.find(part=>part.type==="decimal")?.value??".";let source=Array.from(text.trim(),digit=>format.digits.get(digit)??digit).join("").replace(/[\u061c\u200e\u200f]/g, "").replace(/\u2212/g, "-");
26
+ const pieces=source.split(decimal),whole=pieces[0],spaced=!group.trim();if(pieces.length>2)return NaN;const groups=whole.replace(/^[-+]/, "").split(spaced ? /[\s\u00a0\u202f]/ : group);
27
+ if(groups.length>1&&(!/^\d{1,3}$/.test(groups[0]) || groups.slice(1).some(digits => !/^\d{3}$/.test(digits)))) return NaN;
28
+ source=(/^[-+]/.test(whole) ? whole[0] : "") + groups.join("") + (pieces.length > 1 ? "." + pieces[1] : "");
29
+ return /^[-+]?(\d+\.?\d*|\.\d+)(e[-+]?\d+)?$/i.test(source) ? Number(source) : NaN;
30
+ }
31
+ function editMoment(value,local){const date=typeof value==="string"?new Date(value):null;if(!date||Number.isNaN(date.getTime()))return null;const get=name=>date[(local?"get":"getUTC")+ name]();const seconds=get("Seconds"),milliseconds=get("Milliseconds");const time=editPad(get("Hours"))+ ":"+editPad(get("Minutes"))+
32
+ (seconds||milliseconds?":"+editPad(seconds)+(milliseconds?"."+editPad(milliseconds,3):""):"");return{date:editPad(get("FullYear"),4)+ "-"+editPad(get("Month")+ 1)+ "-"+editPad(get("Date")),time}}
33
+ function editFromParts(date,time,local){const[clock,fraction=""]=time.split("."),milliseconds=Number(fraction.padEnd(3,"0"));const[year,month,day]=date.split("-").map(Number),[hours,minutes,seconds=0]=clock.split(":").map(Number);const moment=gridMomentFromParts(year,month,day,hours,minutes,seconds,local);if(!moment||!Number.isInteger(milliseconds)||milliseconds<0||milliseconds>999)return null;moment.setUTCMilliseconds(milliseconds);return moment.toISOString().replace(".000Z","Z")}
34
+ function editDateEditor(context,withDate,withTime){const local=context.timeZone==="local",moment=context.column.kind==="datetime";const input=editInput(context,withDate&&withTime?"datetime-local":withDate?"date":"time");if(moment&&withTime)input.step="0.001";const parts=moment?editMoment(context.value,local):null;const start=!moment?typeof context.value==="string"?context.value:"":!parts?"":
35
+ withDate&&withTime?parts.date+"T"+parts.time:withDate?parts.date:parts.time;input.value=start;const displayed=input.value;let changed=false;input.addEventListener("input",()=>{changed=true});const read=()=>{if(!changed&&input.value===displayed||input.value===start)return context.value??null;if(!input.value)return null;if(!moment)return input.value;const[date,time]=withDate&&withTime?input.value.split("T"):withDate?[input.value,parts?parts.time:"00:00"]:
36
+ [(parts||editMoment(new Date().toISOString(),local)).date,input.value];return editFromParts(date,time,local)};return{element:input,getValue:read,error:()=>input.validity?.badInput||moment&&input.value&&read()===null?fill(context.labels.gridEditDate,{column:context.column.title}):null}}
37
+ function editNumberPlaces(value){const[coefficient,exponent="0"]=String(value).split("e");return Math.max(0,(coefficient.split(".")[1]||"").length-Number(exponent))}
38
+ const EDIT_EDITORS={text:{create(context){const input=editInput(context);input.value=context.initial??(context.value==null?"":String(context.value));return{element:input,getValue:()=>input.value===""&&context.value==null?null:input.value}}},number:{create(context){const input=editInput(context),step=context.editor.step||1,rules=context.rules;const decimals=Math.max(editNumberPlaces(step),editNumberPlaces(rules.min??0));let plain;try{plain=new Intl.NumberFormat(context.locale||undefined,{useGrouping:false,maximumSignificantDigits:21})}
39
+ catch(error){plain=new Intl.NumberFormat(undefined,{useGrouping:false,maximumSignificantDigits:21})}
40
+ input.inputMode="decimal";input.value=context.initial??(typeof context.value==="number"?plain.format(context.value):"");const read=()=>input.value.trim()===""?null:editParseNumber(input.value,context.locale);input.addEventListener("keydown",event=>{if(event.key!=="ArrowUp"&&event.key!=="ArrowDown"||event.altKey)return;const value=read();if(!Number.isNaN(value)){let next=(value??0)+(event.key==="ArrowUp"?step:-step);if(typeof rules.min==="number")next=Math.max(rules.min,next);if(typeof rules.max==="number")next=Math.min(rules.max,next);input.value=plain.format(decimals<=100?Number(next.toFixed(decimals)):next)}
41
+ event.preventDefault()});return{element:input,getValue:read,error:()=>Number.isNaN(read())?fill(context.labels.gridEditNumber,{column:context.column.title}):null}}},datetime:{create:context=>editDateEditor(context,true,true)},date:{create:context=>editDateEditor(context,true,false)},time:{create:context=>editDateEditor(context,false,true)},select:{create(context){const select=h("select","cx-editor"),options=(context.options||[]).slice();if(context.value!=null&&!options.some(option=>option.value===context.value))
42
+ options.unshift({value:context.value,label:context.text||String(context.value)});if(!context.rules.required||context.value==null)select.appendChild(new Option(context.labels.gridEditNone,""));options.forEach((option,index)=>select.appendChild(new Option(option.label,String(index))));const at=options.findIndex(option=>option.value===context.value);select.value=at<0?"":String(at);return{element:select,getValue:()=>select.value===""?null:options[Number(select.value)].value}}},toggle:{create(context){const box=h("input","cx-editor cx-editor-toggle");box.type="checkbox";box.checked=context.value===true;return{element:box,getValue:()=>box.checked}}},lookup:{create:context=>editLookupEditor(context)}};function editLookupEditor(context){const input=editInput(context),popup=h("div","cx-editor-list"),note=h("div","cx-editor-note"),list=h("div"),id=context.id+"-list",labels=context.labels;input.setAttribute("role","combobox");input.setAttribute("aria-autocomplete","list");input.setAttribute("aria-expanded","false");input.setAttribute("aria-controls",id);list.id=id;list.setAttribute("role","listbox");list.setAttribute("aria-label",context.column.title);popup.popover="manual";popup.append(note,list);popup.addEventListener("pointerdown",event=>event.preventDefault());let options=[],active=-1,timer=0,request=null;let chosen=context.initial!==undefined||context.value==null?null:{value:context.value,label:context.text||String(context.value)};input.value=context.initial??(chosen?chosen.label:"");const paint=()=>{[...list.querySelectorAll("[role=option]")].forEach((option,index)=>option.setAttribute("aria-selected",String(index===active)));if(active>=0){input.setAttribute("aria-activedescendant",id+"-"+active);list.querySelector("#"+CSS.escape(id+"-"+active))?.scrollIntoView({block:"nearest"})}
43
+ else input.removeAttribute("aria-activedescendant")};const close=()=>{clearTimeout(timer);request?.abort();input.setAttribute("aria-expanded","false");input.removeAttribute("aria-activedescendant");if(popup.matches(":popover-open"))popup.hidePopover()};const show=(items,message)=>{options=items;active=items.length?0:-1;list.replaceChildren();list.hidden=!items.length;setText(note,message||"");note.hidden=!message;items.forEach((item,index)=>{const option=h("div","cx-editor-option");option.id=id+"-"+index;option.setAttribute("role","option");setText(option,item.label);option.addEventListener("click",()=>pick(index));list.appendChild(option)});if(!input.isConnected)return;const box=input.getBoundingClientRect();popup.style.left=box.left+"px";popup.style.top=box.bottom+2+"px";popup.style.minWidth=box.width+"px";if(!popup.matches(":popover-open"))popup.showPopover();input.setAttribute("aria-expanded","true");paint()};const pick=index=>{chosen=options[index];input.value=chosen.label;context.remember(chosen);close()};const search=()=>{clearTimeout(timer);request?.abort();const text=input.value;if(text.length<context.lookupMinLength){close();return}
44
+ show([],labels.gridEditSearching);timer=setTimeout(()=>{const controller=request=new AbortController();Promise.resolve(context.lookup(text,controller.signal)).then(items=>{if(!controller.signal.aborted)show(items,items.length?null:labels.gridEditNoMatches)},error=>{if(!controller.signal.aborted&&error.name!=="AbortError")show([],error.message)})},200)};input.addEventListener("input",()=>{chosen=null;search()});input.addEventListener("keydown",event=>{const open=input.getAttribute("aria-expanded")==="true";if(event.key==="ArrowDown"||event.key==="ArrowUp"){if(!open)search();else if(options.length){active=Math.max(0,Math.min(options.length-1,active +(event.key==="ArrowDown"?1:-1)));paint()}
45
+ event.preventDefault()}else if(event.key==="Enter"&&open&&active>=0){pick(active);event.preventDefault()}
46
+ else if(event.key==="Escape"&&open){close();event.preventDefault();event.stopPropagation()}});const typed=()=>{const text=foldText(input.value.trim());return options.find(option=>foldText(option.label)===text)||(chosen&&foldText(chosen.label)===text?chosen:null)};const element=h("span","cx-editor-combo");element.append(input,popup);if(typeof context.initial==="string")queueMicrotask(search);return{element,control:input,getValue:()=>!input.value.trim()?null:(chosen||typed())?.value??null,error:()=>input.value.trim()&&!chosen&&!typed()?fill(labels.gridEditChoice,{column:context.column.title}):null,destroy(){clearTimeout(timer);request?.abort();close();popup.remove()}}}
47
+ Object.assign(DataGrid.prototype,{editColumnIndex(id){return this.visibleColumns().findIndex(column=>column.id===id)},editCreate(record,spec,mode,initial){const state=this.edit,abort=new AbortController(),cell=record.cells[spec.id];const context=Object.freeze({id:this.id+"-ed" + ++state.sequence,value:cellValue(cell),text:cellText(cell,spec.column,this.fmt),row:editCopy(record),column:Object.freeze({id:spec.id,title:spec.column.title,kind:spec.column.kind}),editor:spec.editor,options:spec.options,rules:spec.rules,labels:Object.freeze({...this.labels,...state.labels}),locale:this.lang,timeZone:this.local?"local":"utc",mode,initial,signal:abort.signal,lookupMinLength:spec.lookup?spec.lookup.minLength:1,commit:()=>{(mode==="form"?this.editFormSave():this.endEdit()).catch(error=>this.emit("error",{error}))},cancel:()=>mode==="form"?state.form?.dialog.close():this.cancelEdit(),lookup:(search,signal)=>this.editLookup(spec,search,record,signal),remember:option=>this.editRemember(spec.id,option)});const instance=state.editors[spec.type].create(context);if(!instance||!(instance.element instanceof Element)||typeof instance.getValue!=="function")
48
+ throw new Error("CanopyX: the editor "+spec.type+" must return an element and getValue.");const host=h("div","cx-editor-host"),control=instance.control||instance.element;host.appendChild(instance.element);control.setAttribute("aria-label",fill(state.labels.gridEditCell,{column:spec.column.title}));return{spec,instance,host,control,abort,record}},editDispose(field){field.abort.abort();try{field.instance.destroy?.()}catch(error){this.emit("error",{error})}
49
+ field.host.remove();field.cell?.classList.remove("cx-editing")},editPreview(record,values){const preview={...record,cells:Object.assign(Object.create(null),record.cells)};for(const[spec,value]of values)if(!editSame(cellValue(record.cells[spec.id]),value))
50
+ preview.cells[spec.id]=editCell(record.cells[spec.id],value,this.editText(spec,value,record));return preview},editStart(wrapper,column,initial){const state=this.edit,open=state.session;if(open?.wrapper===wrapper&&open.fields.has(column)){open.fields.get(column).control.focus({preventScroll:true});return true}
51
+ if(open&&!this.editClose(true))return false;const element=this.rendered.get(wrapper);if(!element||wrapper.status||this.editReadOnly)return false;const cells=this.rowCells(element),visible=this.visibleColumns();const ids=(state.mode==="row"?visible.map(item=>item.id):[column]).filter(id=>this.canEditCell(wrapper.h,id)&&this.editColumnIndex(id)>=0);if(!ids.includes(column))return false;const session={id:wrapper.key,wrapper,mode:state.mode,fields:new Map()};for(const id of ids){const field=this.editCreate(wrapper.h,state.specs.get(id),state.mode,id===column?initial:undefined);field.cell=cells[this.editColumnIndex(id)];field.cell.classList.add("cx-editing");field.cell.appendChild(field.host);field.host.addEventListener("keydown",event=>this.editKey(event,field));field.host.addEventListener("focusout",event=>this.editBlur(event));session.fields.set(id,field)}
52
+ state.session=session;state.lastRow=wrapper.key;const field=session.fields.get(column);this.revealInRow(field.cell,field.cell,cells[0]);field.instance.focus?field.instance.focus():field.control.focus({preventScroll:true});if(typeof initial==="string"&&field.control.setSelectionRange&&/^(text|search)$/.test(field.control.type)) field.control.setSelectionRange(initial.length, initial.length);
53
+ this.editShowMessage(field);return true},async beginEdit(id,column,options={}){requireIdentifier(id,"edited row ID");if(this.destroyed||!this.acceptedQuery||this.pendingQuery||this.editReadOnly)return false;const wrapper=[...this.rowCache.values()].find(row=>row.key===id);if(!wrapper)return false;const target=column??this.visibleColumns().find(item=>this.canEditCell(wrapper.h,item.id))?.id;if(!target||!this.canEditCell(wrapper.h,target)||this.editColumnIndex(target)<0)return false;const session=this.edit.session;if(session&&session.id===id&&session.fields.has(target)){session.fields.get(target).control.focus({preventScroll:true});return true}
54
+ if(session&&!this.editClose(true))return false;this.setFocus(wrapper,false);return this.editStart(wrapper,target,options.initial)},editClose(apply,force){const state=this.edit,session=state.session;if(!session)return true;const values=[],record=this.editRecord(session.id)||session.wrapper.h;if(apply)for(const field of session.fields.values()){const parse=field.instance.error?.()||null,value=parse?undefined:field.instance.getValue();const message=parse||(!editScalar(value)?fill(state.labels.gridEditChoice,{column:field.spec.column.title}):null);if(message&&!force){this.editShowMessage(field,message);this.editAnnounce(message);return false}
55
+ if(!parse&&editScalar(value))values.push([field.spec,value,field])}
56
+ const preview=this.editPreview(record,values),shape=JSON.stringify(preview.cells);for(const item of values){const result=this.editRules(item[0],item[1],preview),message=result&&typeof result.then!=="function"?result:null;if(message&&!force){this.editShowMessage(item[2],message);this.editAnnounce(message);return false}
57
+ item[2]={result,row:shape}}
58
+ state.session=null;for(const field of session.fields.values())this.editDispose(field);this.editHideMessage();this.editGroup(()=>{for(const[spec,value,checked]of values)this.editApply(session.id,spec,value,"edit",record,checked)});return true},async endEdit(){const session=this.edit.session;if(!session)return true;const inside=[...session.fields.values()].some(field=>field.host.contains(document.activeElement)),column=[...session.fields.keys()][0];if(!this.editClose(true))return false;if(inside)this.editFocusCell(session.wrapper,column);if(session.mode==="row")await this.commitRow(session.id);return true},cancelEdit(){const session=this.edit?.session;if(!session)return;const field=[...session.fields.values()].find(item=>item.host.contains(document.activeElement));this.editClose(false);if(field)this.editFocusCell(session.wrapper,field.spec.id)},isEditing(){return!!this.edit.session},editFocusCell(wrapper,column){const index=this.editColumnIndex(column);if(this.rendered.has(wrapper)&&index>=0)this.focusCell(wrapper,index,null);else this.setFocus(wrapper,true)},editKey(event,field){event.stopPropagation();const session=this.edit.session;if(event.defaultPrevented||event.isComposing||!session||event.altKey||event.ctrlKey||event.metaKey)return;const{wrapper,mode}=session,column=field.spec.id;if(event.key==="Escape"){event.preventDefault();this.cancelEdit()}
59
+ else if(event.key==="Enter"){event.preventDefault();if(!this.editClose(true))return;this.editFocusCell(wrapper,column);if(mode==="row")this.commitRow(wrapper.key).catch(error=>this.emit("error",{error}));const index=wrapper._ri +(event.shiftKey?-1:1),bounds=this.rowBounds();if(index>=bounds.first&&index<=bounds.last)this.focusResult(index,true,this.editColumnIndex(column),null).catch(()=>{})}else if(event.key==="Tab"){event.preventDefault();if(mode==="row"){const fields=[...session.fields.values()],next=fields[(fields.indexOf(field)+(event.shiftKey?fields.length-1:1))%fields.length];next.control.focus({preventScroll:true});this.revealInRow(next.cell,next.cell,this.rowCells(this.rendered.get(wrapper))[0]);this.editShowMessage(next)}else if(this.editClose(true))this.editMoveAcross(wrapper,column,event.shiftKey?-1:1).catch(()=>{})}},async editMoveAcross(wrapper,column,step){const ids=this.visibleColumns().map(item=>item.id),bounds=this.rowBounds();for(let index=wrapper._ri,start=ids.indexOf(column)+ step,pass=0;pass<2;pass++,index +=step,start=step>0?0:ids.length-1){if(index<bounds.first||index>bounds.last)break;const row=pass?await this.waitForRow(index,this.queryEpoch):wrapper;if(!row||row.status)break;for(let at=start;at>=0&&at<ids.length;at +=step)
60
+ if(this.canEditCell(row.h,ids[at])){this.setFocus(row,false);if(this.editStart(row,ids[at]))return}}
61
+ this.editFocusCell(wrapper,column)},editBlur(event){const session=this.edit.session,next=event.relatedTarget;if(!session||next&&[...session.fields.values()].some(field=>field.host.contains(next)))return;setTimeout(()=>{if(this.edit.session!==session||!document.hasFocus()||[...session.fields.values()].some(field=>field.host.contains(document.activeElement)))return;this.editClose(true,true);if(session.mode==="row")this.commitRow(session.id).catch(error=>this.emit("error",{error}))})}});const editErrors=entry=>Object.assign(Object.create(null),entry.rowErrors,entry.errors);const editInvalid=entry=>!!(entry.error||entry.rowError||Object.keys(editErrors(entry)).length);const editMessage=value=>typeof value==="string"&&value.trim()?value:null;const EDIT_VALIDATIONS=new WeakMap();function editCurrentValidation(grid,result){const state=grid.edit,entry=state.changes.get(result.id),stamp=EDIT_VALIDATIONS.get(result);if(!entry)return{id:result.id,valid:!grid.destroyed,errors:{}};const columns=Object.keys(entry.values),fresh=stamp&&stamp.entry===entry&&
62
+ columns.length===Object.keys(stamp.values).length&&columns.every(column=>editSame(entry.values[column],stamp.values[column]))&&
63
+ JSON.stringify(entry.row.cells)===stamp.row&&columns.every(column=>!state.checks.has(result.id+"\u0000"+column));return{id:result.id,valid:!!result.valid&&!!fresh&&!grid.destroyed&&!editInvalid({...entry,error:null}),errors:editErrors(entry),error:entry.rowError||null}}
64
+ Object.assign(DataGrid.prototype,{editRules(spec,value,record){const rules=spec.rules,column=spec.column,labels=this.edit.labels;const say=(name,values)=>rules.message||fill(labels[name],{column:column.title,...values});const shown=bound=>column.kind==="datetime"?cellText(bound,column,this.fmt):typeof bound==="number"?numberFormat(this.lang).format(bound):String(bound);if(value==null||value===""){if(rules.required)return say("gridEditRequired")}
65
+ else{if(column.kind==="number"&&!(typeof value==="number"&&Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER))return say("gridEditNumber");if(column.kind==="datetime"&&!(typeof value==="string"&&/^\d{4}-\d\d-\d\dT.*(?:Z|[+-]\d\d:\d\d)$/.test(value) && Number.isFinite(Date.parse(value)))) return say("gridEditDate");
66
+ if(typeof value==="number"||column.kind==="datetime"){const at=column.kind==="datetime"?Date.parse(value):value,of=bound=>typeof bound==="string"?Date.parse(bound):bound;if(rules.min!==undefined&&at<of(rules.min))return say("gridEditMin",{min:shown(rules.min)});if(rules.max!==undefined&&at>of(rules.max))return say("gridEditMax",{max:shown(rules.max)});const base=typeof rules.min==="number"?rules.min:0,steps=rules.step&&(value-base)/ rules.step;
67
+ if(typeof value==="number"&&rules.step&&Math.abs(steps-Math.round(steps))>1e-9)return say("gridEditStep",{step:shown(rules.step)})}
68
+ if(typeof value==="string"&&column.kind!=="datetime"){if(rules.minLength!==undefined&&value.length<rules.minLength)return say("gridEditMinLength",{min:rules.minLength});if(rules.maxLength!==undefined&&value.length>rules.maxLength)return say("gridEditMaxLength",{max:rules.maxLength});if(spec.pattern&&!spec.pattern.test(value))return say("gridEditPattern")}}
69
+ const validators=this.edit.options.validators,validator=validators&&Object.hasOwn(validators,spec.id)?validators[spec.id]:null;if(typeof validator!=="function")return null;let result;try{result=validator(value,editCopy(record))}catch(error){return error.message||String(error)}
70
+ if(!result||typeof result.then!=="function")return editMessage(result);return Promise.resolve(result).then(editMessage,error=>error?.message||String(error))},editCheck(id,spec,checked){const state=this.edit,entry=state.changes.get(id);if(!entry||!Object.hasOwn(entry.values,spec.id))return Promise.resolve(null);const value=entry.values[spec.id],key=id+"\u0000"+spec.id;const token={value,row:checked?.row??JSON.stringify(entry.row.cells)};state.checks.set(key,token);(entry.checked||=new Map()).set(spec.id,token);const settle=message=>{if(state.checks.get(key)!==token)return message;state.checks.delete(key);const current=state.changes.get(id);if(current&&editSame(current.values[spec.id],value)){if(message)current.errors[spec.id]=message;else delete current.errors[spec.id];this.editChanged();this.editRefreshMessage()}
71
+ return message};const result=checked?checked.result:this.editRules(spec,value,entry.row);if(result&&typeof result.then==="function")return token.promise=result.then(settle);return token.promise=Promise.resolve(settle(result||null))},async editValidateRow(id,refresh=false){const state=this.edit,same=(left,right)=>Object.keys(left).length===Object.keys(right).length&&Object.keys(left).every(column=>editSame(left[column],right[column]));let entry,stable=false;for(let round=0;round<5;round++){entry=state.changes.get(id);if(!entry)return{id,valid:true,errors:{}};if(this.destroyed)break;const values={...entry.values},shape=JSON.stringify(entry.row.cells),used=new Map();const unchanged=()=>!this.destroyed&&state.changes.get(id)===entry&&same(values,entry.values)&&JSON.stringify(entry.row.cells)===shape;await Promise.all(Object.keys(values).map(column=>{const pending=state.checks.get(id+"\u0000"+column),checked=entry.checked?.get(column);const matches=token=>token&&token.row===shape&&editSame(token.value,values[column]);const kept=!refresh&&matches(checked)?checked:matches(pending)?pending:null;const result=kept?.promise??this.editCheck(id,state.specs.get(column));used.set(column,entry.checked?.get(column));return result}));for(const[column,token]of used)if(entry.checked?.get(column)===token)entry.checked.delete(column);const validator=state.options.validateRow;if(validator&&unchanged()){let result;try{result=await validator(editCopy(entry.row),this.editChangeOf(entry))}catch(error){result=error.message||String(error)}
72
+ if(unchanged()){entry.rowError=editMessage(result);entry.rowErrors=Object.create(null);if(result&&typeof result==="object")for(const[column,message]of Object.entries(result))
73
+ if(editMessage(message)&&this.columns.some(item=>item.id===column))entry.rowErrors[column]=message;this.editChanged();this.editRefreshMessage()}}
74
+ if(unchanged()&&Object.keys(values).every(column=>!state.checks.has(id+"\u0000"+column))){stable=true;break}}
75
+ const current=state.changes.get(id);if(!current)return{id,valid:true,errors:{}};const result={id,valid:stable&&!editInvalid({...current,error:null}),errors:editErrors(current),error:current.rowError||null};EDIT_VALIDATIONS.set(result,{entry:current,values:{...current.values},row:JSON.stringify(current.row.cells)});return result},async validate(ids){if(ids!==undefined&&!Array.isArray(ids))throw new Error("CanopyX: validate takes an array of row IDs.");const rows=(await Promise.all((ids??[...this.edit.changes.keys()]).map(id=>this.editValidateRow(id,true))))
76
+ .map(result=>editCurrentValidation(this,result));return editCopy({valid:rows.every(row=>row.valid),rows})},editChangeOf(entry){if(entry.inserted)return editCopy({op:"insert",id:entry.id,values:{...entry.values}});const change={id:entry.id,values:{...entry.values},original:Object.create(null)};for(const column of Object.keys(entry.values))change.original[column]=cellValue(entry.original[column]);return editCopy(change)}});function editCommitAnswer(result,batch,columns){checkDocument(result,"gridCommitResult");if(result.view!==batch.view||!Array.isArray(result.rows))throw new Error("CanopyX: the commit result does not answer its batch.");const asked=new Set(batch.rows.map(row=>row.id)),inserts=new Set(batch.rows.filter(row=>row.op==="insert").map(row=>row.id)),rows=new Map(),text=value=>value===undefined||typeof value==="string";for(const row of result.rows){if(!row||!asked.has(row.id)||rows.has(row.id)||!["saved","error","conflict"].includes(row.status)||!text(row.error)||
77
+ row.errors!==undefined&&(Object(row.errors)!==row.errors||Array.isArray(row.errors)||!Object.values(row.errors).every(text))||
78
+ row.cells!==undefined&&(row.status!=="saved"||Object(row.cells)!==row.cells)||
79
+ row.newId!==undefined&&(row.status!=="saved"||!inserts.has(row.id)||typeof row.newId!=="string"))
80
+ throw new Error("CanopyX: malformed commit result row.");if(row.newId!==undefined)requireIdentifier(row.newId,"saved row ID");if(row.cells!==undefined)validateGridRows([{id:row.id,cells:row.cells}],columns);rows.set(row.id,row)}
81
+ return{rows,revision:result.revision}}
82
+ Object.assign(DataGrid.prototype,{editEnterRow(id){const state=this.edit,previous=state.lastRow,entry=previous&&previous!==id?state.changes.get(previous):null;state.lastRow=id;if(entry&&!entry.committed&&!entry.running&&entry.status!=="saving"&&state.session?.id!==previous)
83
+ this.commitRow(previous).catch(error=>this.emit("error",{error}))},async commitRow(id){requireIdentifier(id,"committed row ID");const state=this.edit;if(state.session?.id===id&&!this.editClose(true))return editCopy({id,ok:false,status:"invalid",errors:{}});const entry=state.changes.get(id);if(!entry||!Object.keys(entry.values).length&&entry.status!=="saving")return editCopy({id,ok:true,status:"unchanged"});if(entry.status==="saving")return entry.saving;return entry.running||=this.editCommitRow(id,entry).finally(()=>{entry.running=null})},async editCommitRow(id,entry){const state=this.edit,check=editCurrentValidation(this,await this.editValidateRow(id));if(!check.valid)return editCopy({id,ok:false,status:"invalid",errors:check.errors,error:check.error});if(state.changes.get(id)!==entry||entry.status==="saving")return entry.saving||editCopy({id,ok:true,status:"unchanged"});let response,answered=false;const respondWith=value=>{if(answered)throw new Error("CanopyX: the row commit has an answer already.");answered=true;response=value};const detail={...this.editChangeOf(entry),row:editCopy(entry.row),revision:this.snapshot?.revision??null,respondWith};const admitted={entry,values:{...detail.values}};let go=this.host.dispatchEvent(new CustomEvent("canopyx:rowCommit",{detail,cancelable:true})),result;if(typeof this.opts.onRowCommit==="function"){try{result=this.opts.onRowCommit(detail)}catch(error){result=Promise.reject(error)}
84
+ if(result===false)go=false;else if(result!==undefined&&!answered)respondWith(result===true?{status:"saved"}:result)}
85
+ if(!go)return editCopy({id,ok:false,status:"cancelled"});entry.committed=true;if(!answered)return editCopy({id,ok:true,status:"pending"});return this.editSave([admitted],()=>Promise.resolve(response).then(value=>
86
+ editCommitAnswer({protocol:PROTOCOL,type:"gridCommitResult",view:this.view.id,rows:[value===true?{id,status:"saved"}:{...value,id}]},{view:this.view.id,rows:[entry.inserted?{id,op:"insert"}:{id}]},this.view.columns))).then(rows=>rows[0])},async commitChanges(options={}){if(Object(options)!==options||options.ids!==undefined&&!Array.isArray(options.ids))throw new Error("CanopyX: invalid commit options.");const state=this.edit,open=state.session;if(open&&!this.editClose(true))return editCopy({ok:false,saved:0,failed:1,rows:[{id:open.id,ok:false,status:"invalid",errors:{}}]});const ready=id=>state.changes.get(id)?.status!=="saving"&&Object.keys(state.changes.get(id)?.values||{}).length>0;const ids=[...new Set(options.ids??state.changes.keys())];ids.forEach(id=>requireIdentifier(id,"committed row ID"));const checks=(await Promise.all(ids.filter(ready).map(id=>this.editValidateRow(id))))
87
+ .map(result=>editCurrentValidation(this,result));const rows=checks.filter(check=>!check.valid).map(check=>({id:check.id,ok:false,status:"invalid",errors:check.errors,error:check.error}));const valid=checks.filter(check=>check.valid&&ready(check.id)).map(check=>check.id);let revision;if(valid.length){const batch=editCopy({protocol:PROTOCOL,type:"gridCommit",view:this.view.id,revision:this.snapshot?.revision??null,rows:valid.map(id=>this.editChangeOf(state.changes.get(id)))});const writer=state.options.commit||(typeof this.source.commit==="function"?(body,context)=>this.source.commit(body,context):null);const local=!writer&&this.view.source.kind==="inline";const admitted=valid.map((id,index)=>({entry:state.changes.get(id),values:batch.rows[index].values}));rows.push(...await this.editSave(admitted,async signal=>{if(writer){const answer=editCommitAnswer(await writer(batch,{signal}),batch,this.view.columns);revision=answer.revision;return answer}
88
+ return{rows:new Map(local?valid.map(id=>[id,{id,status:"saved"}]):[])}},options.signal))}
89
+ const saved=rows.filter(row=>row.ok).length;const result=editCopy({ok:saved===rows.length,saved,failed:rows.length-saved,rows,...(revision?{revision}:{})});if(rows.length)this.editAnnounce([saved&&fill(state.labels.gridEditSaved,{count:saved}),result.failed&&fill(state.labels.gridEditFailed,{count:result.failed})].filter(Boolean).join(" · "));this.emit("commit",result);return result},editSave(admitted,write,signal){const entries=admitted.map(row=>row.entry);for(const{entry,values}of admitted){entry.status="saving";entry.saved={...values};entry.error=null}
90
+ this.editChanged();const run=this.runGridHook(write,signal).then(answer=>this.editSaved(entries,answer,null),error=>this.editSaved(entries,null,error));entries.forEach((entry,index)=>{entry.saving=run.then(results=>results[index])});return run},async editSaved(entries,answer,failure){const state=this.edit;const results=entries.map(entry=>{const row=answer?.rows.get(entry.id),id=entry.id;let problem=failure;if(state.changes.get(id)!==entry)return{id,ok:false,status:"superseded"};entry.status=undefined;if(row?.status==="saved"){try{if(entry.inserted){this.editAcceptInsert(entry,row.cells,row.newId);return{id,ok:true,status:"saved",...(row.newId&&row.newId!==id?{newId:row.newId}:{})}}
91
+ this.editAccept(entry,row.cells);return{id,ok:true,status:"saved"}}catch(error){problem=error}}
92
+ const status=row&&row.status!=="saved"?row.status:"error";entry.status=status;entry.saved=null;entry.error=row?.error||(status==="conflict"?state.labels.gridEditConflict:problem?fill(state.labels.gridEditRejected,{error:problem.message}):
93
+ row?.errors?null:state.labels.gridEditNotSaved);for(const[column,message]of Object.entries(row?.errors||{}))if(Object.hasOwn(entry.values,column)&&editMessage(message))entry.errors[column]=message;if(!Object.keys(entry.values).length)state.changes.delete(id);return{id,ok:false,status,...(entry.error?{error:entry.error}:{}),...(row?.errors?{errors:row.errors}:{})}});this.editChanged();this.editRefreshMessage();if(!this.destroyed&&this.view.source.kind==="inline"&&(!this.opts.source||this.enhanceStore)&&results.some(row=>row.ok))
94
+ await this.setQuery({...this.queryForChange(true),revision:undefined},this.pendingPaging).catch(()=>{});return results.map(editCopy)},editAccept(entry,canonical){const record=entry.row,local=this.view.source.kind==="inline"&&(!this.opts.source||this.enhanceStore),inline=local?this.view.rows.find(row=>row.id===entry.id):null;for(const[column,value]of Object.entries(entry.saved)){const current=cellValue(record.cells[column]),saved=canonical&&Object.hasOwn(canonical,column)?canonical[column]:
95
+ editCell(entry.original[column],value,this.editText(this.edit.specs.get(column),value,record));if(editSame(current,value)||editSame(current,cellValue(saved))){delete entry.values[column];delete entry.original[column];delete entry.errors[column];record.cells[column]=saved}else{entry.original[column]=saved;entry.values[column]=current}}
96
+ for(const[column,cell]of Object.entries(canonical||{})){if(Object.hasOwn(entry.values,column)&&!Object.hasOwn(entry.saved,column)){if(editSame(entry.values[column],cellValue(cell))){delete entry.values[column];delete entry.original[column];delete entry.errors[column]}
97
+ else entry.original[column]=cell}
98
+ if(!Object.hasOwn(entry.values,column))record.cells[column]=cell}
99
+ if(inline)for(const column of new Set([...Object.keys(entry.saved),...Object.keys(canonical||{})])){const cell=Object.hasOwn(entry.values,column)?entry.original[column]:record.cells[column];if(cell===undefined)delete inline.cells[column];else inline.cells[column]=JSON.parse(JSON.stringify(cell))}
100
+ if(inline){if(this.enhanceStore)this.enhanceStore.version++;else if(this.view.rowData)this.source.invalidate();else this.source=inlineGridSource(this.view)}
101
+ if(local)RECORD_IMPL.invalidate?.(this,entry.id);entry.saved=null;entry.error=null;entry.committed=false;if(!Object.keys(entry.values).length)this.edit.changes.delete(entry.id)}});Object.assign(DataGrid.prototype,{async insertRow(input){const result=this.editInsertNow(input,"api");if(!result.applied)return editCopy(result);const check=await this.editValidateRow(result.id);return editCopy({id:result.id,applied:true,valid:check.valid,...(check.valid?{}:{errors:check.errors})})},editInsertNow(input,source){if(Object(input)!==input||Object(input.cells)!==input.cells||Array.isArray(input.cells))throw new Error("CanopyX: insertRow takes { id?, cells }.");const id=input.id??this.editNewId();requireIdentifier(id,"new row ID");if(!id.startsWith("new-"))throw new Error("CanopyX: a new row ID starts with new-.");if(this.editRecord(id)||this.edit.changes.has(id))throw new Error("CanopyX: a row "+id+" exists already.");const row={id,cells:JSON.parse(JSON.stringify(input.cells))};validateGridRows([row],this.view.columns);if(!this.emit("rowInserting",{id,row:editCopy(row)}))return{id,applied:false,valid:true};this.editInsert(row,source);return{id,applied:true}},editGroupIf(run){const state=this.edit,outer=state.group,steps=state.group=outer||[],mark=steps.length;let kept=false;try{kept=run()===true;return kept}finally{state.group=outer;if(!kept)steps.length=mark;else if(!outer&&steps.length&&state.limit){state.undo.push(steps);if(state.undo.length>state.limit)state.undo.shift();state.redo.length=0}}},editOwnsRows(){return this.view.source.kind==="inline"&&(!this.opts.source||!!this.enhanceStore)},editRowsChanged(){if(this.enhanceStore)this.enhanceStore.version++;else if(this.view.rowData&&this.source.invalidate)this.source.invalidate();else this.source=inlineGridSource(this.view)},editNewId(){let id;do id="new-" +(++this.edit.sequence);while(this.editRecord(id)||this.edit.changes.has(id));return id},editInsert(row,source){if(this.edit.changes.has(row.id))return false;const state=this.edit,entry={id:row.id,inserted:true,original:Object.create(null),values:Object.create(null),errors:Object.create(null),error:null,row:JSON.parse(JSON.stringify(row)),committed:false};for(const column of state.specs.keys())entry.values[column]=cellValue(row.cells[column])??null;state.changes.set(row.id,entry);state.inserted.set(row.id,entry.row);if(this.editOwnsRows()){this.view.rows.push(JSON.parse(JSON.stringify(row)));this.editRowsChanged()}
102
+ if(source!=="undo"&&source!=="redo"){const step={id:row.id,insert:JSON.parse(JSON.stringify(row))};if(state.group)state.group.push(step);else if(state.limit){state.undo.push([step]);if(state.undo.length>state.limit)state.undo.shift();state.redo.length=0}}
103
+ for(const column of state.specs.keys())this.editCheck(row.id,state.specs.get(column));this.editChanged();this.emit("rowAdded",{id:row.id,row:editCopy(entry.row)});if(this.editOwnsRows())Promise.resolve(this.reload?.()).catch(()=>{})},editRemoveInserted(id,forget){const state=this.edit;if(!state.inserted.has(id)||state.changes.get(id)?.status==="saving")return false;state.inserted.delete(id);state.changes.delete(id);if(forget)for(const list of[state.undo,state.redo])for(let at=list.length-1;at>=0;at--)if(list[at].some(step=>step.insert&&step.id===id))list.splice(at,1);if(this.editOwnsRows()){const at=this.view.rows.findIndex(row=>row.id===id);if(at>=0){this.view.rows.splice(at,1);this.editRowsChanged()}
104
+ Promise.resolve(this.reload?.()).catch(()=>{})}
105
+ this.editChanged();this.emit("rowRemoved",{id});return true},editAcceptInsert(entry,canonical,newId){const state=this.edit,from=entry.id;let to=newId??from;if(to!==from)requireIdentifier(to,"saved row ID");if(to!==from&&(state.changes.has(to)||this.editOwnsRows()&&this.view.rows.some(row=>row.id===to))){this.emit("error",{error:new Error("CanopyX: the writer stored a new row as "+to+", which another row has."),id:from});to=from}
106
+ const record=entry.row,later=entry.saved?Object.keys(entry.values).filter(column=>JSON.stringify(entry.values[column])!==JSON.stringify(entry.saved[column]??null)&&state.specs.has(column)):[];for(const column of later)record.cells[column]=canonical&&Object.hasOwn(canonical,column)?canonical[column]:entry.saved[column];for(const[column,cell]of Object.entries(canonical||{}))record.cells[column]=cell;record.id=to;state.changes.delete(from);state.inserted.delete(from);if(this.editOwnsRows()){const inline=this.view.rows.find(row=>row.id===from);if(inline){inline.id=to;for(const column of Object.keys(entry.saved||{}))if(Object.hasOwn(record.cells,column))inline.cells[column]=JSON.parse(JSON.stringify(record.cells[column]))}
107
+ this.editRowsChanged()}
108
+ for(const list of[state.undo,state.redo])for(let at=list.length-1;at>=0;at--)if(list[at].some(step=>step.insert&&step.id===from))list.splice(at,1);if(to!==from)for(const steps of[...state.undo,...state.redo])for(const step of steps)if(step.id===from){step.id=to;if(step.insert)step.insert.id=to}
109
+ if(to!==from)this.emit("rowIdChanged",{from,to});if(later.length){const outer=state.group,target=this.editRecord(to)||record;state.group=[];try{for(const column of later)this.editApply(to,state.specs.get(column),entry.values[column],"accept",target)}
110
+ finally{state.group=outer}}
111
+ if(to!==from&&this.editOwnsRows())Promise.resolve(this.reload?.()).catch(()=>{})}});GRID_ICONS.undo='<path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/>';GRID_ICONS.redo='<path d="m15 14 5-5-5-5"/><path d="M20 9H9.5a5.5 5.5 0 0 0 0 11H13"/>';const editAttr=(element,name,value)=>{if(value==null)element.removeAttribute(name);else if(element.getAttribute(name)!==value)element.setAttribute(name,value)};Object.assign(DataGrid.prototype,{buildEditing(){const state=this.edit,signal=this.lifetime.signal,grid=this.grid,labels=state.labels;state.tip=h("div","cx-edit-tip");state.tip.id=this.id+"-edit-tip";state.tip.popover="manual";state.notes=h("div","cx-sr-only");state.notes.id=this.id+"-edit-notes";this.host.append(state.tip,state.notes);grid.addEventListener("keydown",event=>this.editGridKey(event),{capture:true,signal});grid.addEventListener("pointerdown",event=>{const cell=event.target.closest?.(".cx-cell");state.armed=cell&&cell===document.activeElement?cell:null},{capture:true,signal});grid.addEventListener("click",event=>this.editClick(event),{signal});grid.addEventListener("dblclick",event=>{const at=!this.phone&&this.editCellAt(event.target);if(at){event.stopPropagation();this.setFocus(at.row,false);this.editStart(at.row,at.column)}},{capture:true,signal});this.canvas.addEventListener("focusin",event=>{const row=event.target.closest(".cx-row")?._w;if(row&&!row.status)this.editEnterRow(row.key);this.editRefreshMessage()},{signal});this.canvas.addEventListener("focusout",event=>{if(!event.relatedTarget||!this.canvas.contains(event.relatedTarget))this.editHideMessage()},{signal});grid.addEventListener("scroll",()=>{if(state.tip.matches(":popover-open"))this.editRefreshMessage()},{passive:true,signal});signal.addEventListener("abort",()=>{if(state.session)for(const field of state.session.fields.values())this.editDispose(field);state.tip.remove();state.notes.remove();state.barEl?.remove()},{once:true});if(!state.bar)return;const bar=state.barEl=h("div","cx-editbar"),status=h("span","cx-editbar-text");state.barText=status.appendChild(h("span"));state.barErrors=status.appendChild(h("span","cx-editbar-errors"));status.setAttribute("role","status");const act=run=>()=>Promise.resolve().then(run).catch(error=>this.emit("error",{error}));const icon=(name,text,run)=>{const button=gridDecorateButton(gridButton("",act(run)),name,true);button.classList.add("icon");button.setAttribute("aria-label",text);button.title=text;return button};state.undoButton=icon("undo",labels.gridEditUndo,()=>this.undo());state.redoButton=icon("redo",labels.gridEditRedo,()=>this.redo());state.discardButton=gridDecorateButton(gridButton(labels.gridEditDiscard,act(()=>this.revertChanges())),null,true);state.saveButton=gridDecorateButton(gridButton(labels.gridEditSave,act(()=>this.commitChanges())),"check");state.saveButton.classList.add("cx-edit-save");bar.append(status,state.undoButton,state.redoButton,state.discardButton,state.saveButton);(this.spacer&&this.spacer.parentNode===this.toolbar?this.spacer:this.toolbar.lastChild)?.after(bar);this.syncEditBar()},editGridKey(event){const state=this.edit;if(event.defaultPrevented||event.isComposing||event.target.closest(".cx-editor-host"))return;const key=event.key.toLowerCase(),command=(event.ctrlKey||event.metaKey)&&!event.altKey;if(command&&(key==="z"||key==="y")&&!event.target.closest("input,textarea,select,[contenteditable]")){event.preventDefault();event.stopPropagation();if(key==="y"||event.shiftKey)this.redo();else this.undo();return}
112
+ const at=!command&&!event.altKey&&this.editCellAt(event.target,true);if(!at)return;const typed=event.key.length===1&&event.key!==" ",empty=event.key==="Backspace"||event.key==="Delete";if((typed||empty||event.key==="F2"||event.key==="Enter")&&this.editStart(at.row,at.column,typed?event.key:empty?"":undefined)){event.preventDefault();event.stopPropagation()}
113
+ if(state.session===null&&event.key==="Escape")this.editHideMessage()},editCellAt(target,own){const cell=target.closest?.(".cx-cell"),element=cell?.parentNode,row=element?._w;if(!row||row.status||own&&target!==cell||target.closest(".cx-editor-host"))return null;const column=this.visibleColumns()[this.rowCells(element).indexOf(cell)];return column&&this.canEditCell(row.h,column.id)?{row,column:column.id,cell}:null},editClick(event){const at=!this.phone&&!event.target.closest("a,button,input,select,textarea,[contenteditable]")&&this.editCellAt(event.target);if(!at||event.detail>1)return;if(this.edit.armed===at.cell)this.editStart(at.row,at.column);else this.focusCell(at.row,this.editColumnIndex(at.column),null)},editNote(key,text){const state=this.edit;let note=state.messages.get(key);if(!note){note=h("span");note.id=this.id+"-en" + ++state.sequence;state.notes.appendChild(note);state.messages.set(key,note)}
114
+ if(note.textContent!==text)setText(note,text);return note.id},paintEdits(element,row){const state=this.edit,entry=state.changes.get(row.key),session=state.session?.wrapper===row?state.session:null;const rowError=entry&&(entry.error||entry.rowError);element.classList.toggle("cx-row-invalid",!!rowError);editAttr(element,"aria-describedby",rowError?this.editNote("\u0001"+row.key,rowError):null);editAttr(element,"aria-busy",entry?.status==="saving"?"true":null);const errors=entry?editErrors(entry):Object.create(null);this.cols.forEach((column,index)=>{const slot=element._cells[index],cell=slot.cell,dirty=!!entry&&Object.hasOwn(entry.values,column.id),error=errors[column.id];const editable=state.specs.has(column.id)&&this.canEditCell(row.h,column.id),field=session?.fields.get(column.id);cell.classList.toggle("cx-dirty",dirty);cell.classList.toggle("cx-invalid",!!error);cell.classList.toggle("cx-editable",editable);editAttr(cell,"aria-readonly",editable?null:"true");editAttr(cell,"aria-invalid",error?"true":null);editAttr(cell,"aria-describedby",[dirty&&this.editNote("edited",state.labels.gridEditEdited),error&&this.editNote(row.key+"\u0000"+column.id,error)].filter(Boolean).join(" ")||null);if(field&&field.cell!==cell){field.cell.classList.remove("cx-editing");field.cell=cell}
115
+ if(field){cell.classList.add("cx-editing");if(field.host.parentNode!==cell)cell.appendChild(field.host)}})},editShowMessage(field,message){const state=this.edit,entry=state.changes.get(field.record.id);const text=message||(entry&&!Object.hasOwn(entry.values,field.spec.id)?null:entry&&editErrors(entry)[field.spec.id])||null;editAttr(field.control,"aria-invalid",text?"true":null);editAttr(field.control,"aria-describedby",text?state.tip.id:null);if(text)this.editTip(field.cell||field.host,text);else this.editHideMessage()},editRefreshMessage(){const state=this.edit,active=document.activeElement,session=state.session;const field=session&&[...session.fields.values()].find(item=>item.host.contains(active));if(field){this.editShowMessage(field);return}
116
+ const cell=active?.classList?.contains("cx-cell")&&this.canvas.contains(active)?active:null,row=cell?.parentNode._w;const column=row&&this.visibleColumns()[this.rowCells(cell.parentNode).indexOf(cell)],entry=row&&state.changes.get(row.key);const text=entry&&column?editErrors(entry)[column.id]||(column===this.visibleColumns()[0]?entry.error||entry.rowError:null):null;if(text)this.editTip(cell,text);else this.editHideMessage()},editTip(anchor,text){const tip=this.edit.tip,box=anchor.getBoundingClientRect();if(!anchor.isConnected||!box.width){this.editHideMessage();return}
117
+ setText(tip,text);tip.style.left=Math.max(4,Math.min(box.left,innerWidth-280))+ "px";tip.style.top=box.bottom+4+"px";if(!tip.matches(":popover-open"))tip.showPopover()},editHideMessage(){const tip=this.edit?.tip;if(tip?.matches(":popover-open"))tip.hidePopover()},editAnnounce(text){if(text)setText(this.live,text +((this.edit.flip=!this.edit.flip)?" ":""))},syncEditBar(){const state=this.edit;if(!state)return;for(const[key,note]of state.messages){const row=key[0]==="\u0001",[id,column]=(row?key.slice(1):key).split("\u0000"),entry=state.changes.get(id);if(key!=="edited"&&!(entry&&(row?entry.error||entry.rowError:editErrors(entry)[column]))){note.remove();state.messages.delete(key)}}
118
+ if(!state.barEl)return;const entries=this.editEntries(),count=entries.length,errors=entries.filter(editInvalid).length,saving=[...state.changes.values()].some(entry=>entry.status==="saving");state.barEl.hidden=!count&&!state.undo.length&&!state.redo.length&&!saving;setText(state.barText,saving?state.labels.gridEditSaving:fill(state.labels.gridEditChanges,{count:numberFormat(this.lang).format(count)}));setText(state.barErrors,errors&&!saving?" · "+fill(state.labels.gridEditErrors,{count:numberFormat(this.lang).format(errors)}):"");state.undoButton.disabled=!state.undo.length;state.redoButton.disabled=!state.redo.length;state.discardButton.disabled=state.saveButton.disabled=!count||saving},editRemember(column,option){const seen=(this.edit.seen||=new Map()),list=seen.get(column)||[];if(!list.some(item=>item.value===option.value)){list.push(option);if(list.length>1000)list.shift()}
119
+ seen.set(column,list)},async editLookup(spec,search,record,signal){const limit=spec.lookup.limit,lookups=this.edit.options.lookups,own=lookups&&Object.hasOwn(lookups,spec.id)?lookups[spec.id]:null,text=foldText(search.trim());const matching=list=>list.filter(option=>foldText(option.label).includes(text)).slice(0,limit);if(typeof own==="function")return editOptions(await own({column:spec.id,search,limit,row:editCopy(record),signal}),spec.id).slice(0,limit);if(spec.options)return matching(spec.options);const column=this.columns.find(item=>item.id===spec.lookup.column);if(this.view.source.kind==="inline"){await this.source.decoded;const values=new Map();for(const row of this.view.rows){const value=cellValue(row.cells[column.id]);if(value!=null&&!values.has(value))values.set(value,{value,label:cellText(row.cells[column.id],column,this.fmt)})}
120
+ return matching([...values.values()].sort((left,right)=>left.label.localeCompare(right.label,this.lang)))}
121
+ if(typeof this.source.facet!=="function"||!this.acceptedQuery)return[];const query=gridQuery(this.view,{...this.acceptedQuery,search:"",filters:[],filterGroup:undefined});const request=editCopy({protocol:PROTOCOL,type:"gridFacetQuery",query,column:column.id,search,limit,includeCurrentFilter:false});const response=await this.runGridHook(hook=>this.source.facet(request,{signal:hook}),signal);const page=validateGridFacetPage(response,this.view,request,{total:this.snapshot.total,matched:this.snapshot.total});return page.items.filter(item=>item.value!=null).map(item=>({value:item.value,label:cellText(item.value,column,this.fmt)}))}});const EDIT_FILL_ROW=DataGrid.prototype.fillRow;DataGrid.prototype.fillRow=function(element,row){EDIT_FILL_ROW.call(this,element,row);if(this.edit&&!row.status)this.paintEdits(element,row)};const editDrawerOn=grid=>{const state=grid.edit;return!!state&&state.specs.size>0&&state.drawer!=="never"&&(state.drawer!=="phone"||grid.phone)};DataGrid.drawerHook(10,function(dialog){dialog.addEventListener("cx-drawer-paint",()=>{const body=dialog.querySelector(".cx-drawer-body");if(body&&dialog.isConnected&&editDrawerOn(this))this.editForm(dialog,body,dialog._cxRecord?.id)});dialog.addEventListener("close",()=>{if(this.edit?.form?.dialog===dialog)this.editFormDispose()},{once:true})});Object.assign(DataGrid.prototype,{editFormDispose(){const form=this.edit?.form;if(form){form.fields.forEach(field=>this.editDispose(field));this.edit.form=null}},recordKeepField(id,record){return editDrawerOn(this)&&!this.editReadOnly&&this.edit.specs.has(id)&&!!record&&!!this.editRecord(record.id)&&this.canEditCell(this.editRecord(record.id),id)},editForm(dialog,body,id){const state=this.edit,record=this.editRecord(id),labels=state.labels;const kept=state.form&&state.form.dialog===dialog&&state.form.record.id===id&&record?state.form:null;if(!kept)this.editFormDispose();if(!record||this.editReadOnly||this.destroyed){this.editFormDispose();return}
122
+ const form=kept||(state.form={dialog,record,fields:[]}),existing=new Map(form.fields.map(field=>[field.spec.id,field]));for(const{column,term,value}of this.drawerFields(dialog)){if(!column||value.tagName!=="DD"||!state.specs.has(column.id)||!this.canEditCell(record,column.id))continue;let field=existing.get(column.id);if(!field){field=this.editCreate(record,state.specs.get(column.id),"form");field.message=h("div","cx-edit-message");field.message.id=this.id+"-em" + ++state.sequence;field.message.hidden=true;form.fields.push(field)}
123
+ field.label=term;if(field.host.parentNode!==value||field.message.parentNode!==value)value.replaceChildren(field.host,field.message)}
124
+ dialog.classList.toggle("cx-edit-form",form.fields.length>0);let foot=dialog.querySelector(".cx-drawer-f");if(!form.fields.length){foot?.remove();return}
125
+ if(!foot){foot=h("div","cx-drawer-f");const summary=h("p","cx-drawer-summary");summary.setAttribute("role","alert");summary.hidden=true;const cancel=gridButton(labels.gridEditCancel,()=>dialog.close());const save=gridButton(labels.gridEditSave,()=>this.editFormSave().catch(error=>this.emit("error",{error})));save.classList.add("cx-edit-save");foot.append(summary,cancel,save);dialog.appendChild(foot);dialog.addEventListener("keydown",event=>{if(event.key==="Enter"&&!event.defaultPrevented&&!event.isComposing&&event.target.matches("input:not([type=checkbox])")){event.preventDefault();this.editFormSave().catch(error=>this.emit("error",{error}))}})}
126
+ form.summary=foot.querySelector(".cx-drawer-summary");form.summary.hidden=true},editFormMessage(field,message){setText(field.message,message||"");field.message.hidden=!message;editAttr(field.control,"aria-invalid",message?"true":null);editAttr(field.control,"aria-describedby",message?field.message.id:null)},async editFormSave(){const form=this.edit.form,labels=this.edit.labels;if(!form||form.busy)return false;form.busy=true;try{const values=[],invalid=[];for(const field of form.fields){const parse=field.instance.error?.()||null,value=parse?undefined:field.instance.getValue();const message=parse||(!editScalar(value)?fill(labels.gridEditChoice,{column:field.spec.column.title}):null);if(message){this.editFormMessage(field,message);invalid.push(field)}
127
+ else values.push([field.spec,value,field])}
128
+ const preview=this.editPreview(form.record,values),shape=JSON.stringify(preview.cells);for(const item of values){const field=item[2];let message=this.editRules(field.spec,item[1],preview);if(message&&typeof message.then==="function")message=await message;this.editFormMessage(field,message);if(message)invalid.push(field);item[2]={result:message,row:shape}}
129
+ if(this.edit.form!==form)return false;if(!invalid.length){this.editGroup(()=>{for(const[spec,value,checked]of values)this.editApply(form.record.id,spec,value,"form",form.record,checked)});const result=await this.commitRow(form.record.id);if(this.edit.form!==form)return result.ok;if(result.ok){form.dialog.close();return true}
130
+ for(const field of form.fields)if(Object.hasOwn(result.errors||{},field.spec.id)&&result.errors[field.spec.id]){this.editFormMessage(field,result.errors[field.spec.id]);invalid.push(field)}
131
+ form.summary.hidden=false;setText(form.summary,result.error||fill(labels.gridEditErrors,{count:invalid.length||1}))}else{form.summary.hidden=false;setText(form.summary,fill(labels.gridEditErrors,{count:invalid.length}))}
132
+ (invalid[0]?.control||form.summary).focus?.({preventScroll:false});return false}finally{form.busy=false}}});for(const name of["beginEdit","endEdit","setCellValue","validate","commitRow","commitChanges","insertRow"]){const run=DataGrid.prototype[name];DataGrid.prototype[name]=function(...args){return this.edit?run.apply(this,args):this.loadEditing().then(()=>run.apply(this,args))}}
133
+ for(const name of["isEditing","hasChanges","undo","redo","canUndo","canRedo","cancelEdit","revertChanges","getChanges","setReadOnly"]){const run=DataGrid.prototype[name];DataGrid.prototype[name]=function(...args){if(this.edit)return run.apply(this,args);if(name==="setReadOnly")this.editReadOnly=!!args[0];return name==="getChanges"?[]:false}}
134
+ installed.add(context.DataGrid.prototype)}};(global.CanopyXParts||=[]).push(["editing",part])})(typeof window!=="undefined"?window:globalThis);
@@ -0,0 +1,126 @@
1
+ (function(global){"use strict";const installed=new WeakSet();const part={version:"31162b1ffa622bf8",install(context){if(installed.has(context.GRID_ENHANCE_IMPL))return;const{DataGrid,GRID_ENHANCE_IMPL,PROTOCOL,createDataGrid,h,setText,gridQueryRows,gridCompare,gridResultShape,isIdentifier,GRID_REPORTING,loadPart,GRID_PAGER}=context;const ENHANCE_KINDS={text:"text",number:"number",datetime:"datetime",date:"datetime"};const ENHANCE_DATE=/^(\d{4}-\d\d-\d\d)(?:[T ](\d\d:\d\d(?::\d\d(?:\.\d{1,9})?)?)(Z|[+-]\d\d:?\d\d)?)?$/;
2
+ const ENHANCE_NUMBER=/^([^\p{L}\d\s+\-\u2212.,()]{0,3})\s?([+\-\u2212]?)(\(?)(\d[\d\s'\u2019.,\u00a0\u202f]*?)(\)?)(?:\s?[^\p{L}\d\s()]{1,3}|\s\p{L}{1,3})?$/u;
3
+ let enhanceDecimal=null;function enhanceDecimalMark(){if(enhanceDecimal===null){try{enhanceDecimal=new Intl.NumberFormat(document.documentElement.lang||undefined).formatToParts(1.5).find(part=>part.type==="decimal")?.value||"."}
4
+ catch(_){enhanceDecimal="."}}
5
+ return enhanceDecimal}
6
+ function enhanceNumber(text){const match=typeof text==="string"&&text.length<=64?ENHANCE_NUMBER.exec(text.trim()):null;if(!match)return null;if(!match[3]!==!match[5]||match[3]&&match[2])return null;const digits=match[4].replace(/[\s'\u2019\u00a0\u202f]/g, ""), dot = digits.lastIndexOf("."), comma = digits.lastIndexOf(",");
7
+ let decimal="";if(dot>=0&&comma>=0)decimal=dot>comma?".":",";else if(dot>=0||comma>=0){const mark=dot>=0?".":",",pieces=digits.split(mark);if(pieces.length===2)decimal=mark===enhanceDecimalMark()||pieces[1].length!==3?mark:""}
8
+ const at=decimal?digits.lastIndexOf(decimal):-1,whole=at<0?digits:digits.slice(0,at),fraction=at<0?"":digits.slice(at+1);const groups=whole.split(/[.,]/);
9
+ if(!/^\d*$/.test(fraction) || groups.some(group => !/^\d+$/.test(group)) || groups.slice(1).some(group => group.length !== 3) ||
10
+ groups.length>1&&groups[0].length>3)return null;const value=Number((match[2]&&match[2]!=="+"||match[3]?"-":"")+ groups.join("")+(fraction?"."+fraction:""));return Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER?value:null}
11
+ function enhanceDate(value){if(value instanceof Date)return Number.isFinite(value.getTime())?value.toISOString():null;const match=typeof value==="string"?ENHANCE_DATE.exec(value.trim()):null;if(!match)return null;const[year,month,day]=match[1].split("-").map(Number),leap=year%4===0&&(year%100!==0||year%400===0);if(month<1||month>12||day<1||day>[31,leap?29:28,31,30,31,30,31,31,30,31,30,31][month-1])return null;const zone=!match[3]||match[3]==="Z"?"Z":match[3].length===5?match[3].slice(0,3)+ ":"+match[3].slice(3):match[3];const iso=match[1]+ "T" +(match[2]||"00:00")+ zone;return Number.isFinite(Date.parse(iso))?iso:null}
12
+ function enhanceValue(kind,value){if(value===undefined||value===null||value==="")return null;if(kind==="number"){const number=typeof value==="number"?value:typeof value==="string"?enhanceNumber(value):null;return number!==null&&Number.isFinite(number)&&Math.abs(number)<=Number.MAX_SAFE_INTEGER?number:null}
13
+ if(kind==="datetime")return typeof value==="number"&&Number.isFinite(value)?enhanceDate(new Date(value)):enhanceDate(value);return typeof value==="string"||typeof value==="boolean"?value:value instanceof Date?value.toISOString():
14
+ typeof value==="object"?JSON.stringify(value):String(value)}
15
+ function enhanceInfer(values,numericText,parsed){let kind=null;for(let index=0;index<values.length;index++){const value=values[index];if(value===null||value===undefined||value===""){parsed[index]=null;continue}
16
+ if(kind!=="datetime"){const number=typeof value==="number"?Number.isFinite(value)&&Math.abs(value)<=Number.MAX_SAFE_INTEGER?value:null:numericText?enhanceNumber(value):null;if(number!==null){kind="number";parsed[index]=number;continue}}
17
+ const date=kind==="number"?null:enhanceDate(value);if(date===null)return "text";kind="datetime";parsed[index]=date}
18
+ return kind||"text"}
19
+ function enhanceSortKey(value){if(typeof value==="number")return Number.isFinite(value)?value:null;if(value===null||value===undefined)return null;const text=String(value),number=/^[+-]?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i.test(text.trim()) ? Number(text) : NaN;
20
+ return Number.isFinite(number)?number:text.slice(0,4096)}
21
+ function enhanceCell(kind,raw,text,sort,search,defaultText,tone,read){const value=read?raw:enhanceValue(kind,raw);if(value===null&&(text===null||text==="")){if(defaultText!==undefined)text=defaultText;else if(sort==null&&search==null&&!tone)return null}
22
+ if(kind==="text"&&typeof value==="string"&&(text===null||text===value)&&sort===undefined&&search===undefined&&!tone)return value;const cell={value};if(text!==null&&text!==undefined&&(typeof value!=="string"||text!==value))cell.text=String(text);if(sort!==undefined&&sort!==null)cell.sort=enhanceSortKey(sort);if(search!==undefined&&search!==null)cell.searchText=String(search);else if(cell.text!==undefined&&kind!=="text")cell.searchText=cell.text;if(tone)cell.tone=tone;return cell}
23
+ function enhancePath(record,path){if(record===null||typeof record!=="object"||path===null||path===undefined)return undefined;if(typeof path==="number"||Object.hasOwn(record,path))return record[path];let value=record;for(const key of String(path).split(".")){value=value!==null&&typeof value==="object"?value[key]:undefined;if(value===undefined)break}
24
+ return value}
25
+ function enhanceColumnId(spec,index,used){let id=spec.id!==undefined?spec.id:typeof spec.data==="string"&&spec.data?spec.data:"c"+index;if(spec.id!==undefined&&(!isIdentifier(id)||used.has(id)))throw new Error("CanopyX: column ids must be unique identifiers.");for(let suffix=2;used.has(id);suffix++)id=(spec.data||"c"+index)+ "-"+suffix;used.add(id);return id}
26
+ const ENHANCE_TABLE_OPTIONS=["label","noun","paging","pageLength","pageLengths","lengthMenu","info","pageNumbers","pageInput","search","searchText","searchMode","order","sortable","selection","stateSave","markup","footer","height","theme","density","zebra"];const ENHANCE_COLUMN_OPTIONS=["id","title","type","sortable","searchable","visible","hidden","priority","width","minWidth","maxWidth","flex","pinned","wrap","alwaysVisible","tooltip","autoSize","defaultContent","section","rowHeader"];const ENHANCE_TEXT_OPTIONS=["label","noun","searchText","height","id","title","tooltip","defaultContent","section"];function enhanceAttributes(element,allowed){const options={};if(!element||!element.hasAttributes())return options;for(const attribute of element.attributes){if(!attribute.name.startsWith("data-"))continue;const name=attribute.name.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
27
+ if(!allowed.includes(name))continue;let value=attribute.value;if(!ENHANCE_TEXT_OPTIONS.includes(name))try{value=JSON.parse(value)}catch(_){value=attribute.value}
28
+ options[name]=value}
29
+ return options}
30
+ function enhanceText(node){const text=node.textContent;return /\s\s|[\t\n\r\f]/.test(text) ? text.replace(/\s+/g, " ").trim() : text.trim();
31
+ }
32
+ function enhanceLayout(rows){const slots=rows.map(()=>[]);rows.forEach((row,r)=>{let c=0;for(const cell of row.cells){while(slots[r][c])c++;const across=Math.min(Math.max(cell.colSpan||1,1),1000),down=cell.rowSpan===0?rows.length-r:Math.min(Math.max(cell.rowSpan||1,1),rows.length-r);for(let y=r;y<r+down;y++)for(let x=c;x<c+across;x++)slots[y][x]=cell;c +=across}});return slots}
33
+ function enhanceReadCell(cell){const text=enhanceText(cell),markup=cell.firstElementChild?cell:null;if(!markup&&cell.tagName==="TD"&&!cell.hasAttributes())return text;const attribute=name=>cell.getAttribute(name);return{text,order:attribute("data-order")??attribute("data-sort"),search:attribute("data-search")??attribute("data-filter"),tone:attribute("data-tone"),markup,header:cell.tagName==="TH"}}
34
+ function enhanceReadRows(rows,skip){const read=[],carry=[];for(const[index,row]of rows.entries()){if(row===skip)continue;const values=[];let c=0;const carried=()=>{while(carry[c]&&carry[c].left>0){values[c]=carry[c].value;carry[c].left--;c++}};for(const cell of row.cells){carried();const value=enhanceReadCell(cell),across=Math.min(Math.max(cell.colSpan||1,1),1000),down=cell.rowSpan===0?rows.length-index:Math.max(cell.rowSpan||1,1);for(let x=0;x<across;x++){values[c+x]=x?"":value;carry[c+x]=down>1?{value:x?"":value,left:down-1}:null}
35
+ c +=across}
36
+ for(let x=c;x<carry.length;x++)if(carry[x]&&carry[x].left>0){values[x]=carry[x].value;carry[x].left--}
37
+ read.push({element:row,values})}
38
+ return read}
39
+ function enhanceReadTable(table){const head=table.tHead?[...table.tHead.rows]:[],bodies=[...table.tBodies];const first=bodies[0]&&bodies[0].rows[0],fromBody=!head.length&&first&&first.cells.length&&[...first.cells].every(cell=>cell.tagName==="TH");if(fromBody)head.push(first);const slots=enhanceLayout(head),bottom=slots[slots.length-1]||[],rows=bodies.flatMap(body=>enhanceReadRows([...body.rows],fromBody?first:null));const width=rows.reduce((widest,row)=>Math.max(widest,row.values.length),Math.max(bottom.length,1)),columns=[];let identity=false;for(let index=0;index<width;index++){const cell=bottom[index]||null,groups=[];for(let r=0;r<slots.length-1;r++)if(slots[r][index]&&slots[r][index]!==cell&&!groups.some(group=>group.cell===slots[r][index]))groups.push({cell:slots[r][index],row:r});const rowHeader=!identity&&rows.length>0&&rows.every(row=>row.values[index]&&row.values[index].header);identity||=rowHeader;columns.push({title:cell?enhanceText(cell):"",cell,groups:groups.map(group=>({cell:group.cell,row:group.row,title:enhanceText(group.cell)})),options:enhanceAttributes(cell,ENHANCE_COLUMN_OPTIONS),rowHeader})}
40
+ const footer=table.tFoot?enhanceReadRows([...table.tFoot.rows]).map(row=>row.values.map(value=>typeof value==="string"?value:value.text)):[];const caption=table.caption?enhanceText(table.caption):"";return{columns,rows,footer,caption,options:enhanceAttributes(table,ENHANCE_TABLE_OPTIONS)}}
41
+ function enhanceHeaderGroups(read,ids){const levels=[];read.columns.forEach((column,index)=>column.groups.forEach((group,level)=>{const row=levels[level]||=[],last=row[row.length-1];if(last&&last.cell===group.cell)last.columns.push(ids[index]);else row.push({cell:group.cell,title:group.title,columns:[ids[index]]})}));return levels.map(row=>row.map(group=>Object.freeze({title:group.title,columns:Object.freeze(group.columns)})))}
42
+ function enhanceColumnGroups(read,ids){const cells=new Map();read.columns.forEach((column,index)=>column.groups.forEach(group=>{let node=cells.get(group.cell);if(!node)cells.set(group.cell,node={row:group.row,title:group.title.trim().slice(0,256),all:[],groups:[]});node.all.push(ids[index])}));const rows=[...new Set([...cells.values()].map(node=>node.row))].sort((a,b)=>a-b).slice(-3);const kept=[...cells.values()].filter(node=>rows.includes(node.row)).sort((a,b)=>a.row-b.row),top=[];kept.forEach((node,index)=>{node.id="group-"+rows.indexOf(node.row)+ "-"+index;const parents=kept.filter(parent=>parent.row<node.row&&node.all.every(id=>parent.all.includes(id)));(parents.length?parents[parents.length-1].groups:top).push(node)});const writeAll=list=>list.flatMap(node=>node.title?[write(node)]:writeAll(node.groups));const write=node=>{const nested=new Set(node.groups.flatMap(group=>group.all)),columns=node.all.filter(id=>!nested.has(id)),groups=writeAll(node.groups);return{id:node.id,title:node.title,...(columns.length?{columns}:{}),...(groups.length?{groups}:{})}};return writeAll(top).filter(group=>group.columns||group.groups)}
43
+ function enhanceTableColumns(read,specs){return read.columns.map((column,index)=>
44
+ Object.assign({data:index,title:column.title||String(index+1),rowHeader:column.rowHeader||undefined},column.options,specs&&specs[index]||{}))}
45
+ const ENHANCE_OWN_COLUMN_FIELDS=["data","type","render","searchable","visible","defaultContent","id"];const ENHANCE_TONE=/^[a-z][a-z0-9-]{0,31}$/;
46
+ function enhanceColumns(specs,sortable){if(!Array.isArray(specs)||!specs.length)throw new Error("CanopyX: a grid needs columns, or records to read them from.");const used=new Set();return specs.map((spec,index)=>{if(spec===null||typeof spec!=="object"||Array.isArray(spec))throw new Error("CanopyX: each column option is an object.");const type=spec.type===undefined?spec.kind:spec.type;if(type!==undefined&&type!=="auto"&&!ENHANCE_KINDS[type])throw new Error("CanopyX: column type is text, number, datetime, date or auto.");if(spec.render!==undefined&&typeof spec.render!=="function"&&(spec.render===null||typeof spec.render!=="object"))
47
+ throw new Error("CanopyX: a column render is a function or an object of functions and field paths by type.");const grid={};for(const key of Object.keys(spec))if(!ENHANCE_OWN_COLUMN_FIELDS.includes(key))grid[key]=spec[key];grid.id=enhanceColumnId(spec,index,used);grid.title=String(spec.title??(typeof spec.data==="string"?spec.data:index+1));grid.sortable=spec.sortable===undefined?sortable:!!spec.sortable;if(spec.visible===false&&spec.hidden===undefined)grid.hidden=true;return{grid,index,data:spec.data===undefined?index:spec.data,kind:type===undefined||type==="auto"?null:ENHANCE_KINDS[type],render:spec.render,searchable:spec.searchable!==false,defaultContent:spec.defaultContent===undefined?undefined:String(spec.defaultContent)}})}
48
+ function enhanceRecordSpecs(records){const first=records&&records.find(record=>record!==null&&typeof record==="object");if(!first)throw new Error("CanopyX: a grid needs columns, or records to read them from.");return Array.isArray(first)?first.map((_,index)=>({data:index})):Object.keys(first).map(key=>({data:key,title:key}))}
49
+ function enhanceRender(column,raw,type,record,index){const render=column.render,meta={row:index,column:column.index,id:column.grid.id};const spec=typeof render==="function"?render:Object.hasOwn(render,type)?render[type]:render._;return typeof spec==="function"?spec(raw,type,record,meta):spec===undefined?raw:enhancePath(record,spec)}
50
+ function enhanceTyped(column,record,index){const raw=enhancePath(record,column.data);return column.render?enhanceRender(column,raw,"type",record,index):raw}
51
+ function enhanceDisplay(value){return value===null||value===undefined?null:typeof Node!=="undefined"&&value instanceof Node?enhanceText(value):String(value)}
52
+ function enhanceRecordCell(column,record,index,position=index){const raw=enhancePath(record,column.data);if(!column.render)return enhanceCell(column.kind,raw,null,undefined,undefined,column.defaultContent);const typed=column.typed?column.typed[position]:enhanceRender(column,raw,"type",record,index),display=enhanceDisplay(enhanceRender(column,raw,"display",record,index));const sort=enhanceRender(column,raw,"sort",record,index),filter=enhanceRender(column,raw,"filter",record,index);return enhanceCell(column.kind,typed,display,sort===typed?undefined:sort,filter===undefined||filter===null?undefined:String(filter),column.defaultContent)}
53
+ function enhanceRowId(value,index,ids){if(value!==undefined&&value!==null&&value!==""){const id=String(value);if(!isIdentifier(id)||ids.has(id))throw new Error("CanopyX: row ids must be unique identifiers; "+id+" is not.");ids.add(id);return id}
54
+ let id="r" +(index+1);while(ids.has(id))id +="~";ids.add(id);return id}
55
+ function enhanceKinds(columns,count,valueAt,numericText){columns.forEach(column=>{column.parsed=null;column.typed=null;if(column.kind)return;const values=new Array(count),parsed=new Array(count);for(let index=0;index<count;index++)values[index]=valueAt(column,index);if(column.render)column.typed=values;column.kind=enhanceInfer(values,numericText,parsed);if(column.kind!=="text")column.parsed=parsed});columns.forEach(column=>{column.grid.kind=column.kind})}
56
+ function enhanceRecordRows(records,columns,rowId,start=0,ids=new Set()){if(!Array.isArray(records))throw new Error("CanopyX: data is an array of records.");enhanceKinds(columns,records.length,(column,index)=>enhanceTyped(column,records[index],start+index),false);const rows=new Array(records.length),byId=new Map();records.forEach((record,index)=>{if(record===null||typeof record!=="object")throw new Error("CanopyX: each record is an array or an object.");const cells={};for(const column of columns){const cell=enhanceRecordCell(column,record,start+index,index);if(cell!==null)cells[column.grid.id]=cell}
57
+ const id=enhanceRowId(rowId===undefined?undefined:typeof rowId==="function"?rowId(record,start+index):enhancePath(record,rowId),start+index,ids);rows[index]={id,cells};byId.set(id,record)});columns.forEach(column=>{column.parsed=null;column.typed=null});return{rows,records:byId}}
58
+ function enhanceTableRows(read,columns,keepMarkup){const textOf=value=>value===undefined?"":typeof value==="string"?value:value.text;const texts=row=>row.texts||=row.values.map(textOf);enhanceKinds(columns,read.rows.length,(column,index)=>column.render?enhanceTyped(column,texts(read.rows[index]),index):textOf(read.rows[index].values[column.data]),true);const ids=new Set(),rows=new Array(read.rows.length),markup=new Map();read.rows.forEach((row,index)=>{const element=row.element,cells={};for(const column of columns){const value=row.values[column.data],id=column.grid.id;let cell;if(column.render)cell=enhanceRecordCell(column,texts(row),index);else if(value===undefined)continue;else if(typeof value==="string")cell=column.parsed?enhanceCell(column.kind,column.parsed[index],value,undefined,undefined,column.defaultContent,undefined,true):
59
+ enhanceCell(column.kind,value,value,undefined,undefined,column.defaultContent);else{const tone=value.tone&&ENHANCE_TONE.test(value.tone)&&!value.tone.endsWith("-bg")?value.tone:undefined;cell=column.parsed?enhanceCell(column.kind,column.parsed[index],value.text,value.order,value.search,column.defaultContent,tone,true):
60
+ enhanceCell(column.kind,value.text,value.text,value.order,value.search,column.defaultContent,tone);if(keepMarkup&&value.markup)(markup.get(index)||markup.set(index,new Map()).get(index)).set(id,value.markup)}
61
+ if(cell!==null)cells[id]=cell}
62
+ const attribute=name=>element.hasAttributes()?element.getAttribute(name):null;const record={id:enhanceRowId(attribute("id")||attribute("data-id"),index,ids),cells},tone=attribute("data-tone"),className=element.className;if(tone&&ENHANCE_TONE.test(tone)&&!tone.endsWith("-bg"))record.tone=tone;if(className&&className.trim()&&className.length<=1024&&!/[\u0000-\u001f\u007f-\u009f]/.test(className)) record.className = className;
63
+ rows[index]=record});columns.forEach(column=>{column.parsed=null;column.typed=null});const byId=new Map(),positions=new Map(rows.map((row,index)=>[row.id,index]));markup.forEach((cells,index)=>byId.set(rows[index].id,cells));return{rows,markup:byId,records:new Map(),tableRecord:id=>positions.has(id)?texts(read.rows[positions.get(id)]).slice():undefined}}
64
+ function enhanceReadOnce(store,source,columns,rowId){store.loaded=false;store.load=signal=>{if(store.loading&&!store.loading.signal.aborted)return store.loading.promise;const entry={signal,promise:null},retire=()=>{if(store.loading===entry)store.loading=null};const active=()=>{if(signal.aborted||store.loading!==entry||store.grid?.destroyed)throw new DOMException("Grid cancelled.","AbortError")};store.loading=entry;signal.addEventListener("abort",retire,{once:true});entry.promise=Promise.resolve().then(()=>{active();return enhanceRequest(source,source.url,enhanceExtra(source),signal)}).then(json=>{active();const loaded=enhanceRecordRows(enhanceRecordsOf(json,source.dataSrc),columns,rowId);active();Object.assign(store,loaded,{loaded:true});store.version++;if(store.grid)store.grid.view.rows=store.rows}).finally(()=>{signal.removeEventListener("abort",retire);retire()});return entry.promise}}
65
+ function enhanceRowSource(store,view,compare,searchColumns){const columns=new Map(view.columns.map(column=>[column.id,column]));let shape=null,version=-1,ordered=null;const order=query=>{const key=gridResultShape(query);if(key===shape&&version===store.version)return ordered;const rows=gridQueryRows({...view,searchColumns,rows:store.rows},query,columns,null);if(query.sort.length)rows.sort((a,b)=>{for(const sort of query.sort){const result=compare(a,b,columns.get(sort.column),sort.direction);if(result)return result}
66
+ return 0});shape=key;version=store.version;ordered=rows;return rows};const page=(query,signal)=>{if(signal.aborted)throw new DOMException("Grid cancelled.","AbortError");const rows=order(query);return{protocol:PROTOCOL,type:"gridPage",view:view.id,revision:"v"+store.version,offset:query.offset,total:store.rows.length,matched:rows.length,items:rows.slice(query.offset,query.offset+query.limit)}};return{order,query(query,options){const loading=store.load&&!store.loaded?store.load(options.signal):null;return loading?loading.then(()=>page(query,options.signal)):page(query,options.signal)}}}
67
+ function enhanceUrlSource(value){const source=typeof value==="string"?{url:value}:value;if(!source||typeof source.url!=="string"||!source.url||
68
+ ["method","credentials"].some(name=>source[name]!==undefined&&typeof source[name]!=="string")||
69
+ source.headers!==undefined&&(source.headers===null||typeof source.headers!=="object")||
70
+ source.fetch!==undefined&&typeof source.fetch!=="function"||
71
+ source.dataSrc!==undefined&&typeof source.dataSrc!=="string"&&typeof source.dataSrc!=="function")
72
+ throw new Error("CanopyX: a URL source is a URL or { url, method?, headers?, data?, dataSrc?, credentials?, fetch?, serverSide?, facetUrl? }.");return source}
73
+ function enhanceRequest(source,url,body,signal,post){const address=new URL(url,document.baseURI),method=(source.method||(post?"POST":"GET")).toUpperCase();const init={method,credentials:source.credentials||"same-origin",headers:{Accept:"application/json",...source.headers},signal};if(body!==undefined&&method==="GET"){for(const[name,value]of Object.entries(body))if(value!==undefined)address.searchParams.set(name,typeof value==="object"?JSON.stringify(value):String(value))}else if(body!==undefined){init.headers["Content-Type"]="application/json";init.body=JSON.stringify(body)}
74
+ return Promise.resolve((source.fetch||global.fetch)(address.href,init)).then(response=>{if(!response.ok)throw new Error("CanopyX: "+address.pathname+" answered "+response.status+".");return response.json()})}
75
+ function enhanceExtra(source,query){const data=typeof source.data==="function"?source.data(query):source.data;if(data!==undefined&&data!==null&&(typeof data!=="object"||Array.isArray(data)))throw new Error("CanopyX: URL source data is an object.");return data||undefined}
76
+ function enhanceRecordsOf(json,dataSrc){const records=typeof dataSrc==="function"?dataSrc(json):dataSrc===""?json:dataSrc!==undefined?enhancePath(json,dataSrc):
77
+ Array.isArray(json)?json:json&&(Array.isArray(json.data)?json.data:json.items);if(!Array.isArray(records))throw new Error("CanopyX: the URL source answered without an array of records.");return records}
78
+ function enhanceServerSource(source,view,columns,rowId,store){const server={query(query,{signal}){const extra=enhanceExtra(source,query);return enhanceRequest(source,source.url,extra?{...query,...extra}:query,signal,true).then(answer=>{if(answer&&answer.protocol===PROTOCOL&&answer.type==="gridPage"){store.rows=answer.items||[];store.records=new Map(store.rows.map(record=>[record.id,record]));return answer}
79
+ const records=enhanceRecordsOf(answer,source.dataSrc);const built=enhanceRecordRows(records,columns,rowId,query.offset);const total=Number(answer.total??records.length),matched=Number(answer.matched??total);store.rows=built.rows;store.records=built.records;return{protocol:PROTOCOL,type:"gridPage",view:view.id,revision:typeof answer.revision==="string"&&answer.revision?answer.revision:query.revision||"server",offset:query.offset,total,matched,items:built.rows}})}};if(source.facetUrl)server.facet=(request,{signal})=>{const extra=enhanceExtra(source,request.query);return enhanceRequest(source,source.facetUrl,extra?{...request,...extra}:request,signal,true)};return server}
80
+ const ENHANCE_STATE_WRITES=new Map();function enhanceWriteState(settings,envelope){let entry=ENHANCE_STATE_WRITES.get(settings.key);if(entry){entry.next={settings,envelope};return}
81
+ entry={next:{settings,envelope},done:null};ENHANCE_STATE_WRITES.set(settings.key,entry);entry.done=Promise.resolve().then(async()=>{while(entry.next){const{settings,envelope}=entry.next;entry.next=null;try{await settings.save({key:settings.key,view:settings.view,state:envelope})}
82
+ catch(_){/* A failed host write does not discard a later queued state. */ }
83
+ }
84
+ ENHANCE_STATE_WRITES.delete(settings.key)})}
85
+ function enhanceStateSettings(value,view){if(value===undefined||value===null||value===false)return null;const options=value===true?{}:value;if(typeof options!=="object"||Array.isArray(options)||options.storage!==undefined&&!["session","local"].includes(options.storage)||
86
+ options.duration!==undefined&&!(typeof options.duration==="number"&&options.duration>=0)||
87
+ options.url!==undefined&&![false,"query","hash"].includes(options.url)||
88
+ ["load","save"].some(name=>options[name]!==undefined&&typeof options[name]!=="function")||
89
+ ["key","parameter"].some(name=>options[name]!==undefined&&(typeof options[name]!=="string"||!options[name])))
90
+ throw new Error("CanopyX: stateSave is true or { storage?, duration?, key?, url?, parameter?, load?, save? }.");return{storage:options.storage||"session",duration:options.duration===undefined?7200:options.duration,url:options.url||false,key:"cx:state:" +(options.key||global.location.pathname)+ ":"+view.id,parameter:options.parameter||view.id,load:options.load,save:options.save,timeZone:view.timeZone==="local"?"local":"utc",view:view.id,pageLength:view.pageLength}}
91
+ function enhanceStorage(settings){try{return global[settings.storage+"Storage"]}catch(_){return null}}
92
+ function enhanceAddress(settings){const url=new URL(global.location.href),fragment=url.hash.slice(1);if(settings.url==="hash"&&fragment&&!/^[^=&#\/]+=[^&]*(?:&[^=&#\/]+=[^&]*)*$/.test(fragment)) return null;
93
+ return{url,params:settings.url==="hash"?new URLSearchParams(fragment):url.searchParams}}
94
+ async function enhanceLoadState(settings){let envelope=null;try{const pending=ENHANCE_STATE_WRITES.get(settings.key);if(pending)await pending.done;envelope=settings.load?await settings.load({key:settings.key,view:settings.view}):JSON.parse(enhanceStorage(settings)?.getItem(settings.key)||"null")}catch(_){envelope=null}
95
+ if(!envelope||typeof envelope!=="object"||settings.duration&&!(Number.isFinite(envelope.time)&&Date.now()- envelope.time<=settings.duration*1000))envelope=null;let shared=null;if(settings.url){try{shared=JSON.parse(enhanceAddress(settings)?.params.get(settings.parameter)||"null")}catch(_){shared=null}}
96
+ if(shared&&typeof shared==="object"&&!Array.isArray(shared)){const base=envelope&&envelope.current&&typeof envelope.current==="object"?envelope.current:
97
+ {version:1,view:settings.view,columns:[],timeZone:settings.timeZone,selection:[],scrollTop:0,query:{}};const sort=Array.isArray(shared.s)?shared.s.filter(Array.isArray).map(([column,direction])=>({column,direction})):[];const query={...base.query,sort,search:typeof shared.q==="string"?shared.q:"",filters:Array.isArray(shared.f)?shared.f:[]};if(shared.g&&typeof shared.g==="object")query.filterGroup=shared.g;else delete query.filterGroup;const paging={pageLength:Number.isInteger(shared.l)?shared.l:base.paging?.pageLength||settings.pageLength||50,offset:Number.isInteger(shared.o)?shared.o:0};if(shared.a===true)paging.allRows=true;envelope={...envelope,current:{...base,query,paging,scrollTop:0}}}
98
+ return envelope}
99
+ function enhanceSaveState(settings,envelope){envelope.time=Date.now();try{if(settings.save)enhanceWriteState(settings,envelope);else enhanceStorage(settings)?.setItem(settings.key,JSON.stringify(envelope))}catch(_){/* A full or blocked store leaves the grid usable. */ }
100
+ if(!settings.url)return;const query=envelope.current.query,paging=envelope.current.paging||{},shared={};if(query.sort.length)shared.s=query.sort.map(sort=>[sort.column,sort.direction]);if(query.search)shared.q=query.search;if(query.filters.length)shared.f=query.filters;if(query.filterGroup)shared.g=query.filterGroup;if(paging.offset)shared.o=paging.offset;if(paging.pageLength&&paging.pageLength!==settings.pageLength)shared.l=paging.pageLength;if(paging.allRows)shared.a=true;const address=enhanceAddress(settings);if(!address)return;const{url,params}=address,text=Object.keys(shared).length?JSON.stringify(shared):null;if(params.get(settings.parameter)===text)return;if(text===null)params.delete(settings.parameter);else params.set(settings.parameter,text);if(settings.url==="hash")url.hash=params.toString();try{global.history.replaceState(global.history.state,"",url.href)}catch(_){/* Sandboxed documents cannot change their address. */ }
101
+ }
102
+ function enhanceStateStore(grid,settings,envelope){grid.readStoredState=()=>envelope;grid.writeStoredState=function(state){if(this.destroyed)return;enhanceSaveState(settings,{current:state,views:[...this.savedViews].map(([name,value])=>({name,state:value}))})}}
103
+ const ENHANCE_OPTIONS=["id","label","noun","columns","data","url","serverSide","rowId","paging","pageLength","pageLengths","lengthMenu","info","pageNumbers","pageInput","firstLast","search","searchText","searchMode","order","sortable","selection","stateSave","texts","markup","footer","height","theme","density","zebra","view","on"];const ENHANCE_TEXTS={search:"searchPlaceholder",info:"gridPageInfo",infoFiltered:"gridPageInfoFiltered",lengthMenu:"gridPageLength",first:"gridFirstPage",previous:"gridPreviousPage",next:"gridNextPage",last:"gridLastPage",page:"gridPageButton",all:"gridAll",empty:"gridEmpty",emptyReason:"gridEmptyReason",noMatches:"gridNoMatches",loading:"loading",loadFailed:"gridLoadFailed",retry:"gridRetry"};const ENHANCE_DERIVED=["init","draw","order","search","page","length"];const ENHANCE_SETUP=new WeakMap(),ENHANCE_CONTROLLERS=new WeakMap(),ENHANCE_PENDING=new WeakMap();let enhanceSeq=0;const ENHANCE_PREPARE=DataGrid.prototype.prepareState;DataGrid.prototype.prepareState=function(){const setup=ENHANCE_SETUP.get(this.opts);if(setup)setup(this);return ENHANCE_PREPARE.call(this)};function enhanceOrder(order,columns){if(order===undefined||order===null)return[];if(!Array.isArray(order))throw new Error("CanopyX: order is a list of [column, direction] pairs.");return order.map(entry=>{const[reference,direction="asc"]=Array.isArray(entry)?entry:[entry&&entry.column,entry&&entry.direction];const column=typeof reference==="number"?columns[reference]:columns.find(item=>item.grid.id===reference);if(!column||!column.grid.sortable||!["asc","desc"].includes(direction))throw new Error("CanopyX: order names an unknown or unsortable column.");return{column:column.grid.id,direction}})}
104
+ function enhancePager(settings){const pager={lengths:settings.pageLengths===undefined?[10,25,50,100,"all"]:settings.pageLengths};const numbers=settings.pageNumbers===undefined||settings.pageNumbers===true?7:settings.pageNumbers===false?0:settings.pageNumbers;if(numbers)pager.numbers=numbers;for(const name of["lengthMenu","pageInput","firstLast"])if(settings[name]!==undefined)pager[name]=settings[name];pager.info=settings.info===false?"none":settings.info===undefined||settings.info===true?"sentence":settings.info;return pager}
105
+ function enhanceLabels(settings){const labels={};if(settings.texts&&typeof settings.texts==="object")
106
+ for(const[name,text]of Object.entries(settings.texts))if(Object.hasOwn(ENHANCE_TEXTS,name)&&typeof text==="string")labels[ENHANCE_TEXTS[name]]=text;return Object.assign(labels,settings.labels)}
107
+ const ENHANCE_STYLES={caption:"flex:none;padding:var(--cx-size-10px) var(--cx-size-12px) var(--cx-size-2px);color:var(--cx-text);font-size:var(--cx-size-14px);font-weight:600;line-height:1.35",foot:"flex:none;display:flex;flex-direction:column;gap:var(--cx-size-4px);padding:var(--cx-size-8px) var(--cx-size-12px);border-top:var(--cx-size-1px) solid var(--cx-line);color:var(--cx-muted);font-size:var(--cx-size-13px)",row:"display:flex;flex-wrap:wrap;gap:var(--cx-size-4px) var(--cx-size-16px);margin:0",value:"color:var(--cx-text);font-weight:600;font-variant-numeric:tabular-nums"};function enhanceStyled(tag,className,style){const element=h(tag,className);element.style.cssText=ENHANCE_STYLES[style];return element}
108
+ function enhanceFooter(rows,columns){const foot=enhanceStyled("div","cx-enhanced-foot","foot");for(const values of rows){const line=enhanceStyled("p","cx-enhanced-foot-row","row");values.forEach((text,index)=>{if(!text)return;const item=h("span"),title=index&&columns[index]?columns[index].grid.title:"",value=enhanceStyled("b","","value");if(title){const name=h("span");setText(name,title+" ");item.appendChild(name)}
109
+ setText(value,text);item.appendChild(value);line.appendChild(item)});if(line.firstChild)foot.appendChild(line)}
110
+ return foot.firstChild?foot:null}
111
+ GRID_ENHANCE_IMPL.create=function(target,options,tableOnly){const element=typeof target==="string"?document.querySelector(target):target;if(!element||element.nodeType!==1)return Promise.reject(new Error("CanopyX: no element to build a grid in: "+target));if(ENHANCE_CONTROLLERS.has(element))return Promise.resolve(ENHANCE_CONTROLLERS.get(element));if(!ENHANCE_PENDING.has(element))ENHANCE_PENDING.set(element,enhanceCreate(element,options,tableOnly).finally(()=>ENHANCE_PENDING.delete(element)));return ENHANCE_PENDING.get(element)};async function enhanceCreate(element,options,tableOnly){const isTable=element.tagName==="TABLE";if(tableOnly&&!isTable)throw new Error("CanopyX: enhance needs a <table>; CanopyX.grid builds a grid in any element.");if(!isTable&&element.hasAttribute("data-canopyx-mounted"))throw new Error("CanopyX: this element already holds a grid.");if(options!==undefined&&(options===null||typeof options!=="object"||Array.isArray(options)))throw new Error("CanopyX: grid options are an object.");const read=isTable?enhanceReadTable(element):null,settings={...read&&read.options,...options};const urlSource=settings.url===undefined?null:enhanceUrlSource(settings.url),server=!!(urlSource&&(settings.serverSide||urlSource.serverSide));if(settings.data!==undefined&&!Array.isArray(settings.data))throw new Error("CanopyX: data is an array of records.");let records=settings.data;const specs0=read?enhanceTableColumns(read,settings.columns):settings.columns;const typed=Array.isArray(specs0)&&specs0.every(spec=>spec&&spec.type!==undefined&&spec.type!=="auto");if(urlSource&&!server&&records===undefined&&!typed){const timeout=settings.requestTimeoutMs===undefined?30000:settings.requestTimeoutMs;if(!Number.isInteger(timeout)||timeout<1||timeout>2147483647)throw new Error("CanopyX: invalid grid cache or request timeout.");const preparation={lifetime:new AbortController(),destroyed:false,requestTimeoutMs:timeout};records=enhanceRecordsOf(await DataGrid.prototype.runGridHook.call(preparation,signal=>enhanceRequest(urlSource,urlSource.url,enhanceExtra(urlSource),signal)),urlSource.dataSrc)}
112
+ const columns=enhanceColumns(specs0||enhanceRecordSpecs(records),settings.sortable!==false);if(server||urlSource&&records===undefined)columns.forEach(column=>{column.kind||="text";column.grid.kind=column.kind});const store={rows:[],version:0,loading:null,records:null,markup:null,grid:null};const built=records!==undefined?enhanceRecordRows(records,columns,settings.rowId):read&&!urlSource?enhanceTableRows(read,columns,settings.markup!==false):
113
+ {rows:[],records:new Map()};Object.assign(store,built);const id=settings.id||element.id||"grid-" + ++enhanceSeq;const label=String(settings.label||element.getAttribute("aria-label")||read&&read.caption||element.getAttribute("title")||"Table");const paged=settings.paging!==false,pageLength=settings.pageLength===undefined?25:settings.pageLength;const searchColumns=columns.filter(column=>column.searchable).map(column=>column.grid.id);const view={protocol:PROTOCOL,type:"grid",id,label,columns:columns.map(column=>column.grid),selection:settings.selection||"none",search:settings.search!==false&&searchColumns.length>0,pageSize:Math.min(10000,Math.max(server&&paged?1:100,Number.isInteger(pageLength)?pageLength:100)),source:{kind:server?"host":"inline"}};if(!server)view.rows=[];if(paged)Object.assign(view,{presentation:"paged",pageLength,pager:enhancePager(settings)});if(searchColumns.length&&searchColumns.length<columns.length)view.searchColumns=searchColumns;for(const name of["noun","density","zebra"])if(settings[name]!==undefined)view[name]=settings[name];view.query={sort:enhanceOrder(settings.order,columns),search:settings.searchText===undefined?"":String(settings.searchText)};if(settings.searchMode!==undefined)view.query.searchMode=settings.searchMode;const groups=read?enhanceColumnGroups(read,columns.map(column=>column.grid.id)):[];if(groups.length)view.columnGroups=groups;if(settings.view)Object.assign(view,settings.view);const host=isTable?h("div"):element,grid={};for(const[name,value]of Object.entries(settings))if(!ENHANCE_OPTIONS.includes(name)&&name!=="labels")grid[name]=value;const labels=enhanceLabels(settings);if(Object.keys(labels).length)grid.labels=labels;const markup=store.markup&&store.markup.size?Object.fromEntries(columns.filter(column=>[...store.markup.values()].some(cells=>cells.has(column.grid.id)))
114
+ .map(column=>[column.grid.id,(row,cellElement)=>{const cell=store.markup?.get(row.id)?.get(column.grid.id);if(cell){cellElement.append(...Array.from(cell.childNodes,node=>node.cloneNode(true)));return true}}])):null;if(markup||settings.renderers)grid.renderers={...markup,...settings.renderers};const rowSource=server?null:enhanceRowSource(store,view,GRID_REPORTING.compare?.(view)||gridCompare,view.searchColumns||searchColumns);grid.source=server?enhanceServerSource(urlSource,view,columns,settings.rowId,store):rowSource;if(urlSource&&!server&&records===undefined)enhanceReadOnce(store,urlSource,columns,settings.rowId);const state=enhanceStateSettings(settings.stateSave,{...view,timeZone:settings.view&&settings.view.timeZone}),envelope=state?await enhanceLoadState(state):null;ENHANCE_SETUP.set(grid,created=>{store.grid=created;if(!server){created.enhanceStore=store;created.view.rows=store.rows}
115
+ if(state)enhanceStateStore(created,state,envelope)});const fit=length=>{const height="calc("+Math.min(Math.max(length,1),100)+ " * var(--cx-row-h, 34px) + 320px)";if(host.style.height!==height)host.style.height=height};const height=host.style.height,theme=host.getAttribute("data-cx-theme");const restore=()=>{host.style.height=height;if(theme===null)host.removeAttribute("data-cx-theme");else host.setAttribute("data-cx-theme",theme)};if(settings.height)host.style.height=String(settings.height);else if(paged)fit(Number.isInteger(pageLength)?pageLength:25);if(settings.theme)host.setAttribute("data-cx-theme",String(settings.theme));let caption=null;if(read&&read.caption){caption=enhanceStyled("div","cx-enhanced-caption","caption");setText(caption,read.caption);host.appendChild(caption)}
116
+ if(isTable)element.replaceWith(host);let api,events;try{events=enhanceEvents(host,settings.on);if(!settings.height&&paged)events.on("draw",({paging})=>fit(paging.pageLength));await loadPart(GRID_PAGER);api=createDataGrid(host,Object.assign(grid,{view}))}catch(error){events?.abort();caption?.remove();restore();if(isTable)host.replaceWith(element);throw error}
117
+ const footer=read&&settings.footer!==false&&!(settings.view&&settings.view.reporting&&settings.view.reporting.aggregates)?enhanceFooter(read.footer,columns):null;if(footer)host.querySelector(":scope > .cx-grid").after(footer);const controller=enhanceController({api,element,host,isTable,read,columns,store,rowSource,server,events,settings,caption,footer,restore});host.canopyx=controller;ENHANCE_CONTROLLERS.set(element,controller);if(isTable)ENHANCE_CONTROLLERS.set(host,controller);events.start(controller,api);return controller}
118
+ function enhanceEvents(host,initial){const handlers=new Map(),listening=new Set(),lifetime=new AbortController();let controller=null,api=null,previous=null,initialized=false;const call=(handler,detail,event)=>{try{Promise.resolve(handler.call(controller,detail,controller,event)).catch(error=>global.console?.error(error))}
119
+ catch(error){global.console?.error(error)}};const emit=(name,detail,event)=>{for(const handler of[...(handlers.get(name)||[])])call(handler,detail,event)};const on=(name,handler)=>{if(typeof name!=="string"||!/^[A-Za-z][\w-]*$/.test(name) || typeof handler !== "function") throw new Error("CanopyX: on takes an event name and a function.");
120
+ (handlers.get(name)||handlers.set(name,new Set()).get(name)).add(handler);if(!ENHANCE_DERIVED.includes(name)&&!listening.has(name)){listening.add(name);host.addEventListener("canopyx:"+name,event=>emit(name,event.detail,event),{signal:lifetime.signal})}
121
+ if(name==="init"&&initialized)Promise.resolve().then(()=>{if(!lifetime.signal.aborted&&handlers.get(name)?.has(handler))call(handler,{query:api.getQuery()})})};host.addEventListener("canopyx:query",event=>{if(!api)return;const query=event.detail.query,paging=api.getPaging(),before=previous,detail={query,paging};const same=(left,right)=>JSON.stringify(left===undefined?null:left)===JSON.stringify(right===undefined?null:right);previous=detail;if(before){if(!same(before.query.sort,query.sort))emit("order",detail,event);if(before.query.search!==query.search||!same([before.query.filters,before.query.filterGroup],[query.filters,query.filterGroup]))emit("search",detail,event);if(before.paging.pageLength!==paging.pageLength)emit("length",detail,event);if(before.paging.pageIndex!==paging.pageIndex)emit("page",detail,event)}
122
+ emit("draw",detail,event)},{signal:lifetime.signal});try{if(initial!==undefined){if(initial===null||typeof initial!=="object")throw new Error("CanopyX: on is an object of handlers by event name.");for(const[name,handler]of Object.entries(initial))on(name,handler)}}catch(error){lifetime.abort();throw error}
123
+ return{on,off:(name,handler)=>{handlers.get(name)?.delete(handler)},abort:()=>lifetime.abort(),start(created,grid){controller=created;api=grid;api.ready.then(query=>{if(lifetime.signal.aborted)return;initialized=true;emit("init",{query})},()=>{})}}}
124
+ function enhanceController({api,element,host,isTable,read,columns,store,rowSource,server,events,settings,caption,footer,restore}){let destroyed=false;const recordOf=row=>store.records&&store.records.has(row.id)?store.records.get(row.id):store.tableRecord?store.tableRecord(row.id):undefined;const local=name=>{if(server)throw new Error("CanopyX: "+name+" changes inline data; a server source changes on the server, then reload().")};const refresh=()=>{store.version++;if(store.grid)store.grid.view.rows=store.rows;const query=api.getQuery();if(!query)return api.reload();const request={...query,revision:undefined};return store.grid&&store.grid.pageAll?store.grid.setQuery({...request,offset:0},{pageLength:Math.max(1,store.rows.length),allRows:true}):api.setQuery(request)};const own={table:isTable?element:null,headerGroups:read?enhanceHeaderGroups(read,columns.map(column=>column.grid.id)):[],on(name,handler){events.on(name,handler);return controller},off(name,handler){events.off(name,handler);return controller},getRows(scope="all"){if(!["all","filtered","page"].includes(scope))throw new Error("CanopyX: getRows takes all, filtered or page.");const query=api.getQuery();let rows=store.rows;if(!server&&scope!=="all"){rows=query?rowSource.order(query):[];if(scope==="page"){const paging=api.getPaging();rows=rows.slice(Math.max(0,paging.first-1),paging.last)}}
125
+ return rows.map(recordOf)},getRow(id){const row=store.rows.find(item=>item.id===id);return row?recordOf(row):undefined},setData(data){local("setData");const built=enhanceRecordRows(data,columns,settings.rowId);Object.assign(store,{rows:built.rows,records:built.records,markup:null,tableRecord:null,loaded:true,loading:null});return refresh()},addRows(data){local("addRows");if(!Array.isArray(data))throw new Error("CanopyX: data is an array of records.");if(store.load&&!store.loaded)return api.ready.then(()=>controller.addRows(data));const built=enhanceRecordRows(data,columns,settings.rowId,store.rows.length,new Set(store.rows.map(row=>row.id)));store.rows=store.rows.concat(built.rows);built.records.forEach((record,id)=>store.records.set(id,record));return refresh()},removeRows(ids){local("removeRows");if(!Array.isArray(ids))throw new Error("CanopyX: removeRows takes an array of row ids.");if(store.load&&!store.loaded)return api.ready.then(()=>controller.removeRows(ids));const gone=new Set(ids);store.rows=store.rows.filter(row=>!gone.has(row.id));gone.forEach(id=>{store.records.delete(id);store.markup?.delete(id)});return refresh()},destroy(){if(destroyed)return;destroyed=true;events.abort();api.destroy();caption?.remove();footer?.remove();restore();ENHANCE_CONTROLLERS.delete(element);ENHANCE_CONTROLLERS.delete(host);if(isTable)host.replaceWith(element)}};const controller=Object.freeze(Object.create(api,Object.getOwnPropertyDescriptors(own)));return controller}
126
+ installed.add(context.GRID_ENHANCE_IMPL)}};(global.CanopyXParts||=[]).push(["enhance",part])})(typeof window!=="undefined"?window:globalThis);
@@ -0,0 +1,18 @@
1
+ (function(global){"use strict";const installed=new WeakSet();const part={version:"5673e642f32c2f4e",install(context){if(installed.has(context.EXPORT_IMPL))return;const{DataGrid,EXPORT_IMPL,h,setText,gridButton,gridOption,fill,numberFormat,gridOperationActive,formatting,gridQuery,validateGridCursorPage,validateGridPage,GRID_REPORTING,gridFreeze,cellValue,cellText,isCellObject,exportTimeout,exportFormat,EXPORT_FORMATS,gridExportField}=context;function saveExport(blob,name){const url=URL.createObjectURL(blob),link=h("a");link.href=url;link.download=String(name).replace(/[\\/:*?"<>|\x00-\x1f\x7f\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "_").replace(/^[. ]+|[. ]+$/g, "_");
2
+ document.body.appendChild(link);link.click();link.remove();setTimeout(()=>URL.revokeObjectURL(url),30000)}
3
+ Object.assign(EXPORT_IMPL,{async run(grid,id,options,capture,format){if(!format)throw new Error("CanopyX: export format '"+id+"' is not available.");const total=capture.request.recordCount,onProgress=options.onProgress;if(onProgress!==undefined&&typeof onProgress!=="function"||options.download!==undefined&&typeof options.download!=="boolean")
4
+ throw new Error("CanopyX: export progress is a callback and download is a boolean.");const timer=new AbortController(),clock=setTimeout(()=>timer.abort(new DOMException("Export timed out.","TimeoutError")),exportTimeout(grid,options));const signal=AbortSignal.any([grid.lifetime.signal,timer.signal,options.signal].filter(Boolean));const failure=()=>signal.reason?.name==="TimeoutError"?new Error("CanopyX: export timed out."):new DOMException("Export cancelled.","AbortError");const phases=["reading","generating","ready"];let phase=-1,done=0,read=0,stop;const report=(name,completed)=>{const next=phases.indexOf(name),count=total===null?completed:Math.min(completed,total);if(signal.aborted||next<phase||!Number.isSafeInteger(completed)||completed<0||next===phase&&count<=done)return;phase=next;done=Math.max(done,count);onProgress?.(Object.freeze({phase:name,completed:done,total}))};const stopped=new Promise((resolve,reject)=>{stop=()=>reject(failure())});signal.addEventListener("abort",stop,{once:true});try{if(signal.aborted)throw failure();const counted=Object.freeze({request:capture.request,rows:async function*(more){read=0;for await(const row of capture.rows({signal:AbortSignal.any([signal,more?.signal].filter(Boolean))})){yield row;if(++read%100===0)report("reading",read)}
5
+ report("generating",read)}});const base=capture.request.fileName||grid.view.id.replace(/[^a-z0-9_-]/gi, "_");
6
+ report("reading",0);let blob=await Promise.race([Promise.resolve().then(()=>format.write(counted,Object.freeze({signal,fileName:base,format:format.id,progress:completed=>report("generating",completed)}))),stopped]).catch(error=>{throw error instanceof Error||error instanceof DOMException?error:new Error("CanopyX: export format '"+id+"' failed: "+String(error))});if(signal.aborted)throw failure();if(blob instanceof ArrayBuffer||ArrayBuffer.isView(blob))blob=new Blob([blob],{type:format.mime});if(!(blob instanceof Blob))throw new Error("CanopyX: export format '"+id+"' must write a Blob.");const fileName=blob.name||base+"."+format.extension;if(options.download!==false)saveExport(blob,fileName);report("ready",read);return Object.freeze({blob,fileName,format:format.id})}finally{clearTimeout(clock);signal.removeEventListener("abort",stop)}},async*treeRows(grid,request,rows,signal){const fmt=formatting(request.locale,request.timeZone==="local",grid.labels);const columns=request.columns.map(column=>[column.id,grid.columns.findIndex(item=>item.id===column.id)]);for(const{id,level,path,node}of rows){if(grid.destroyed||signal.aborted)throw new DOMException("Grid cancelled.","AbortError");const cells=Object.create(null);for(const[key,index]of columns){const column=grid.columns[index],cell=grid.cellOf(node,column,index),tone=isCellObject(cell)&&cell.tone;cells[key]=Object.freeze({value:cellValue(cell)??null,text:cellText(cell,column,fmt),...(tone?{tone}:{})})}
7
+ yield Object.freeze({id,level,path:Object.freeze(path),cells:Object.freeze(cells)})}}});function gridWriteClipboard(grid,text,signal){return grid.runGridHook(()=>{const clipboard=global.navigator.clipboard;if(typeof clipboard?.writeText!=="function")throw new Error("CanopyX: clipboard unavailable.");return clipboard.writeText(text)},signal)}
8
+ Object.assign(EXPORT_IMPL,{async*exportRows(request,signal){const{scope}=request;const fmt=formatting(request.locale||this.lang,request.timeZone==="local",this.labels);gridOperationActive(this,signal);const remaining=new Set(request.selection),seen=new Set();if(scope==="selected"&&!remaining.size)return;const expected={revision:request.revision,total:request.total,matched:scope==="filtered"?request.matched:this.cursor?undefined:request.total};const tokens=new Set();let cursor,offset=0;while(this.cursor||offset<expected.matched){gridOperationActive(this,signal);const query=gridQuery(this.view,{...request.query,offset:this.cursor?undefined:offset,cursor});const page=(this.cursor?validateGridCursorPage:validateGridPage)(await this.requestPage(query,signal),this.view,query,expected);if(this.cursor&&(page.offset!==offset||scope!=="filtered"&&page.total!==null&&
9
+ (page.matched!==null&&page.matched!==page.total||page.nextCursor===undefined&&page.offset+page.items.length!==page.total)))
10
+ throw new Error("CanopyX: incomplete cursor export.");expected.matched=page.matched;for(const row of page.items){gridOperationActive(this,signal);if(seen.has(row.id))throw new Error("CanopyX: repeated export row ID.");seen.add(row.id);if(scope!=="selected"||remaining.delete(row.id)){const record=GRID_REPORTING.exportRecord?GRID_REPORTING.exportRecord(this,request,row):gridFreeze({id:row.id,cells:Object.fromEntries(request.columns.map(column=>[column.id,{value:cellValue(row.cells[column.id])??null,text:cellText(row.cells[column.id],column,fmt)}]))});yield GRID_REPORTING.formatRecord?GRID_REPORTING.formatRecord(this,row,record):record}}
11
+ offset +=this.cursor?page.items.length:request.query.limit;if(this.cursor){cursor=page.nextCursor;if(cursor===undefined)break;if(tokens.has(cursor))throw new Error("CanopyX: cursor export repeated a token.");tokens.add(cursor)}}
12
+ gridOperationActive(this,signal);if(remaining.size)throw new Error("CanopyX: selected IDs absent from revision.")},async exportText(exported,maxRows=50000,delimiter,signal){if(!Number.isInteger(maxRows)||maxRows<1||maxRows>1000000)throw new Error("CanopyX: export row limit is 1 to 1000000.");const request=exported.request;if(request.recordCount>maxRows)throw new Error("CanopyX: client row limit exceeded.");const lines=[request.columns.map(column=>gridExportField(column.title,delimiter)).join(delimiter)];let count=0;for await(const row of exported.rows()){if(++count>maxRows)throw new Error("CanopyX: client row limit exceeded.");lines.push(request.columns.map(column=>{const value=request.values==="raw"?row.cells[column.id]?.value:row.cells[column.id]?.text;return gridExportField(row.level>1&&column.id===this.columns[0].id?" ".repeat(row.level-1)+(value??""):value,delimiter)}).join(delimiter))}
13
+ gridOperationActive(this,signal);return lines.join("\r\n")+ "\r\n"},toCsv(capture,options){return this.exportText(capture,options.maxRows,",",options.signal)},async copy(capture,options){const text=await this.exportText(capture,options.maxRows===undefined?10000:options.maxRows,"\t",options.signal);await gridWriteClipboard(this,text,options.signal);return text},copyFocused(row,cell){if(this.selected.size)return this.copy({scope:"selected"});const columns=this.visibleColumns(),element=this.rendered.get(row),index=cell&&element?this.rowCells(element).indexOf(cell):-1;const text=index>=0?gridExportField(cellText(row.h.cells[columns[index].id],columns[index],this.fmt),"\t"):
14
+ columns.map(column=>gridExportField(cellText(row.h.cells[column.id],column,this.fmt),"\t")).join("\t");return gridWriteClipboard(this,text)},exportToHost(capture,format,options){if(typeof format!=="string"||!format.trim()||typeof this.opts.onExport!=="function")throw new Error("CanopyX: invalid host export.");return this.runGridHook(signal=>this.opts.onExport({format,request:capture.request,capture,signal}),options.signal,exportTimeout(this,options))}});Object.assign(EXPORT_IMPL,{exportFromMenu(action){const{details,panel}=this.exportControl;this.renderGridExport(panel);details.open=true;if(!this.phone&&this.overflow)this.overflow.details.open=false;panel.cxRun(action)}});const exportMenu=function(panel){if(panel.cxRun)return panel.cxFormats();panel.replaceChildren();const reporting=!!this.view.reporting,scope=h("select"),labels=this.labels;scope.setAttribute("aria-label",labels.gridExport);for(const[value,label]of this.exportScopes||[["filtered","gridFilteredResults"],["selected","gridSelectedRows"],["all","gridAllResults"]])
15
+ scope.appendChild(gridOption(value,labels[label]));const status=h("p","cx-grid-delivery-status");status.setAttribute("role","status");const meter=h("progress");meter.setAttribute("aria-label",labels.gridExport);meter.hidden=true;let operation=null,formats=[];panel.cxExportBusy=()=>operation!==null;const cancel=gridButton(labels.gridCancelExport,()=>operation?.abort());cancel.hidden=true;const progress=value=>{if(this.destroyed||!operation||operation.signal.aborted)return;setText(status,fill(value.total===null?labels.gridExportReading:labels.gridExportProgress,{completed:numberFormat(this.lang).format(value.completed),total:value.total===null?"":numberFormat(this.lang).format(value.total)}));if(value.total===null)meter.removeAttribute("value");else{meter.max=Math.max(1,value.total);meter.value=value.completed}};const canExport=()=>this.view.type!=="grid"||this.defaultExportColumns().length>0;const busy=state=>{scope.disabled=state;cancel.hidden=meter.hidden=!state;for(const button of[csv,...formats,copy,this.printButton])if(button)button.disabled=state||!canExport()};const traced=(options,onProgress)=>reporting?{...options,onProgress}:options;panel.cxRun=action=>{if(operation)return;const controller=operation=new AbortController();busy(true);meter.removeAttribute("value");setText(status,labels.loading);Promise.resolve().then(()=>action({scope:scope.value,signal:controller.signal},progress)).then(()=>{if(!this.destroyed)setText(status,"")},error=>{if(!this.destroyed)setText(status,error.name==="AbortError"?labels.gridExportCancelled:error.message);if(error.name!=="AbortError")this.emit("error",{error})}).finally(()=>{if(operation===controller)operation=null;if(!this.destroyed){const returnFocus=document.activeElement===cancel;busy(false);if(returnFocus)scope.focus()}})};const csv=gridButton(labels.gridDownloadCsv,()=>panel.cxRun(async(options,onProgress)=>{const exported=this.prepareExport(traced(options,onProgress));const text=await this.exportText(exported,50000,",",options.signal);gridOperationActive(this,options.signal);saveExport(new Blob([text],{type:"text/csv;charset=utf-8"}),(exported.request.fileName||this.view.id.replace(/[^a-z0-9_-]/gi, "_")) + ".csv");
16
+ }));const copy=this.copy?gridButton(labels.gridCopy,()=>panel.cxRun((options,onProgress)=>this.copy(traced(options,onProgress)))):h("span");panel.cxFormats=()=>{if(operation)return;formats.forEach(button=>button.remove());try{formats=EXPORT_IMPL.formatsOf(this).map(format=>Object.assign(gridButton(format.label,()=>
17
+ panel.cxRun((options,onProgress)=>this.exportAs(format.id,{...options,onProgress}))),{value:format.id}))}catch(error){formats=[];this.emit("error",{error})}
18
+ copy.before(...formats);busy(false);if(!canExport())setText(status,labels.gridNoExportColumns);else if(status.textContent===labels.gridNoExportColumns)setText(status,"")};panel.append(scope,csv,copy,cancel,meter,status);panel.cxFormats()};DataGrid.prototype.renderGridExport=exportMenu;installed.add(context.EXPORT_IMPL)}};(global.CanopyXParts||=[]).push(["export",part])})(typeof window!=="undefined"?window:globalThis);