55 * Matrix component extension of Question.
66 *
77 * Supports editable legend/hint, row labels, column labels,
8- * mode switching confirmation, and row/column insertion in the editor.
8+ * mode switching confirmation, and row/column insertion/removal in the editor.
99 **/
1010
1111const utilities = require ( './utilities' ) ;
@@ -24,7 +24,8 @@ const SELECTOR_HEADER_ROW = "[data-fb-matrix-header-row]";
2424const SELECTOR_BODY = "[data-fb-matrix-body]" ;
2525const SELECTOR_ADD_ROW = "[data-fb-matrix-add-row]" ;
2626const SELECTOR_ADD_COLUMN = "[data-fb-matrix-add-column]" ;
27-
27+ const SELECTOR_REMOVE_ROW = "[data-fb-matrix-remove-row]" ;
28+ const SELECTOR_REMOVE_COLUMN = "[data-fb-matrix-remove-column]" ;
2829
2930class MatrixQuestion extends Question {
3031 constructor ( $node , config ) {
@@ -35,12 +36,16 @@ class MatrixQuestion extends Question {
3536
3637 $node . addClass ( "MatrixQuestion" ) ;
3738
38- this . _rowCount = Array . isArray ( this . data . rows ) ? this . data . rows . length : 0 ;
39- this . _columnCount = Array . isArray ( this . data . columns ) ? this . data . columns . length : 0 ;
40-
39+ this . initialiseEditableState ( ) ;
4140 this . initialiseMatrixLabelEditing ( config ) ;
4241 this . initialiseModeSwitchConfirmation ( config ) ;
4342 this . initialiseAxisControls ( config ) ;
43+ this . initialiseDeleteControls ( ) ;
44+ }
45+
46+ initialiseEditableState ( ) {
47+ this . $node . find ( SELECTOR_MODE ) . prop ( "disabled" , false ) ;
48+ this . $node . find ( SELECTOR_MODE ) . data ( "fb-matrix-mode-original" , this . data . mode ) ;
4449 }
4550
4651 initialiseMatrixLabelEditing ( config ) {
@@ -59,25 +64,41 @@ class MatrixQuestion extends Question {
5964 } ) ;
6065 }
6166
62- bindRowLabel ( $node , config ) {
67+ bindRowHeading ( $node , config ) {
6368 const editable = createEditableLabel ( $node , config ) ;
64- $node . off ( "blur.matrix-row-label" ) . on ( "blur.matrix-row-label" , ( ) => {
65- questionSyncRowLabel ( this , editable . content , $node . data ( "fb-matrix-row-id" ) ) ;
69+ $node . off ( "blur.matrix-row-heading" ) . on ( "blur.matrix-row-heading" , ( ) => {
70+ this . data . row_heading = editable . content ;
71+ this . editable . emitSaveRequired ( ) ;
6672 } ) ;
6773 }
6874
69- bindRowHeading ( $node , config ) {
75+ bindRowLabel ( $node , config ) {
7076 const editable = createEditableLabel ( $node , config ) ;
71- $node . off ( "blur.matrix-row-heading" ) . on ( "blur.matrix-row-heading" , ( ) => {
72- this . data . row_heading = editable . content ;
77+ $node . off ( "blur.matrix-row-label" ) . on ( "blur.matrix-row-label" , ( ) => {
78+ const rowId = $node . data ( "fb-matrix-row-id" ) ;
79+ const row = findAxisItem ( this . data . rows , rowId ) ;
80+
81+ if ( ! row ) {
82+ return ;
83+ }
84+
85+ row . label = editable . content ;
7386 this . editable . emitSaveRequired ( ) ;
7487 } ) ;
7588 }
7689
7790 bindColumnLabel ( $node , config ) {
7891 const editable = createEditableLabel ( $node , config ) ;
7992 $node . off ( "blur.matrix-column-label" ) . on ( "blur.matrix-column-label" , ( ) => {
80- questionSyncColumnLabel ( this , editable . content , $node . data ( "fb-matrix-column-id" ) ) ;
93+ const columnId = $node . data ( "fb-matrix-column-id" ) ;
94+ const column = findAxisItem ( this . data . columns , columnId ) ;
95+
96+ if ( ! column ) {
97+ return ;
98+ }
99+
100+ column . label = editable . content ;
101+ this . editable . emitSaveRequired ( ) ;
81102 } ) ;
82103 }
83104
@@ -118,6 +139,20 @@ class MatrixQuestion extends Question {
118139 } ) ;
119140 }
120141
142+ initialiseDeleteControls ( ) {
143+ this . $node . on ( "click.matrix-remove-row" , SELECTOR_REMOVE_ROW , ( event ) => {
144+ event . preventDefault ( ) ;
145+ const rowId = $ ( event . currentTarget ) . data ( "fb-matrix-row-id" ) ;
146+ this . removeRow ( rowId ) ;
147+ } ) ;
148+
149+ this . $node . on ( "click.matrix-remove-column" , SELECTOR_REMOVE_COLUMN , ( event ) => {
150+ event . preventDefault ( ) ;
151+ const columnId = $ ( event . currentTarget ) . data ( "fb-matrix-column-id" ) ;
152+ this . removeColumn ( columnId ) ;
153+ } ) ;
154+ }
155+
121156 hasMatrixCellInput ( ) {
122157 let hasInput = false ;
123158
@@ -143,72 +178,143 @@ class MatrixQuestion extends Question {
143178
144179 addRow ( config ) {
145180 const rowId = generateUuid ( ) ;
146- const rowLabel = `Row ${ this . _rowCount + 1 } ` ;
181+ const rowLabel = `Row ${ Array . isArray ( this . data . rows ) ? this . data . rows . length + 1 : 1 } ` ;
147182
148183 if ( ! Array . isArray ( this . data . rows ) ) {
149184 this . data . rows = [ ] ;
150185 }
151186
152187 this . data . rows . push ( { id : rowId , label : rowLabel } ) ;
153- this . _rowCount += 1 ;
154188
155189 const $row = $ ( '<tr class="govuk-table__row"></tr>' ) . attr ( "data-fb-matrix-row-id" , rowId ) ;
156- const $label = $ ( '<th scope="row" class="govuk-table__header" data-fb-matrix-row-label></th>' )
157- . attr ( "data-fb-matrix-row-id" , rowId )
158- . text ( rowLabel ) ;
159-
160- $row . append ( $label ) ;
190+ $row . append ( this . createRowHeader ( rowId , rowLabel ) ) ;
161191
162- Array ( this . data . columns || [ ] ) . forEach ( ( column ) => {
163- $row . append ( this . createCell ( rowId , column . id ) ) ;
192+ const columnIds = this . currentColumnIds ( ) ;
193+ columnIds . forEach ( ( columnId ) => {
194+ $row . append ( this . createCell ( rowId , columnId ) ) ;
164195 } ) ;
165196
166197 this . $node . find ( SELECTOR_BODY ) . append ( $row ) ;
167- this . bindRowLabel ( $label , config ) ;
198+ this . bindRowLabel ( $row . find ( SELECTOR_ROW_LABEL ) , config ) ;
168199 this . editable . emitSaveRequired ( ) ;
169200 }
170201
171202 addColumn ( config ) {
172203 const columnId = generateUuid ( ) ;
173- const columnLabel = `Option ${ this . _columnCount + 1 } ` ;
204+ const columnLabel = `Option ${ Array . isArray ( this . data . columns ) ? this . data . columns . length + 1 : 1 } ` ;
174205
175206 if ( ! Array . isArray ( this . data . columns ) ) {
176207 this . data . columns = [ ] ;
177208 }
178209
179210 this . data . columns . push ( { id : columnId , label : columnLabel } ) ;
180- this . _columnCount += 1 ;
181-
182- const $header = $ ( '<th scope="col" class="govuk-table__header" data-fb-matrix-column-label></th>' )
183- . attr ( "data-fb-matrix-column-id" , columnId )
184- . text ( columnLabel ) ;
185211
212+ const $header = this . createColumnHeader ( columnId , columnLabel ) ;
186213 this . $node . find ( SELECTOR_HEADER_ROW ) . append ( $header ) ;
187- this . bindColumnLabel ( $header , config ) ;
214+ this . bindColumnLabel ( $header . find ( SELECTOR_COLUMN_LABEL ) , config ) ;
188215
189216 this . $node . find ( "tbody tr" ) . each ( ( _ , rowNode ) => {
190217 const $row = $ ( rowNode ) ;
191- const rowId = $row . data ( "fb-matrix-row-id" ) || $row . find ( SELECTOR_ROW_LABEL ) . data ( "fb-matrix-row-id" ) ;
218+ const rowId = $row . data ( "fb-matrix-row-id" ) ;
192219 $row . append ( this . createCell ( rowId , columnId ) ) ;
193220 } ) ;
194221
195222 this . editable . emitSaveRequired ( ) ;
196223 }
197224
225+ removeRow ( rowId ) {
226+ if ( ! rowId || ! Array . isArray ( this . data . rows ) ) {
227+ return ;
228+ }
229+
230+ if ( this . data . rows . length <= 1 ) {
231+ return ;
232+ }
233+
234+ this . data . rows = this . data . rows . filter ( ( row ) => row . id !== rowId ) ;
235+ this . $node . find ( `tr[data-fb-matrix-row-id="${ rowId } "]` ) . remove ( ) ;
236+ this . editable . emitSaveRequired ( ) ;
237+ }
238+
239+ removeColumn ( columnId ) {
240+ if ( ! columnId || ! Array . isArray ( this . data . columns ) ) {
241+ return ;
242+ }
243+
244+ if ( this . data . columns . length <= 1 ) {
245+ return ;
246+ }
247+
248+ this . data . columns = this . data . columns . filter ( ( column ) => column . id !== columnId ) ;
249+ this . $node . find ( `${ SELECTOR_HEADER_ROW } th[data-fb-matrix-column-id="${ columnId } "]` ) . remove ( ) ;
250+ this . $node . find ( `td[data-fb-matrix-column-id="${ columnId } "]` ) . remove ( ) ;
251+ this . editable . emitSaveRequired ( ) ;
252+ }
253+
198254 renderMatrixCells ( ) {
255+ const columnIds = this . currentColumnIds ( ) ;
256+
199257 this . $node . find ( "tbody tr" ) . each ( ( _ , rowNode ) => {
200258 const $row = $ ( rowNode ) ;
201- const rowId = $row . data ( "fb-matrix-row-id" ) || $row . find ( SELECTOR_ROW_LABEL ) . data ( "fb-matrix-row-id" ) ;
259+ const rowId = $row . data ( "fb-matrix-row-id" ) ;
202260
203- $row . find ( "td" ) . remove ( ) ;
204- Array ( this . data . columns || [ ] ) . forEach ( ( column ) => {
205- $row . append ( this . createCell ( rowId , column . id ) ) ;
261+ $row . find ( "td[data-fb-matrix-column-id] " ) . remove ( ) ;
262+ columnIds . forEach ( ( columnId ) => {
263+ $row . append ( this . createCell ( rowId , columnId ) ) ;
206264 } ) ;
207265 } ) ;
208266 }
209267
268+ currentColumnIds ( ) {
269+ const ids = this . $node . find ( `${ SELECTOR_HEADER_ROW } th[data-fb-matrix-column-id]` ) . map ( ( _ , node ) => {
270+ const value = $ ( node ) . data ( "fb-matrix-column-id" ) ;
271+ return value || null ;
272+ } ) . get ( ) . filter ( Boolean ) ;
273+
274+ if ( ids . length > 0 ) {
275+ return ids ;
276+ }
277+
278+ return Array ( this . data . columns || [ ] ) . map ( ( column ) => column . id ) ;
279+ }
280+
281+ createColumnHeader ( columnId , columnLabel ) {
282+ const $header = $ ( '<th scope="col" class="govuk-table__header"></th>' )
283+ . attr ( "data-fb-matrix-column-id" , columnId ) ;
284+
285+ $header . append (
286+ $ ( '<span data-fb-matrix-column-label></span>' )
287+ . attr ( "data-fb-matrix-column-id" , columnId )
288+ . text ( columnLabel )
289+ ) ;
290+
291+ $header . append (
292+ $ ( '<button type="button" class="govuk-link govuk-!-margin-left-1 govuk-!-font-size-16" data-fb-matrix-remove-column>Delete</button>' )
293+ . attr ( "data-fb-matrix-column-id" , columnId )
294+ ) ;
295+
296+ return $header ;
297+ }
298+
299+ createRowHeader ( rowId , rowLabel ) {
300+ const $header = $ ( '<th scope="row" class="govuk-table__header"></th>' ) ;
301+
302+ $header . append (
303+ $ ( '<span data-fb-matrix-row-label></span>' )
304+ . attr ( "data-fb-matrix-row-id" , rowId )
305+ . text ( rowLabel )
306+ ) ;
307+
308+ $header . append (
309+ $ ( '<button type="button" class="govuk-link govuk-!-margin-left-1 govuk-!-font-size-16" data-fb-matrix-remove-row>Delete</button>' )
310+ . attr ( "data-fb-matrix-row-id" , rowId )
311+ ) ;
312+
313+ return $header ;
314+ }
315+
210316 createCell ( rowId , columnId ) {
211- const $cell = $ ( '<td class="govuk-table__cell"></td>' ) ;
317+ const $cell = $ ( '<td class="govuk-table__cell"></td>' ) . attr ( "data-fb-matrix-column-id" , columnId ) ;
212318
213319 if ( this . data . mode === "numeric" ) {
214320 $cell . append (
@@ -217,8 +323,9 @@ class MatrixQuestion extends Question {
217323 ) ;
218324 } else {
219325 $cell . append (
220- $ ( '<input type="radio " disabled>' )
326+ $ ( '<input type="checkbox " disabled>' )
221327 . attr ( "name" , `answers[${ this . data . name } ][${ rowId } ]` )
328+ . attr ( "data-fb-matrix-selection-row-id" , rowId )
222329 . val ( columnId )
223330 ) ;
224331 }
@@ -227,26 +334,6 @@ class MatrixQuestion extends Question {
227334 }
228335}
229336
230- function questionSyncRowLabel ( question , label , rowId ) {
231- const row = findAxisItem ( question . data . rows , rowId ) ;
232- if ( ! row ) {
233- return ;
234- }
235-
236- row . label = label ;
237- question . editable . emitSaveRequired ( ) ;
238- }
239-
240- function questionSyncColumnLabel ( question , label , columnId ) {
241- const column = findAxisItem ( question . data . columns , columnId ) ;
242- if ( ! column ) {
243- return ;
244- }
245-
246- column . label = label ;
247- question . editable . emitSaveRequired ( ) ;
248- }
249-
250337function createEditableLabel ( $node , config ) {
251338 const existing = $node . data ( "instance" ) ;
252339 if ( existing ) {
0 commit comments