ChangeData.js
2 years ago
CheckboxData.js
2 years ago
InputData.js
2 years ago
InputMaskedData.js
2 years ago
MultiSelectData.js
2 years ago
NoListenData.js
2 years ago
RadioData.js
2 years ago
RangeData.js
2 years ago
RenderStateData.js
2 years ago
WysiwygData.js
2 years ago
functions.js
2 years ago
InputData.js
452 lines
| 1 | import LoadingReactiveVar from '../reactive/LoadingReactiveVar'; |
| 2 | import ReactiveVar from '../reactive/ReactiveVar'; |
| 3 | import ReactiveHook from '../reactive/ReactiveHook'; |
| 4 | import { getSignal } from '../signals/functions'; |
| 5 | import { createReport } from '../reporting/functions'; |
| 6 | import { getParsedName } from './functions'; |
| 7 | import { getOffsetTop, isVisible } from '../functions'; |
| 8 | import { STRICT_MODE } from '../signals/BaseSignal'; |
| 9 | |
| 10 | const { doAction } = JetPlugins.hooks; |
| 11 | |
| 12 | /** |
| 13 | * @property {string} rawName |
| 14 | * @property {string} name |
| 15 | * @property {Node|boolean} comment |
| 16 | * @property {HTMLInputElement|*[]} nodes |
| 17 | * @property {ReactiveVar} value |
| 18 | * @property {ConditionChecker|null} checker |
| 19 | * @property {*} calcValue |
| 20 | * @property {AdvancedReporting|BrowserReporting} reporting |
| 21 | * @property {Observable} root |
| 22 | * @property {PageState} page |
| 23 | * @property {LoadingReactiveVar} loading |
| 24 | * @property {Object<ReactiveVar>} attrs |
| 25 | * @property {boolean} isRequired |
| 26 | * @property {null|ReactiveHook} enterKey |
| 27 | * @property {null|string} inputType |
| 28 | * |
| 29 | * @constructor |
| 30 | */ |
| 31 | function InputData() { |
| 32 | this.rawName = ''; |
| 33 | this.name = ''; |
| 34 | this.comment = false; |
| 35 | /** |
| 36 | * @type {HTMLElement[]|HTMLInputElement[]} |
| 37 | */ |
| 38 | this.nodes = []; |
| 39 | this.attrs = {}; |
| 40 | this.enterKey = null; |
| 41 | this.inputType = null; |
| 42 | this.offsetOnFocus = 75; |
| 43 | |
| 44 | /** |
| 45 | * Path from top of form to current field name |
| 46 | * Ex.: [ 'repeater_name', 0, 'text_field' ] |
| 47 | * Where: |
| 48 | * - 'repeater_name': name of repeater, where current field is placed |
| 49 | * - 0: index of repeater row, where current field is placed |
| 50 | * - 'text_field': name of current field name |
| 51 | * @type {Array<String|Number>} |
| 52 | */ |
| 53 | this.path = []; |
| 54 | |
| 55 | /** |
| 56 | * @type {ReactiveVar} |
| 57 | */ |
| 58 | this.value = this.getReactive(); |
| 59 | this.value.watch( this.onChange.bind( this ) ); |
| 60 | |
| 61 | this.isRequired = false; |
| 62 | this.calcValue = null; |
| 63 | |
| 64 | /** |
| 65 | * @type {AdvancedReporting|BrowserReporting} |
| 66 | */ |
| 67 | this.reporting = null; |
| 68 | |
| 69 | this.checker = null; |
| 70 | |
| 71 | /** |
| 72 | * @type {Observable} |
| 73 | */ |
| 74 | this.root = null; |
| 75 | |
| 76 | this.loading = new LoadingReactiveVar( false ); |
| 77 | this.loading.make(); |
| 78 | } |
| 79 | |
| 80 | InputData.prototype.attrs = {}; |
| 81 | |
| 82 | /** |
| 83 | * @param node {HTMLElement} |
| 84 | * @returns {boolean} |
| 85 | */ |
| 86 | InputData.prototype.isSupported = function ( node ) { |
| 87 | return true; |
| 88 | }; |
| 89 | InputData.prototype.addListeners = function () { |
| 90 | const [ node ] = this.nodes; |
| 91 | |
| 92 | node.addEventListener( 'input', event => { |
| 93 | this.value.current = event.target.value; |
| 94 | } ); |
| 95 | |
| 96 | node.addEventListener( 'blur', event => { |
| 97 | this.reportOnBlur(); |
| 98 | } ); |
| 99 | |
| 100 | /** |
| 101 | * @since 3.0.1 |
| 102 | */ |
| 103 | !STRICT_MODE && jQuery( node ).on( 'change', event => { |
| 104 | if ( this.value.current == event.target.value ) { |
| 105 | return; |
| 106 | } |
| 107 | this.callable.lockTrigger(); |
| 108 | this.value.current = event.target.value; |
| 109 | this.callable.unlockTrigger(); |
| 110 | } ); |
| 111 | |
| 112 | if ( 'input' !== this.inputType ) { |
| 113 | return; |
| 114 | } |
| 115 | |
| 116 | this.enterKey = new ReactiveHook(); |
| 117 | node.addEventListener( 'keydown', this.handleEnterKey.bind( this ) ); |
| 118 | }; |
| 119 | InputData.prototype.makeReactive = function () { |
| 120 | this.onObserve(); |
| 121 | this.addListeners(); |
| 122 | this.setValue(); |
| 123 | this.initNotifyValue(); |
| 124 | |
| 125 | this.value.make(); |
| 126 | |
| 127 | doAction( 'jet.fb.input.makeReactive', this ); |
| 128 | }; |
| 129 | InputData.prototype.onChange = function ( prevValue ) { |
| 130 | this.calcValue = this.value.current; |
| 131 | |
| 132 | // apply changes in DOM |
| 133 | this?.callable?.run( prevValue ); |
| 134 | |
| 135 | // show errors |
| 136 | this.report(); |
| 137 | }; |
| 138 | InputData.prototype.report = function () { |
| 139 | this.reporting.validateOnChange(); |
| 140 | }; |
| 141 | InputData.prototype.reportOnBlur = function () { |
| 142 | this.reporting.validateOnBlur(); |
| 143 | }; |
| 144 | /** |
| 145 | * @param callable |
| 146 | * @returns {(function(): *|*[])|*} |
| 147 | */ |
| 148 | InputData.prototype.watch = function ( callable ) { |
| 149 | return this.value.watch( callable ); |
| 150 | }; |
| 151 | InputData.prototype.watchValidity = function ( callable ) { |
| 152 | return this.reporting.validityState.watch( callable ); |
| 153 | }; |
| 154 | /** |
| 155 | * @param callable |
| 156 | * @returns {(function(): *|*[])|*} |
| 157 | */ |
| 158 | InputData.prototype.sanitize = function ( callable ) { |
| 159 | return this.value.sanitize( callable ); |
| 160 | }; |
| 161 | /** |
| 162 | * @param inputData {InputData} |
| 163 | */ |
| 164 | InputData.prototype.merge = function ( inputData ) { |
| 165 | this.nodes = [ ...inputData.getNode() ]; |
| 166 | }; |
| 167 | InputData.prototype.setValue = function () { |
| 168 | if ( this.isArray() ) { |
| 169 | this.value.current = Array.from( this.nodes ). |
| 170 | map( ( { value } ) => value ); |
| 171 | } |
| 172 | else { |
| 173 | this.value.current = this.nodes[ 0 ]?.value; |
| 174 | } |
| 175 | this.calcValue = this.value.current; |
| 176 | }; |
| 177 | /** |
| 178 | * @param node {HTMLElement|HTMLInputElement} |
| 179 | */ |
| 180 | InputData.prototype.setNode = function ( node ) { |
| 181 | this.nodes = [ node ]; |
| 182 | this.rawName = node.name ?? ''; |
| 183 | this.name = getParsedName( this.rawName ); |
| 184 | |
| 185 | this.inputType = node.nodeName.toLowerCase(); |
| 186 | }; |
| 187 | /** |
| 188 | * Runs once in lifecycle. |
| 189 | */ |
| 190 | InputData.prototype.onObserve = function () { |
| 191 | const [ node ] = this.nodes; |
| 192 | |
| 193 | /** |
| 194 | * Save link to this object |
| 195 | * @type {InputData} |
| 196 | */ |
| 197 | node.jfbSync = this; |
| 198 | |
| 199 | this.isRequired = this.checkIsRequired(); |
| 200 | |
| 201 | this.callable = getSignal( node, this ); |
| 202 | this.callable.setInput( this ); |
| 203 | |
| 204 | this.reporting = createReport( this ); |
| 205 | |
| 206 | this.loading.watch( () => this.onChangeLoading() ); |
| 207 | |
| 208 | this.path = [ ...this.getParentPath(), this.name ]; |
| 209 | |
| 210 | if ( |
| 211 | // is ajax |
| 212 | !this.getSubmit().submitter.hasOwnProperty( 'status' ) || |
| 213 | this.hasParent() |
| 214 | ) { |
| 215 | return; |
| 216 | } |
| 217 | |
| 218 | this.getSubmit().submitter.watchReset( () => this.onClear() ); |
| 219 | }; |
| 220 | InputData.prototype.onChangeLoading = function () { |
| 221 | this.getSubmit().lockState.current = this.loading.current; |
| 222 | |
| 223 | const [ node ] = this.nodes; |
| 224 | const wrapper = node.closest( '.jet-form-builder-row' ); |
| 225 | |
| 226 | node.readOnly = this.loading.current; |
| 227 | wrapper.classList.toggle( 'is-loading', this.loading.current ); |
| 228 | }; |
| 229 | /** |
| 230 | * @param observable {Observable} |
| 231 | */ |
| 232 | InputData.prototype.setRoot = function ( observable ) { |
| 233 | this.root = observable; |
| 234 | }; |
| 235 | /** |
| 236 | * By default it runs only if repeater item was removed |
| 237 | */ |
| 238 | InputData.prototype.onRemove = function () { |
| 239 | }; |
| 240 | /** |
| 241 | * @returns {string} |
| 242 | */ |
| 243 | InputData.prototype.getName = function () { |
| 244 | return this.name; |
| 245 | }; |
| 246 | /** |
| 247 | * @returns {array|string} |
| 248 | */ |
| 249 | InputData.prototype.getValue = function () { |
| 250 | return this.value.current; |
| 251 | }; |
| 252 | /** |
| 253 | * @returns {array} |
| 254 | */ |
| 255 | InputData.prototype.getNode = function () { |
| 256 | return this.nodes; |
| 257 | }; |
| 258 | /** |
| 259 | * @returns {boolean} |
| 260 | */ |
| 261 | InputData.prototype.isArray = function () { |
| 262 | return this.rawName.includes( '[]' ); |
| 263 | }; |
| 264 | /** |
| 265 | * @param callable {Function|mixed} |
| 266 | * @param inputContext {InputData|Boolean} |
| 267 | */ |
| 268 | InputData.prototype.beforeSubmit = function ( callable, inputContext = false ) { |
| 269 | this.getSubmit().submitter.promise( callable, inputContext ); |
| 270 | }; |
| 271 | /** |
| 272 | * @returns {FormSubmit} |
| 273 | */ |
| 274 | InputData.prototype.getSubmit = function () { |
| 275 | return this.getRoot().form; |
| 276 | }; |
| 277 | /** |
| 278 | * @returns {Observable} |
| 279 | */ |
| 280 | InputData.prototype.getRoot = function () { |
| 281 | if ( ! this.root?.parent ) { |
| 282 | return this.root; |
| 283 | } |
| 284 | return this.root.parent.getRoot(); |
| 285 | }; |
| 286 | |
| 287 | InputData.prototype.isVisible = function () { |
| 288 | const wrapper = this.getWrapperNode(); |
| 289 | |
| 290 | return isVisible( wrapper ); |
| 291 | }; |
| 292 | |
| 293 | InputData.prototype.onClear = function () { |
| 294 | this.silenceSet( null ); |
| 295 | }; |
| 296 | |
| 297 | InputData.prototype.getReactive = function () { |
| 298 | return new ReactiveVar(); |
| 299 | }; |
| 300 | |
| 301 | InputData.prototype.checkIsRequired = function () { |
| 302 | const [ node ] = this.nodes; |
| 303 | |
| 304 | return node.required ?? !!node.dataset.required?.length; |
| 305 | }; |
| 306 | |
| 307 | InputData.prototype.silenceSet = function ( value ) { |
| 308 | /** |
| 309 | * Related to issue |
| 310 | * @link https://github.com/Crocoblock/issues-tracker/issues/1261#issuecomment-1293290291 |
| 311 | */ |
| 312 | const tempReport = this.report.bind( this ); |
| 313 | |
| 314 | this.report = () => {}; |
| 315 | |
| 316 | this.value.current = value; |
| 317 | |
| 318 | this.report = tempReport; |
| 319 | }; |
| 320 | |
| 321 | InputData.prototype.silenceNotify = function () { |
| 322 | const tempReport = this.report.bind( this ); |
| 323 | |
| 324 | this.report = () => {}; |
| 325 | |
| 326 | this.value.notify(); |
| 327 | |
| 328 | this.report = tempReport; |
| 329 | }; |
| 330 | |
| 331 | /** |
| 332 | * @return {boolean} |
| 333 | */ |
| 334 | InputData.prototype.hasParent = function () { |
| 335 | return !!this.root?.parent; |
| 336 | }; |
| 337 | |
| 338 | /** |
| 339 | * For insert errors in advanced validation mode |
| 340 | * @returns {*} |
| 341 | */ |
| 342 | InputData.prototype.getWrapperNode = function () { |
| 343 | return this.nodes[ 0 ].closest( '.jet-form-builder-row' ); |
| 344 | }; |
| 345 | |
| 346 | InputData.prototype.handleEnterKey = function ( event ) { |
| 347 | // not enter |
| 348 | if ( event.key !== 'Enter' ) { |
| 349 | return; |
| 350 | } |
| 351 | |
| 352 | event.preventDefault(); |
| 353 | |
| 354 | this.onEnterKey(); |
| 355 | }; |
| 356 | |
| 357 | InputData.prototype.onEnterKey = function () { |
| 358 | const canSubmit = this.enterKey.applyFilters( true ); |
| 359 | |
| 360 | if ( canSubmit ) { |
| 361 | this.getSubmit().submit(); |
| 362 | } |
| 363 | }; |
| 364 | |
| 365 | InputData.prototype.initNotifyValue = function () { |
| 366 | this.silenceNotify(); |
| 367 | }; |
| 368 | |
| 369 | InputData.prototype.onFocus = function () { |
| 370 | this.scrollTo(); |
| 371 | this.focusRaw(); |
| 372 | }; |
| 373 | InputData.prototype.focusRaw = function () { |
| 374 | const [ node ] = this.nodes; |
| 375 | |
| 376 | /** |
| 377 | * @see https://github.com/Crocoblock/issues-tracker/issues/2265#issuecomment-1447988718 |
| 378 | */ |
| 379 | if ( [ 'date', 'time', 'datetime-local' ].includes( node.type ) ) { |
| 380 | return; |
| 381 | } |
| 382 | |
| 383 | node?.focus( { preventScroll: true } ); |
| 384 | }; |
| 385 | InputData.prototype.scrollTo = function () { |
| 386 | const wrapper = this.getWrapperNode(); |
| 387 | |
| 388 | window.scrollTo( { |
| 389 | top: getOffsetTop( wrapper ) - this.offsetOnFocus, |
| 390 | behavior: 'smooth', |
| 391 | } ); |
| 392 | }; |
| 393 | |
| 394 | /** |
| 395 | * @return {ReportingContext} |
| 396 | */ |
| 397 | InputData.prototype.getContext = function () { |
| 398 | return this.root.getContext(); |
| 399 | }; |
| 400 | |
| 401 | /** |
| 402 | * @return {boolean|InputData[]} |
| 403 | */ |
| 404 | InputData.prototype.populateInner = function () { |
| 405 | return false; |
| 406 | }; |
| 407 | |
| 408 | /** |
| 409 | * Executed with default browser validation |
| 410 | * |
| 411 | * @returns {boolean} |
| 412 | */ |
| 413 | InputData.prototype.hasAutoScroll = function () { |
| 414 | return true; |
| 415 | }; |
| 416 | |
| 417 | /** |
| 418 | * @returns {HTMLInputElement|HTMLElement} |
| 419 | */ |
| 420 | InputData.prototype.getReportingNode = function () { |
| 421 | return this.nodes[ 0 ]; |
| 422 | }; |
| 423 | |
| 424 | InputData.prototype.getParentPath = function () { |
| 425 | if ( !this.root?.parent ) { |
| 426 | return []; |
| 427 | } |
| 428 | |
| 429 | /** |
| 430 | * @type {Array|Object} |
| 431 | */ |
| 432 | const value = this.root.parent.value.current; |
| 433 | |
| 434 | if ( 'object' !== typeof value ) { |
| 435 | return []; |
| 436 | } |
| 437 | |
| 438 | for ( const [ index, row ] of Object.entries( value ) ) { |
| 439 | if ( row !== this.root ) { |
| 440 | continue; |
| 441 | } |
| 442 | return [ |
| 443 | ...this.root.parent.getParentPath(), |
| 444 | this.root.parent.name, |
| 445 | index, |
| 446 | ]; |
| 447 | } |
| 448 | |
| 449 | return []; |
| 450 | }; |
| 451 | |
| 452 | export default InputData; |