@@ -261,36 +261,60 @@ class DataHarmonizer {
261261 remove_row : {
262262 name : 'Remove row' ,
263263 callback ( ) {
264- // Enables removal of a row and all dependent table rows.
265- // If there are 1-many cascading deletes, verify if that's ok.
266- let selection = self . hot . getSelected ( ) [ 0 ] [ 0 ] ;
267- let [ change_report , change_message ] = self . getChangeReport ( self . template_name ) ;
268- if ( ! change_message . length ) {
269- self . hot . alter ( 'remove_row' , selection ) ;
270- return true ;
264+ // Collect every selected visual row from all selection ranges
265+ // (user may have Ctrl-clicked non-contiguous rows or dragged a block).
266+ const selectedRanges = self . hot . getSelected ( ) || [ ] ;
267+ const visualRowSet = new Set ( ) ;
268+ for ( const [ r1 , , r2 ] of selectedRanges ) {
269+ const lo = Math . min ( r1 , r2 ) ;
270+ const hi = Math . max ( r1 , r2 ) ;
271+ for ( let r = lo ; r <= hi ; r ++ ) visualRowSet . add ( r ) ;
271272 }
272- /*
273- * For deletes: (For now, ignore duplicate root key case: If
274- * encountering foreign key involving root_class slot, test if that has
275- * > 1 row. If so, delete ok without examining other dependents.)
276- */
277-
278- // Some cascading deletes to confirm here.
279- if (
280- confirm (
281- 'WARNING: If you proceed, this will include deletion of one\n or more dependent records, and this cannot be undone:\n' +
282- change_message
283- )
284- ) {
285- // User has seen the warning and has confirmed ok to proceed.
286- for ( let [ dependent_name , dependent_obj ] of Object . entries ( change_report ) ) {
287- if ( dependent_obj . rows ?. length ) {
288- // rows are physical (source) indices. Delete via source-data reload
289- // so hidden rows are also removed without needing toVisualRow().
290- self . context . crudDeleteRowsByPhysical ( dependent_name , dependent_obj . rows ) ;
273+ const visualRows = [ ...visualRowSet ] . sort ( ( a , b ) => a - b ) ;
274+ const rowsToDelete = [ ] ;
275+
276+ for ( let i = 0 ; i < visualRows . length ; i ++ ) {
277+ const vRow = visualRows [ i ] ;
278+
279+ // Point HOT's selection at this row so crudGetDependentKeyVals
280+ // reads its primary-key values when building the change report.
281+ self . hot . selectCell ( vRow , 0 , vRow , 0 , false ) ;
282+ const [ change_report , change_message ] = self . getChangeReport ( self . template_name ) ;
283+
284+ if ( ! change_message . length ) {
285+ // No cascading dependents — safe to delete without prompting.
286+ rowsToDelete . push ( vRow ) ;
287+ } else {
288+ /*
289+ * For deletes: (For now, ignore duplicate root key case: If
290+ * encountering foreign key involving root_class slot, test if
291+ * that has > 1 row. If so, delete ok without examining other
292+ * dependents.)
293+ */
294+ const rowLabel = visualRows . length > 1
295+ ? `row ${ i + 1 } of ${ visualRows . length } `
296+ : 'this row' ;
297+ if ( confirm (
298+ `WARNING: Deleting ${ rowLabel } will also delete dependent records and cannot be undone:\n${ change_message } `
299+ ) ) {
300+ // rows are physical (source) indices. Delete via source-data
301+ // reload so hidden rows are also removed without toVisualRow().
302+ for ( const [ dependent_name , dependent_obj ] of Object . entries ( change_report ) ) {
303+ if ( dependent_obj . rows ?. length ) {
304+ self . context . crudDeleteRowsByPhysical ( dependent_name , dependent_obj . rows ) ;
305+ }
306+ }
307+ rowsToDelete . push ( vRow ) ;
291308 }
292- } ;
293- self . hot . alter ( 'remove_row' , selection ) ;
309+ // If the user cancels this row it is skipped; remaining rows
310+ // in the selection continue to be processed.
311+ }
312+ }
313+
314+ // Delete confirmed primary rows in descending visual order so
315+ // earlier indices are not shifted by later removals.
316+ for ( const vRow of rowsToDelete . sort ( ( a , b ) => b - a ) ) {
317+ self . hot . alter ( 'remove_row' , vRow ) ;
294318 }
295319 } ,
296320 } ,
@@ -379,7 +403,7 @@ class DataHarmonizer {
379403 // Hide if no locales
380404 const current_row = schema . current_selection [ 0 ] ;
381405 if ( current_row === null || current_row === undefined || current_row < 0 )
382- return false ;
406+ return true ;
383407 const locales = schema . hot . getCellMeta ( current_row , 0 ) . locales ;
384408 return ! locales ;
385409 } ,
@@ -688,7 +712,11 @@ class DataHarmonizer {
688712 if ( Object . keys ( self . invalid_cells ) . length === 0 ) {
689713 $ ( `#tab-bar-${ self . template_name } > a` ) . removeClass ( 'tab-has-errors' ) ;
690714 $ ( '#next-error-button' ) . hide ( ) ;
691- $ ( '#no-error-button' ) . show ( ) . delay ( 5000 ) . fadeOut ( 'slow' ) ;
715+ // Only show "No errors" if the user actually fixed a previously
716+ // invalid cell — not when editing a cell that was already valid.
717+ if ( prevInvalidChangedCells . length > 0 ) {
718+ $ ( '#no-error-button' ) . show ( ) . delay ( 5000 ) . fadeOut ( 'slow' ) ;
719+ }
692720 }
693721 // Track cells that became valid (for picker/dropdown Tab navigation in beforeKeyDown).
694722 const firstFixed = prevInvalidChangedCells . find (
@@ -838,6 +866,24 @@ class DataHarmonizer {
838866 }
839867 } ,
840868
869+ // Commit drag-and-drop row reordering into the underlying source data.
870+ // manualRowMove only updates the visual→physical index mapping; calling
871+ // loadData with the rows read in visual order resets the mapper so
872+ // physical order matches what the user sees.
873+ afterRowMove : ( movedRows , finalIndex , dropIndex , movePossible , orderChanged ) => {
874+ if ( ! orderChanged ) return ;
875+ const count = self . hot . countSourceRows ( ) ;
876+ const reordered = [ ] ;
877+ for ( let vRow = 0 ; vRow < count ; vRow ++ ) {
878+ const physRow = self . hot . toPhysicalRow ( vRow ) ;
879+ if ( physRow !== null ) {
880+ reordered . push ( [ ...self . hot . getSourceDataAtRow ( physRow ) ] ) ;
881+ }
882+ }
883+ self . invalid_cells = { } ; // physical row indices will reset; stale cache must be cleared
884+ self . hot . loadData ( reordered ) ;
885+ } ,
886+
841887 // Reposition the validation status bar tooltip when the table scrolls
842888 // so it stays aligned with the focused cell even after a column shift.
843889 afterScrollHorizontally : ( ) => self . _updateStatusBar ( ) ,
@@ -1718,6 +1764,16 @@ class DataHarmonizer {
17181764 }
17191765
17201766 switch ( slot . datatype ) {
1767+ case 'xsd:boolean' :
1768+ // Only use native checkbox for pure boolean fields. Fields that also
1769+ // have vocabulary sources (e.g. NullValueField mixed with boolean)
1770+ // remain key-value-list so their picklist values are preserved.
1771+ if ( ! slot . sources ) {
1772+ col . type = 'checkbox' ;
1773+ col . checkedTemplate = true ;
1774+ col . uncheckedTemplate = false ;
1775+ }
1776+ break ;
17211777 case 'xsd:date' :
17221778 col . type = 'dh.date' ;
17231779 col . allowInvalid = true ; // making a difference?
@@ -2533,6 +2589,21 @@ class DataHarmonizer {
25332589 */
25342590 matrixFieldChangeRules ( matrix ) {
25352591 Object . entries ( this . slots ) . forEach ( ( field , col ) => {
2592+ // Convert string boolean values to native JS booleans for xsd:boolean
2593+ // checkbox cells. Fields that also have vocabulary sources (e.g. NullValueField)
2594+ // remain key-value-list and store string values instead.
2595+ if ( this . slots [ col ] . datatype === 'xsd:boolean' && ! this . slots [ col ] . sources ) {
2596+ for ( let row = 0 ; row < matrix . length ; row ++ ) {
2597+ const val = matrix [ row ] [ col ] ;
2598+ if ( val === null || val === undefined || val === '' ) continue ;
2599+ if ( typeof val === 'string' ) {
2600+ const lower = val . toLowerCase ( ) ;
2601+ if ( lower === 'true' || lower === '1' ) matrix [ row ] [ col ] = true ;
2602+ else if ( lower === 'false' || lower === '0' ) matrix [ row ] [ col ] = false ;
2603+ }
2604+ }
2605+ }
2606+
25362607 // Test field against capitalization change.
25372608 if ( field . capitalize ) {
25382609 for ( let row = 0 ; row < matrix . length ; row ++ ) {
@@ -2785,6 +2856,21 @@ class DataHarmonizer {
27852856 cellChanges . push ( [ row , col , cellVal , 'prevalidation' ] ) ;
27862857 }
27872858
2859+ // Normalise string booleans to JS booleans for checkbox cells only.
2860+ // Skip fields with vocabulary sources (e.g. NullValueField); those
2861+ // remain as strings handled by validateValAgainstVocab below.
2862+ if ( datatype === 'xsd:boolean' && ! field . sources && typeof cellVal === 'string' && cellVal !== '' ) {
2863+ const lower = cellVal . toLowerCase ( ) ;
2864+ let canonical = null ;
2865+ if ( lower === 'true' || lower === '1' ) canonical = true ;
2866+ else if ( lower === 'false' || lower === '0' ) canonical = false ;
2867+ if ( canonical !== null ) {
2868+ cellVal = canonical ;
2869+ data [ row ] [ col ] = cellVal ;
2870+ cellChanges . push ( [ row , col , canonical , 'prevalidation' ] ) ;
2871+ }
2872+ }
2873+
27882874 if ( cellVal && datatype === 'xsd:token' && typeof cellVal === 'string' ) {
27892875 // console.log("valdation", cellVal)
27902876 const minimized = cellVal
@@ -2829,14 +2915,21 @@ class DataHarmonizer {
28292915 }
28302916 }
28312917 if ( cellChanges . length ) {
2832- console . log (
2833- 'doPreValidationRepairs: setDataAtCell and afterChange Hook'
2834- ) ;
2835- this . hot . addHookOnce ( 'afterChange' , resolve ) ;
2836- this . hot . setDataAtCell ( cellChanges , 'validation' ) ;
2837- } else {
2838- resolve ( ) ;
2918+ // Update HOT source data directly to avoid setDataAtCell's internal
2919+ // rendering machinery (done/getCell callbacks), which crashes for HOT
2920+ // instances in hidden tabs whose viewport calculator has never been
2921+ // initialized. getSourceData() returns the internal reference, so
2922+ // modifying it updates HOT's state without triggering rendering hooks.
2923+ const sourceData = this . hot . getSourceData ( ) ;
2924+ for ( const [ vRow , col , val ] of cellChanges ) {
2925+ const pRow = this . hot . toPhysicalRow ( vRow ) ;
2926+ if ( pRow !== null && sourceData [ pRow ] ) {
2927+ sourceData [ pRow ] [ col ] = val ;
2928+ }
2929+ }
2930+ this . hot . render ( ) ;
28392931 }
2932+ resolve ( ) ;
28402933 } ) ;
28412934 }
28422935
0 commit comments