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