InlineControlContainer.ts 43.1 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

14
15
import AjaxRequest = require('TYPO3/CMS/Core/Ajax/AjaxRequest');
import {MessageUtility} from '../../Utility/MessageUtility';
16
17
import {AjaxDispatcher} from './../InlineRelation/AjaxDispatcher';
import {InlineResponseInterface} from './../InlineRelation/InlineResponseInterface';
18
import DocumentService = require('TYPO3/CMS/Core/DocumentService');
19
20
import NProgress = require('nprogress');
import Sortable = require('Sortable');
21
22
23
24
25
26
import FormEngine = require('TYPO3/CMS/Backend/FormEngine');
import FormEngineValidation = require('TYPO3/CMS/Backend/FormEngineValidation');
import Icons = require('../../Icons');
import InfoWindow = require('../../InfoWindow');
import Modal = require('../../Modal');
import Notification = require('../../Notification');
27
import RegularEvent = require('TYPO3/CMS/Core/Event/RegularEvent');
28
29
30
31
import Severity = require('../../Severity');
import Utility = require('../../Utility');

enum Selectors {
32
  toggleSelector = '[data-bs-toggle="formengine-inline"]',
33
34
35
36
37
38
39
40
41
  controlSectionSelector = '.t3js-formengine-irre-control',
  createNewRecordButtonSelector = '.t3js-create-new-button',
  createNewRecordBySelectorSelector = '.t3js-create-new-selector',
  deleteRecordButtonSelector = '.t3js-editform-delete-inline-record',
  enableDisableRecordButtonSelector = '.t3js-toggle-visibility-button',
  infoWindowButton = '[data-action="infowindow"]',
  synchronizeLocalizeRecordButtonSelector = '.t3js-synchronizelocalize-button',
  uniqueValueSelectors = 'select.t3js-inline-unique',
  revertUniqueness = '.t3js-revert-unique',
42
  controlContainer = '.t3js-inline-controls',
43
44
45
46
47
48
}

enum States {
  new = 'inlineIsNewRecord',
  visible = 'panel-visible',
  collapsed = 'panel-collapsed',
49
  notLoaded = 't3js-not-loaded',
50
51
52
53
54
55
56
57
58
59
60
}

enum Separators {
  structureSeparator = '-',
}

enum SortDirections {
  DOWN = 'down',
  UP = 'up',
}

61
62
interface RequestQueue {
  [key: string]: AjaxRequest;
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
}

interface ProgressQueue {
  [key: string]: any;
}

interface Appearance {
  expandSingle?: boolean;
  useSortable?: boolean;
}

interface UniqueDefinition {
  elTable: string;
  field: string;
  max: number;
  possible: { [key: string]: string };
  selector: string;
  table: string;
  type: string;
  used: UniqueDefinitionCollection;
}

interface UniqueDefinitionCollection {
  [key: string]: UniqueDefinitionUsed;
}

interface UniqueDefinitionUsed {
  table: string;
  uid: string | number;
}

class InlineControlContainer {
  private container: HTMLElement = null;
  private ajaxDispatcher: AjaxDispatcher = null;
  private appearance: Appearance = null;
98
  private requestQueue: RequestQueue = {};
99
100
101
102
103
104
105
106
107
108
109
  private progessQueue: ProgressQueue = {};
  private noTitleString: string = (TYPO3.lang ? TYPO3.lang['FormEngine.noRecordTitle'] : '[No title]');

  /**
   * @param {string} objectId
   * @return HTMLDivElement
   */
  private static getInlineRecordContainer(objectId: string): HTMLDivElement {
    return <HTMLDivElement>document.querySelector('[data-object-id="' + objectId + '"]');
  }

110
111
112
113
114
115
116
117
  /**
   * @param {string} objectId
   * @return HTMLButtonElement
   */
  private static getCollapseButton(objectId: string): HTMLButtonElement {
    return <HTMLButtonElement>document.querySelector('[aria-controls="' + objectId + '_fields"]');
  }

118
119
120
121
122
123
  /**
   * @param {string} objectId
   */
  private static toggleElement(objectId: string): void {
    const recordContainer = InlineControlContainer.getInlineRecordContainer(objectId);
    if (recordContainer.classList.contains(States.collapsed)) {
124
      InlineControlContainer.expandElement(recordContainer, objectId);
125
    } else {
126
      InlineControlContainer.collapseElement(recordContainer, objectId);
127
128
129
    }
  }

130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
  /**
   * @param {HTMLDivElement} recordContainer
   * @param {string} objectId
   */
  private static collapseElement(recordContainer: HTMLDivElement, objectId: string): void {
    const collapseButton = InlineControlContainer.getCollapseButton(objectId);
    recordContainer.classList.remove(States.visible);
    recordContainer.classList.add(States.collapsed);
    collapseButton.setAttribute('aria-expanded', 'false');
  }

  /**
   * @param {HTMLDivElement} recordContainer
   * @param {string} objectId
   */
  private static expandElement(recordContainer: HTMLDivElement, objectId: string): void {
    const collapseButton = InlineControlContainer.getCollapseButton(objectId);
    recordContainer.classList.remove(States.collapsed);
    recordContainer.classList.add(States.visible);
    collapseButton.setAttribute('aria-expanded', 'true');
  }

152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
  /**
   * @param {string} objectId
   * @return boolean
   */
  private static isNewRecord(objectId: string): boolean {
    const recordContainer = InlineControlContainer.getInlineRecordContainer(objectId);
    return recordContainer.classList.contains(States.new);
  }

  /**
   * @param {string} objectId
   * @param {boolean} value
   */
  private static updateExpandedCollapsedStateLocally(objectId: string, value: boolean): void {
    const recordContainer = InlineControlContainer.getInlineRecordContainer(objectId);

    const ucName = 'uc[inlineView]'
      + '[' + recordContainer.dataset.topmostParentTable + ']'
      + '[' + recordContainer.dataset.topmostParentUid + ']'
      + recordContainer.dataset.fieldName;
    const ucFormObj = document.getElementsByName(ucName);

    if (ucFormObj.length) {
      (<HTMLInputElement>ucFormObj[0]).value = value ? '1' : '0';
    }
  }

  /**
   * @param {UniqueDefinitionCollection} hashmap
   */
  private static getValuesFromHashMap(hashmap: UniqueDefinitionCollection): Array<any> {
183
    return Object.keys(hashmap).map((key: string) => hashmap[key]);
184
185
  }

186
187
188
189
  private static selectOptionValueExists(selectElement: HTMLSelectElement, value: string): boolean {
    return selectElement.querySelector('option[value="' + value + '"]') !== null;
  }

190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
  /**
   * @param {HTMLSelectElement} selectElement
   * @param {string} value
   */
  private static removeSelectOptionByValue(selectElement: HTMLSelectElement, value: string): void {
    const option = selectElement.querySelector('option[value="' + value + '"]');
    if (option !== null) {
      option.remove();
    }
  }

  /**
   * @param {HTMLSelectElement} selectElement
   * @param {string} value
   * @param {UniqueDefinition} unique
   */
  private static reAddSelectOption(selectElement: HTMLSelectElement, value: string, unique: UniqueDefinition): void {
207
208
209
210
    if (InlineControlContainer.selectOptionValueExists(selectElement, value)) {
      return;
    }

211
    const options: NodeListOf<HTMLOptionElement> = selectElement.querySelectorAll('option');
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
    let index: number = -1;

    for (let possibleValue of Object.keys(unique.possible)) {
      if (possibleValue === value) {
        break;
      }

      for (let k = 0; k < options.length; ++k) {
        const option = options[k];
        if (option.value === possibleValue) {
          index = k;
          break;
        }
      }
    }

    if (index === -1) {
      index = 0;
    } else if (index < options.length) {
      index++;
    }
    // recreate the <option> tag
    const readdOption = document.createElement('option');
    readdOption.text = unique.possible[value];
    readdOption.value = value;
    // add the <option> at the right position
    selectElement.insertBefore(readdOption, selectElement.options[index]);
  }

  /**
   * @param {string} elementId
   */
  constructor(elementId: string) {
245
    DocumentService.ready().then((document: Document): void => {
246
      this.container = <HTMLElement>document.getElementById(elementId);
247
248
249
250
251
252
253
      this.ajaxDispatcher = new AjaxDispatcher(this.container.dataset.objectGroup);

      this.registerEvents();
    });
  }

  private registerEvents(): void {
254
255
256
257
258
259
260
261
    this.registerInfoButton();
    this.registerSort();
    this.registerCreateRecordButton();
    this.registerEnableDisableButton();
    this.registerDeleteButton();
    this.registerSynchronizeLocalize();
    this.registerRevertUniquenessAction();
    this.registerToggle();
262

263
264
    this.registerCreateRecordBySelector();
    this.registerUniqueSelectFieldChanged();
265

266
    new RegularEvent('message', this.handlePostMessage).bindTo(window);
267
268

    if (this.getAppearance().useSortable) {
269
      const recordListContainer = <HTMLDivElement>document.getElementById(this.container.getAttribute('id') + '_records');
270
271
272
273
274
275
276
277
278
279
280
      // tslint:disable-next-line:no-unused-expression
      new Sortable(recordListContainer, {
        group: recordListContainer.getAttribute('id'),
        handle: '.sortableHandle',
        onSort: (): void => {
          this.updateSorting();
        },
      });
    }
  }

281
282
283
284
285
  private registerToggle(): void {
    const me = this;
    new RegularEvent('click', function(this: HTMLElement, e: Event) {
      e.preventDefault();
      e.stopImmediatePropagation();
286

287
288
      me.loadRecordDetails(this.closest(Selectors.toggleSelector).parentElement.dataset.objectId);
    }).delegateTo(this.container, `${Selectors.toggleSelector} .form-irre-header-cell:not(${Selectors.controlSectionSelector}`);
289
290
  }

291
292
293
294
295
  private registerSort(): void {
    const me = this;
    new RegularEvent('click', function(this: HTMLElement, e: Event) {
      e.preventDefault();
      e.stopImmediatePropagation();
296

297
298
299
300
301
      me.changeSortingByButton(
        (<HTMLDivElement>this.closest('[data-object-id]')).dataset.objectId,
        <SortDirections>this.dataset.direction,
      );
    }).delegateTo(this.container, Selectors.controlSectionSelector + ' [data-action="sort"]');
302
303
  }

304
305
306
307
308
  private registerCreateRecordButton(): void {
    const me = this;
    new RegularEvent('click', function(this: HTMLElement, e: Event) {
      e.preventDefault();
      e.stopImmediatePropagation();
309

310
311
312
313
314
      if (me.isBelowMax()) {
        let objectId = me.container.dataset.objectGroup;
        if (typeof this.dataset.recordUid !== 'undefined') {
          objectId += Separators.structureSeparator + this.dataset.recordUid;
        }
315

316
        me.importRecord([objectId, (me.container.querySelector(Selectors.createNewRecordBySelectorSelector) as HTMLInputElement)?.value], this.dataset.recordUid ?? null);
317
      }
318
    }).delegateTo(this.container, Selectors.createNewRecordButtonSelector);
319
320
  }

321
322
323
324
325
  private registerCreateRecordBySelector(): void {
    const me = this;
    new RegularEvent('change', function(this: HTMLElement, e: Event) {
      e.preventDefault();
      e.stopImmediatePropagation();
326

327
328
      const selectTarget = <HTMLSelectElement>this;
      const recordUid = selectTarget.options[selectTarget.selectedIndex].getAttribute('value');
329

330
331
      me.importRecord([me.container.dataset.objectGroup, recordUid]);
    }).delegateTo(this.container, Selectors.createNewRecordBySelectorSelector);
332
333
334
335
336
337
338
  }

  /**
   * @param {MessageEvent} e
   */
  private handlePostMessage = (e: MessageEvent): void => {
    if (!MessageUtility.verifyOrigin(e.origin)) {
339
      throw 'Denied message sent by ' + e.origin;
340
341
    }

342
    if (e.data.actionName === 'typo3:foreignRelation:insert') {
343
344
345
      if (typeof e.data.objectGroup === 'undefined') {
        throw 'No object group defined for message';
      }
346

347
348
349
350
      if (e.data.objectGroup !== this.container.dataset.objectGroup) {
        // Received message isn't provisioned for current InlineControlContainer instance
        return;
      }
351

352
353
354
355
      if (this.isUniqueElementUsed(parseInt(e.data.uid, 10), e.data.table)) {
        Notification.error('There is already a relation to the selected element');
        return;
      }
356

357
358
359
360
361
362
363
364
365
366
367
      this.importRecord([e.data.objectGroup, e.data.uid]).then((): void => {
        if (e.source) {
          const message = {
            actionName: 'typo3:foreignRelation:inserted',
            objectGroup: e.data.objectId,
            table: e.data.table,
            uid: e.data.uid,
          };
          MessageUtility.send(message, e.source as Window);
        }
      });
368
369
    } else {
      console.warn(`Unhandled action "${e.data.actionName}"`);
370
    }
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
  }

  /**
   * @param {string} uid
   * @param {string} markup
   * @param {string} afterUid
   * @param {string} selectedValue
   */
  private createRecord(uid: string, markup: string, afterUid: string = null, selectedValue: string = null): void {
    let objectId = this.container.dataset.objectGroup;
    if (afterUid !== null) {
      objectId += Separators.structureSeparator + afterUid;
    }

    if (afterUid !== null) {
      InlineControlContainer.getInlineRecordContainer(objectId).insertAdjacentHTML('afterend', markup);
      this.memorizeAddRecord(uid, afterUid, selectedValue);
    } else {
389
      document.getElementById(this.container.getAttribute('id') + '_records').insertAdjacentHTML('beforeend', markup);
390
391
392
393
394
395
396
397
      this.memorizeAddRecord(uid, null, selectedValue);
    }
  }

  /**
   * @param {Array} params
   * @param {string} afterUid
   */
398
399
  private async importRecord(params: Array<any>, afterUid?: string): Promise<void> {
    return this.ajaxDispatcher.send(
400
401
      this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_create')),
      params,
402
    ).then(async (response: InlineResponseInterface): Promise<void> => {
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
      if (this.isBelowMax()) {
        this.createRecord(
          response.compilerInput.uid,
          response.data,
          typeof afterUid !== 'undefined' ? afterUid : null,
          typeof response.compilerInput.childChildUid !== 'undefined' ? response.compilerInput.childChildUid : null,
        );

        FormEngine.reinitialize();
        FormEngine.Validation.initializeInputFields();
        FormEngine.Validation.validate();
      }
    });
  }

418
  private registerEnableDisableButton(): void {
419
    new RegularEvent('click', (e: Event, target: HTMLElement): void => {
420
421
422
      e.preventDefault();
      e.stopImmediatePropagation();

423
      const objectId = (<HTMLDivElement>target.closest('[data-object-id]')).dataset.objectId;
424
      const recordContainer = InlineControlContainer.getInlineRecordContainer(objectId);
425
      const hiddenFieldName = 'data' + recordContainer.dataset.fieldName + '[' + target.dataset.hiddenField + ']';
426
427
428
429
430
431
      const hiddenValueCheckBox = <HTMLInputElement>document.querySelector('[data-formengine-input-name="' + hiddenFieldName + '"');
      const hiddenValueInput = <HTMLInputElement>document.querySelector('[name="' + hiddenFieldName + '"');

      if (hiddenValueCheckBox !== null && hiddenValueInput !== null) {
        hiddenValueCheckBox.checked = !hiddenValueCheckBox.checked;
        hiddenValueInput.value = hiddenValueCheckBox.checked ? '1' : '0';
432
        TBE_EDITOR.fieldChanged(this.container.dataset.localTable, this.container.dataset.uid, this.container.dataset.localField, hiddenFieldName);
433
      }
434

435
436
      const hiddenClass = 't3-form-field-container-inline-hidden';
      const isHidden = recordContainer.classList.contains(hiddenClass);
437
      let toggleIcon: string;
438

439
440
441
442
443
444
445
      if (isHidden) {
        toggleIcon = 'actions-edit-hide';
        recordContainer.classList.remove(hiddenClass);
      } else {
        toggleIcon = 'actions-edit-unhide';
        recordContainer.classList.add(hiddenClass);
      }
446

447
      Icons.getIcon(toggleIcon, Icons.sizes.small).then((markup: string): void => {
448
        target.replaceChild(document.createRange().createContextualFragment(markup), target.querySelector('.t3js-icon'));
449
450
451
      });
    }).delegateTo(this.container, Selectors.enableDisableRecordButtonSelector);
  }
452

453
454
455
456
  private registerInfoButton(): void {
    new RegularEvent('click', function(this: HTMLElement, e: Event) {
      e.preventDefault();
      e.stopImmediatePropagation();
457

458
459
      InfoWindow.showItem(this.dataset.infoTable, this.dataset.infoUid);
    }).delegateTo(this.container, Selectors.infoWindowButton);
460
461
  }

462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
  private registerDeleteButton(): void {
    const me = this;
    new RegularEvent('click', function(this: HTMLElement, e: Event) {
      e.preventDefault();
      e.stopImmediatePropagation();

      const title = TYPO3.lang['label.confirm.delete_record.title'] || 'Delete this record?';
      const content = TYPO3.lang['label.confirm.delete_record.content'] || 'Are you sure you want to delete this record?';
      const $modal = Modal.confirm(title, content, Severity.warning, [
        {
          text: TYPO3.lang['buttons.confirm.delete_record.no'] || 'Cancel',
          active: true,
          btnClass: 'btn-default',
          name: 'no',
        },
        {
          text: TYPO3.lang['buttons.confirm.delete_record.yes'] || 'Yes, delete this record',
          btnClass: 'btn-warning',
          name: 'yes',
        },
      ]);
      $modal.on('button.clicked', (modalEvent: Event): void => {
        if ((<HTMLAnchorElement>modalEvent.target).name === 'yes') {
          const objectId = (<HTMLDivElement>this.closest('[data-object-id]')).dataset.objectId;
          me.deleteRecord(objectId);
        }
488

489
490
491
        Modal.dismiss();
      });
    }).delegateTo(this.container, Selectors.deleteRecordButtonSelector);
492
493
494
495
496
  }

  /**
   * @param {Event} e
   */
497
498
499
500
501
502
503
504
505
506
  private registerSynchronizeLocalize(): void {
    const me = this;
    new RegularEvent('click', function(this: HTMLElement, e: Event) {
      e.preventDefault();
      e.stopImmediatePropagation();

      me.ajaxDispatcher.send(
        me.ajaxDispatcher.newRequest(me.ajaxDispatcher.getEndpoint('record_inline_synchronizelocalize')),
        [me.container.dataset.objectGroup, this.dataset.type],
      ).then(async (response: InlineResponseInterface): Promise<any> => {
507
        document.getElementById(me.container.getAttribute('id') + '_records').insertAdjacentHTML('beforeend', response.data);
508
509
510
511
512

        const objectIdPrefix = me.container.dataset.objectGroup + Separators.structureSeparator;
        for (let itemUid of response.compilerInput.delete) {
          me.deleteRecord(objectIdPrefix + itemUid, true);
        }
513

514
        for (let item of Object.values(response.compilerInput.localize)) {
515
516
517
518
          if (typeof item.remove !== 'undefined') {
            const removableRecordContainer = InlineControlContainer.getInlineRecordContainer(objectIdPrefix + item.remove);
            removableRecordContainer.parentElement.removeChild(removableRecordContainer);
          }
519

520
          me.memorizeAddRecord(item.uid, null, item.selectedValue);
521
        }
522
523
      });
    }).delegateTo(this.container, Selectors.synchronizeLocalizeRecordButtonSelector);
524
525
  }

526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
  private registerUniqueSelectFieldChanged(): void {
    const me = this;
    new RegularEvent('change', function(this: HTMLElement, e: Event) {
      e.preventDefault();
      e.stopImmediatePropagation();

      const recordContainer = (<HTMLDivElement>this.closest('[data-object-id]'));
      if (recordContainer !== null) {
        const objectId = recordContainer.dataset.objectId;
        const objectUid = recordContainer.dataset.objectUid;
        me.handleChangedField(<HTMLSelectElement>this, objectId);

        const formField = me.getFormFieldForElements();
        if (formField === null) {
          return;
        }
        me.updateUnique(<HTMLSelectElement>this, formField, objectUid);
543
      }
544
    }).delegateTo(this.container, Selectors.uniqueValueSelectors);
545
546
  }

547
548
549
550
551
  private registerRevertUniquenessAction(): void {
    const me = this;
    new RegularEvent('click', function(this: HTMLElement, e: Event) {
      e.preventDefault();
      e.stopImmediatePropagation();
552

553
554
      me.revertUnique(this.dataset.uid);
    }).delegateTo(this.container, Selectors.revertUniqueness);
555
556
557
558
559
560
  }

  /**
   * @param {string} objectId
   */
  private loadRecordDetails(objectId: string): void {
561
    const recordFieldsContainer = document.getElementById(objectId + '_fields');
562
    const recordContainer = InlineControlContainer.getInlineRecordContainer(objectId);
563
    const isLoading = typeof this.requestQueue[objectId] !== 'undefined';
564
    const isLoaded = recordFieldsContainer !== null && !recordContainer.classList.contains(States.notLoaded);
565
566

    if (!isLoaded) {
567
      const progress = this.getProgress(objectId, recordContainer.dataset.objectIdHash);
568
569

      if (!isLoading) {
570
571
572
573
574
        const ajaxRequest = this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_details'));
        const request = this.ajaxDispatcher.send(ajaxRequest, [objectId]);

        request.then(async (response: InlineResponseInterface): Promise<any> => {
          delete this.requestQueue[objectId];
575
576
          delete this.progessQueue[objectId];

577
          recordContainer.classList.remove(States.notLoaded);
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
          recordFieldsContainer.innerHTML = response.data;
          this.collapseExpandRecord(objectId);

          progress.done();

          FormEngine.reinitialize();
          FormEngine.Validation.initializeInputFields();
          FormEngine.Validation.validate();

          if (this.hasObjectGroupDefinedUniqueConstraints()) {
            const recordContainer = InlineControlContainer.getInlineRecordContainer(objectId);
            this.removeUsed(recordContainer);
          }
        });

593
        this.requestQueue[objectId] = ajaxRequest;
594
595
596
        progress.start();
      } else {
        // Abort loading if collapsed again
597
        this.requestQueue[objectId].abort();
598
        delete this.requestQueue[objectId];
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
        delete this.progessQueue[objectId];
        progress.done();
      }

      return;
    }

    this.collapseExpandRecord(objectId);
  }

  /**
   * Collapses or expands a record and stores the state either in a form field or directly in backend user's UC, depending
   * on whether the record is new or already existing.
   *
   * @param {String} objectId
   */
  private collapseExpandRecord(objectId: string): void {
    const recordElement = InlineControlContainer.getInlineRecordContainer(objectId);
    const expandSingle = this.getAppearance().expandSingle === true;
    const isCollapsed: boolean = recordElement.classList.contains(States.collapsed);
    let collapse: Array<string> = [];
    const expand: Array<string> = [];

    if (expandSingle && isCollapsed) {
      collapse = this.collapseAllRecords(recordElement.dataset.objectUid);
    }

    InlineControlContainer.toggleElement(objectId);

    if (InlineControlContainer.isNewRecord(objectId)) {
      InlineControlContainer.updateExpandedCollapsedStateLocally(objectId, isCollapsed);
    } else if (isCollapsed) {
      expand.push(recordElement.dataset.objectUid);
    } else if (!isCollapsed) {
      collapse.push(recordElement.dataset.objectUid);
    }

    this.ajaxDispatcher.send(
637
638
      this.ajaxDispatcher.newRequest(this.ajaxDispatcher.getEndpoint('record_inline_expandcollapse')),
      [objectId, expand.join(','), collapse.join(',')]
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
    );
  }

  /**
   * @param {string} newUid
   * @param {string} afterUid
   * @param {string} selectedValue
   */
  private memorizeAddRecord(newUid: string, afterUid: string = null, selectedValue: string = null): void {
    const formField = this.getFormFieldForElements();
    if (formField === null) {
      return;
    }

    let records = Utility.trimExplode(',', (<HTMLInputElement>formField).value);
    if (afterUid) {
      const newRecords = [];
      for (let i = 0; i < records.length; i++) {
        if (records[i].length) {
          newRecords.push(records[i]);
        }
        if (afterUid === records[i]) {
          newRecords.push(newUid);
        }
      }
      records = newRecords;
    } else {
      records.push(newUid);
    }

    (<HTMLInputElement>formField).value = records.join(',');
    (<HTMLInputElement>formField).classList.add('has-change');
671
    document.dispatchEvent(new Event('change'));
672
673

    this.redrawSortingButtons(this.container.dataset.objectGroup, records);
674
    this.setUnique(newUid, selectedValue);
675
676
677
678
679

    if (!this.isBelowMax()) {
      this.toggleContainerControls(false);
    }

680
    TBE_EDITOR.fieldChanged(this.container.dataset.localTable, this.container.dataset.uid, this.container.dataset.localField, formField);
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
  }

  /**
   * @param {String} objectUid
   * @return Array<string>
   */
  private memorizeRemoveRecord(objectUid: string): Array<string> {
    const formField = this.getFormFieldForElements();
    if (formField === null) {
      return [];
    }

    let records = Utility.trimExplode(',', (<HTMLInputElement>formField).value);
    const indexOfRemoveUid = records.indexOf(objectUid);
    if (indexOfRemoveUid > -1) {
      delete records[indexOfRemoveUid];

      (<HTMLInputElement>formField).value = records.join(',');
      (<HTMLInputElement>formField).classList.add('has-change');
700
      document.dispatchEvent(new Event('change'));
701
702
703
704
705
706
707
708
709
710
711
712
713
714

      this.redrawSortingButtons(this.container.dataset.objectGroup, records);
    }

    return records;
  }

  /**
   * @param {string} objectId
   * @param {SortDirections} direction
   */
  private changeSortingByButton(objectId: string, direction: SortDirections): void {
    const currentRecordContainer = InlineControlContainer.getInlineRecordContainer(objectId);
    const recordUid = currentRecordContainer.dataset.objectUid;
715
    const recordListContainer = <HTMLDivElement>document.getElementById(this.container.getAttribute('id') + '_records');
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
    const records = Array.from(recordListContainer.children).map((child: HTMLElement) => child.dataset.objectUid);
    let position = records.indexOf(recordUid);
    let isChanged = false;

    if (direction === SortDirections.UP && position > 0) {
      records[position] = records[position - 1];
      records[position - 1] = recordUid;
      isChanged = true;
    } else if (direction === SortDirections.DOWN && position < records.length - 1) {
      records[position] = records[position + 1];
      records[position + 1] = recordUid;
      isChanged = true;
    }

    if (isChanged) {
      const objectIdPrefix = this.container.dataset.objectGroup + Separators.structureSeparator;
      const adjustment = direction === SortDirections.UP ? 1 : 0;
      currentRecordContainer.parentElement.insertBefore(
        InlineControlContainer.getInlineRecordContainer(objectIdPrefix + records[position - adjustment]),
        InlineControlContainer.getInlineRecordContainer(objectIdPrefix + records[position + 1 - adjustment]),
      );

      this.updateSorting();
    }
  }

  private updateSorting(): void {
    const formField = this.getFormFieldForElements();
    if (formField === null) {
      return;
    }

748
    const recordListContainer = <HTMLDivElement>document.getElementById(this.container.getAttribute('id') + '_records');
749
    const records = Array.from(recordListContainer.querySelectorAll('[data-placeholder-record="0"]')).map((child: HTMLElement) => child.dataset.objectUid);
750
751
752

    (<HTMLInputElement>formField).value = records.join(',');
    (<HTMLInputElement>formField).classList.add('has-change');
753
754
    document.dispatchEvent(new Event('inline:sorting-changed'));
    document.dispatchEvent(new Event('change'));
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776

    this.redrawSortingButtons(this.container.dataset.objectGroup, records);
  }

  /**
   * @param {String} objectId
   * @param {Boolean} forceDirectRemoval
   */
  private deleteRecord(objectId: string, forceDirectRemoval: boolean = false): void {
    const recordContainer = InlineControlContainer.getInlineRecordContainer(objectId);
    const objectUid = recordContainer.dataset.objectUid;

    recordContainer.classList.add('t3js-inline-record-deleted');

    if (!InlineControlContainer.isNewRecord(objectId) && !forceDirectRemoval) {
      const deleteCommandInput = this.container.querySelector('[name="cmd' + recordContainer.dataset.fieldName + '[delete]"]');
      deleteCommandInput.removeAttribute('disabled');

      // Move input field to inline container so we can remove the record container
      recordContainer.parentElement.insertAdjacentElement('afterbegin', deleteCommandInput);
    }

777
    new RegularEvent('transitionend', (): void => {
778
      recordContainer.parentElement.removeChild(recordContainer);
779
      FormEngineValidation.validate();
780
    }).bindTo(recordContainer);
781

782
783
    this.revertUnique(objectUid);
    this.memorizeRemoveRecord(objectUid);
784
785
786
787
788
789
790
791
792
793
794
    recordContainer.classList.add('form-irre-object--deleted');

    if (this.isBelowMax()) {
      this.toggleContainerControls(true);
    }
  }

  /**
   * @param {boolean} visible
   */
  private toggleContainerControls(visible: boolean): void {
795
796
    const controlContainer = this.container.querySelector(Selectors.controlContainer);
    const controlContainerButtons = controlContainer.querySelectorAll('a');
797
798
799
    controlContainerButtons.forEach((button: HTMLElement): void => {
      button.style.display = visible ? null : 'none';
    });
800
801
802
803
  }

  /**
   * @param {string} objectId
804
   * @param {string} objectIdHash
805
   */
806
807
  private getProgress(objectId: string, objectIdHash: string): any {
    const headerIdentifier = '#' + objectIdHash + '_header';
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
    let progress: any;

    if (typeof this.progessQueue[objectId] !== 'undefined') {
      progress = this.progessQueue[objectId];
    } else {
      progress = NProgress;
      progress.configure({parent: headerIdentifier, showSpinner: false});
      this.progessQueue[objectId] = progress;
    }

    return progress;
  }

  /**
   * @param {string} excludeUid
   */
  private collapseAllRecords(excludeUid: string): Array<string> {
    const formField = this.getFormFieldForElements();
    const collapse: Array<string> = [];

    if (formField !== null) {
      const records = Utility.trimExplode(',', (<HTMLInputElement>formField).value);
      for (let recordUid of records) {
        if (recordUid === excludeUid) {
          continue;
        }

        const recordObjectId = this.container.dataset.objectGroup + Separators.structureSeparator + recordUid;
        const recordContainer = InlineControlContainer.getInlineRecordContainer(recordObjectId);
        if (recordContainer.classList.contains(States.visible)) {
838
          InlineControlContainer.collapseElement(recordContainer, recordObjectId);
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882

          if (InlineControlContainer.isNewRecord(recordObjectId)) {
            InlineControlContainer.updateExpandedCollapsedStateLocally(recordObjectId, false);
          } else {
            collapse.push(recordUid);
          }
        }
      }
    }

    return collapse;
  }

  /**
   * @return HTMLInputElement | void
   */
  private getFormFieldForElements(): HTMLInputElement | null {
    const formFields = document.getElementsByName(this.container.dataset.formField);
    if (formFields.length > 0) {
      return <HTMLInputElement>formFields[0];
    }

    return null;
  }

  /**
   * Redraws rhe sorting buttons of each record
   *
   * @param {string} objectId
   * @param {Array<string>} records
   */
  private redrawSortingButtons(objectId: string, records: Array<string> = []): void {
    if (records.length === 0) {
      const formField = this.getFormFieldForElements();
      if (formField !== null) {
        records = Utility.trimExplode(',', (<HTMLInputElement>formField).value);
      }
    }

    if (records.length === 0) {
      return;
    }

    records.forEach((recordUid: string, index: number): void => {
883
884
      const recordContainer = InlineControlContainer.getInlineRecordContainer(objectId + Separators.structureSeparator + recordUid);
      const headerIdentifier = recordContainer.dataset.objectIdHash + '_header';
885
      const headerElement = document.getElementById(headerIdentifier);
886
      const sortUp = headerElement.querySelector('[data-action="sort"][data-direction="' + SortDirections.UP + '"]');
887

888
889
890
891
892
893
894
895
      if (sortUp !== null) {
        let iconIdentifier = 'actions-move-up';
        if (index === 0) {
          sortUp.classList.add('disabled');
          iconIdentifier = 'empty-empty';
        } else {
          sortUp.classList.remove('disabled');
        }
896
        Icons.getIcon(iconIdentifier, Icons.sizes.small).then((markup: string): void => {
897
898
899
900
901
902
903
904
905
906
907
908
909
          sortUp.replaceChild(document.createRange().createContextualFragment(markup), sortUp.querySelector('.t3js-icon'));
        });
      }

      const sortDown = headerElement.querySelector('[data-action="sort"][data-direction="' + SortDirections.DOWN + '"]');
      if (sortDown !== null) {
        let iconIdentifier = 'actions-move-down';
        if (index === records.length - 1) {
          sortDown.classList.add('disabled');
          iconIdentifier = 'empty-empty';
        } else {
          sortDown.classList.remove('disabled');
        }
910
        Icons.getIcon(iconIdentifier, Icons.sizes.small).then((markup: string): void => {
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
          sortDown.replaceChild(document.createRange().createContextualFragment(markup), sortDown.querySelector('.t3js-icon'));
        });
      }
    });
  }

  /**
   * @return {boolean}
   */
  private isBelowMax(): boolean {
    const formField = this.getFormFieldForElements();
    if (formField === null) {
      return true;
    }

    if (typeof TYPO3.settings.FormEngineInline.config[this.container.dataset.objectGroup] !== 'undefined') {
      const records = Utility.trimExplode(',', (<HTMLInputElement>formField).value);
      if (records.length >= TYPO3.settings.FormEngineInline.config[this.container.dataset.objectGroup].max) {
        return false;
      }

      if (this.hasObjectGroupDefinedUniqueConstraints()) {
        const unique = TYPO3.settings.FormEngineInline.unique[this.container.dataset.objectGroup];
        if (unique.used.length >= unique.max && unique.max >= 0) {
          return false;
        }
      }
    }

    return true;
  }

  /**
   * @param {number} uid
   * @param {string} table
   */
  private isUniqueElementUsed(uid: number, table: string): boolean {
    if (!this.hasObjectGroupDefinedUniqueConstraints()) {
      return false;
    }

    const unique: UniqueDefinition = TYPO3.settings.FormEngineInline.unique[this.container.dataset.objectGroup];
    const values = InlineControlContainer.getValuesFromHashMap(unique.used);

    if (unique.type === 'select' && values.indexOf(uid) !== -1) {
      return true;
    }

    if (unique.type === 'groupdb') {
      for (let i = values.length - 1; i >= 0; i--) {
        // if the pair table:uid is already used:
        if (values[i].table === table && values[i].uid === uid) {
          return true;
        }
      }
    }

    return false;
  }

  /**
   * @param {HTMLDivElement} recordContainer
   */
  private removeUsed(recordContainer: HTMLDivElement): void {
    if (!this.hasObjectGroupDefinedUniqueConstraints()) {
      return;
    }

    const unique: UniqueDefinition = TYPO3.settings.FormEngineInline.unique[this.container.dataset.objectGroup];
    if (unique.type !== 'select') {
      return;
    }

    let uniqueValueField = <HTMLSelectElement>recordContainer.querySelector(
      '[name="data[' + unique.table + '][' + recordContainer.dataset.objectUid + '][' + unique.field + ']"]',
    );
    const values = InlineControlContainer.getValuesFromHashMap(unique.used);

    if (uniqueValueField !== null) {
      const selectedValue = uniqueValueField.options[uniqueValueField.selectedIndex].value;
991
992
993
      for (let value of values) {
        if (value !== selectedValue) {
          InlineControlContainer.removeSelectOptionByValue(uniqueValueField, value);
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
        }
      }
    }
  }

  /**
   * @param {string} recordUid
   * @param {string} selectedValue
   */
  private setUnique(recordUid: string, selectedValue: string): void {
    if (!this.hasObjectGroupDefinedUniqueConstraints()) {
      return;
    }
1007
1008
    const selectorElement: HTMLSelectElement = <HTMLSelectElement>document.getElementById(
      this.container.dataset.objectGroup + '_selector',
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
    );
    const unique: UniqueDefinition = TYPO3.settings.FormEngineInline.unique[this.container.dataset.objectGroup];
    if (unique.type === 'select') {
      if (!(unique.selector && unique.max === -1)) {
        const formField = this.getFormFieldForElements();
        const recordObjectId = this.container.dataset.objectGroup + Separators.structureSeparator + recordUid;
        const recordContainer = InlineControlContainer.getInlineRecordContainer(recordObjectId);
        let uniqueValueField = <HTMLSelectElement>recordContainer.querySelector(
          '[name="data[' + unique.table + '][' + recordUid + '][' + unique.field + ']"]',
        );
        const values = InlineControlContainer.getValuesFromHashMap(unique.used);
        if (selectorElement !== null) {
          // remove all items from the new select-item which are already used in other children
          if (uniqueValueField !== null) {
1023
1024
            for (let value of values) {
              InlineControlContainer.removeSelectOptionByValue(uniqueValueField, value);
1025
1026
1027
1028
1029
1030
1031
1032
1033
            }
            // set the selected item automatically to the first of the remaining items if no selector is used
            if (!unique.selector) {
              selectedValue = uniqueValueField.options[0].value;
              uniqueValueField.options[0].selected = true;
              this.updateUnique(uniqueValueField, formField, recordUid);
              this.handleChangedField(uniqueValueField, this.container.dataset.objectGroup + '[' + recordUid + ']');
            }
          }
1034
1035
          for (let value of values) {
            InlineControlContainer.removeSelectOptionByValue(uniqueValueField, value);
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
          }
          if (typeof unique.used.length !== 'undefined') {
            unique.used = {};
          }
          unique.used[recordUid] = {
            table: unique.elTable,
            uid: selectedValue,
          };
        }
        // remove the newly used item from each select-field of the child records
1046
        if (formField !== null && InlineControlContainer.selectOptionValueExists(selectorElement, selectedValue)) {
1047
          const records = Utility.trimExplode(',', (<HTMLInputElement>formField).value);
1048
          for (let record of records) {
1049
            uniqueValueField = <HTMLSelectElement>document.querySelector(
1050
              '[name="data[' + unique.table + '][' + record + '][' + unique.field + ']"]',
1051
            );
1052
            if (uniqueValueField !== null && record !== recordUid) {
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
              InlineControlContainer.removeSelectOptionByValue(uniqueValueField, selectedValue);
            }
          }
        }
      }
    } else if (unique.type === 'groupdb') {
      // add the new record to the used items:
      unique.used[recordUid] = {
        table: unique.elTable,
        uid: selectedValue,
      };
    }

    // remove used items from a selector-box
1067
    if (unique.selector === 'select' && InlineControlContainer.selectOptionValueExists(selectorElement, selectedValue)) {
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
      InlineControlContainer.removeSelectOptionByValue(selectorElement, selectedValue);
      unique.used[recordUid] = {
        table: unique.elTable,
        uid: selectedValue,
      };
    }
  }

  /**
   * @param {HTMLSelectElement} srcElement
   * @param {HTMLInputElement} formField
   * @param {string} recordUid
   */
  private updateUnique(srcElement: HTMLSelectElement, formField: HTMLInputElement, recordUid: string): void {
    if (!this.hasObjectGroupDefinedUniqueConstraints()) {
      return;
    }
    const unique = TYPO3.settings.FormEngineInline.unique[this.container.dataset.objectGroup];
    const oldValue = unique.used[recordUid];

    if (unique.selector === 'select') {
1089
1090
      const selectorElement: HTMLSelectElement = <HTMLSelectElement>document.getElementById(
        this.container.dataset.objectGroup + '_selector',
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
      );
      InlineControlContainer.removeSelectOptionByValue(selectorElement, srcElement.value);
      if (typeof oldValue !== 'undefined') {
        InlineControlContainer.reAddSelectOption(selectorElement, oldValue, unique);
      }
    }

    if (unique.selector && unique.max === -1) {
      return;
    }

    if (!unique || formField === null) {
      return;
    }

    const records = Utility.trimExplode(',', formField.value);
    let uniqueValueField;
1108
    for (let record of records) {
1109
      uniqueValueField = <HTMLSelectElement>document.querySelector(
1110
        '[name="data[' + unique.table + '][' + record + '][' + unique.field + ']"]',
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
      );
      if (uniqueValueField !== null && uniqueValueField !== srcElement) {
        InlineControlContainer.removeSelectOptionByValue(uniqueValueField, srcElement.value);
        if (typeof oldValue !== 'undefined') {
          InlineControlContainer.reAddSelectOption(uniqueValueField, oldValue, unique);
        }
      }
    }
    unique.used[recordUid] = srcElement.value;
  }

  /**
   * @param {string} recordUid
   */
  private revertUnique(recordUid: string): void {
    if (!this.hasObjectGroupDefinedUniqueConstraints()) {
      return;
    }

    const unique = TYPO3.settings.FormEngineInline.unique[this.container.dataset.objectGroup];
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
    const recordObjectId = this.container.dataset.objectGroup + Separators.structureSeparator + recordUid;
    const recordContainer = InlineControlContainer.getInlineRecordContainer(recordObjectId);

    let uniqueValueField = <HTMLSelectElement>recordContainer.querySelector(
      '[name="data[' + unique.table + '][' + recordContainer.dataset.objectUid + '][' + unique.field + ']"]',
    );
    if (unique.type === 'select') {
      let uniqueValue;
      if (uniqueValueField !== null) {
        uniqueValue = uniqueValueField.value;
      } else if (recordContainer.dataset.tableUniqueOriginalValue !== '') {
        uniqueValue = recordContainer.dataset.tableUniqueOriginalValue;
      } else {
        return;
      }

      if (unique.selector === 'select') {
        if (!isNaN(parseInt(uniqueValue, 10))) {
1149
1150
          const selectorElement: HTMLSelectElement = <HTMLSelectElement>document.getElementById(
            this.container.dataset.objectGroup + '_selector',
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
          );
          InlineControlContainer.reAddSelectOption(selectorElement, uniqueValue, unique);
        }
      }

      if (unique.selector && unique.max === -1) {
        return;
      }

      const formField = this.getFormFieldForElements();
      if (formField === null) {
        return;
      }

      const records = Utility.trimExplode(',', formField.value);
      let recordObj;
      // walk through all inline records on that level and get the select field
      for (let i = 0; i < records.length; i++) {
        recordObj = <HTMLSelectElement>document.querySelector(
          '[name="data[' + unique.table + '][' + records[i] + '][' + unique.field + ']"]',
        );
        if (recordObj !== null) {
          InlineControlContainer.reAddSelectOption(recordObj, uniqueValue, unique);
        }
      }

      delete unique.used[recordUid];
    } else if (unique.type === 'groupdb') {
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
      delete unique.used[recordUid];
    }
  }

  /**
   * @return {boolean}
   */
  private hasObjectGroupDefinedUniqueConstraints(): boolean {
    return typeof TYPO3.settings.FormEngineInline.unique !== 'undefined'
      && typeof TYPO3.settings.FormEngineInline.unique[this.container.dataset.objectGroup] !== 'undefined';
  }

  /**
   * @param {HTMLInputElement | HTMLSelectElement} formField
   * @param {string} objectId
   */
  private handleChangedField(formField: HTMLInputElement | HTMLSelectElement, objectId: string): void {
    let value;
    if (formField instanceof HTMLSelectElement) {
      value = formField.options[formField.selectedIndex].text;
    } else {
      value = formField.value;
    }
1202
    document.getElementById(objectId + '_label').textContent = value.length ? value : this.noTitleString;
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
  }

  /**
   * @return {Object}
   */
  private getAppearance(): Appearance {
    if (this.appearance === null) {
      this.appearance = {};

      if (typeof this.container.dataset.appearance === 'string') {
        try {
          this.appearance = JSON.parse(this.container.dataset.appearance);
        } catch (e) {
          console.error(e);
        }
      }
    }

    return this.appearance;
  }
}

export = InlineControlContainer;