PluginProbe
TablePress – Tables in WordPress made easy / 3.0
TablePress – Tables in WordPress made easy v3.0
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 / editor.js

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

1,145 lines 44.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 { applyFilters, doAction } from '@wordpress/hooks';
17 import { __, sprintf } from '@wordpress/i18n';
18 import { buildQueryString } from '@wordpress/url';
19
20 /**
21 * Internal dependencies.
22 */
23 import { $ } from '../common/functions';
24 import contextMenu from './data/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
33 // Initial selection: cell A1.
34 tp.helpers.selection = tp.helpers.selection || {
35 rows: [ 0 ],
36 columns: [ 0 ],
37 };
38
39 tp.helpers.unsaved_changes = tp.helpers.unsaved_changes || {};
40
41 /**
42 * [unsaved_changes.unload_dialog description]
43 *
44 * @param {Event} event [description]
45 */
46 tp.helpers.unsaved_changes.unload_dialog = function ( event ) {
47 event.preventDefault(); // Cancel the event as stated by the standard.
48 event.returnValue = ''; // Chrome requires returnValue to be set.
49 };
50
51 /**
52 * [unsaved_changes.set description]
53 */
54 tp.helpers.unsaved_changes.set = function () {
55 // Bail early if this function was already called.
56 if ( tp.made_changes ) {
57 return;
58 }
59 tp.made_changes = true;
60 window.addEventListener( 'beforeunload', tp.helpers.unsaved_changes.unload_dialog );
61 };
62
63 /**
64 * [unsaved_changes.unset description]
65 */
66 tp.helpers.unsaved_changes.unset = function () {
67 tp.made_changes = false;
68 window.removeEventListener( 'beforeunload', tp.helpers.unsaved_changes.unload_dialog );
69 };
70
71 tp.helpers.meta = tp.helpers.meta || {};
72
73 /**
74 * Sets table meta to new values and triggers the necessary updates.
75 *
76 * @param {Object} meta The meta data ({metaName: value}) to update.
77 */
78 tp.helpers.meta.update = function ( meta ) {
79 tp.table.meta = { ...tp.table.meta, ...meta };
80
81 // Trigger the "metaUpdated" action for the updated meta keys.
82 doAction( 'tablepress.metaUpdated', meta );
83 tp.helpers.unsaved_changes.set();
84 };
85
86 tp.helpers.visibility = tp.helpers.visibility || {};
87
88 /**
89 * [visibility.load description]
90 */
91 tp.helpers.visibility.load = function () {
92 const num_rows = tp.table.visibility.rows.length;
93 const num_columns = tp.table.visibility.columns.length;
94 const meta = {};
95 // Collect meta data for hidden rows.
96 for ( let row_idx = 0; row_idx < num_rows; row_idx++ ) {
97 if ( 1 === tp.table.visibility.rows[ row_idx ] ) {
98 continue;
99 }
100 for ( let col_idx = 0; col_idx < num_columns; col_idx++ ) {
101 const cell_name = jspreadsheet.getColumnNameFromId( [ col_idx, row_idx ] );
102 meta[ cell_name ] = meta[ cell_name ] || {};
103 meta[ cell_name ].row_hidden = true;
104 }
105 }
106 // Collect meta data for hidden columns.
107 for ( let col_idx = 0; col_idx < num_columns; col_idx++ ) {
108 if ( 1 === tp.table.visibility.columns[ col_idx ] ) {
109 continue;
110 }
111 for ( let row_idx = 0; row_idx < num_rows; row_idx++ ) {
112 const cell_name = jspreadsheet.getColumnNameFromId( [ col_idx, row_idx ] );
113 meta[ cell_name ] = meta[ cell_name ] || {};
114 meta[ cell_name ].column_hidden = true;
115 }
116 }
117 return meta;
118 };
119
120 /**
121 * [visibility.update description]
122 */
123 tp.helpers.visibility.update = function () {
124 // Set all rows and columns to visible first.
125 tp.table.visibility.rows = [];
126 for ( let row_idx = 0; row_idx < tp.editor.options.data.length; row_idx++ ) {
127 tp.table.visibility.rows[ row_idx ] = 1;
128 }
129 tp.table.visibility.columns = [];
130 for ( let col_idx = 0; col_idx < tp.editor.options.columns.length; col_idx++ ) {
131 tp.table.visibility.columns[ col_idx ] = 1;
132 }
133 // Get all hidden cells and mark their rows/columns as hidden.
134 Object.keys( tp.editor.options.meta ).forEach( function ( cell_name ) {
135 const cell = jspreadsheet.getIdFromColumnName( cell_name, true ); // Returns [ col_idx, row_idx ].
136 if ( 1 === tp.table.visibility.rows[ cell[1] ] && tp.editor.options.meta[ cell_name ].row_hidden ) {
137 tp.table.visibility.rows[ cell[1] ] = 0;
138 }
139 if ( 1 === tp.table.visibility.columns[ cell[0] ] && tp.editor.options.meta[ cell_name ].column_hidden ) {
140 tp.table.visibility.columns[ cell[0] ] = 0;
141 }
142 } );
143 };
144
145 /**
146 * Check whether the Hide or Unhide entries in the context menu should be disabled, by comparing
147 * whether any of the selected rows/columns have a different visibility state than what the entry would set.
148 *
149 * @param {string} type What to hide or unhide ("rows" or "columns").
150 * @param {boolean} visibility 0 for hidden, 1 for visible.
151 * @return {boolean} True if the entry shall be shown, false if not.
152 */
153 tp.helpers.visibility.selection_contains = function ( type, visibility ) {
154 // Show the entry as soon as one of the selected rows/columns does not have the intended visibility state.
155 return tp.helpers.selection[ type ].some( ( roc_idx ) => ( tp.table.visibility[ type ][ roc_idx ] === visibility ) );
156 };
157
158 /**
159 * For the context menu and button, determine whether moving the rows/columns of the current selection is allowed.
160 *
161 * @param {[type]} type [description]
162 * @param {[type]} direction [description]
163 * @return {boolean} Whether the move is allowed or not.
164 */
165 tp.helpers.move_allowed = function ( type, direction ) {
166 // When moving up or left, or to top or first, test the first row/column of the selected range.
167 let roc_to_test = tp.helpers.selection[ type ][0];
168 let min_max_roc = 0; // First row/column.
169 // When moving down or right, or bottom or last, test the last row/column of the selected range.
170 if ( 'down' === direction || 'right' === direction || 'bottom' === direction || 'last' === direction ) {
171 roc_to_test = tp.helpers.selection[ type ][ tp.helpers.selection[ type ].length - 1 ];
172 min_max_roc = ( 'rows' === type ) ? tp.editor.options.data.length - 1 : tp.editor.options.columns.length - 1;
173 }
174 // Moving is disallowed if the first/last row/column is already at the target edge.
175 if ( min_max_roc === roc_to_test ) {
176 return false;
177 }
178 // Otherwise allow the move.
179 return true;
180 };
181
182 /**
183 * Determines whether merging the current selection is allowed.
184 *
185 * Note that this does currently not take into account hidden rows!
186 *
187 * This is e.g. used to give feedback in the context menu and "Combine/Merge" button.
188 *
189 * @param {string} errors Whether errors should also be alert()ed.
190 * @param {Object} error_message Call-by-reference object for the error message.
191 * @return {boolean} Whether the merge is allowed or not.
192 */
193 tp.helpers.cell_merge_allowed = function ( errors, error_message = {} ) {
194 const alert_on_error = ( 'alert' === errors );
195
196 const first_selected_row_idx = tp.helpers.selection.rows[0];
197 const last_selected_row_idx = tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ];
198
199 const first_body_row_idx = tp.table.options.table_head;
200 const last_body_row_idx = tp.editor.options.data.length - 1 - tp.table.options.table_foot;
201
202 // If table header rows are used and the "Enable Visitor Features" option is active, cell merging is only allowed in the table header and footer rows.
203 if ( tp.table.options.table_head > 0 && tp.table.options.use_datatables && ! ( first_selected_row_idx < first_body_row_idx && last_selected_row_idx < first_body_row_idx ) && ! ( first_selected_row_idx > last_body_row_idx && last_selected_row_idx > last_body_row_idx ) ) {
204 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' ) ) +
205 ' ' + __( 'When the Table Features for Site Visitors are used, merging is only allowed in the table header and footer rows.', 'tablepress' );
206 if ( alert_on_error ) {
207 window.alert( error_message.text );
208 }
209 return false;
210 }
211
212 // If table header rows are used, and a header row and at least one adjacent body row are selected, disable merging cells.
213 if ( first_selected_row_idx < first_body_row_idx && last_selected_row_idx >= first_body_row_idx ) {
214 error_message.text = sprintf( __( 'You can not combine these cells, because the “%1$s” setting in the “%2$s” section is active.', 'tablepress' ), __( 'Table Header', 'tablepress' ), __( 'Table Options', 'tablepress' ) );
215 if ( alert_on_error ) {
216 window.alert( error_message.text );
217 }
218 return false;
219 }
220
221 // If table footer rows are used, and a footer row and at least one adjacent body row are selected, disable merging cells.
222 if ( first_selected_row_idx <= last_body_row_idx && last_selected_row_idx > last_body_row_idx ) {
223 error_message.text = sprintf( __( 'You can not combine these cells, because the “%1$s” setting in the “%2$s” section is active.', 'tablepress' ), __( 'Table Footer', 'tablepress' ), __( 'Table Options', 'tablepress' ) );
224 if ( alert_on_error ) {
225 window.alert( error_message.text );
226 }
227 return false;
228 }
229
230 // Otherwise allow the merge.
231 return true;
232 };
233
234 tp.helpers.editor = tp.helpers.editor || {};
235
236 /**
237 * [editor_reselect description]
238 *
239 * @param {[type]} el [description]
240 * @param {[type]} obj Jspreadsheet instance, passed e.g. by onblur. If not present, we use tp.editor.
241 */
242 tp.helpers.editor.reselect = function ( el, obj ) {
243 if ( 'undefined' === typeof obj ) {
244 obj = tp.editor;
245 }
246 obj.updateSelectionFromCoords(
247 tp.helpers.selection.columns[0],
248 tp.helpers.selection.rows[0],
249 tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ],
250 tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ]
251 );
252 };
253
254 /**
255 * Checks if the table has merged cells in the visible body rows and columns.
256 *
257 * @return {boolean} True if the table has merged cells in the body, false otherwise.
258 */
259 tp.helpers.editor.has_merged_body_cells = function () {
260 const first_body_row_idx = tp.table.options.table_head;
261 const first_footer_row_idx = tp.editor.options.data.length - tp.table.options.table_foot;
262 const num_columns = tp.editor.options.columns.length;
263 // Loop through all cells after the table header rows and before the table footer rows.
264 for ( let row_idx = first_body_row_idx; row_idx < first_footer_row_idx; row_idx++ ) {
265 for ( let col_idx = 0; col_idx < num_columns; col_idx++ ) {
266 if ( ( '#rowspan#' === tp.editor.options.data[ row_idx ][ col_idx ] || '#colspan#' === tp.editor.options.data[ row_idx ][ col_idx ] ) && 1 === tp.table.visibility.rows[ row_idx ] && 1 === tp.table.visibility.columns[ col_idx ] ) {
267 return true;
268 }
269 }
270 }
271 return false;
272 };
273
274 /**
275 * Creates the sorting function that is used when sorting the table by a column.
276 *
277 * @param {number} direction Sorting direction. 0 for ascending, 1 for descending.
278 * @return {Function} Sorting function.
279 */
280 tp.helpers.editor.sorting = function ( direction ) {
281 direction = direction ? -1 : 1;
282 return function ( a, b ) {
283 // The actual value is stored in the second array element, the first contains the row index.
284 const sortResult = a[1].localeCompare( b[1], undefined, {
285 numeric: true,
286 sensitivity: 'base',
287 } );
288 return direction * sortResult;
289 };
290 };
291
292 tp.callbacks = tp.callbacks || {};
293
294 tp.callbacks.editor = tp.callbacks.editor || {};
295
296 /**
297 * [editor_onselection description]
298 *
299 * @param {[type]} instance [description]
300 * @param {[type]} x1 [description]
301 * @param {[type]} y1 [description]
302 * @param {[type]} x2 [description]
303 * @param {[type]} y2 [description]
304 * @param {[type]} origin [description]
305 */
306 tp.callbacks.editor.onselection = function ( instance, x1, y1, x2, y2 /*, origin */ ) {
307 tp.helpers.selection = {
308 rows: [],
309 columns: [],
310 };
311 for ( let row_idx = y1; row_idx <= y2; row_idx++ ) {
312 tp.helpers.selection.rows.push( row_idx );
313 }
314 for ( let col_idx = x1; col_idx <= x2; col_idx++ ) {
315 tp.helpers.selection.columns.push( col_idx );
316 }
317 };
318
319 /**
320 * [editor_onupdatetable description]
321 *
322 * @param {[type]} instance [description]
323 * @param {[type]} cell [description]
324 * @param {[type]} col_idx [description]
325 * @param {[type]} row_idx [description]
326 * @param {[type]} value [description]
327 * @param {[type]} label [description]
328 * @param {[type]} cell_name [description]
329 */
330 tp.callbacks.editor.onupdatetable = function ( instance, cell, col_idx, row_idx, value, label, cell_name ) {
331 const meta = instance.jspreadsheet.options.meta[ cell_name ];
332
333 // Add class to cells (td) of hidden columns.
334 cell.classList.toggle( 'column-hidden', Boolean( meta?.column_hidden ) );
335
336 // 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.
337 if ( 0 === col_idx ) {
338 cell.parentNode.classList.toggle( 'row-hidden', Boolean( meta?.row_hidden ) );
339 cell.parentNode.classList.remove( 'head-row', 'foot-row' );
340
341 // After processing the last row, potentially add classes to the head and foot rows.
342 if ( row_idx === instance.jspreadsheet.rows.length - 1 ) {
343 const visible_rows = instance.jspreadsheet.content.querySelectorAll( ':scope tbody tr:not(.row-hidden)' );
344 for ( let idx = 0; idx < tp.table.options.table_head; idx++ ) {
345 visible_rows[ idx ]?.classList.add( 'head-row' );
346 }
347 // Designating footer rows only makes sense for tables that have enough rows to show them.
348 if ( visible_rows.length >= tp.table.options.table_head + tp.table.options.table_foot ) {
349 const last_row_idx = visible_rows.length - 1;
350 for ( let idx = 0; idx < tp.table.options.table_foot; idx++ ) {
351 visible_rows[ last_row_idx - idx ]?.classList.add( 'foot-row' );
352 }
353 }
354 }
355 }
356 };
357
358 /**
359 * [editor_oninsertroc description]
360 *
361 * Abbreviations:
362 * roc: row or column
363 * cor: column or row
364 *
365 * @param {[type]} type [description]
366 * @param {[type]} action [description]
367 * @param {[type]} el [description]
368 * @param {[type]} roc_idx [description]
369 * @param {[type]} num_rocs [description]
370 * @param {[type]} roc_records [description]
371 * @param {[type]} insertBefore [description]
372 */
373 tp.callbacks.editor.oninsertroc = function ( type, action, el, roc_idx, num_rocs, roc_records, insertBefore ) {
374 const handling_rows = ( 'rows' === type );
375 const property = handling_rows ? 'column_hidden' : 'row_hidden';
376 const duplicating = ( 'duplicate' === action );
377
378 const from_roc_idx = roc_idx + ( insertBefore ? num_rocs : 0 );
379 const num_cors = handling_rows ? tp.editor.options.columns.length : tp.editor.options.data.length;
380
381 // Get data of row/column that is copied.
382 const from_meta = {};
383 for ( let cor_idx = 0; cor_idx < num_cors; cor_idx++ ) {
384 const cell_idx = handling_rows ? [ cor_idx, from_roc_idx ] : [ from_roc_idx, cor_idx ];
385 const meta = tp.editor.options.meta[ jspreadsheet.getColumnNameFromId( cell_idx ) ];
386 if ( ! meta ) {
387 continue;
388 }
389 // When duplicating, copy full cell meta, otherwise only the necessary property (row visibility for columns, column visibility for rows).
390 if ( duplicating ) {
391 from_meta[ cor_idx ] = meta;
392 } else if ( meta[ property ] ) {
393 from_meta[ cor_idx ] = from_meta[ cor_idx ] || {};
394 from_meta[ cor_idx ][ property ] = true;
395 }
396 }
397
398 const from_meta_keys = Object.keys( from_meta );
399 // Bail early if there's nothing to copy.
400 if ( ! from_meta_keys.length ) {
401 return;
402 }
403
404 // Construct meta data for target rows/columns.
405 const to_meta = {};
406 if ( ! insertBefore ) {
407 roc_idx++; // When appending (i.e. insert after), we start after the current row or column.
408 }
409 for ( let new_roc = 0; new_roc < num_rocs; new_roc++ ) {
410 const to_roc_idx = roc_idx + new_roc;
411 from_meta_keys.forEach( function ( cor_idx ) {
412 const cell_idx = handling_rows ? [ cor_idx, to_roc_idx ] : [ to_roc_idx, cor_idx ];
413 to_meta[ jspreadsheet.getColumnNameFromId( cell_idx ) ] = from_meta[ cor_idx ];
414 } );
415 }
416
417 tp.editor.setMeta( to_meta );
418 tp.editor.updateTable(); // Redraw table.
419 };
420
421 /**
422 * [editor_onmove description]
423 *
424 * @param {[type]} el [description]
425 * @param {[type]} old_roc_idx [description]
426 * @param {[type]} new_roc_idx [description]
427 */
428 tp.callbacks.editor.onmove = function ( /* el, old_roc_idx, new_roc_idx */ ) {
429 tp.helpers.editor.reselect();
430 tp.helpers.unsaved_changes.set();
431 };
432
433 /**
434 * [editor_onsort description]
435 *
436 * @param {[type]} el [description]
437 * @param {[type]} column [description]
438 * @param {[type]} order [description]
439 */
440 tp.callbacks.editor.onsort = function ( /* el, column, order */ ) {
441 tp.editor.updateTable(); // Redraw table.
442 tp.helpers.unsaved_changes.set();
443 };
444
445 /**
446 * Copy the generated link or image HTML code from the helper textarea to the first selected table cell.
447 */
448 tp.helpers.editor.insert_from_helper_textarea = function () {
449 tp.editor.setValueFromCoords( tp.helpers.selection.columns[0], tp.helpers.selection.rows[0], this.value );
450 };
451
452 tp.callbacks.insert_link = {};
453
454 /**
455 * Open the wpLink dialog for inserting links.
456 *
457 * @param {HTMLElement|null} $active_textarea Active textarea of the table editor or null.
458 */
459 tp.callbacks.insert_link.open_dialog = function ( $active_textarea = null ) {
460 const $helper_textarea = $( '#textarea-insert-helper' );
461 $helper_textarea.value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
462 if ( $active_textarea ) {
463 $helper_textarea.selectionStart = $active_textarea.selectionStart;
464 $helper_textarea.selectionEnd = $active_textarea.selectionEnd;
465 } else {
466 $helper_textarea.selectionStart = $helper_textarea.value.length;
467 $helper_textarea.selectionEnd = $helper_textarea.value.length;
468 }
469 const cell_name = jexcel.getColumnNameFromId( [ tp.helpers.selection.columns[0], tp.helpers.selection.rows[0] ] );
470 $( '#link-modal-title' ).textContent = sprintf( __( 'Insert Link into cell %1$s', 'tablepress' ), cell_name );
471 wpLink.open( 'textarea-insert-helper' );
472 jexcel.current = null; // This is necessary to prevent problems with the focus when the "Insert Link" dialog is called from the context menu.
473 };
474
475 tp.callbacks.insert_image = {};
476
477 /**
478 * Open the WP Media library for inserting images.
479 *
480 * @param {HTMLElement|null} $active_textarea Active textarea of the table editor or null.
481 */
482 tp.callbacks.insert_image.open_dialog = function ( $active_textarea = null ) {
483 const $helper_textarea = $( '#textarea-insert-helper' );
484 $helper_textarea.value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
485 if ( $active_textarea ) {
486 $helper_textarea.selectionStart = $active_textarea.selectionStart;
487 $helper_textarea.selectionEnd = $active_textarea.selectionEnd;
488 } else {
489 $helper_textarea.selectionStart = $helper_textarea.value.length;
490 $helper_textarea.selectionEnd = $helper_textarea.value.length;
491 }
492 wp.media.editor.open( 'textarea-insert-helper', {
493 frame: 'post',
494 state: 'insert',
495 title: wp.media.view.l10n.addMedia,
496 multiple: true,
497 } );
498 const cell_name = jexcel.getColumnNameFromId( [ tp.helpers.selection.columns[0], tp.helpers.selection.rows[0] ] );
499 document.querySelector( '#media-frame-title h1' ).textContent = sprintf( __( 'Add media to cell %1$s', 'tablepress' ), cell_name );
500 jexcel.current = null; // This is necessary to prevent problems with the focus when the "Insert Link" dialog is called from the context menu.
501 };
502
503 tp.callbacks.advanced_editor = {};
504
505 tp.callbacks.advanced_editor.$textarea = $( '#advanced-editor-content' );
506
507 /**
508 * Open the wpdialog for the Advanced Editor.
509 *
510 * @param {HTMLElement|null} $active_textarea Active textarea of the table editor or null.
511 */
512 tp.callbacks.advanced_editor.open_dialog = function ( $active_textarea = null ) {
513 tp.callbacks.advanced_editor.$textarea.value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
514
515 const cell_name = jexcel.getColumnNameFromId( [ tp.helpers.selection.columns[0], tp.helpers.selection.rows[0] ] );
516 const title = sprintf( __( 'Advanced Editor for cell %1$s', 'tablepress' ), cell_name );
517 $( '#advanced-editor-label' ).textContent = title; // Screen reader label for the "Advanced Editor" textarea.
518 $( '#link-modal-title' ).textContent = sprintf( __( 'Insert Link into cell %1$s', 'tablepress' ), cell_name );
519
520 jQuery( '#advanced-editor' ).wpdialog( {
521 width: 600,
522 modal: true,
523 title,
524 resizable: false, // Height of textarea does not increase when resizing editor height.
525 closeOnEscape: true,
526 buttons: [
527 {
528 text: __( 'Cancel', 'tablepress' ),
529 class: 'button button-cancel',
530 click() {
531 jQuery( this ).wpdialog( 'close' );
532 },
533 },
534 {
535 text: __( 'OK', 'tablepress' ),
536 class: 'button button-primary button-ok',
537 click: tp.callbacks.advanced_editor.confirm_save,
538 },
539 ],
540 } );
541
542 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.
543 if ( $active_textarea ) {
544 tp.callbacks.advanced_editor.$textarea.selectionStart = $active_textarea.selectionStart;
545 tp.callbacks.advanced_editor.$textarea.selectionEnd = $active_textarea.selectionEnd;
546 } else {
547 tp.callbacks.advanced_editor.$textarea.selectionStart = tp.callbacks.advanced_editor.$textarea.value.length;
548 tp.callbacks.advanced_editor.$textarea.selectionEnd = tp.callbacks.advanced_editor.$textarea.value.length;
549 }
550 tp.callbacks.advanced_editor.$textarea.focus();
551 };
552
553 /**
554 * Confirm and save changes of the Advanced Editor.
555 */
556 tp.callbacks.advanced_editor.confirm_save = function () {
557 const current_value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
558 // Only set the cell content if changes were made to not wrongly call tp.helpers.unsaved_changes.set().
559 if ( tp.callbacks.advanced_editor.$textarea.value !== current_value ) {
560 tp.editor.setValueFromCoords( tp.helpers.selection.columns[0], tp.helpers.selection.rows[0], tp.callbacks.advanced_editor.$textarea.value );
561 }
562 jQuery( this ).wpdialog( 'close' );
563 };
564
565 tp.callbacks.help_box = {};
566
567 /**
568 * Open the wpdialog for a help box.
569 *
570 * @param {string|Event} helpbox [description]
571 */
572 tp.callbacks.help_box.open_dialog = function ( helpbox ) {
573 if ( 'string' !== typeof helpbox ) {
574 helpbox = helpbox.target.dataset.helpBox;
575 }
576 const $helpbox = $( helpbox );
577 jQuery( $helpbox ).wpdialog( {
578 height: $helpbox.dataset.height,
579 width: $helpbox.dataset.width,
580 minWidth: 260,
581 modal: true,
582 closeOnEscape: true,
583 buttons: [
584 {
585 text: __( 'OK', 'tablepress' ),
586 class: 'button button-ok',
587 click() {
588 jQuery( this ).wpdialog( 'close' );
589 },
590 },
591 ],
592 open( /* event, ui */ ) {
593 jQuery( this ).next().find( '.button-ok' ).trigger( 'focus' );
594 },
595 } );
596 };
597
598 tp.callbacks.table_preview = {};
599
600 /**
601 * Handle showing the table preview.
602 *
603 * @param {Event} event [description]
604 */
605 tp.callbacks.table_preview.process = function ( event ) {
606 // Never follow the link of the Preview button, everything is handled with JS.
607 event.preventDefault();
608
609 let table_name = tp.table.meta.name;
610 if ( '' === table_name.trim() ) {
611 table_name = __( '(no name)', 'tablepress' );
612 }
613
614 // Initialize the Table Preview wpdialog.
615 tp.callbacks.table_preview.$dialog = jQuery( '#table-preview' ).wpdialog( {
616 autoOpen: false,
617 width: window.innerWidth - 80,
618 height: window.innerHeight - 80,
619 modal: true,
620 title: sprintf( __( 'Preview of table “%1$s” (ID %2$s)', 'tablepress' ), table_name, tp.table.id ),
621 closeOnEscape: true,
622 buttons: [
623 {
624 text: __( 'OK', 'tablepress' ),
625 class: 'button button-ok',
626 click() {
627 jQuery( this ).wpdialog( 'close' );
628 },
629 },
630 ],
631 } );
632
633 // For tables without unsaved changes, show an externally rendered table from a URL in an iframe in a wpdialog.
634 if ( ! tp.made_changes ) {
635 const $iframe = $( '#table-preview-iframe' );
636 $iframe.src = event.target.href;
637 $iframe.removeAttribute( 'srcdoc' );
638 tp.callbacks.table_preview.$dialog.wpdialog( 'open' );
639 return;
640 }
641
642 // For tables with unsaved changes, get the table preview HTML code for the iframe via AJAX.
643
644 // Update information about hidden rows and columns.
645 tp.helpers.visibility.update();
646
647 // Prepare the data for the AJAX request.
648 const request_data = {
649 action: 'tablepress_preview_table',
650 _ajax_nonce: tp.nonces.preview_table,
651 tablepress: {
652 id: tp.table.id,
653 new_id: tp.table.meta.newId,
654 name: tp.table.meta.name,
655 description: tp.table.meta.description,
656 data: JSON.stringify( tp.editor.options.data ),
657 options: JSON.stringify( tp.table.options ),
658 visibility: JSON.stringify( tp.table.visibility ),
659 number: {
660 rows: tp.editor.options.data.length,
661 columns: tp.editor.options.columns.length,
662 },
663 },
664 };
665
666 // Add spinner, disable "Preview" buttons, and change cursor.
667 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' ) }"></span>` );
668 $( '.button-preview' ).forEach( ( button ) => button.classList.add( 'disabled' ) );
669 document.body.classList.add( 'wait' );
670
671 // Load the table preview data from the server via an AJAX request.
672 fetch( ajaxurl, {
673 method: 'POST',
674 headers: {
675 'Content-Type': 'application/x-www-form-urlencoded',
676 Accept: 'application/json',
677 },
678 body: buildQueryString( request_data ),
679 } )
680 // Check for HTTP connection problems.
681 .then( ( response ) => {
682 if ( ! response.ok ) {
683 throw new Error( `There was a problem with the server, HTTP response code ${ response.status } (${ response.statusText }).` );
684 }
685 return response.json();
686 } )
687 // Check for problems with the transmitted data.
688 .then( ( data ) => {
689 if ( 'undefined' === typeof data || null === data || '-1' === data || 'undefined' === typeof data.success ) {
690 throw new Error( 'The JSON data returned from the server is unclear or incomplete.' );
691 }
692
693 if ( true !== data.success ) {
694 throw new Error( 'The preview could not be loaded.' );
695 }
696
697 tp.callbacks.table_preview.success( data );
698 } )
699 // Handle errors.
700 .catch( ( error ) => tp.callbacks.table_preview.error( error.message ) )
701 .finally( () => {
702 $( '#spinner-table-preview' ).remove();
703 $( '.button-preview' ).forEach( ( button ) => button.classList.remove( 'disabled' ) );
704 document.body.classList.remove( 'wait' );
705 } );
706 };
707
708 /**
709 * [success description]
710 *
711 * @param {[type]} data [description]
712 */
713 tp.callbacks.table_preview.success = function ( data ) {
714 const $iframe = $( '#table-preview-iframe' );
715 $iframe.src = '';
716 $iframe.srcdoc = `<!DOCTYPE html><html><head>${ data.head_html }</head><body>${ data.body_html }</body></html>`;
717
718 tp.callbacks.table_preview.$dialog.wpdialog( 'open' );
719 };
720
721 /**
722 * [error description]
723 *
724 * @param {[type]} message [description]
725 */
726 tp.callbacks.table_preview.error = function ( message ) {
727 message = __( 'Attention: Unfortunately, an error occurred.', 'tablepress' ) + ' ' + message + '<br>' + sprintf( __( 'Please see the <a href="%s" target="_blank">TablePress FAQ page</a> for suggestions.', 'tablepress' ), 'https://tablepress.org/faq/common-errors/' );
728 const div_id = `show-preview-${ Date.now() }`;
729
730 $( '#spinner-table-preview' ).parentNode.insertAdjacentHTML( 'afterend', `<div id="${ div_id }" class="ajax-alert notice notice-error"><p>${ message }</p></div>` );
731
732 const $notice = $( `#${ div_id }` );
733 void $notice.offsetWidth; // Trick browser layout engine. Necessary to make CSS transition work.
734 $notice.style.opacity = 0;
735 $notice.addEventListener( 'transitionend', ( event ) => {
736 if ( event.target.matches( '.notice' ) ) {
737 $notice.remove();
738 }
739 } );
740 };
741
742 tp.callbacks.save_changes = {};
743
744 /**
745 * Save Changes to the server.
746 *
747 * @param {Event} event [description]
748 */
749 tp.callbacks.save_changes.process = function ( event ) {
750 // Validate input fields.
751 if ( ! applyFilters( 'tablepress.optionsValidateFields', true, tp.table.options ) ) {
752 return;
753 }
754
755 // Collect information about hidden rows and columns.
756 tp.helpers.visibility.update();
757
758 // Prepare the data for the AJAX request.
759 const request_data = {
760 action: 'tablepress_save_table',
761 _ajax_nonce: tp.nonces.edit_table,
762 tablepress: {
763 id: tp.table.id,
764 new_id: tp.table.meta.newId,
765 name: tp.table.meta.name,
766 description: tp.table.meta.description,
767 data: JSON.stringify( tp.editor.options.data ),
768 options: JSON.stringify( tp.table.options ),
769 visibility: JSON.stringify( tp.table.visibility ),
770 number: {
771 rows: tp.editor.options.data.length,
772 columns: tp.editor.options.columns.length,
773 },
774 },
775 };
776
777 // Add spinner, disable "Save Changes" buttons, and change cursor.
778 event.target.parentNode.insertAdjacentHTML( 'beforeend', `<span id="spinner-save-changes" class="spinner-save-changes spinner is-active" title="${ __( 'Changes are being saved …', 'tablepress' ) }"></span>` );
779 $( '.button-save-changes' ).forEach( ( button ) => ( button.disabled = true ) );
780 document.body.classList.add( 'wait' );
781
782 // Save the table data to the server via an AJAX request.
783 fetch( ajaxurl, {
784 method: 'POST',
785 headers: {
786 'Content-Type': 'application/x-www-form-urlencoded',
787 Accept: 'application/json',
788 },
789 body: buildQueryString( request_data ),
790 } )
791 // Check for HTTP connection problems.
792 .then( ( response ) => {
793 if ( ! response.ok ) {
794 throw new Error( `There was a problem with the server, HTTP response code ${ response.status } (${ response.statusText }).` );
795 }
796 return response.json();
797 } )
798 // Check for problems with the transmitted data.
799 .then( ( data ) => {
800 if ( 'undefined' === typeof data || null === data || '-1' === data || 'undefined' === typeof data.success ) {
801 throw new Error( 'The JSON data returned from the server is unclear or incomplete.' );
802 }
803
804 if ( true !== data.success ) {
805 const error_introduction = __( 'These errors were encountered:', 'tablepress' );
806 const debug_html = data.error_details ? `</p><p>${ error_introduction }</p><pre>${ data.error_details }</pre><p>` : '';
807 throw new Error( `The table could not be saved to the database properly.${ debug_html }` );
808 }
809
810 tp.callbacks.save_changes.success( data );
811 } )
812 // Handle errors.
813 .catch( ( error ) => tp.callbacks.save_changes.error( error.message ) )
814 .finally( () => {
815 $( '#spinner-save-changes' ).remove();
816 $( '.button-save-changes' ).forEach( ( button ) => ( button.disabled = false ) );
817 document.body.classList.remove( 'wait' );
818 } );
819 };
820
821 /**
822 * [success description]
823 *
824 * @param {[type]} data [description]
825 */
826 tp.callbacks.save_changes.success = function ( data ) {
827 // Saving was successful, so the original ID has changed to the (maybe) new ID -> we need to adjust all occurrences.
828 if ( tp.table.id !== data.table_id && window?.history?.pushState ) {
829 // Update URL, but only if the table ID changed, to not get dummy entries in the browser history.
830 window.history.pushState( '', '', window.location.href.replace( /table_id=[a-zA-Z0-9_-]+/gi, `table_id=${ data.table_id }` ) );
831 }
832
833 tp.table.id = data.table_id;
834
835 tp.helpers.meta.update( {
836 newId: data.table_id,
837 lastModified: data.last_modified,
838 lastEditor: data.last_editor,
839 } );
840
841 // Update the nonces.
842 tp.nonces.edit_table = data.new_edit_nonce;
843 tp.nonces.preview_table = data.new_preview_nonce;
844 tp.nonces.copy_table = data.new_copy_nonce;
845 tp.nonces.delete_table = data.new_delete_nonce;
846
847 // Update URLs in Preview, Copy, and Delete links/buttons.
848 [ 'preview', 'copy', 'delete' ].forEach( ( action ) => {
849 $( `.button-${ action }` ).forEach( ( button ) => {
850 button.href = button.href
851 .replace( /item=[a-zA-Z0-9_-]+/g, `item=${ data.table_id }` ) // Updates both the "item" and the "return_item" parameters.
852 .replace( /&_wpnonce=[a-zA-Z0-9]+/g, `&_wpnonce=${ data[ `new_${ action }_nonce` ] }` );
853 } );
854 } );
855
856 // Update URL in Export links/buttons.
857 $( '.button-export' ).forEach( ( button ) => {
858 button.href = button.href
859 .replace( /table_id=[a-zA-Z0-9_-]+/g, `table_id=${ data.table_id }` );
860 } );
861
862 tp.helpers.unsaved_changes.unset();
863
864 const action_messages = {};
865 action_messages.success_save = __( 'The table was saved successfully.', 'tablepress' );
866 action_messages.success_save_success_id_change = action_messages.success_save + ' ' + __( 'The table ID was changed.', 'tablepress' );
867 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' );
868
869 if ( 'success_save_error_id_change' === data.message && data.error_details ) {
870 const error_introduction = __( 'These errors were encountered:', 'tablepress' );
871 action_messages.success_save_error_id_change += `</p><p>${ error_introduction }</p><pre>${ data.error_details }</pre><p>`;
872 }
873
874 const type = ( data.message.includes( 'error' ) ) ? 'error' : 'success';
875 tp.callbacks.save_changes.after_saving_notice( type, action_messages[ data.message ] );
876 };
877
878 /**
879 * [error description]
880 *
881 * @param {[type]} message [description]
882 */
883 tp.callbacks.save_changes.error = function ( message ) {
884 message = __( 'Attention: Unfortunately, an error occurred.', 'tablepress' ) + ' ' + message + '<br>' + sprintf( __( 'Please see the <a href="%s" target="_blank">TablePress FAQ page</a> for suggestions.', 'tablepress' ), 'https://tablepress.org/faq/common-errors/' );
885 tp.callbacks.save_changes.after_saving_notice( 'error', message );
886 };
887
888 /**
889 * [after_saving_notice description]
890 *
891 * @param {[type]} type [description]
892 * @param {[type]} message [description]
893 */
894 tp.callbacks.save_changes.after_saving_notice = function ( type, message ) {
895 const div_id = `save-changes-${ Date.now() }`;
896
897 $( '#spinner-save-changes' ).parentNode.insertAdjacentHTML( 'afterend', `<div id="${ div_id }" class="ajax-alert notice notice-${ type }"><p>${ message }</p></div>` );
898
899 const $notice = $( `#${ div_id }` );
900 void $notice.offsetWidth; // Trick browser layout engine. Necessary to make CSS transition work.
901 $notice.style.opacity = 0;
902 $notice.addEventListener( 'transitionend', ( event ) => {
903 if ( event.target.matches( '.notice' ) ) {
904 $notice.remove();
905 }
906 } );
907 };
908
909 /**
910 * Inserts or duplicates rows or columns before each currently selected row/column.
911 *
912 * @param {string} action The action to perform on the selected rows/columns ("insert" or "duplicate").
913 * @param {string} type What to insert or duplicate ("rows" or "columns").
914 * @param {string} position Where to insert or duplicate ("before" or "after"). Default "before".
915 */
916 tp.callbacks.insert_duplicate = function ( action, type, position = 'before' ) {
917 const handling_rows = ( 'rows' === type );
918 const insert_function = handling_rows ? tp.editor.insertRow : tp.editor.insertColumn;
919 const getData_function = handling_rows ? tp.editor.getRowData : tp.editor.getColumnData;
920 const duplicating = ( 'duplicate' === action );
921 // Dynamically set the event handler, so that we have the action available in it.
922 tp.editor.options[ handling_rows ? 'oninsertrow' : 'oninsertcolumn' ] = tp.callbacks.editor.oninsertroc.bind( null, type, action );
923 tp.helpers.selection[ type ].forEach( function ( roc_idx, array_idx ) {
924 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.
925 const data = duplicating ? getData_function( shifted_roc_idx ) : 1;
926 const position_bool = 'before' === position; // true means "before".
927 insert_function( data, shifted_roc_idx, position_bool );
928 } );
929 tp.helpers.unsaved_changes.set();
930
931 // Select both inserted/duplicated rows/columns if more than one were selected.
932 const num_selected_rocs = tp.helpers.selection[ type ].length;
933 if ( num_selected_rocs > 1 ) {
934 tp.editor.updateSelectionFromCoords(
935 tp.helpers.selection.columns[0],
936 tp.helpers.selection.rows[0],
937 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,
938 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 ]
939 );
940 }
941 };
942
943 /**
944 * Removes currently selected rows or columns.
945 *
946 * @param {string} type What to remove ("rows" or "columns").
947 */
948 tp.callbacks.remove = function ( type ) {
949 const handling_rows = 'rows' === type;
950 const num_cors = handling_rows ? tp.editor.options.columns.length : tp.editor.options.data.length;
951 const last_roc_idx = handling_rows ? tp.editor.options.data.length - 1 : tp.editor.options.columns.length - 1;
952
953 // Visibility meta information has to be deleted manually, as otherwise the Jspreadsheet meta information can get out of sync.
954 if ( tp.editor.options.meta ) {
955 tp.helpers.selection[ type ].forEach( function ( roc_idx ) {
956 for ( let cor_idx = 0; cor_idx < num_cors; cor_idx++ ) {
957 const cell_idx = handling_rows ? [ cor_idx, roc_idx ] : [ roc_idx, cor_idx ];
958 delete tp.editor.options.meta[ jspreadsheet.getColumnNameFromId( cell_idx ) ];
959 }
960 } );
961 }
962
963 const delete_function = handling_rows ? tp.editor.deleteRow : tp.editor.deleteColumn;
964 delete_function( tp.helpers.selection[ type ][0], tp.helpers.selection[ type ].length );
965 tp.helpers.unsaved_changes.set();
966
967 // Reselect last visible row/column, if last rows/columns were deleted.
968 if ( last_roc_idx === tp.helpers.selection[ type ][ tp.helpers.selection[ type ].length - 1 ] ) {
969 const col_idx = handling_rows ? tp.helpers.selection.columns[0] : tp.helpers.selection.columns[0] - 1;
970 const row_idx = handling_rows ? tp.helpers.selection.rows[0] - 1 : tp.helpers.selection.rows[0];
971 tp.editor.updateSelectionFromCoords( col_idx, row_idx, col_idx, row_idx );
972 }
973 };
974
975 /**
976 * Appends rows or columns at the bottom or right end of the table.
977 *
978 * @param {string} type What to append ("rows" or "columns").
979 * @param {number} num_rocs Number of rows or columns to append.
980 */
981 tp.callbacks.append = function ( type, num_rocs ) {
982 const handling_rows = ( 'rows' === type );
983 const insert_function = handling_rows ? tp.editor.insertRow : tp.editor.insertColumn;
984 // Dynamically set the event handler, so that we have the action available in it.
985 tp.editor.options[ handling_rows ? 'oninsertrow' : 'oninsertcolumn' ] = tp.callbacks.editor.oninsertroc.bind( null, type, 'append' );
986 insert_function( num_rocs );
987 tp.helpers.unsaved_changes.set();
988 };
989
990 /**
991 * Moves currently selected rows or columns.
992 *
993 * @param {string} direction Where to move the selected rows or columns (for rows: "up"/"down"/"top"/"bottom", for columns: "left"/right"/"first"/"last").
994 * @param {string} type What to move ("rows" or "columns").
995 */
996 tp.callbacks.move = function ( direction, type ) {
997 const handling_rows = ( 'rows' === type );
998
999 // Default case: up/left
1000 let rocs = tp.helpers.selection[ type ]; // When moving up or left, start with the first row/column of the selected range.
1001 let position_difference = -1; // New row/column number is one smaller than current row/column number.
1002 // Alternate case: down/right
1003 if ( 'down' === direction || 'right' === direction ) {
1004 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.
1005 position_difference = 1; // New row/column number is one higher than current row/column number.
1006 } else if ( 'top' === direction || 'first' === direction ) {
1007 position_difference = -rocs[0];
1008 } else if ( 'bottom' === direction || 'last' === direction ) {
1009 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.
1010 const min_max_roc = ( 'rows' === type ) ? tp.editor.options.data.length - 1 : tp.editor.options.columns.length - 1;
1011 position_difference = min_max_roc - rocs[0];
1012 }
1013
1014 // Bail early if there is nothing to do (e.g. when the selected range is already at the target edge).
1015 if ( 0 === position_difference ) {
1016 return;
1017 }
1018
1019 // Move the selected rows/columns individually.
1020 const move_function = handling_rows ? tp.editor.moveRow : tp.editor.moveColumn;
1021 rocs.forEach( ( roc_idx ) => move_function( roc_idx, roc_idx + position_difference ) );
1022 tp.helpers.unsaved_changes.set();
1023
1024 // Reselect moved selection.
1025 tp.editor.updateSelectionFromCoords(
1026 handling_rows ? tp.helpers.selection.columns[0] : tp.helpers.selection.columns[0] + position_difference,
1027 handling_rows ? tp.helpers.selection.rows[0] + position_difference : tp.helpers.selection.rows[0],
1028 handling_rows ? tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ] : tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ] + position_difference,
1029 handling_rows ? tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ] + position_difference : tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ]
1030 );
1031 };
1032
1033 /**
1034 * Sorts the table data by the first currently selected column.
1035 *
1036 * @param {string} direction Sort order/direction ("asc" for ascending, "desc" for descending).
1037 */
1038 tp.callbacks.sort = function ( direction ) {
1039 tp.editor.orderBy( tp.helpers.selection.columns[0], ( 'desc' === direction ) );
1040 };
1041
1042 /**
1043 * Hides or unhides selected rows or columns.
1044 *
1045 * @param {string} action The action to perform on the rows/columns ("hide" or "unhide").
1046 * @param {string} type What to hide or unhide ("rows" or "columns").
1047 */
1048 tp.callbacks.hide_unhide = function ( action, type ) {
1049 const handling_rows = ( 'rows' === type );
1050 const property = handling_rows ? 'row_hidden' : 'column_hidden';
1051 const num_cors = handling_rows ? tp.editor.options.columns.length : tp.editor.options.data.length;
1052 const cell_hidden = ( 'hide' === action );
1053 const meta = {};
1054 tp.helpers.selection[ type ].forEach( function ( roc_idx ) {
1055 for ( let cor_idx = 0; cor_idx < num_cors; cor_idx++ ) {
1056 const cell_idx = handling_rows ? [ cor_idx, roc_idx ] : [ roc_idx, cor_idx ];
1057 const cell_name = jspreadsheet.getColumnNameFromId( cell_idx );
1058 meta[ cell_name ] = {};
1059 meta[ cell_name ][ property ] = cell_hidden;
1060 }
1061 } );
1062 tp.editor.setMeta( meta );
1063 tp.helpers.unsaved_changes.set();
1064 tp.editor.updateTable(); // Redraw table.
1065 };
1066
1067 /**
1068 * Combines/merges the currently selected cells.
1069 */
1070 tp.callbacks.merge_cells = function () {
1071 const current_col_idx = tp.helpers.selection.columns[0];
1072 const current_row_idx = tp.helpers.selection.rows[0];
1073 const colspan = tp.helpers.selection.columns.length;
1074 const rowspan = tp.helpers.selection.rows.length;
1075 for ( let row_idx = 1; row_idx < rowspan; row_idx++ ) {
1076 tp.editor.setValueFromCoords( current_col_idx, current_row_idx + row_idx, '#rowspan#' );
1077 }
1078 for ( let col_idx = 1; col_idx < colspan; col_idx++ ) {
1079 tp.editor.setValueFromCoords( current_col_idx + col_idx, current_row_idx, '#colspan#' );
1080 }
1081 for ( let row_idx = 1; row_idx < rowspan; row_idx++ ) {
1082 for ( let col_idx = 1; col_idx < colspan; col_idx++ ) {
1083 tp.editor.setValueFromCoords( current_col_idx + col_idx, current_row_idx + row_idx, '#span#' );
1084 }
1085 }
1086 tp.helpers.unsaved_changes.set();
1087 };
1088
1089 /*
1090 * Initialize Jspreadsheet.
1091 */
1092 tp.editor = jspreadsheet( $( '#table-editor' ), {
1093 data: tp.table.data,
1094 meta: tp.helpers.visibility.load(),
1095 wordWrap: true,
1096 rowDrag: true,
1097 rowResize: true,
1098 columnSorting: true,
1099 columnDrag: true,
1100 columnResize: true,
1101 defaultColWidth: tp.screen_options.table_editor_column_width,
1102 defaultColAlign: 'left',
1103 parseFormulas: false,
1104 allowExport: false,
1105 allowComments: false,
1106 allowManualInsertRow: false, // To prevent addition of new row when Enter is pressed in last row.
1107 allowManualInsertColumn: false, // To prevent addition of new column when Tab is pressed in last column.
1108 about: false,
1109 secureFormulas: false,
1110 detachForUpdates: true,
1111 onselection: tp.callbacks.editor.onselection,
1112 updateTable: tp.callbacks.editor.onupdatetable,
1113 contextMenu,
1114 sorting: tp.helpers.editor.sorting,
1115 // Keep the selection when certain events occur and the table loses focus.
1116 onmoverow: tp.callbacks.editor.onmove,
1117 onmovecolumn: tp.callbacks.editor.onmove,
1118 onblur: tp.helpers.editor.reselect,
1119 onload: tp.helpers.editor.reselect, // When the table is loaded, select the top-left cell A1.
1120 onchange: tp.helpers.unsaved_changes.set,
1121 onsort: tp.callbacks.editor.onsort,
1122 } );
1123
1124 // Register click callbacks for the "Help" buttons.
1125 $( '#tablepress-page' ).addEventListener( 'click', ( event ) => {
1126 if ( event?.target?.matches( '.button-show-help-box' ) ) {
1127 tp.callbacks.help_box.open_dialog( event );
1128 }
1129 } );
1130
1131 // Register callback for inserting a link into a cell after it has been constructed in the wpLink dialog.
1132 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.
1133
1134 // Move all "Help" buttons inside the postbox header.
1135 document.querySelectorAll( '#tablepress-body .button-module-help' ).forEach( ( $button ) => ( $button.closest( '.postbox' ).querySelector( '.handle-actions' ).prepend( $button ) ) );
1136
1137 // This code requires jQuery, and it must run when the DOM is ready.
1138 jQuery( () => {
1139 // Fix issue with wpLink input fields not being usable, when called through the "Advanced Editor". They are immediately losing focus without this.
1140 jQuery( '#wp-link' ).on( 'focus', 'input', ( event ) => ( event.stopPropagation() ) );
1141
1142 // 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.
1143 jQuery( 'body' ).on( 'focus', '.media-modal .media-frame-content input, .media-modal .media-frame-content textarea', ( event ) => ( event.stopPropagation() ) );
1144 } );
1145