PluginProbe
TablePress – Tables in WordPress made easy / 3.4
TablePress – Tables in WordPress made easy v3.4
3.4 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 All 45 releases
tablepress / admin / js / edit / editor.js

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

785 lines 32.9 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 jexcel, jQuery, jspreadsheet, tp, wp, wpLink */
11 /* eslint-disable jsdoc/check-param-names */
12
13 /**
14 * WordPress dependencies.
15 */
16 import { __, sprintf } from '@wordpress/i18n';
17
18 /**
19 * Internal dependencies.
20 */
21 import { $ } from '../common/functions';
22 import contextMenu from './data/contextmenu';
23
24 // Ensure the global `tp` object exists.
25 window.tp = window.tp || {};
26
27 tp.made_changes = false;
28
29 tp.helpers = tp.helpers || {};
30
31 // Initial selection: cell A1.
32 tp.helpers.selection = tp.helpers.selection || {
33 rows: [ 0 ],
34 columns: [ 0 ],
35 };
36
37 tp.helpers.unsaved_changes = tp.helpers.unsaved_changes || {};
38
39 /**
40 * [unsaved_changes.unload_dialog description]
41 *
42 * @param {Event} event [description]
43 */
44 tp.helpers.unsaved_changes.unload_dialog = function ( event ) {
45 event.preventDefault(); // Cancel the event as stated by the standard.
46 event.returnValue = ''; // Chrome requires returnValue to be set.
47 };
48
49 /**
50 * [unsaved_changes.set description]
51 */
52 tp.helpers.unsaved_changes.set = function () {
53 // Bail early if this function was already called.
54 if ( tp.made_changes ) {
55 return;
56 }
57 tp.made_changes = true;
58 window.addEventListener( 'beforeunload', tp.helpers.unsaved_changes.unload_dialog );
59 };
60
61 /**
62 * [unsaved_changes.unset description]
63 */
64 tp.helpers.unsaved_changes.unset = function () {
65 tp.made_changes = false;
66 window.removeEventListener( 'beforeunload', tp.helpers.unsaved_changes.unload_dialog );
67 };
68
69 tp.helpers.visibility = tp.helpers.visibility || {};
70
71 /**
72 * [visibility.load description]
73 */
74 tp.helpers.visibility.load = function () {
75 const num_rows = tp.table.visibility.rows.length;
76 const num_columns = tp.table.visibility.columns.length;
77 const meta = {};
78 // Collect meta data for hidden rows.
79 for ( let row_idx = 0; row_idx < num_rows; row_idx++ ) {
80 if ( 1 === tp.table.visibility.rows[ row_idx ] ) {
81 continue;
82 }
83 for ( let col_idx = 0; col_idx < num_columns; col_idx++ ) {
84 const cell_name = jspreadsheet.getColumnNameFromId( [ col_idx, row_idx ] );
85 meta[ cell_name ] = meta[ cell_name ] || {};
86 meta[ cell_name ].row_hidden = true;
87 }
88 }
89 // Collect meta data for hidden columns.
90 for ( let col_idx = 0; col_idx < num_columns; col_idx++ ) {
91 if ( 1 === tp.table.visibility.columns[ col_idx ] ) {
92 continue;
93 }
94 for ( let row_idx = 0; row_idx < num_rows; row_idx++ ) {
95 const cell_name = jspreadsheet.getColumnNameFromId( [ col_idx, row_idx ] );
96 meta[ cell_name ] = meta[ cell_name ] || {};
97 meta[ cell_name ].column_hidden = true;
98 }
99 }
100 return meta;
101 };
102
103 /**
104 * [visibility.update description]
105 */
106 tp.helpers.visibility.update = function () {
107 // Set all rows and columns to visible first.
108 tp.table.visibility.rows = [];
109 for ( let row_idx = 0; row_idx < tp.editor.options.data.length; row_idx++ ) {
110 tp.table.visibility.rows[ row_idx ] = 1;
111 }
112 tp.table.visibility.columns = [];
113 for ( let col_idx = 0; col_idx < tp.editor.options.columns.length; col_idx++ ) {
114 tp.table.visibility.columns[ col_idx ] = 1;
115 }
116 // Get all hidden cells and mark their rows/columns as hidden.
117 Object.keys( tp.editor.options.meta ).forEach( function ( cell_name ) {
118 const cell = jspreadsheet.getIdFromColumnName( cell_name, true ); // Returns [ col_idx, row_idx ].
119 if ( 1 === tp.table.visibility.rows[ cell[1] ] && tp.editor.options.meta[ cell_name ].row_hidden ) {
120 tp.table.visibility.rows[ cell[1] ] = 0;
121 }
122 if ( 1 === tp.table.visibility.columns[ cell[0] ] && tp.editor.options.meta[ cell_name ].column_hidden ) {
123 tp.table.visibility.columns[ cell[0] ] = 0;
124 }
125 } );
126 };
127
128 /**
129 * Check whether the Hide or Unhide entries in the context menu should be disabled, by comparing
130 * whether any of the selected rows/columns have a different visibility state than what the entry would set.
131 *
132 * @param {string} type What to hide or unhide ("rows" or "columns").
133 * @param {boolean} visibility 0 for hidden, 1 for visible.
134 * @return {boolean} True if the entry shall be shown, false if not.
135 */
136 tp.helpers.visibility.selection_contains = function ( type, visibility ) {
137 // Show the entry as soon as one of the selected rows/columns does not have the intended visibility state.
138 return tp.helpers.selection[ type ].some( ( roc_idx ) => ( tp.table.visibility[ type ][ roc_idx ] === visibility ) );
139 };
140
141 /**
142 * For the context menu and button, determine whether moving the rows/columns of the current selection is allowed.
143 *
144 * @param {[type]} type [description]
145 * @param {[type]} direction [description]
146 * @return {boolean} Whether the move is allowed or not.
147 */
148 tp.helpers.move_allowed = function ( type, direction ) {
149 // When moving up or left, or to top or first, test the first row/column of the selected range.
150 let roc_to_test = tp.helpers.selection[ type ][0];
151 let min_max_roc = 0; // First row/column.
152 // When moving down or right, or bottom or last, test the last row/column of the selected range.
153 if ( 'down' === direction || 'right' === direction || 'bottom' === direction || 'last' === direction ) {
154 roc_to_test = tp.helpers.selection[ type ][ tp.helpers.selection[ type ].length - 1 ];
155 min_max_roc = ( 'rows' === type ) ? tp.editor.options.data.length - 1 : tp.editor.options.columns.length - 1;
156 }
157 // Moving is disallowed if the first/last row/column is already at the target edge.
158 if ( min_max_roc === roc_to_test ) {
159 return false;
160 }
161 // Otherwise allow the move.
162 return true;
163 };
164
165 /**
166 * Determines whether merging the current selection is allowed.
167 *
168 * Note that this does currently not take into account hidden rows!
169 *
170 * This is e.g. used to give feedback in the context menu and "Combine/Merge" button.
171 *
172 * @param {Object} error_message Call-by-reference object for the error message.
173 * @return {boolean} Whether the merge is allowed or not.
174 */
175 tp.helpers.cell_merge_allowed = function ( error_message = {} ) {
176 const first_selected_row_idx = tp.helpers.selection.rows[0];
177 const last_selected_row_idx = tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ];
178
179 const first_body_row_idx = tp.table.options.table_head;
180 const last_body_row_idx = tp.editor.options.data.length - 1 - tp.table.options.table_foot;
181
182 // 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.
183 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 ) ) {
184 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' ) ) +
185 ' ' + __( 'When the Table Features for Site Visitors are used, merging is only allowed in the table header and footer rows.', 'tablepress' );
186 return false;
187 }
188
189 // If table header rows are used, and a header row and at least one adjacent body row are selected, disable merging cells.
190 if ( first_selected_row_idx < first_body_row_idx && last_selected_row_idx >= first_body_row_idx ) {
191 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' ) );
192 return false;
193 }
194
195 // If table footer rows are used, and a footer row and at least one adjacent body row are selected, disable merging cells.
196 if ( first_selected_row_idx <= last_body_row_idx && last_selected_row_idx > last_body_row_idx ) {
197 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' ) );
198 return false;
199 }
200
201 // Otherwise allow the merge.
202 return true;
203 };
204
205 tp.helpers.editor = tp.helpers.editor || {};
206
207 /**
208 * [editor_reselect description]
209 *
210 * @param {[type]} el [description]
211 * @param {[type]} obj Jspreadsheet instance, passed e.g. by onblur. If not present, we use tp.editor.
212 */
213 tp.helpers.editor.reselect = function ( el, obj ) {
214 if ( 'undefined' === typeof obj ) {
215 obj = tp.editor;
216 }
217 obj.updateSelectionFromCoords(
218 tp.helpers.selection.columns[0],
219 tp.helpers.selection.rows[0],
220 tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ],
221 tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ]
222 );
223 };
224
225 /**
226 * Checks if the table has merged cells in the visible body rows and columns.
227 *
228 * @return {boolean} True if the table has merged cells in the body, false otherwise.
229 */
230 tp.helpers.editor.has_merged_body_cells = function () {
231 const first_body_row_idx = tp.table.options.table_head;
232 const first_footer_row_idx = tp.editor.options.data.length - tp.table.options.table_foot;
233 const num_columns = tp.editor.options.columns.length;
234 // Loop through all cells after the table header rows and before the table footer rows.
235 for ( let row_idx = first_body_row_idx; row_idx < first_footer_row_idx; row_idx++ ) {
236 for ( let col_idx = 0; col_idx < num_columns; col_idx++ ) {
237 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 ] ) {
238 return true;
239 }
240 }
241 }
242 return false;
243 };
244
245 /**
246 * Creates the sorting function that is used when sorting the table by a column.
247 *
248 * @param {number} direction Sorting direction. 0 for ascending, 1 for descending.
249 * @return {Function} Sorting function.
250 */
251 tp.helpers.editor.sorting = function ( direction ) {
252 direction = direction ? -1 : 1;
253 return function ( a, b ) {
254 // The actual value is stored in the second array element, the first contains the row index.
255 const sortResult = a[1].localeCompare( b[1], undefined, {
256 numeric: true,
257 sensitivity: 'base',
258 } );
259 return direction * sortResult;
260 };
261 };
262
263 tp.callbacks = tp.callbacks || {};
264
265 tp.callbacks.editor = tp.callbacks.editor || {};
266
267 /**
268 * [editor_onselection description]
269 *
270 * @param {[type]} instance [description]
271 * @param {[type]} x1 [description]
272 * @param {[type]} y1 [description]
273 * @param {[type]} x2 [description]
274 * @param {[type]} y2 [description]
275 * @param {[type]} origin [description]
276 */
277 tp.callbacks.editor.onselection = function ( instance, x1, y1, x2, y2 /*, origin */ ) {
278 tp.helpers.selection = {
279 rows: [],
280 columns: [],
281 };
282 for ( let row_idx = y1; row_idx <= y2; row_idx++ ) {
283 tp.helpers.selection.rows.push( row_idx );
284 }
285 for ( let col_idx = x1; col_idx <= x2; col_idx++ ) {
286 tp.helpers.selection.columns.push( col_idx );
287 }
288 };
289
290 /**
291 * [editor_onupdatetable description]
292 *
293 * @param {[type]} instance [description]
294 * @param {[type]} cell [description]
295 * @param {[type]} col_idx [description]
296 * @param {[type]} row_idx [description]
297 * @param {[type]} value [description]
298 * @param {[type]} label [description]
299 * @param {[type]} cell_name [description]
300 */
301 tp.callbacks.editor.onupdatetable = function ( instance, cell, col_idx, row_idx, value, label, cell_name ) {
302 const meta = instance.jspreadsheet.options.meta[ cell_name ];
303
304 // Add class to cells (td) of hidden columns.
305 cell.classList.toggle( 'column-hidden', Boolean( meta?.column_hidden ) );
306
307 // 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.
308 if ( 0 === col_idx ) {
309 cell.parentNode.classList.toggle( 'row-hidden', Boolean( meta?.row_hidden ) );
310 cell.parentNode.classList.remove( 'head-row', 'foot-row' );
311
312 // After processing the last row, potentially add classes to the head and foot rows.
313 if ( row_idx === instance.jspreadsheet.rows.length - 1 ) {
314 const visible_rows = instance.jspreadsheet.content.querySelectorAll( ':scope tbody tr:not(.row-hidden)' );
315 for ( let idx = 0; idx < tp.table.options.table_head; idx++ ) {
316 visible_rows[ idx ]?.classList.add( 'head-row' );
317 }
318 // Designating footer rows only makes sense for tables that have enough rows to show them.
319 if ( visible_rows.length >= tp.table.options.table_head + tp.table.options.table_foot ) {
320 const last_row_idx = visible_rows.length - 1;
321 for ( let idx = 0; idx < tp.table.options.table_foot; idx++ ) {
322 visible_rows[ last_row_idx - idx ]?.classList.add( 'foot-row' );
323 }
324 }
325 }
326 }
327 };
328
329 /**
330 * [editor_oninsertroc description]
331 *
332 * Abbreviations:
333 * roc: row or column
334 * cor: column or row
335 *
336 * @param {[type]} type [description]
337 * @param {[type]} action [description]
338 * @param {[type]} el [description]
339 * @param {[type]} roc_idx [description]
340 * @param {[type]} num_rocs [description]
341 * @param {[type]} roc_records [description]
342 * @param {[type]} insertBefore [description]
343 */
344 tp.callbacks.editor.oninsertroc = function ( type, action, el, roc_idx, num_rocs, roc_records, insertBefore ) {
345 const handling_rows = ( 'rows' === type );
346 const property = handling_rows ? 'column_hidden' : 'row_hidden';
347 const duplicating = ( 'duplicate' === action );
348
349 const from_roc_idx = roc_idx + ( insertBefore ? num_rocs : 0 );
350 const num_cors = handling_rows ? tp.editor.options.columns.length : tp.editor.options.data.length;
351
352 // Get data of row/column that is copied.
353 const from_meta = {};
354 for ( let cor_idx = 0; cor_idx < num_cors; cor_idx++ ) {
355 const cell_idx = handling_rows ? [ cor_idx, from_roc_idx ] : [ from_roc_idx, cor_idx ];
356 const meta = tp.editor.options.meta[ jspreadsheet.getColumnNameFromId( cell_idx ) ];
357 if ( ! meta ) {
358 continue;
359 }
360 // When duplicating, copy full cell meta, otherwise only the necessary property (row visibility for columns, column visibility for rows).
361 if ( duplicating ) {
362 from_meta[ cor_idx ] = meta;
363 } else if ( meta[ property ] ) {
364 from_meta[ cor_idx ] = from_meta[ cor_idx ] || {};
365 from_meta[ cor_idx ][ property ] = true;
366 }
367 }
368
369 const from_meta_keys = Object.keys( from_meta );
370 // Bail early if there's nothing to copy.
371 if ( ! from_meta_keys.length ) {
372 return;
373 }
374
375 // Construct meta data for target rows/columns.
376 const to_meta = {};
377 if ( ! insertBefore ) {
378 roc_idx++; // When appending (i.e. insert after), we start after the current row or column.
379 }
380 for ( let new_roc = 0; new_roc < num_rocs; new_roc++ ) {
381 const to_roc_idx = roc_idx + new_roc;
382 from_meta_keys.forEach( function ( cor_idx ) {
383 const cell_idx = handling_rows ? [ cor_idx, to_roc_idx ] : [ to_roc_idx, cor_idx ];
384 to_meta[ jspreadsheet.getColumnNameFromId( cell_idx ) ] = from_meta[ cor_idx ];
385 } );
386 }
387
388 tp.editor.setMeta( to_meta );
389 tp.editor.updateTable(); // Redraw table.
390 };
391
392 /**
393 * [editor_onmove description]
394 *
395 * @param {[type]} el [description]
396 * @param {[type]} old_roc_idx [description]
397 * @param {[type]} new_roc_idx [description]
398 */
399 tp.callbacks.editor.onmove = function ( /* el, old_roc_idx, new_roc_idx */ ) {
400 tp.helpers.editor.reselect();
401 tp.helpers.unsaved_changes.set();
402 };
403
404 /**
405 * [editor_onsort description]
406 *
407 * @param {[type]} el [description]
408 * @param {[type]} column [description]
409 * @param {[type]} order [description]
410 */
411 tp.callbacks.editor.onsort = function ( /* el, column, order */ ) {
412 tp.editor.updateTable(); // Redraw table.
413 tp.helpers.unsaved_changes.set();
414 };
415
416 /**
417 * Copy the generated link or image HTML code from the helper textarea to the first selected table cell.
418 */
419 tp.helpers.editor.insert_from_helper_textarea = function () {
420 tp.editor.setValueFromCoords( tp.helpers.selection.columns[0], tp.helpers.selection.rows[0], this.value );
421 };
422
423 tp.callbacks.insert_link = {};
424
425 /**
426 * Open the wpLink dialog for inserting links.
427 *
428 * @param {HTMLElement|null} $active_textarea Active textarea of the table editor or null.
429 */
430 tp.callbacks.insert_link.open_dialog = function ( $active_textarea = null ) {
431 const $helper_textarea = $( '#textarea-insert-helper' );
432 $helper_textarea.value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
433 if ( $active_textarea ) {
434 $helper_textarea.selectionStart = $active_textarea.selectionStart;
435 $helper_textarea.selectionEnd = $active_textarea.selectionEnd;
436 } else {
437 $helper_textarea.selectionStart = $helper_textarea.value.length;
438 $helper_textarea.selectionEnd = $helper_textarea.value.length;
439 }
440 const cell_name = jexcel.getColumnNameFromId( [ tp.helpers.selection.columns[0], tp.helpers.selection.rows[0] ] );
441 $( '#link-modal-title' ).textContent = sprintf( __( 'Insert Link into cell %1$s', 'tablepress' ), cell_name );
442 wpLink.open( 'textarea-insert-helper' );
443 jexcel.current = null; // This is necessary to prevent problems with the focus when the "Insert Link" dialog is called from the context menu.
444 };
445
446 tp.callbacks.insert_image = {};
447
448 /**
449 * Open the WP Media library for inserting images.
450 *
451 * @param {HTMLElement|null} $active_textarea Active textarea of the table editor or null.
452 */
453 tp.callbacks.insert_image.open_dialog = function ( $active_textarea = null ) {
454 const $helper_textarea = $( '#textarea-insert-helper' );
455 $helper_textarea.value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
456 if ( $active_textarea ) {
457 $helper_textarea.selectionStart = $active_textarea.selectionStart;
458 $helper_textarea.selectionEnd = $active_textarea.selectionEnd;
459 } else {
460 $helper_textarea.selectionStart = $helper_textarea.value.length;
461 $helper_textarea.selectionEnd = $helper_textarea.value.length;
462 }
463 wp.media.editor.open( 'textarea-insert-helper', {
464 frame: 'post',
465 state: 'insert',
466 title: wp.media.view.l10n.addMedia,
467 multiple: true,
468 } );
469 const cell_name = jexcel.getColumnNameFromId( [ tp.helpers.selection.columns[0], tp.helpers.selection.rows[0] ] );
470 document.querySelector( '.media-frame-title h1' ).textContent = sprintf( __( 'Add media to cell %1$s', 'tablepress' ), cell_name );
471 jexcel.current = null; // This is necessary to prevent problems with the focus when the "Insert Link" dialog is called from the context menu.
472 };
473
474 tp.callbacks.advanced_editor = {};
475
476 tp.callbacks.advanced_editor.$textarea = $( '#advanced-editor-content' );
477
478 /**
479 * Open the wpdialog for the Advanced Editor.
480 *
481 * @param {HTMLElement|null} $active_textarea Active textarea of the table editor or null.
482 */
483 tp.callbacks.advanced_editor.open_dialog = function ( $active_textarea = null ) {
484 tp.callbacks.advanced_editor.$textarea.value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
485
486 const cell_name = jexcel.getColumnNameFromId( [ tp.helpers.selection.columns[0], tp.helpers.selection.rows[0] ] );
487 const title = sprintf( __( 'Advanced Editor for cell %1$s', 'tablepress' ), cell_name );
488 $( '#advanced-editor-label' ).textContent = title; // Screen reader label for the "Advanced Editor" textarea.
489 $( '#link-modal-title' ).textContent = sprintf( __( 'Insert Link into cell %1$s', 'tablepress' ), cell_name );
490
491 jQuery( '#advanced-editor' ).wpdialog( {
492 width: 600,
493 modal: true,
494 title,
495 resizable: false, // Height of textarea does not increase when resizing editor height.
496 closeOnEscape: true,
497 buttons: [
498 {
499 text: __( 'Cancel', 'tablepress' ),
500 class: 'button button-cancel',
501 click() {
502 jQuery( this ).wpdialog( 'close' );
503 },
504 },
505 {
506 text: __( 'OK', 'tablepress' ),
507 class: 'button button-primary button-ok',
508 click: tp.callbacks.advanced_editor.confirm_save,
509 },
510 ],
511 } );
512
513 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.
514 if ( $active_textarea ) {
515 tp.callbacks.advanced_editor.$textarea.selectionStart = $active_textarea.selectionStart;
516 tp.callbacks.advanced_editor.$textarea.selectionEnd = $active_textarea.selectionEnd;
517 } else {
518 tp.callbacks.advanced_editor.$textarea.selectionStart = tp.callbacks.advanced_editor.$textarea.value.length;
519 tp.callbacks.advanced_editor.$textarea.selectionEnd = tp.callbacks.advanced_editor.$textarea.value.length;
520 }
521 tp.callbacks.advanced_editor.$textarea.focus();
522 };
523
524 /**
525 * Confirm and save changes of the Advanced Editor.
526 */
527 tp.callbacks.advanced_editor.confirm_save = function () {
528 const current_value = tp.editor.options.data[ tp.helpers.selection.rows[0] ][ tp.helpers.selection.columns[0] ];
529 // Only set the cell content if changes were made to not wrongly call tp.helpers.unsaved_changes.set().
530 if ( tp.callbacks.advanced_editor.$textarea.value !== current_value ) {
531 tp.editor.setValueFromCoords( tp.helpers.selection.columns[0], tp.helpers.selection.rows[0], tp.callbacks.advanced_editor.$textarea.value );
532 }
533 jQuery( this ).wpdialog( 'close' );
534 };
535
536 /**
537 * Inserts or duplicates rows or columns before each currently selected row/column.
538 *
539 * @param {string} action The action to perform on the selected rows/columns ("insert" or "duplicate").
540 * @param {string} type What to insert or duplicate ("rows" or "columns").
541 * @param {string} position Where to insert or duplicate ("before" or "after"). Default "before".
542 */
543 tp.callbacks.insert_duplicate = function ( action, type, position = 'before' ) {
544 const handling_rows = ( 'rows' === type );
545 const insert_function = handling_rows ? tp.editor.insertRow : tp.editor.insertColumn;
546 const getData_function = handling_rows ? tp.editor.getRowData : tp.editor.getColumnData;
547 const duplicating = ( 'duplicate' === action );
548 // Dynamically set the event handler, so that we have the action available in it.
549 tp.editor.options[ handling_rows ? 'oninsertrow' : 'oninsertcolumn' ] = tp.callbacks.editor.oninsertroc.bind( null, type, action );
550 tp.helpers.selection[ type ].forEach( function ( roc_idx, array_idx ) {
551 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.
552 const data = duplicating ? getData_function( shifted_roc_idx ) : 1;
553 const position_bool = 'before' === position; // true means "before".
554 insert_function( data, shifted_roc_idx, position_bool );
555 } );
556 tp.helpers.unsaved_changes.set();
557
558 // Select both the inserted and the selected rows/columns if more than one were selected or when duplicating, otherwise select just the original row/column.
559 const num_selected_rocs = tp.helpers.selection[ type ].length;
560 if ( num_selected_rocs > 1 || duplicating ) {
561 tp.editor.updateSelectionFromCoords(
562 tp.helpers.selection.columns[0],
563 tp.helpers.selection.rows[0],
564 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,
565 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 ]
566 );
567 } else {
568 // eslint-disable-next-line no-lonely-if
569 if ( 'before' === position ) {
570 tp.editor.updateSelectionFromCoords(
571 handling_rows ? tp.helpers.selection.columns[0] : tp.helpers.selection.columns[0] + num_selected_rocs,
572 handling_rows ? tp.helpers.selection.rows[0] + num_selected_rocs : tp.helpers.selection.rows[0],
573 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,
574 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 ]
575 );
576 } else {
577 // Call not needed, as the selection is still set when inserting after the selected (single!) row/column.
578 // tp.helpers.editor.reselect();
579 }
580
581 }
582 };
583
584 /**
585 * Removes currently selected rows or columns.
586 *
587 * @param {string} type What to remove ("rows" or "columns").
588 */
589 tp.callbacks.remove = function ( type ) {
590 const handling_rows = 'rows' === type;
591 const num_cors = handling_rows ? tp.editor.options.columns.length : tp.editor.options.data.length;
592
593 // Visibility meta information has to be deleted manually, as otherwise the Jspreadsheet meta information can get out of sync.
594 if ( tp.editor.options.meta ) {
595 tp.helpers.selection[ type ].forEach( function ( roc_idx ) {
596 for ( let cor_idx = 0; cor_idx < num_cors; cor_idx++ ) {
597 const cell_idx = handling_rows ? [ cor_idx, roc_idx ] : [ roc_idx, cor_idx ];
598 delete tp.editor.options.meta[ jspreadsheet.getColumnNameFromId( cell_idx ) ];
599 }
600 } );
601 }
602
603 const delete_function = handling_rows ? tp.editor.deleteRow : tp.editor.deleteColumn;
604 delete_function( tp.helpers.selection[ type ][0], tp.helpers.selection[ type ].length );
605 tp.helpers.unsaved_changes.set();
606
607 let first_row_idx = tp.helpers.selection.rows[0];
608 let first_col_idx = tp.helpers.selection.columns[0];
609 let last_row_idx = tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ];
610 let last_col_idx = tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ];
611 const last_roc_idx = handling_rows ? tp.editor.options.data.length : tp.editor.options.columns.length; // Index of last row/column AFTER deletion!
612 // Reselect last visible row/column, if last rows/columns were deleted.
613 if ( last_roc_idx === tp.helpers.selection[ type ][ tp.helpers.selection[ type ].length - 1 ] ) {
614 if ( handling_rows ) {
615 first_row_idx = Math.max( 0, first_row_idx - 1 );
616 last_row_idx = Math.max( 0, last_row_idx - 1 );
617 } else {
618 first_col_idx = Math.max( 0, first_col_idx - 1 );
619 last_col_idx = Math.max( 0, last_col_idx - 1 );
620 }
621 }
622 tp.editor.updateSelectionFromCoords( first_col_idx, first_row_idx, last_col_idx, last_row_idx );
623 };
624
625 /**
626 * Appends rows or columns at the bottom or right end of the table.
627 *
628 * @param {string} type What to append ("rows" or "columns").
629 * @param {number} num_rocs Number of rows or columns to append.
630 */
631 tp.callbacks.append = function ( type, num_rocs ) {
632 const handling_rows = ( 'rows' === type );
633 const insert_function = handling_rows ? tp.editor.insertRow : tp.editor.insertColumn;
634 // Dynamically set the event handler, so that we have the action available in it.
635 tp.editor.options[ handling_rows ? 'oninsertrow' : 'oninsertcolumn' ] = tp.callbacks.editor.oninsertroc.bind( null, type, 'append' );
636 insert_function( num_rocs );
637 tp.helpers.unsaved_changes.set();
638 };
639
640 /**
641 * Moves currently selected rows or columns.
642 *
643 * @param {string} direction Where to move the selected rows or columns (for rows: "up"/"down"/"top"/"bottom", for columns: "left"/right"/"first"/"last").
644 * @param {string} type What to move ("rows" or "columns").
645 */
646 tp.callbacks.move = function ( direction, type ) {
647 const handling_rows = ( 'rows' === type );
648
649 // Default case: up/left
650 let rocs = tp.helpers.selection[ type ]; // When moving up or left, start with the first row/column of the selected range.
651 let position_difference = -1; // New row/column number is one smaller than current row/column number.
652 // Alternate case: down/right
653 if ( 'down' === direction || 'right' === direction ) {
654 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.
655 position_difference = 1; // New row/column number is one higher than current row/column number.
656 } else if ( 'top' === direction || 'first' === direction ) {
657 position_difference = -rocs[0];
658 } else if ( 'bottom' === direction || 'last' === direction ) {
659 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.
660 const min_max_roc = ( 'rows' === type ) ? tp.editor.options.data.length - 1 : tp.editor.options.columns.length - 1;
661 position_difference = min_max_roc - rocs[0];
662 }
663
664 // Bail early if there is nothing to do (e.g. when the selected range is already at the target edge).
665 if ( 0 === position_difference ) {
666 return;
667 }
668
669 // Move the selected rows/columns individually.
670 const move_function = handling_rows ? tp.editor.moveRow : tp.editor.moveColumn;
671 rocs.forEach( ( roc_idx ) => move_function( roc_idx, roc_idx + position_difference ) );
672 tp.helpers.unsaved_changes.set();
673
674 // Reselect moved selection.
675 tp.editor.updateSelectionFromCoords(
676 handling_rows ? tp.helpers.selection.columns[0] : tp.helpers.selection.columns[0] + position_difference,
677 handling_rows ? tp.helpers.selection.rows[0] + position_difference : tp.helpers.selection.rows[0],
678 handling_rows ? tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ] : tp.helpers.selection.columns[ tp.helpers.selection.columns.length - 1 ] + position_difference,
679 handling_rows ? tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ] + position_difference : tp.helpers.selection.rows[ tp.helpers.selection.rows.length - 1 ]
680 );
681 };
682
683 /**
684 * Sorts the table data by the first currently selected column.
685 *
686 * @param {string} direction Sort order/direction ("asc" for ascending, "desc" for descending).
687 */
688 tp.callbacks.sort = function ( direction ) {
689 tp.editor.orderBy( tp.helpers.selection.columns[0], ( 'desc' === direction ) );
690 };
691
692 /**
693 * Hides or unhides selected rows or columns.
694 *
695 * @param {string} action The action to perform on the rows/columns ("hide" or "unhide").
696 * @param {string} type What to hide or unhide ("rows" or "columns").
697 */
698 tp.callbacks.hide_unhide = function ( action, type ) {
699 const handling_rows = ( 'rows' === type );
700 const property = handling_rows ? 'row_hidden' : 'column_hidden';
701 const num_cors = handling_rows ? tp.editor.options.columns.length : tp.editor.options.data.length;
702 const cell_hidden = ( 'hide' === action );
703 const meta = {};
704 tp.helpers.selection[ type ].forEach( function ( roc_idx ) {
705 for ( let cor_idx = 0; cor_idx < num_cors; cor_idx++ ) {
706 const cell_idx = handling_rows ? [ cor_idx, roc_idx ] : [ roc_idx, cor_idx ];
707 const cell_name = jspreadsheet.getColumnNameFromId( cell_idx );
708 meta[ cell_name ] = {};
709 meta[ cell_name ][ property ] = cell_hidden;
710 }
711 } );
712 tp.editor.setMeta( meta );
713 tp.helpers.unsaved_changes.set();
714 tp.editor.updateTable(); // Redraw table.
715 };
716
717 /**
718 * Combines/merges the currently selected cells.
719 */
720 tp.callbacks.merge_cells = function () {
721 const current_col_idx = tp.helpers.selection.columns[0];
722 const current_row_idx = tp.helpers.selection.rows[0];
723 const colspan = tp.helpers.selection.columns.length;
724 const rowspan = tp.helpers.selection.rows.length;
725 for ( let row_idx = 1; row_idx < rowspan; row_idx++ ) {
726 tp.editor.setValueFromCoords( current_col_idx, current_row_idx + row_idx, '#rowspan#' );
727 }
728 for ( let col_idx = 1; col_idx < colspan; col_idx++ ) {
729 tp.editor.setValueFromCoords( current_col_idx + col_idx, current_row_idx, '#colspan#' );
730 }
731 for ( let row_idx = 1; row_idx < rowspan; row_idx++ ) {
732 for ( let col_idx = 1; col_idx < colspan; col_idx++ ) {
733 tp.editor.setValueFromCoords( current_col_idx + col_idx, current_row_idx + row_idx, '#span#' );
734 }
735 }
736 tp.helpers.unsaved_changes.set();
737 };
738
739 /*
740 * Initialize Jspreadsheet.
741 */
742 tp.editor = jspreadsheet( $( '#table-editor' ), {
743 data: tp.table.data,
744 meta: tp.helpers.visibility.load(),
745 wordWrap: true,
746 rowDrag: true,
747 rowResize: true,
748 columnSorting: true,
749 columnDrag: true,
750 columnResize: true,
751 defaultColWidth: tp.screenOptions.table_editor_column_width,
752 defaultColAlign: 'left',
753 parseFormulas: false,
754 allowExport: false,
755 allowComments: false,
756 allowManualInsertRow: false, // To prevent addition of new row when Enter is pressed in last row.
757 allowManualInsertColumn: false, // To prevent addition of new column when Tab is pressed in last column.
758 about: false,
759 secureFormulas: false,
760 detachForUpdates: true,
761 onselection: tp.callbacks.editor.onselection,
762 updateTable: tp.callbacks.editor.onupdatetable,
763 contextMenu,
764 sorting: tp.helpers.editor.sorting,
765 // Keep the selection when certain events occur and the table loses focus.
766 onmoverow: tp.callbacks.editor.onmove,
767 onmovecolumn: tp.callbacks.editor.onmove,
768 onblur: tp.helpers.editor.reselect,
769 onload: tp.helpers.editor.reselect, // When the table is loaded, select the top-left cell A1.
770 onchange: tp.helpers.unsaved_changes.set,
771 onsort: tp.callbacks.editor.onsort,
772 } );
773
774 // Register callback for inserting a link into a cell after it has been constructed in the wpLink dialog.
775 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.
776
777 // This code requires jQuery, and it must run when the DOM is ready.
778 jQuery( () => {
779 // Fix issue with wpLink input fields not being usable, when called through the "Advanced Editor". They are immediately losing focus without this.
780 jQuery( '#wp-link' ).on( 'focus', 'input', ( event ) => ( event.stopPropagation() ) );
781
782 // 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.
783 jQuery( 'body' ).on( 'focus', '.media-modal .media-frame-content input, .media-modal .media-frame-content textarea', ( event ) => ( event.stopPropagation() ) );
784 } );
785