PluginProbe
TablePress – Tables in WordPress made easy / 3.3.2
TablePress – Tables in WordPress made easy v3.3.2
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.3.2, at admin/js/edit/editor.js

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