PluginProbe
TablePress – Tables in WordPress made easy / 2.3.1
TablePress – Tables in WordPress made easy v2.3.1
3.3.4 3.3.3 3.3.2 3.3.1 trunk 1.12 1.14 1.9.2 2.0.4 2.1.7 2.1.8 2.2 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.3 2.3.1 2.3.2 2.4 2.4.1 2.4.2 2.4.3 2.4.4 All 44 releases
tablepress / admin / js / edit.js

edit.js in TablePress – Tables in WordPress made easy 2.3.1, at admin/js/edit.js

1,620 lines 61.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * JavaScript code for the "Edit" screen.
3 *
4 * @package TablePress
5 * @subpackage Views JavaScript
6 * @author Tobias Bäthge
7 * @since 2.0.0
8 */
9
10 /* globals tp, wp, ajaxurl, JSON, jspreadsheet, jexcel, wpLink, jQuery */
11 /* eslint-disable jsdoc/check-param-names, jsdoc/valid-types */
12
13 /**
14 * WordPress dependencies.
15 */
16 import { __, _x, sprintf } from '@wordpress/i18n';
17 import { doAction as do_action, applyFilters as apply_filters } from '@wordpress/hooks';
18 import { buildQueryString } from '@wordpress/url';
19
20 /**
21 * Internal dependencies.
22 */
23 import { $ } from './common/functions';
24 import contextMenu from './edit/contextmenu';
25
26 // Ensure the global `tp` object exists.
27 window.tp = window.tp || {};
28
29 tp.made_changes = false;
30
31 tp.helpers = tp.helpers || {};
32 tp.callbacks = tp.callbacks || {};
33
34 // Initial selection: cell A1.
35 tp.helpers.selection = tp.helpers.selection || {
36 rows: [ 0 ],
37 columns: [ 0 ],
38 };
39
40 tp.helpers.unsaved_changes = tp.helpers.unsaved_changes || {};
41
42 /**
43 * [unsaved_changes.unload_dialog description]
44 *
45 * @param {Event} event [description]
46 */
47 tp.helpers.unsaved_changes.unload_dialog = function ( event ) {
48 event.preventDefault(); // Cancel the event as stated by the standard.
49 event.returnValue = ''; // Chrome requires returnValue to be set.
50 };
51
52 /**
53 * [unsaved_changes.set description]
54 */
55 tp.helpers.unsaved_changes.set = function () {
56 // Bail early if this function was already called.
57 if ( tp.made_changes ) {
58 return;
59 }
60 tp.made_changes = true;
61 window.addEventListener( 'beforeunload', tp.helpers.unsaved_changes.unload_dialog );
62 };
63
64 /**
65 * [unsaved_changes.unset description]
66 */
67 tp.helpers.unsaved_changes.unset = function () {
68 tp.made_changes = false;
69 window.removeEventListener( 'beforeunload', tp.helpers.unsaved_changes.unload_dialog );
70 };
71
72 tp.helpers.options = tp.helpers.options || {};
73
74 /**
75 * Loads table options and sets DOM element states appropriately.
76 */
77 tp.helpers.options.load = function () {
78 Object.keys( tp.table.options ).forEach( function ( option_name ) {
79 // Skip entries that are not actually option fields.
80 if ( 'last_editor' === option_name ) {
81 return;
82 }
83
84 // Allow skipping options, e.g. when custom loading is used.
85 option_name = apply_filters( 'tablepress.optionsLoad', option_name );
86 if ( '' === option_name ) {
87 return;
88 }
89
90 let $field = $( `#option-${ option_name }` );
91 if ( ! $field ) {
92 // If no field with just that option_name is found, it could be a radio button, which have IDs based on option_name and value.
93 $field = $( `#option-${ option_name }-${ tp.table.options[ option_name ] }` );
94 }
95 if ( ! $field ) {
96 // If there's still no field, the field might be missing. For example, the "Custom Commands" only exists if a user is allowed to use `unfiltered_html`.
97 return;
98 }
99
100 if ( $field instanceof HTMLInputElement && 'checkbox' === $field.type ) {
101 // For checkboxes, the `checked` state is based on the value (true/false).
102 $field.checked = tp.table.options[ option_name ];
103 } else if ( $field instanceof HTMLInputElement && 'radio' === $field.type ) {
104 // For checkboxes, the `checked` state is true, as only the field corresponding to the value is selected.
105 $field.checked = true;
106 } else {
107 // For all other fields, the form field value is set according to the option value.
108 $field.value = tp.table.options[ option_name ];
109 }
110 } );
111
112 // Turn off "Enable Visitor Features" if the table has merged cells.
113 if ( tp.table.options.use_datatables && tp.helpers.editor.has_merged_cells() ) {
114 tp.table.options.use_datatables = false;
115 $( '#option-use_datatables' ).checked = false;
116 }
117
118 tp.helpers.options.check_dependencies();
119 };
120
121 /**
122 * Sets the table option property when the DOM element (form field) is changed.
123 *
124 * @param {Event} event [description]
125 */
126 tp.helpers.options.change = function ( event ) {
127 if ( ! event.target ) {
128 return;
129 }
130
131 const option_name = event.target.name || '';
132
133 // Skip input fields that don't have a valid `name` attribute, as these don't directly reflect table options.
134 if ( '' === option_name ) {
135 return;
136 }
137
138 const property = ( event.target instanceof HTMLInputElement && 'checkbox' === event.target.type ) ? 'checked' : 'value';
139 tp.table.options[ option_name ] = event.target[ property ];
140
141 // Save numeric options as numbers.
142 if ( event.target instanceof HTMLInputElement && 'number' === event.target.type ) {
143 tp.table.options[ option_name ] = parseInt( tp.table.options[ option_name ], 10 );
144 }
145
146 // Turn off "Enable Visitor Features" if the table has merged cells.
147 if ( 'use_datatables' === option_name && tp.table.options.use_datatables && tp.helpers.editor.has_merged_cells() ) {
148 tp.table.options.use_datatables = false;
149 $( '#option-use_datatables' ).checked = false;
150 window.alert( __( 'You can not enable the Table Features for Site Visitors, because your table contains combined/merged cells.', 'tablepress' ) );
151 }
152
153 do_action( 'tablepress.optionsChange', option_name, property, event );
154
155 tp.helpers.options.check_dependencies();
156 tp.helpers.unsaved_changes.set();
157 tp.editor.updateTable(); // Redraw table.
158 };
159
160 /**
161 * Checks dependencies of options and sets DOM state ("disabled") appropriately.
162 */
163 tp.helpers.options.check_dependencies = function () {
164 $( '#option-use_datatables' ).disabled = ! tp.table.options.table_head;
165 $( '#notice-datatables-head-row' ).style.display = tp.table.options.table_head ? 'none' : 'block';
166
167 $( '#option-print_name_position' ).disabled = ! tp.table.options.print_name;
168 $( '#option-print_description_position' ).disabled = ! tp.table.options.print_description;
169
170 const js_features_enabled = ( tp.table.options.use_datatables && tp.table.options.table_head );
171 $( '#tablepress_edit-datatables-features' ).querySelectorAll( ':scope input:not(#option-use_datatables), :scope textarea' ).forEach( ( $field ) => ( $field.disabled = ! js_features_enabled ) );
172
173 const pagination_enabled = ( js_features_enabled && tp.table.options.datatables_paginate );
174 $( '#option-datatables_lengthchange' ).disabled = ! pagination_enabled;
175 $( '#option-datatables_paginate_entries' ).disabled = ! pagination_enabled;
176
177 do_action( 'tablepress.optionsCheckDependencies' );
178 };
179
180 /**
181 * Validate certain form fields, before saving or generating a preview.
182 */
183 tp.helpers.options.validate_fields = function () {
184 // The pagination entries value must be a positive number.
185 if ( tp.table.options.datatables_paginate && ( isNaN( tp.table.options.datatables_paginate_entries ) || tp.table.options.datatables_paginate_entries < 1 || tp.table.options.datatables_paginate_entries > 9999 ) ) {
186 window.alert( sprintf( __( 'The entered value in the “%1$s” field is invalid.', 'tablepress' ), __( 'Pagination Entries', 'tablepress' ) ) );
187 const $field = $( '#option-datatables_paginate_entries' );
188 $field.focus();
189 $field.select();
190 return false;
191 }
192
193 // The "Extra CSS classes" must not contain invalid characters.
194 if ( ( /[^A-Za-z0-9- _:]/ ).test( tp.table.options.extra_css_classes ) ) {
195 window.alert( sprintf( __( 'The entered value in the “%1$s” field is invalid.', 'tablepress' ), __( 'Extra CSS Classes', 'tablepress' ) ) );
196 const $field = $( '#option-extra_css_classes' );
197 $field.focus();
198 $field.select();
199 return false;
200 }
201
202 return apply_filters( 'tablepress.optionsValidateFields', true );
203 };
204
205 tp.helpers.visibility = tp.helpers.visibility || {};
206
207 /**
208 * [visibility.load description]
209 */
210 tp.helpers.visibility.load = function () {
211 const num_rows = tp.table.visibility.rows.length;
212 const num_columns = tp.table.visibility.columns.length;
213 const meta = {};
214 // Collect meta data for hidden rows.
215 for ( let row_idx = 0; row_idx < num_rows; row_idx++ ) {
216 if ( 1 === tp.table.visibility.rows[ row_idx ] ) {
217 continue;
218 }
219 for ( let col_idx = 0; col_idx < num_columns; col_idx++ ) {
220 const cell_name = jspreadsheet.getColumnNameFromId( [ col_idx, row_idx ] );
221 meta[ cell_name ] = meta[ cell_name ] || {};
222 meta[ cell_name ].row_hidden = true;
223 }
224 }
225 // Collect meta data for hidden columns.
226 for ( let col_idx = 0; col_idx < num_columns; col_idx++ ) {
227 if ( 1 === tp.table.visibility.columns[ col_idx ] ) {
228 continue;
229 }
230 for ( let row_idx = 0; row_idx < num_rows; row_idx++ ) {
231 const cell_name = jspreadsheet.getColumnNameFromId( [ col_idx, row_idx ] );
232 meta[ cell_name ] = meta[ cell_name ] || {};
233 meta[ cell_name ].column_hidden = true;
234 }
235 }
236 return meta;
237 };
238
239 /**
240 * [visibility.update description]
241 */
242 tp.helpers.visibility.update = function () {
243 // Set all rows and columns to visible first.
244 tp.table.visibility.rows = [];
245 for ( let row_idx = 0; row_idx < tp.editor.options.data.length; row_idx++ ) {
246 tp.table.visibility.rows[ row_idx ] = 1;
247 }
248 tp.table.visibility.columns = [];
249 for ( let col_idx = 0; col_idx < tp.editor.options.columns.length; col_idx++ ) {
250 tp.table.visibility.columns[ col_idx ] = 1;
251 }
252 // Get all hidden cells and mark their rows/columns as hidden.
253 Object.keys( tp.editor.options.meta ).forEach( function ( cell_name ) {
254 const cell = jspreadsheet.getIdFromColumnName( cell_name, true ); // Returns [ col_idx, row_idx ].
255 if ( 1 === tp.table.visibility.rows[ cell[1] ] && tp.editor.options.meta[ cell_name ].row_hidden ) {
256 tp.table.visibility.rows[ cell[1] ] = 0;
257 }
258 if ( 1 === tp.table.visibility.columns[ cell[0] ] && tp.editor.options.meta[ cell_name ].column_hidden ) {
259 tp.table.visibility.columns[ cell[0] ] = 0;
260 }
261 } );
262 };
263
264 /**
265 * Check whether the Hide or Unhide entries in the context menu should be disabled, by comparing
266 * whether any of the selected rows/columns have a different visibility state than what the entry would set.
267 *
268 * @param {string} type What to hide or unhide ("rows" or "columns").
269 * @param {boolean} visibility 0 for hidden, 1 for visible.
270 * @return {boolean} True if the entry shall be shown, false if not.
271 */
272 tp.helpers.visibility.selection_contains = function ( type, visibility ) {
273 // Show the entry as soon as one of the selected rows/columns does not have the intended visibility state.
274 return tp.helpers.selection[ type ].some( ( roc_idx ) => ( tp.table.visibility[ type ][ roc_idx ] === visibility ) );
275 };
276
277 /**
278 * For the context menu and button, determine whether moving the rows/columns of the current selection is allowed.
279 *
280 * @param {[type]} type [description]
281 * @param {[type]} direction [description]
282 * @return {boolean} Whether the move is allowed or not.
283 */
284 tp.helpers.move_allowed = function ( type, direction ) {
285 // When moving up or left, or to top or first, test the first row/column of the selected range.
286 let roc_to_test = tp.helpers.selection[ type ][0];
287 let min_max_roc = 0; // First row/column.
288 // When moving down or right, or bottom or last, test the last row/column of the selected range.
289 if ( 'down' === direction || 'right' === direction || 'bottom' === direction || 'last' === direction ) {
290 roc_to_test = tp.helpers.selection[ type ][ tp.helpers.selection[ type ].length - 1 ];
291 min_max_roc = ( 'rows' === type ) ? tp.editor.options.data.length - 1 : tp.editor.options.columns.length - 1;
292 }
293 // Moving is disallowed if the first/last row/column is already at the target edge.
294 if ( min_max_roc === roc_to_test ) {
295 return false;
296 }
297 // Otherwise allow the move.
298 return true;
299 };
300
301 /**
302 * For the context menu and button, determine whether merging the current selection is allowed.
303 *
304 * @param {string} errors Whether errors should also be alert()ed.
305 * @param {Object} error_message Call-by-reference object for the error message.
306 * @return {boolean} Whether the merge is allowed or not.
307 */
308 tp.helpers.cell_merge_allowed = function ( errors, error_message = {} ) {
309 const alert_on_error = ( 'alert' === errors );
310
311 // If the "Table Head Row" and Enable Visitor Features" options are enabled, disable merging cells.
312 if ( tp.table.options.table_head && tp.table.options.use_datatables ) {
313 error_message.text = sprintf( __( 'You can not combine these cells, because the “%1$s” checkbox in the “%2$s” section is checked.', 'tablepress' ), __( 'Enable Visitor Features', 'tablepress' ), __( 'Table Features for Site Visitors', 'tablepress' ) ) +
314 ' ' + __( 'The Table Features for Site Visitors are not compatible with merged cells.', 'tablepress' );
315 if ( alert_on_error ) {
316 window.alert( error_message.text );
317 }
318 return false;
319 }
320
321 const first_selected_row = tp.helpers.selection.rows[0];
322 const last_selected_row = tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ];
323
324 // If the head row option is enabled, and the first and (at least) second row are selected, disable merging cells.
325 if ( tp.table.options.table_head && 0 === first_selected_row && last_selected_row > 0 ) {
326 error_message.text = sprintf( __( 'You can not combine these cells, because the “%1$s” checkbox in the “%2$s” section is checked.', 'tablepress' ), __( 'Table Head Row', 'tablepress' ), __( 'Table Options', 'tablepress' ) );
327 if ( alert_on_error ) {
328 window.alert( error_message.text );
329 }
330 return false;
331 }
332
333 // If the foot row option is enabled, and the last and (at least) next to last row are selected, disable merging cells.
334 const last_row_idx = tp.editor.options.data.length - 1;
335 if ( tp.table.options.table_foot && last_row_idx === last_selected_row && first_selected_row < last_row_idx ) {
336 error_message.text = sprintf( __( 'You can not combine these cells, because the “%1$s” checkbox in the “%2$s” section is checked.', 'tablepress' ), __( 'Table Foot Row', 'tablepress' ), __( 'Table Options', 'tablepress' ) );
337 if ( alert_on_error ) {
338 window.alert( error_message.text );
339 }
340 return false;
341 }
342
343 // Otherwise allow the merge.
344 return true;
345 };
346
347 tp.helpers.editor = tp.helpers.editor || {};
348
349 /**
350 * [editor_reselect description]
351 *
352 * @param {[type]} el [description]
353 * @param {[type]} obj Jspreadsheet instance, passed e.g. by onblur. If not present, we use tp.editor.
354 */
355 tp.helpers.editor.reselect = function ( el, obj ) {
356 if ( 'undefined' === typeof obj ) {
357 obj = tp.editor;
358 }
359 obj.updateSelectionFromCoords(
360 tp.helpers.selection.columns[0],
361 tp.helpers.selection.rows[0],
362 tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ],
363 tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ]
364 );
365 };
366
367 /**
368 * [editor_has_merged_cells description]
369 */
370 tp.helpers.editor.has_merged_cells = function () {
371 const num_rows = tp.editor.options.data.length;
372 const num_columns = tp.editor.options.columns.length;
373 for ( let row_idx = 1; row_idx < num_rows; row_idx++ ) {
374 for ( let col_idx = 1; col_idx < num_columns; col_idx++ ) {
375 if ( '#rowspan#' === tp.editor.options.data[ row_idx ][ col_idx ] || '#colspan#' === tp.editor.options.data[ row_idx ][ col_idx ] ) {
376 return true;
377 }
378 }
379 }
380 return false;
381 };
382
383 /**
384 * Creates the sorting function that is used when sorting the table by a column.
385 *
386 * @param {number} direction Sorting direction. 0 for ascending, 1 for descending.
387 * @return {Function} Sorting function.
388 */
389 tp.helpers.editor.sorting = function( direction ) {
390 direction = direction ? -1 : 1;
391 return function( a, b ) {
392 // The actual value is stored in the second array element, the first contains the row index.
393 const sortResult = a[1].localeCompare( b[1], undefined, {
394 numeric: true,
395 sensitivity: 'base'
396 } );
397 return direction * sortResult;
398 };
399 };
400
401 tp.callbacks.editor = tp.callbacks.editor || {};
402
403 /**
404 * [editor_onselection description]
405 *
406 * @param {[type]} instance [description]
407 * @param {[type]} x1 [description]
408 * @param {[type]} y1 [description]
409 * @param {[type]} x2 [description]
410 * @param {[type]} y2 [description]
411 * @param {[type]} origin [description]
412 */
413 tp.callbacks.editor.onselection = function ( instance, x1, y1, x2, y2 /*, origin */ ) {
414 tp.helpers.selection = {
415 rows: [],
416 columns: [],
417 };
418 for ( let row_idx = y1; row_idx <= y2; row_idx++ ) {
419 tp.helpers.selection.rows.push( row_idx );
420 }
421 for ( let col_idx = x1; col_idx <= x2; col_idx++ ) {
422 tp.helpers.selection.columns.push( col_idx );
423 }
424 };
425
426 /**
427 * [editor_onupdatetable description]
428 *
429 * @param {[type]} instance [description]
430 * @param {[type]} cell [description]
431 * @param {[type]} col_idx [description]
432 * @param {[type]} row_idx [description]
433 * @param {[type]} value [description]
434 * @param {[type]} label [description]
435 * @param {[type]} cell_name [description]
436 */
437 tp.callbacks.editor.onupdatetable = function ( instance, cell, col_idx, row_idx, value, label, cell_name ) {
438 const meta = instance.jspreadsheet.options.meta[ cell_name ];
439
440 // Add class to cells (td) of hidden columns.
441 cell.classList.toggle( 'column-hidden', Boolean( meta?.column_hidden ) );
442
443 // Add classes to row (tr) for hidden rows and head/foot row. Only needs to be done once per row, thus when processing the first column.
444 if ( 0 === col_idx ) {
445 cell.parentNode.classList.toggle( 'row-hidden', Boolean( meta?.row_hidden ) );
446 cell.parentNode.classList.remove( 'head-row', 'foot-row' );
447
448 // After processing the last row, potentially add classes to the head and foot rows.
449 if ( row_idx === instance.jspreadsheet.rows.length - 1 ) {
450 const visible_rows = instance.jspreadsheet.content.querySelectorAll( ':scope tbody tr:not(.row-hidden)' );
451 // Designating a head and a foot row only makes sense for tables with more than one row. Single-row tables will only have a table body.
452 if ( 1 < visible_rows.length ) {
453 if ( tp.table.options.table_head ) {
454 visible_rows[0].classList.add( 'head-row' );
455 }
456 if ( tp.table.options.table_foot ) {
457 visible_rows[ visible_rows.length - 1 ].classList.add( 'foot-row' );
458 }
459 }
460 }
461 }
462 };
463
464 /**
465 * [editor_oninsertroc description]
466 *
467 * Abbreviations:
468 * roc: row or column
469 * cor: column or row
470 *
471 * @param {[type]} type [description]
472 * @param {[type]} action [description]
473 * @param {[type]} el [description]
474 * @param {[type]} roc_idx [description]
475 * @param {[type]} num_rocs [description]
476 * @param {[type]} roc_records [description]
477 * @param {[type]} insertBefore [description]
478 */
479 tp.callbacks.editor.oninsertroc = function ( type, action, el, roc_idx, num_rocs, roc_records, insertBefore ) {
480 const handling_rows = ( 'rows' === type );
481 const property = handling_rows ? 'column_hidden' : 'row_hidden';
482 const duplicating = ( 'duplicate' === action );
483
484 const from_roc_idx = roc_idx + ( insertBefore ? num_rocs : 0 );
485 const num_cors = handling_rows ? tp.editor.options.columns.length : tp.editor.options.data.length;
486
487 // Get data of row/column that is copied.
488 const from_meta = {};
489 for ( let cor_idx = 0; cor_idx < num_cors; cor_idx++ ) {
490 const cell_idx = handling_rows ? [ cor_idx, from_roc_idx ] : [ from_roc_idx, cor_idx ];
491 const meta = tp.editor.options.meta[ jspreadsheet.getColumnNameFromId( cell_idx ) ];
492 if ( ! meta ) {
493 continue;
494 }
495 // When duplicating, copy full cell meta, otherwise only the necessary property (row visibility for columns, column visibility for rows).
496 if ( duplicating ) {
497 from_meta[ cor_idx ] = meta;
498 } else if ( meta[ property ] ) {
499 from_meta[ cor_idx ] = from_meta[ cor_idx ] || {};
500 from_meta[ cor_idx ][ property ] = true;
501 }
502 }
503
504 const from_meta_keys = Object.keys( from_meta );
505 // Bail early if there's nothing to copy.
506 if ( ! from_meta_keys.length ) {
507 return;
508 }
509
510 // Construct meta data for target rows/columns.
511 const to_meta = {};
512 if ( ! insertBefore ) {
513 roc_idx++; // When appending (i.e. insert after), we start after the current row or column.
514 }
515 for ( let new_roc = 0; new_roc < num_rocs; new_roc++ ) {
516 const to_roc_idx = roc_idx + new_roc;
517 from_meta_keys.forEach( function ( cor_idx ) {
518 const cell_idx = handling_rows ? [ cor_idx, to_roc_idx ] : [ to_roc_idx, cor_idx ];
519 to_meta[ jspreadsheet.getColumnNameFromId( cell_idx ) ] = from_meta[ cor_idx ];
520 } );
521 }
522
523 tp.editor.setMeta( to_meta );
524 tp.editor.updateTable(); // Redraw table.
525 };
526
527 /**
528 * [editor_onmove description]
529 *
530 * @param {[type]} el [description]
531 * @param {[type]} old_roc_idx [description]
532 * @param {[type]} new_roc_idx [description]
533 */
534 tp.callbacks.editor.onmove = function (/* el, old_roc_idx, new_roc_idx */) {
535 tp.helpers.editor.reselect();
536 tp.helpers.unsaved_changes.set();
537 };
538
539 /**
540 * [editor_onsort description]
541 *
542 * @param {[type]} el [description]
543 * @param {[type]} column [description]
544 * @param {[type]} order [description]
545 */
546 tp.callbacks.editor.onsort = function (/* el, column, order */) {
547 tp.editor.updateTable(); // Redraw table.
548 tp.helpers.unsaved_changes.set();
549 };
550
551 /**
552 * Copy the generated link or image HTML code from the helper textarea to the first selected table cell.
553 */
554 tp.helpers.editor.insert_from_helper_textarea = function () {
555 tp.editor.setValueFromCoords( tp.helpers.selection.columns[0], tp.helpers.selection.rows[0], this.value );
556 };
557
558 tp.callbacks.insert_link = {};
559
560 /**
561 * Open the wpLink dialog for inserting links.
562 *
563 * @param {HTMLElement|null} $active_textarea Active textarea of the table editor or null.
564 */
565 tp.callbacks.insert_link.open_dialog = function ( $active_textarea = null ) {
566 const $helper_textarea = $( '#textarea-insert-helper' );
567 $helper_textarea.value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
568 if ( $active_textarea ) {
569 $helper_textarea.selectionStart = $active_textarea.selectionStart;
570 $helper_textarea.selectionEnd = $active_textarea.selectionEnd;
571 } else {
572 $helper_textarea.selectionStart = $helper_textarea.value.length;
573 $helper_textarea.selectionEnd = $helper_textarea.value.length;
574 }
575 const cell_name = jexcel.getColumnNameFromId( [ tp.helpers.selection.columns[0], tp.helpers.selection.rows[0] ] );
576 $( '#link-modal-title' ).textContent = sprintf( __( 'Insert Link into cell %1$s', 'tablepress' ), cell_name );
577 wpLink.open( 'textarea-insert-helper' );
578 jexcel.current = null; // This is necessary to prevent problems with the focus when the "Insert Link" dialog is called from the context menu.
579 };
580
581 tp.callbacks.insert_image = {};
582
583 /**
584 * Open the WP Media library for inserting images.
585 *
586 * @param {HTMLElement|null} $active_textarea Active textarea of the table editor or null.
587 */
588 tp.callbacks.insert_image.open_dialog = function ( $active_textarea = null ) {
589 const $helper_textarea = $( '#textarea-insert-helper' );
590 $helper_textarea.value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
591 if ( $active_textarea ) {
592 $helper_textarea.selectionStart = $active_textarea.selectionStart;
593 $helper_textarea.selectionEnd = $active_textarea.selectionEnd;
594 } else {
595 $helper_textarea.selectionStart = $helper_textarea.value.length;
596 $helper_textarea.selectionEnd = $helper_textarea.value.length;
597 }
598 wp.media.editor.open( 'textarea-insert-helper', {
599 frame: 'post',
600 state: 'insert',
601 title: wp.media.view.l10n.addMedia,
602 multiple: true,
603 } );
604 const cell_name = jexcel.getColumnNameFromId( [ tp.helpers.selection.columns[0], tp.helpers.selection.rows[0] ] );
605 document.querySelector( '#media-frame-title h1' ).textContent = sprintf( __( 'Add media to cell %1$s', 'tablepress' ), cell_name );
606 jexcel.current = null; // This is necessary to prevent problems with the focus when the "Insert Link" dialog is called from the context menu.
607 };
608
609 tp.callbacks.advanced_editor = {};
610
611 tp.callbacks.advanced_editor.$textarea = $( '#advanced-editor-content' );
612
613 /**
614 * Open the wpdialog for the Advanced Editor.
615 *
616 * @param {HTMLElement|null} $active_textarea Active textarea of the table editor or null.
617 */
618 tp.callbacks.advanced_editor.open_dialog = function ( $active_textarea = null ) {
619 tp.callbacks.advanced_editor.$textarea.value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
620
621 const cell_name = jexcel.getColumnNameFromId( [ tp.helpers.selection.columns[0], tp.helpers.selection.rows[0] ] );
622 const title = sprintf( __( 'Advanced Editor for cell %1$s', 'tablepress' ), cell_name );
623 $( '#advanced-editor-label' ).textContent = title; // Screen reader label for the "Advanced Editor" textarea.
624 $( '#link-modal-title' ).textContent = sprintf( __( 'Insert Link into cell %1$s', 'tablepress' ), cell_name );
625
626 jQuery( '#advanced-editor' ).wpdialog( {
627 width: 600,
628 modal: true,
629 title,
630 resizable: false, // Height of textarea does not increase when resizing editor height.
631 closeOnEscape: true,
632 buttons: [
633 {
634 text: __( 'Cancel', 'tablepress' ),
635 class: 'button button-cancel',
636 click() {
637 jQuery( this ).wpdialog( 'close' );
638 },
639 },
640 {
641 text: __( 'OK', 'tablepress' ),
642 class: 'button button-primary button-ok',
643 click: tp.callbacks.advanced_editor.confirm_save,
644 },
645 ],
646 } );
647
648 jexcel.current = null; // This is necessary to prevent problems with the focus and cells being emptied when the Advanced Editor is called from the context menu.
649 if ( $active_textarea ) {
650 tp.callbacks.advanced_editor.$textarea.selectionStart = $active_textarea.selectionStart;
651 tp.callbacks.advanced_editor.$textarea.selectionEnd = $active_textarea.selectionEnd;
652 } else {
653 tp.callbacks.advanced_editor.$textarea.selectionStart = tp.callbacks.advanced_editor.$textarea.value.length;
654 tp.callbacks.advanced_editor.$textarea.selectionEnd = tp.callbacks.advanced_editor.$textarea.value.length;
655 }
656 tp.callbacks.advanced_editor.$textarea.focus();
657 };
658
659 /**
660 * Confirm and save changes of the Advanced Editor.
661 */
662 tp.callbacks.advanced_editor.confirm_save = function () {
663 const current_value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
664 // Only set the cell content if changes were made to not wrongly call tp.helpers.unsaved_changes.set().
665 if ( tp.callbacks.advanced_editor.$textarea.value !== current_value ) {
666 tp.editor.setValueFromCoords( tp.helpers.selection.columns[0], tp.helpers.selection.rows[0], tp.callbacks.advanced_editor.$textarea.value );
667 }
668 jQuery( this ).wpdialog( 'close' );
669 };
670
671 tp.callbacks.help_box = {};
672
673 /**
674 * Open the wpdialog for a help box.
675 *
676 * @param {Event} event [description]
677 */
678 tp.callbacks.help_box.open_dialog = function ( event ) {
679 const $helpbox = $( event.target.dataset.helpBox );
680 jQuery( $helpbox ).wpdialog( {
681 height: $helpbox.dataset.height,
682 width: $helpbox.dataset.width,
683 minWidth: 260,
684 modal: true,
685 closeOnEscape: true,
686 buttons: [
687 {
688 text: __( 'OK', 'tablepress' ),
689 class: 'button button-ok',
690 click() {
691 jQuery( this ).wpdialog( 'close' );
692 },
693 },
694 ],
695 open( /* event, ui */ ) {
696 jQuery( this ).next().find( '.button-ok' ).trigger( 'focus' );
697 },
698 } );
699 };
700
701 tp.callbacks.table_preview = {};
702
703 /**
704 * Handle showing the table preview.
705 *
706 * @param {Event} event [description]
707 */
708 tp.callbacks.table_preview.process = function ( event ) {
709 // Never follow the link of the Preview button, everything is handled with JS.
710 event.preventDefault();
711
712 let table_name = $( '#table-name' ).value;
713 if ( '' === table_name.trim() ) {
714 table_name = __( '(no name)', 'tablepress' );
715 }
716
717 // Initialize the Table Preview wpdialog.
718 tp.callbacks.table_preview.$dialog = jQuery( '#table-preview' ).wpdialog( {
719 autoOpen: false,
720 width: window.innerWidth - 80,
721 height: window.innerHeight - 80,
722 modal: true,
723 title: sprintf( __( 'Preview of table “%1$s” (ID %2$s)', 'tablepress' ), table_name, tp.table.id ),
724 closeOnEscape: true,
725 buttons: [
726 {
727 text: __( 'OK', 'tablepress' ),
728 class: 'button button-ok',
729 click() {
730 jQuery( this ).wpdialog( 'close' );
731 },
732 },
733 ],
734 } );
735
736 // For tables without unsaved changes, show an externally rendered table from a URL in an iframe in a wpdialog.
737 if ( ! tp.made_changes ) {
738 const $iframe = $( '#table-preview-iframe' );
739 $iframe.src = event.target.href;
740 $iframe.removeAttribute( 'srcdoc' );
741 tp.callbacks.table_preview.$dialog.wpdialog( 'open' );
742 return;
743 }
744
745 // For tables with unsaved changes, get the table preview HTML code for the iframe via AJAX.
746
747 // Collect information about hidden rows and columns.
748 tp.helpers.visibility.update();
749
750 // Prepare the data for the AJAX request.
751 const request_data = {
752 action: 'tablepress_preview_table',
753 _ajax_nonce: tp.nonces.preview_table,
754 tablepress: {
755 id: tp.table.id,
756 new_id: tp.table.new_id,
757 name: $( '#table-name' ).value,
758 description: $( '#table-description' ).value,
759 data: JSON.stringify( tp.editor.options.data ),
760 options: JSON.stringify( tp.table.options ),
761 visibility: JSON.stringify( tp.table.visibility ),
762 number: {
763 rows: tp.editor.options.data.length,
764 columns: tp.editor.options.columns.length,
765 },
766 },
767 };
768
769 // Add spinner, disable "Preview" buttons, and change cursor.
770 event.target.parentNode.insertAdjacentHTML( 'beforeend', `<span id="spinner-table-preview" class="spinner-table-preview spinner is-active" title="${ __( 'The Table Preview is being loaded …', 'tablepress' ) }"/>` );
771 $( '.button-preview' ).forEach( ( button ) => button.classList.add( 'disabled' ) );
772 document.body.classList.add( 'wait' );
773
774 // Load the table preview data from the server via an AJAX request.
775 fetch( ajaxurl, {
776 method: 'POST',
777 headers: {
778 'Content-Type': 'application/x-www-form-urlencoded',
779 Accept: 'application/json',
780 },
781 body: buildQueryString( request_data ),
782 } )
783 // Check for HTTP connection problems.
784 .then( ( response ) => {
785 if ( ! response.ok ) {
786 throw new Error( `There was a problem with the server, HTTP response code ${ response.status } (${ response.statusText }).` );
787 }
788 return response.json();
789 } )
790 // Check for problems with the transmitted data.
791 .then( ( data ) => {
792 if ( 'undefined' === typeof data || null === data || '-1' === data || 'undefined' === typeof data.success ) {
793 throw new Error( 'The JSON data returned from the server is unclear or incomplete.' );
794 }
795
796 if ( true !== data.success ) {
797 throw new Error( 'The preview could not be loaded.' );
798 }
799
800 tp.callbacks.table_preview.success( data );
801 } )
802 // Handle errors.
803 .catch( ( error ) => tp.callbacks.table_preview.error( error.message ) )
804 .finally( () => {
805 $( '#spinner-table-preview' ).remove();
806 $( '.button-preview' ).forEach( ( button ) => button.classList.remove( 'disabled' ) );
807 document.body.classList.remove( 'wait' );
808 } );
809 };
810
811 /**
812 * [success description]
813 *
814 * @param {[type]} data [description]
815 */
816 tp.callbacks.table_preview.success = function ( data ) {
817 const $iframe = $( '#table-preview-iframe' );
818 $iframe.src = '';
819 $iframe.srcdoc = `<!DOCTYPE html><html><head>${ data.head_html }</head><body>${ data.body_html }</body></html>`;
820
821 tp.callbacks.table_preview.$dialog.wpdialog( 'open' );
822 };
823
824 /**
825 * [error description]
826 *
827 * @param {[type]} message [description]
828 */
829 tp.callbacks.table_preview.error = function ( message ) {
830 message = __( 'Attention: Unfortunately, an error occurred.', 'tablepress' ) + ' ' + message;
831 const div_id = `show-preview-${ Date.now() }`;
832
833 $( '#spinner-table-preview' ).parentNode.insertAdjacentHTML( 'afterend', `<div id="${ div_id }" class="ajax-alert notice notice-error"><p>${ message }</p></div>` );
834
835 const $notice = $( `#${ div_id }` );
836 void $notice.offsetWidth; // Trick browser layout engine. Necessary to make CSS transition work.
837 $notice.style.opacity = 0;
838 $notice.addEventListener( 'transitionend', () => $notice.remove() );
839 };
840
841 tp.callbacks.save_changes = {};
842
843 /**
844 * Save Changes to the server.
845 *
846 * @param {Event} event [description]
847 */
848 tp.callbacks.save_changes.process = function ( event ) {
849 // Validate input fields.
850 if ( ! tp.helpers.options.validate_fields() ) {
851 return;
852 }
853
854 // Collect information about hidden rows and columns.
855 tp.helpers.visibility.update();
856
857 // Prepare the data for the AJAX request.
858 const request_data = {
859 action: 'tablepress_save_table',
860 _ajax_nonce: tp.nonces.edit_table,
861 tablepress: {
862 id: tp.table.id,
863 new_id: tp.table.new_id,
864 name: $( '#table-name' ).value,
865 description: $( '#table-description' ).value,
866 data: JSON.stringify( tp.editor.options.data ),
867 options: JSON.stringify( tp.table.options ),
868 visibility: JSON.stringify( tp.table.visibility ),
869 number: {
870 rows: tp.editor.options.data.length,
871 columns: tp.editor.options.columns.length,
872 },
873 },
874 };
875
876 // Add spinner, disable "Save Changes" buttons, and change cursor.
877 event.target.parentNode.insertAdjacentHTML( 'beforeend', `<span id="spinner-save-changes" class="spinner-save-changes spinner is-active" title="${ __( 'Changes are being saved …', 'tablepress' ) }"/>` );
878 $( '.button-save-changes' ).forEach( ( button ) => ( button.disabled = true ) );
879 document.body.classList.add( 'wait' );
880
881 // Save the table data to the server via an AJAX request.
882 fetch( ajaxurl, {
883 method: 'POST',
884 headers: {
885 'Content-Type': 'application/x-www-form-urlencoded',
886 Accept: 'application/json',
887 },
888 body: buildQueryString( request_data ),
889 } )
890 // Check for HTTP connection problems.
891 .then( ( response ) => {
892 if ( ! response.ok ) {
893 throw new Error( `There was a problem with the server, HTTP response code ${ response.status } (${ response.statusText }).` );
894 }
895 return response.json();
896 } )
897 // Check for problems with the transmitted data.
898 .then( ( data ) => {
899 if ( 'undefined' === typeof data || null === data || '-1' === data || 'undefined' === typeof data.success ) {
900 throw new Error( 'The JSON data returned from the server is unclear or incomplete.' );
901 }
902
903 if ( true !== data.success ) {
904 const error_introduction = __( 'These errors were encountered:', 'tablepress' );
905 const debug_html = data.error_details ? `</p><p>${ error_introduction }</p><pre>${ data.error_details }</pre><p>` : '';
906 throw new Error( `The table could not be saved to the database properly.${ debug_html }` );
907 }
908
909 tp.callbacks.save_changes.success( data );
910 } )
911 // Handle errors.
912 .catch( ( error ) => tp.callbacks.save_changes.error( error.message ) )
913 .finally( () => {
914 $( '#spinner-save-changes' ).remove();
915 $( '.button-save-changes' ).forEach( ( button ) => ( button.disabled = false ) );
916 document.body.classList.remove( 'wait' );
917 } );
918 };
919
920 /**
921 * [success description]
922 *
923 * @param {[type]} data [description]
924 */
925 tp.callbacks.save_changes.success = function ( data ) {
926 // Saving was successful, so the original ID has changed to the (maybe) new ID -> we need to adjust all occurrences.
927 if ( tp.table.id !== data.table_id && window?.history?.pushState ) {
928 // Update URL, but only if the table ID changed, to not get dummy entries in the browser history.
929 window.history.pushState( '', '', window.location.href.replace( /table_id=[0-9a-zA-Z-_]+/gi, `table_id=${ data.table_id }` ) );
930 }
931
932 // Update table ID in input field.
933 tp.table.id = data.table_id;
934 tp.table.new_id = data.table_id;
935 $( '#table-id' ).value = data.table_id;
936 const $shortcode_field = $( '#table-information-shortcode' );
937 if ( $shortcode_field ) {
938 $shortcode_field.value = `[${ tp.table.shortcode } id=${ data.table_id } /]`;
939 }
940
941 // Update the nonces.
942 tp.nonces.edit_table = data.new_edit_nonce;
943 tp.nonces.preview_table = data.new_preview_nonce;
944 tp.nonces.copy_table = data.new_copy_nonce;
945 tp.nonces.delete_table = data.new_delete_nonce;
946
947 // Update URLs in Preview, Copy, and Delete links/buttons.
948 [ 'preview', 'copy', 'delete' ].forEach( ( action ) => {
949 $( `.button-${ action }` ).forEach( ( button ) => {
950 button.href = button.href
951 .replace( /item=[a-zA-Z0-9_-]+/g, `item=${ data.table_id }` ) // Updates both the "item" and the "return_item" parameters.
952 .replace( /&_wpnonce=[a-z0-9]+/ig, `&_wpnonce=${ data[ `new_${ action }_nonce` ] }` );
953 } );
954 } );
955
956 // Update URL in Export links/buttons.
957 $( '.button-export' ).forEach( ( button ) => {
958 button.href = button.href
959 .replace( /table_id=[a-zA-Z0-9_-]+/g, `table_id=${ data.table_id }` );
960 } );
961
962 // Update last-modified date and user nickname.
963 $( '#last-modified' ).textContent = data.last_modified;
964 $( '#last-editor' ).textContent = data.last_editor;
965
966 tp.helpers.unsaved_changes.unset();
967
968 const action_messages = {};
969 action_messages.success_save = __( 'The table was saved successfully.', 'tablepress' );
970 action_messages.success_save_success_id_change = action_messages.success_save + ' ' + __( 'The table ID was changed.', 'tablepress' );
971 action_messages.success_save_error_id_change = action_messages.success_save + ' ' + __( 'The table ID could not be changed, probably because the new ID is already in use!', 'tablepress' );
972
973 if ( 'success_save_error_id_change' === data.message && data.error_details ) {
974 const error_introduction = __( 'These errors were encountered:', 'tablepress' );
975 action_messages.success_save_error_id_change += `</p><p>${ error_introduction }</p><pre>${ data.error_details }</pre><p>`;
976 }
977
978 const type = ( data.message.includes( 'error' ) ) ? 'error' : 'success';
979 tp.callbacks.save_changes.after_saving_notice( type, action_messages[ data.message ] );
980 };
981
982 /**
983 * [error description]
984 *
985 * @param {[type]} message [description]
986 */
987 tp.callbacks.save_changes.error = function ( message ) {
988 message = __( 'Attention: Unfortunately, an error occurred.', 'tablepress' ) + ' ' + message;
989 tp.callbacks.save_changes.after_saving_notice( 'error', message );
990 };
991
992 /**
993 * [after_saving_notice description]
994 *
995 * @param {[type]} type [description]
996 * @param {[type]} message [description]
997 */
998 tp.callbacks.save_changes.after_saving_notice = function ( type, message ) {
999 const div_id = `save-changes-${ Date.now() }`;
1000
1001 $( '#spinner-save-changes' ).parentNode.insertAdjacentHTML( 'afterend', `<div id="${ div_id }" class="ajax-alert notice notice-${ type }"><p>${ message }</p></div>` );
1002
1003 const $notice = $( `#${ div_id }` );
1004 void $notice.offsetWidth; // Trick browser layout engine. Necessary to make CSS transition work.
1005 $notice.style.opacity = 0;
1006 $notice.addEventListener( 'transitionend', () => $notice.remove() );
1007 };
1008
1009 tp.callbacks.screen_options = {};
1010
1011 /**
1012 * Updates table editor layout with new screen option values.
1013 *
1014 * @param {Event} event `input` event of the screen options fields.
1015 */
1016 tp.callbacks.screen_options.update = function ( event ) {
1017 if ( ! event.target ) {
1018 return;
1019 }
1020
1021 if ( 'table_editor_line_clamp' === event.target.id ) {
1022 tp.editor.el.style.setProperty( '--table-editor-line-clamp', parseInt( event.target.value, 10 ) );
1023 tp.editor.updateCornerPosition();
1024 return;
1025 }
1026
1027 if ( 'table_editor_column_width' === event.target.id ) {
1028 tp.screen_options.table_editor_column_width = parseInt( event.target.value, 10 );
1029 tp.screen_options.table_editor_column_width = Math.max( tp.screen_options.table_editor_column_width, 30 ); // Ensure a minimum column width of 30 pixesl.
1030 tp.screen_options.table_editor_column_width = Math.min( tp.screen_options.table_editor_column_width, 9999 ); // Ensure a maximum column width of 9999 pixesl.
1031 tp.editor.colgroup.forEach( ( col ) => col.setAttribute( 'width', tp.screen_options.table_editor_column_width ) );
1032 tp.editor.updateCornerPosition();
1033 return;
1034 }
1035 };
1036
1037 /**
1038 * Designates a screen option field to have been changed, so that the value is sent to the server when it is blurred.
1039 *
1040 * @param {Event} event `change` event of the screen options fields.
1041 */
1042 tp.callbacks.screen_options.set_was_changed = function ( event ) {
1043 if ( ! event.target ) {
1044 return;
1045 }
1046
1047 event.target.was_changed = true;
1048 };
1049
1050 /**
1051 * Saves screen options to the server after they have been changed and the field is blurred.
1052 *
1053 * @param {Event} event `blur` event of the screen options fields.
1054 */
1055 tp.callbacks.screen_options.save = function ( event ) {
1056 if ( ! event.target ) {
1057 return;
1058 }
1059
1060 if ( ! event.target.was_changed ) {
1061 return;
1062 }
1063
1064 event.target.was_changed = false;
1065
1066 // Prepare the data for the AJAX request.
1067 const request_data = {
1068 action: 'tablepress_save_screen_options',
1069 _ajax_nonce: tp.nonces.screen_options,
1070 tablepress: {
1071 [ event.target.id ]: parseInt( event.target.value, 10 ),
1072 },
1073 };
1074
1075 // Add spinner and change cursor.
1076 event.target.parentNode.insertAdjacentHTML( 'beforeend', `<span id="spinner-save-changes" class="spinner-save-changes spinner is-active" title="${ __( 'Changes are being saved …', 'tablepress' ) }"/>` );
1077 document.body.classList.add( 'wait' );
1078
1079 // Save the table data to the server via an AJAX request.
1080 fetch( ajaxurl, {
1081 method: 'POST',
1082 headers: {
1083 'Content-Type': 'application/x-www-form-urlencoded',
1084 Accept: 'application/json',
1085 },
1086 body: buildQueryString( request_data ),
1087 } )
1088 .finally( () => {
1089 $( '#spinner-save-changes' ).remove();
1090 document.body.classList.remove( 'wait' );
1091 } );
1092 };
1093
1094 tp.callbacks.table_id = tp.callbacks.table_id || {};
1095
1096 /**
1097 * [sanitize_table_id description]
1098 */
1099 tp.callbacks.table_id.sanitize = function () {
1100 this.value = this.value.replace( /[^0-9a-zA-Z-_]/g, '' );
1101 };
1102
1103 /**
1104 * [change_table_id description]
1105 */
1106 tp.callbacks.table_id.change = function () {
1107 // The table IDs "" and "0" are not allowed, or in other words, the table ID has to fulfill /[A-Za-z1-9-_]|[A-Za-z0-9-_]{2,}/.
1108 if ( '' === this.value || '0' === this.value ) {
1109 window.alert( __( 'This table ID is invalid. Please enter a different table ID.', 'tablepress' ) );
1110 this.value = tp.table.new_id;
1111 this.focus();
1112 this.select();
1113 return;
1114 }
1115
1116 if ( ! window.confirm( __( 'Do you really want to change the Table ID? All blocks and Shortcodes for this table in your posts and pages will have to be adjusted!', 'tablepress' ) ) ) {
1117 this.value = tp.table.new_id;
1118 return;
1119 }
1120
1121 // Set the new table ID.
1122 tp.table.new_id = this.value;
1123 const $shortcode_field = $( '#table-information-shortcode' );
1124 if ( $shortcode_field ) {
1125 $shortcode_field.value = `[${ tp.table.shortcode } id=${ tp.table.new_id } /]`;
1126 $shortcode_field.focus();
1127 $shortcode_field.select();
1128 }
1129 tp.helpers.unsaved_changes.set();
1130 };
1131
1132 /**
1133 * Inserts or duplicates rows or columns before each currently selected row/column.
1134 *
1135 * @param {string} action The action to perform on the selected rows/columns ("insert" or "duplicate").
1136 * @param {string} type What to insert or duplicate ("rows" or "columns").
1137 * @param {string} position Where to insert or duplicate ("before" or "after"). Default "before".
1138 */
1139 tp.callbacks.insert_duplicate = function ( action, type, position = 'before' ) {
1140 const handling_rows = ( 'rows' === type );
1141 const insert_function = handling_rows ? tp.editor.insertRow : tp.editor.insertColumn;
1142 const getData_function = handling_rows ? tp.editor.getRowData : tp.editor.getColumnData;
1143 const duplicating = ( 'duplicate' === action );
1144 // Dynamically set the event handler, so that we have the action available in it.
1145 tp.editor.options[ handling_rows ? 'oninsertrow' : 'oninsertcolumn' ] = tp.callbacks.editor.oninsertroc.bind( null, type, action );
1146 tp.helpers.selection[ type ].forEach( function ( roc_idx, array_idx ) {
1147 const shifted_roc_idx = roc_idx + array_idx; // Not having to deal with shifted indices is possible by looping through the reversed array, but that's likely slower.
1148 const data = duplicating ? getData_function( shifted_roc_idx ) : 1;
1149 const position_bool = 'before' === position; // true means "before".
1150 insert_function( data, shifted_roc_idx, position_bool );
1151 } );
1152 tp.helpers.unsaved_changes.set();
1153
1154 // Select both inserted/duplicated rows/columns if more than one were selected.
1155 const num_selected_rocs = tp.helpers.selection[ type ].length;
1156 if ( num_selected_rocs > 1 ) {
1157 tp.editor.updateSelectionFromCoords(
1158 tp.helpers.selection.columns[0],
1159 tp.helpers.selection.rows[0],
1160 handling_rows ? tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ] : tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ] + num_selected_rocs,
1161 handling_rows ? tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ] + num_selected_rocs : tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ]
1162 );
1163 }
1164 };
1165
1166 /**
1167 * Removes currently selected rows or columns.
1168 *
1169 * @param {string} type What to remove ("rows" or "columns").
1170 */
1171 tp.callbacks.remove = function ( type ) {
1172 const handling_rows = 'rows' === type;
1173 const num_cors = handling_rows ? tp.editor.options.columns.length : tp.editor.options.data.length;
1174 const last_roc_idx = handling_rows ? tp.editor.options.data.length - 1 : tp.editor.options.columns.length - 1;
1175
1176 // Visibility meta information has to be deleted manually, as otherwise the Jspreadsheet meta information can get out of sync.
1177 if ( tp.editor.options.meta ) {
1178 tp.helpers.selection[ type ].forEach( function ( roc_idx ) {
1179 for ( let cor_idx = 0; cor_idx < num_cors; cor_idx++ ) {
1180 const cell_idx = handling_rows ? [ cor_idx, roc_idx ] : [ roc_idx, cor_idx ];
1181 delete tp.editor.options.meta[ jspreadsheet.getColumnNameFromId( cell_idx ) ];
1182 }
1183 } );
1184 }
1185
1186 const delete_function = handling_rows ? tp.editor.deleteRow : tp.editor.deleteColumn;
1187 delete_function( tp.helpers.selection[ type ][0], tp.helpers.selection[ type ].length );
1188 tp.helpers.unsaved_changes.set();
1189
1190 // Reselect last visible row/column, if last rows/columns were deleted.
1191 if ( last_roc_idx === tp.helpers.selection[ type ][ tp.helpers.selection[ type ].length - 1 ] ) {
1192 const col_idx = handling_rows ? tp.helpers.selection.columns[0] : tp.helpers.selection.columns[0] - 1;
1193 const row_idx = handling_rows ? tp.helpers.selection.rows[0] - 1 : tp.helpers.selection.rows[0];
1194 tp.editor.updateSelectionFromCoords( col_idx, row_idx, col_idx, row_idx );
1195 }
1196 };
1197
1198 /**
1199 * Appends rows or columns at the bottom or right end of the table.
1200 *
1201 * @param {string} type What to append ("rows" or "columns").
1202 * @param {number} num_rocs Number of rows or columns to append.
1203 */
1204 tp.callbacks.append = function ( type, num_rocs ) {
1205 const handling_rows = ( 'rows' === type );
1206 const insert_function = handling_rows ? tp.editor.insertRow : tp.editor.insertColumn;
1207 // Dynamically set the event handler, so that we have the action available in it.
1208 tp.editor.options[ handling_rows ? 'oninsertrow' : 'oninsertcolumn' ] = tp.callbacks.editor.oninsertroc.bind( null, type, 'append' );
1209 insert_function( num_rocs );
1210 tp.helpers.unsaved_changes.set();
1211 };
1212
1213 /**
1214 * Moves currently selected rows or columns.
1215 *
1216 * @param {string} direction Where to move the selected rows or columns (for rows: "up"/"down"/"top"/"bottom", for columns: "left"/right"/"first"/"last").
1217 * @param {string} type What to move ("rows" or "columns").
1218 */
1219 tp.callbacks.move = function ( direction, type ) {
1220 const handling_rows = ( 'rows' === type );
1221
1222 // Default case: up/left
1223 let rocs = tp.helpers.selection[ type ]; // When moving up or left, start with the first row/column of the selected range.
1224 let position_difference = -1; // New row/column number is one smaller than current row/column number.
1225 // Alternate case: down/right
1226 if ( 'down' === direction || 'right' === direction ) {
1227 rocs = rocs.slice().reverse(); // When moving down or right, reverse the order, to start with the last row/column of the selected range. slice() is needed here to create an array copy.
1228 position_difference = 1; // New row/column number is one higher than current row/column number.
1229 } else if ( 'top' === direction || 'first' === direction ) {
1230 position_difference = -rocs[0];
1231 } else if ( 'bottom' === direction || 'last' === direction ) {
1232 rocs = rocs.slice().reverse(); // When moving down or right, reverse the order, to start with the last row/column of the selected range. slice() is needed here to create an array copy.
1233 const min_max_roc = ( 'rows' === type ) ? tp.editor.options.data.length - 1 : tp.editor.options.columns.length - 1;
1234 position_difference = min_max_roc - rocs[0];
1235 }
1236
1237 // Bail early if there is nothing to do (e.g. when the selected range is already at the target edge).
1238 if ( 0 === position_difference ) {
1239 return;
1240 }
1241
1242 // Move the selected rows/columns individually.
1243 const move_function = handling_rows ? tp.editor.moveRow : tp.editor.moveColumn;
1244 rocs.forEach( ( roc_idx ) => move_function( roc_idx, roc_idx + position_difference ) );
1245 tp.helpers.unsaved_changes.set();
1246
1247 // Reselect moved selection.
1248 tp.editor.updateSelectionFromCoords(
1249 handling_rows ? tp.helpers.selection.columns[0] : tp.helpers.selection.columns[0] + position_difference,
1250 handling_rows ? tp.helpers.selection.rows[0] + position_difference : tp.helpers.selection.rows[0],
1251 handling_rows ? tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ] : tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ] + position_difference,
1252 handling_rows ? tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ] + position_difference : tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ]
1253 );
1254 };
1255
1256 /**
1257 * Sorts the table data by the first currently selected column.
1258 *
1259 * @param {string} direction Sort order/direction ("asc" for ascending, "desc" for descending).
1260 */
1261 tp.callbacks.sort = function ( direction ) {
1262 tp.editor.orderBy( tp.helpers.selection.columns[0], ( 'desc' === direction ) );
1263 };
1264
1265 /**
1266 * Hides or unhides selected rows or columns.
1267 *
1268 * @param {string} action The action to perform on the rows/columns ("hide" or "unhide").
1269 * @param {string} type What to hide or unhide ("rows" or "columns").
1270 */
1271 tp.callbacks.hide_unhide = function ( action, type ) {
1272 const handling_rows = ( 'rows' === type );
1273 const property = handling_rows ? 'row_hidden' : 'column_hidden';
1274 const num_cors = handling_rows ? tp.editor.options.columns.length : tp.editor.options.data.length;
1275 const cell_hidden = ( 'hide' === action );
1276 const meta = {};
1277 tp.helpers.selection[ type ].forEach( function ( roc_idx ) {
1278 for ( let cor_idx = 0; cor_idx < num_cors; cor_idx++ ) {
1279 const cell_idx = handling_rows ? [ cor_idx, roc_idx ] : [ roc_idx, cor_idx ];
1280 const cell_name = jspreadsheet.getColumnNameFromId( cell_idx );
1281 meta[ cell_name ] = {};
1282 meta[ cell_name ][ property ] = cell_hidden;
1283 }
1284 } );
1285 tp.editor.setMeta( meta );
1286 tp.helpers.unsaved_changes.set();
1287 tp.editor.updateTable(); // Redraw table.
1288 };
1289
1290 /**
1291 * Combines/merges the currently selected cells.
1292 */
1293 tp.callbacks.merge_cells = function () {
1294 const current_col_idx = tp.helpers.selection.columns[0];
1295 const current_row_idx = tp.helpers.selection.rows[0];
1296 const colspan = tp.helpers.selection.columns.length;
1297 const rowspan = tp.helpers.selection.rows.length;
1298 for ( let row_idx = 1; row_idx < rowspan; row_idx++ ) {
1299 tp.editor.setValueFromCoords( current_col_idx, current_row_idx + row_idx, '#rowspan#' );
1300 }
1301 for ( let col_idx = 1; col_idx < colspan; col_idx++ ) {
1302 tp.editor.setValueFromCoords( current_col_idx + col_idx, current_row_idx, '#colspan#' );
1303 }
1304 for ( let row_idx = 1; row_idx < rowspan; row_idx++ ) {
1305 for ( let col_idx = 1; col_idx < colspan; col_idx++ ) {
1306 tp.editor.setValueFromCoords( current_col_idx + col_idx, current_row_idx + row_idx, '#span#' );
1307 }
1308 }
1309 tp.helpers.unsaved_changes.set();
1310 };
1311
1312 /**
1313 * Registers keyboard events and triggers corresponding actions by emulating button clicks.
1314 *
1315 * @param {Event} event Keyboard event.
1316 */
1317 tp.callbacks.keyboard_shortcuts = function ( event ) {
1318 let action = '';
1319 let move_direction = '';
1320 let move_type = '';
1321
1322 if ( event.ctrlKey || event.metaKey ) {
1323 if ( 80 === event.keyCode ) {
1324 // Preview: Ctrl/Cmd + P.
1325 action = 'preview';
1326 } else if ( 83 === event.keyCode ) {
1327 // Save Changes: Ctrl/Cmd + S.
1328 action = 'save-changes';
1329 } else if ( 76 === event.keyCode ) {
1330 // Insert Link: Ctrl/Cmd + L.
1331 action = 'insert_link';
1332 } else if ( 73 === event.keyCode ) {
1333 // Insert Image: Ctrl/Cmd + I.
1334 action = 'insert_image';
1335 } else if ( 69 === event.keyCode ) {
1336 // Advanced Editor: Ctrl/Cmd + E.
1337 action = 'advanced_editor';
1338 } else if ( event.shiftKey && event.altKey && 38 === event.keyCode ) {
1339 // Move up: Ctrl/Cmd + Alt/Option + Shift + ↑.
1340 action = 'move';
1341 move_direction = 'top';
1342 move_type = 'rows';
1343 } else if ( event.shiftKey && event.altKey && 40 === event.keyCode ) {
1344 // Move down: Ctrl/Cmd + Alt/Option + Shift + ↓.
1345 action = 'move';
1346 move_direction = 'bottom';
1347 move_type = 'rows';
1348 } else if ( event.shiftKey && event.altKey && 37 === event.keyCode ) {
1349 // Move left: Ctrl/Cmd + Alt/Option + Shift + ←.
1350 action = 'move';
1351 move_direction = 'first';
1352 move_type = 'columns';
1353 } else if ( event.shiftKey && event.altKey && 39 === event.keyCode ) {
1354 // Move r: Ctrl/Cmd + Alt/Option + Shift + →.
1355 action = 'move';
1356 move_direction = 'last';
1357 move_type = 'columns';
1358 } else if ( event.shiftKey && 38 === event.keyCode ) {
1359 // Move up: Ctrl/Cmd + Shift + ↑.
1360 action = 'move';
1361 move_direction = 'up';
1362 move_type = 'rows';
1363 } else if ( event.shiftKey && 40 === event.keyCode ) {
1364 // Move down: Ctrl/Cmd + Shift + ↓.
1365 action = 'move';
1366 move_direction = 'down';
1367 move_type = 'rows';
1368 } else if ( event.shiftKey && 37 === event.keyCode ) {
1369 // Move left: Ctrl/Cmd + Shift + ←.
1370 action = 'move';
1371 move_direction = 'left';
1372 move_type = 'columns';
1373 } else if ( event.shiftKey && 39 === event.keyCode ) {
1374 // Move r: Ctrl/Cmd + Shift + →.
1375 action = 'move';
1376 move_direction = 'right';
1377 move_type = 'columns';
1378 }
1379 }
1380
1381 if ( 'save-changes' === action || 'preview' === action ) {
1382 // Blur the focussed element to make sure that all change events were triggered.
1383 document.activeElement.blur(); // eslint-disable-line @wordpress/no-global-active-element
1384
1385 /*
1386 * Emulate a click on the button corresponding to the action.
1387 * This way, things like notices will be shown, compared to directly calling the buttons' callbacks.
1388 */
1389 document.querySelector( `#tablepress_edit-buttons-2-submit .button-${ action }` ).click();
1390
1391 // Prevent the browser's native handling of the shortcut, i.e. showing the Save or Print dialogs.
1392 event.preventDefault();
1393 } else if ( 'insert_link' === action || 'insert_image' === action || 'advanced_editor' === action ) {
1394 // Only open the dialogs if an element in the table editor is focussed, to e.g. prevent multiple dialogs to be opened.
1395 if ( $( '#table-editor' ).contains( document.activeElement ) ) { // eslint-disable-line @wordpress/no-global-active-element
1396 const $active_textarea = ( 'TEXTAREA' === document.activeElement.tagName ) ? document.activeElement : null; // eslint-disable-line @wordpress/no-global-active-element
1397 // Open the "Insert Link", "Insert Image", or Advanced Editor" dialog.
1398 tp.callbacks[ action ].open_dialog( $active_textarea );
1399 }
1400
1401 // Prevent the browser's native handling of the shortcut.
1402 event.preventDefault();
1403 } else if ( 'move' === action ) {
1404 // Only move rows or columns if an element in the table editor is focussed, but not if the cell is being edited (to not prevent the browser's original shortcuts).
1405 if ( $( '#table-editor' ).contains( document.activeElement ) && 'TEXTAREA' !== document.activeElement.tagName ) { // eslint-disable-line @wordpress/no-global-active-element
1406 // Move the selected rows or columns.
1407 if ( tp.helpers.move_allowed( move_type, move_direction ) ) {
1408 tp.callbacks.move( move_direction, move_type );
1409 }
1410 }
1411
1412 // Stop the event propagation so that Jspreadsheet doesn't understand the arrow key as movement of the cursor, and prevent the browser's native handling of the shortcut.
1413 event.stopImmediatePropagation();
1414 }
1415 };
1416
1417 /*
1418 * Initialize Jspreadsheet.
1419 */
1420 tp.editor = jspreadsheet( $( '#table-editor' ), {
1421 data: tp.table.data,
1422 meta: tp.helpers.visibility.load(),
1423 wordWrap: true,
1424 rowDrag: true,
1425 rowResize: true,
1426 columnSorting: true,
1427 columnDrag: true,
1428 columnResize: true,
1429 defaultColWidth: tp.screen_options.table_editor_column_width,
1430 defaultColAlign: 'left',
1431 parseFormulas: false,
1432 allowExport: false,
1433 allowComments: false,
1434 allowManualInsertRow: false, // To prevent addition of new row when Enter is pressed in last row.
1435 allowManualInsertColumn: false, // To prevent addition of new column when Tab is pressed in last column.
1436 about: false,
1437 secureFormulas: false,
1438 detachForUpdates: true,
1439 onselection: tp.callbacks.editor.onselection,
1440 updateTable: tp.callbacks.editor.onupdatetable,
1441 contextMenu,
1442 sorting: tp.helpers.editor.sorting,
1443 // Keep the selection when certain events occur and the table loses focus.
1444 onmoverow: tp.callbacks.editor.onmove,
1445 onmovecolumn: tp.callbacks.editor.onmove,
1446 onblur: tp.helpers.editor.reselect,
1447 onload: tp.helpers.editor.reselect, // When the table is loaded, select the top-left cell A1.
1448 onchange: tp.helpers.unsaved_changes.set,
1449 onsort: tp.callbacks.editor.onsort,
1450 } );
1451
1452 tp.helpers.options.load();
1453
1454 /*
1455 * Register click callback for the "Preview" and "Save Changes" buttons.
1456 */
1457 $( '#tablepress-page' ).addEventListener( 'click', ( event ) => {
1458 if ( ! event.target ) {
1459 return;
1460 }
1461
1462 if ( event.target.matches( '.button-preview' ) ) {
1463 tp.callbacks.table_preview.process( event );
1464 return;
1465 }
1466
1467 if ( event.target.matches( '.button-save-changes' ) ) {
1468 tp.callbacks.save_changes.process( event );
1469 return;
1470 }
1471
1472 if ( event.target.matches( '.button-show-help-box' ) ) {
1473 tp.callbacks.help_box.open_dialog( event );
1474 return;
1475 }
1476 } );
1477
1478 /*
1479 * Register click callbacks for the table manipulation buttons.
1480 */
1481 $( '#tablepress-manipulation-controls' ).addEventListener( 'click', ( event ) => {
1482 if ( ! event.target ) {
1483 return;
1484 }
1485
1486 /*
1487 * Events that don't require a selection.
1488 */
1489
1490 if ( event.target.matches( '.button-append' ) ) {
1491 const type = event.target.dataset.type;
1492 const $input_field = $( `#${ type }-append-number` );
1493 const num_rocs = parseInt( $input_field.value, 10 );
1494 if ( isNaN( num_rocs ) || num_rocs < 1 || num_rocs > 99999 ) {
1495 const message = ( 'rows' === event.target.dataset.type ) ? __( 'The value for the number of rows is invalid!', 'tablepress' ) : __( 'The value for the number of columns is invalid!', 'tablepress' );
1496 window.alert( message );
1497 $input_field.focus();
1498 $input_field.select();
1499 return;
1500 }
1501
1502 tp.callbacks.append( type, num_rocs );
1503 return;
1504 }
1505
1506 /*
1507 * Events that do require a selection.
1508 */
1509
1510 if ( 'button-insert-link' === event.target.id ) {
1511 tp.callbacks.insert_link.open_dialog();
1512 return;
1513 }
1514
1515 if ( 'button-insert-image' === event.target.id ) {
1516 tp.callbacks.insert_image.open_dialog();
1517 return;
1518 }
1519
1520 if ( 'button-advanced-editor' === event.target.id ) {
1521 tp.callbacks.advanced_editor.open_dialog();
1522 return;
1523 }
1524
1525 if ( event.target.matches( '.button-insert-duplicate' ) ) {
1526 tp.callbacks.insert_duplicate( event.target.dataset.action, event.target.dataset.type );
1527 return;
1528 }
1529
1530 if ( event.target.matches( '.button-move' ) ) {
1531 if ( ! tp.helpers.move_allowed( event.target.dataset.type, event.target.dataset.direction ) ) {
1532 window.alert( __( 'You can not do this move, because you reached the border of the table.', 'tablepress' ) );
1533 return;
1534 }
1535 tp.callbacks.move( event.target.dataset.direction, event.target.dataset.type );
1536 return;
1537 }
1538
1539 if ( event.target.matches( '.button-remove' ) ) {
1540 const handling_rows = ( 'rows' === event.target.dataset.type );
1541 const num_rocs = handling_rows ? tp.editor.options.data.length : tp.editor.options.columns.length;
1542
1543 if ( num_rocs === tp.helpers.selection[ event.target.dataset.type ].length ) {
1544 const message = handling_rows ? __( 'You can not delete all table rows!', 'tablepress' ) : __( 'You can not delete all table columns!', 'tablepress' );
1545 window.alert( message );
1546 return;
1547 }
1548
1549 tp.callbacks.remove( event.target.dataset.type );
1550 return;
1551 }
1552
1553 if ( event.target.matches( '.button-merge-unmerge' ) ) {
1554 if ( tp.helpers.cell_merge_allowed( 'alert' ) ) {
1555 tp.callbacks.merge_cells();
1556 }
1557 return;
1558 }
1559
1560 if ( event.target.matches( '.button-hide-unhide' ) ) {
1561 tp.callbacks.hide_unhide( event.target.dataset.action, event.target.dataset.type );
1562 return;
1563 }
1564 } );
1565
1566 // Register callbacks for the table ID text field.
1567 const $table_id_field = $( '#table-id' );
1568 $table_id_field.addEventListener( 'input', tp.callbacks.table_id.sanitize );
1569 $table_id_field.addEventListener( 'change', tp.callbacks.table_id.change );
1570
1571 // Select Shortcode input field content when it's focussed.
1572 const $table_information_shortcode = $( '#table-information-shortcode' );
1573 if ( $table_information_shortcode ) {
1574 $table_information_shortcode.addEventListener( 'focus', function() {
1575 this.select();
1576 } );
1577 }
1578
1579 // Register callback for inserting a link into a cell after it has been constructed in the wpLink dialog.
1580 jQuery( '#textarea-insert-helper' ).on( 'change', tp.helpers.editor.insert_from_helper_textarea ); // This must use jQuery, as wpLink triggers jQuery events, which can not be observed by native JS listeners.
1581
1582 // Register change callbacks for the table name, description, and options.
1583 [ '#table-name', '#table-description' ].forEach( ( field_id ) => $( field_id ).addEventListener( 'change', tp.helpers.unsaved_changes.set ) );
1584 const options_meta_boxes = apply_filters( 'tablepress.optionsMetaBoxes', [ '#tablepress_edit-table-options', '#tablepress_edit-datatables-features' ] );
1585 options_meta_boxes.forEach( ( meta_box_id ) => $( meta_box_id ).addEventListener( 'change', tp.helpers.options.change ) );
1586
1587 // Move all "Help" buttons inside the postbox header.
1588 document.querySelectorAll( '#tablepress-body .button-module-help' ).forEach( ( $button ) => ( $button.closest( '.postbox' ).querySelector( '.handle-actions' ).prepend( $button ) ) );
1589
1590 // Register callbacks for the screen options.
1591 const $tablepress_screen_options = $( '#tablepress-screen-options' );
1592 $tablepress_screen_options.addEventListener( 'input', tp.callbacks.screen_options.update );
1593 $tablepress_screen_options.addEventListener( 'change', tp.callbacks.screen_options.set_was_changed );
1594 $tablepress_screen_options.addEventListener( 'focusout', tp.callbacks.screen_options.save ); // Use the `focusout` event instead of `blur` as that does not bubble.
1595
1596 // Register keyboard shortcut handler.
1597 window.addEventListener( 'keydown', tp.callbacks.keyboard_shortcuts, true );
1598
1599 // Add keyboard shortcuts as title attributes to "Preview" and "Save Changes" buttons, with correct modifier key for Mac/non-Mac.
1600 const modifier_key = ( window?.navigator?.platform?.includes( 'Mac' ) ) ?
1601 _x( '', 'keyboard shortcut modifier key on a Mac keyboard', 'tablepress' ) :
1602 _x( 'Ctrl+', 'keyboard shortcut modifier key on a non-Mac keyboard', 'tablepress' );
1603 document.querySelectorAll( '.button[data-shortcut]' ).forEach( ( $button ) => {
1604 const shortcut = sprintf( $button.dataset.shortcut, modifier_key ); // eslint-disable-line @wordpress/valid-sprintf
1605 $button.title = sprintf( __( 'Keyboard Shortcut: %s', 'tablepress' ), shortcut );
1606 } );
1607
1608 // This code requires jQuery, and it must run when the DOM is ready. Therefore, move it outside of the main function.
1609 jQuery( function () {
1610 // Fix issue with wpLink input fields not being usable, when called through the "Advanced Editor". They are immediately losing focus without this.
1611 jQuery( '#wp-link' ).on( 'focus', 'input', function ( event ) {
1612 event.stopPropagation();
1613 } );
1614
1615 // Fix issue with Media Library input fields in the sidebar not being usable, when called through the "Advanced Editor". They are immediately losing focus without this.
1616 jQuery( 'body' ).on( 'focus', '.media-modal .media-frame-content input, .media-modal .media-frame-content textarea', function ( event ) {
1617 event.stopPropagation();
1618 } );
1619 } );
1620