Your IP : 216.73.217.95


Current Path : /var/www/surf/TYPO3/vendor/mask/mask/Build/Sources/js/
Upload File :
Current File : /var/www/surf/TYPO3/vendor/mask/mask/Build/Sources/js/mask.js

import $ from 'jquery';
import Vue from 'vue';
import draggable from 'vuedraggable';
import nestedDraggable from './components/nested-draggable.js';
import formField from './components/form-field.js';
import fieldKey from './components/field-key.js';
import elementKey from './components/element-key.js';
import splashscreen from './components/splash-screen.js';
import buttonBar from './components/button-bar.js';
import fontIconPicker from './components/font-icon-picker.js';
import fieldGroup from './components/field-group.js';
import elementColorPicker from './components/element-color-picker.js';
import AjaxRequest from '@typo3/core/ajax/ajax-request.js';
import Icons from '@typo3/backend/icons.js';
import Modal from '@typo3/backend/modal.js';
import Severity from '@typo3/backend/severity.js';
import Notification from '@typo3/backend/notification.js';
import MultiStepWizard from '@typo3/backend/multi-step-wizard.js';
import DeferredAction from '@typo3/backend/action-button/deferred-action.js';

(function() {
  if (!document.getElementById('mask')) {
    return;
  }

  new Vue({
    el: '#mask',
    components: {
      draggable,
      nestedDraggable,
      formField,
      elementKey,
      fieldKey,
      splashscreen,
      buttonBar,
      fontIconPicker,
      fieldGroup,
      elementColorPicker,
    },
    data: function () {
      return {
        version: '',
        mode: 'list',
        type: '',
        sidebar: 'fields',
        searchString: '',
        groups: [],
        elements: [],
        element: {},
        backendLayouts: [],
        fieldTypes: [],
        tcaFields: {},
        onlineMedia: [],
        linkHandlerList: [],
        tabs: {},
        fields: [],
        language: [],
        nonOverrideableOptions: [],
        features: [],
        icons: {},
        faIcons: {},
        availableTca: {},
        multiUseElements: {},
        optionalExtensionStatus: {},
        migrationsDone: false,
        restructuringNeeded: false,
        fieldErrors: {
          elementKeyAvailable: true,
          elementKey: false,
          elementLabel: false,
          emptyKeyFields: [],
          emptyTabLabels: [],
          emptyGroupAllowedFields: [],
          emptyRadioItems: [],
          existingFieldKeyFields: []
        },
        global: {
          typo3Version: 12,
          activeField: {},
          clonedField: {},
          richtextConfiguration: {},
          currentTab: 'general',
          ctypes: {},
          structuralFields: ['linebreak', 'palette', 'tab'],
          nonShareableFields: ['inline', 'palette', 'linebreak', 'tab'],
          maskPrefix: 'tx_mask_',
          deletedFields: [],
        },
        loaded: false,
        missingFilesOrFolders: {
          missing: false,
          missingFolders: {},
          missingTemplates: {},
        },
        setupConfiguration: {
          extension: '',
          loader: 'json',
          error: '',
        },
        saving: false,
        ticks: 0,
      }
    },
    mounted: function () {
      this.init();
    },
    watch: {
      element: {
        handler() {
          this.validate();
        },
        deep: true
      },
      fields: {
        handler() {
          this.validate();
        },
        deep: true
      },
      'global.activeField.fields': function () {
        this.validate();
      },
      'element.key': function () {
        if (this.mode !== 'new') {
          return;
        }
        const validKey = this.checkAllowedCharacters(this.element.key);
        if (this.element.key !== validKey) {
          this.element.key = validKey;
          return;
        }
        new AjaxRequest(TYPO3.settings.ajaxUrls.mask_check_element_key)
          .withQueryArguments({key: this.element.key})
          .get()
          .then(
            async response => {
              const result = await response.resolve();
              this.fieldErrors.elementKeyAvailable = result.isAvailable;
            }
          );
      },
    },
    methods: {
      init() {
        const setupCompletePromise = new AjaxRequest(TYPO3.settings.ajaxUrls.mask_setup_complete).get()
          .then(
            async response => {
              return await response.resolve();
            }
          );

        Promise.resolve(setupCompletePromise)
          .then(
            setupCompleteResult => {
              const promises = [];

              // fetch mask and typo3 version
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_versions)).get()
                .then(
                  async response => {
                    const versions = await response.resolve();
                    this.version = versions.mask;
                    this.global.typo3Version = versions.typo3;
                  }
                ));

              // Fetch language
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_language)).get()
                .then(
                  async response => {
                    this.language = await response.resolve();
                  }
                ));

              // Fetch non overrideable options
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_non_overrideable_options)).get()
                .then(
                  async response => {
                    this.nonOverrideableOptions = await response.resolve();
                  }
                ));

              // Fetch features
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_features)).get()
                .then(
                  async response => {
                    this.features = await response.resolve();
                  }
                ));

              // Return early, if setup is incomplete.
              if (!setupCompleteResult.setupComplete) {
                this.mode = 'setup';
                if (setupCompleteResult.loader !== '') {
                  this.setupConfiguration.loader = setupCompleteResult.loader;
                }
                Promise.all(promises).then(() => {
                  this.loaded = true;
                });
                return;
              } else {
                this.mode = 'list';
              }

              // Fetch tcaFields for existing core and mask fields
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_tca_fields)).get()
                .then(
                  async response => {
                    this.tcaFields = await response.resolve();
                  }
                ));

              // Fetch online media
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_online_media)).get()
                .then(
                  async response => {
                    this.onlineMedia = await response.resolve();
                  }
                ));

              // Fetch link handler
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_link_handler)).get()
                .then(
                  async response => {
                    this.linkHandlerList = await response.resolve();
                  }
                ));

              // fetch tab declarations
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_tabs)).get()
                .then(
                  async response => {
                    this.tabs = await response.resolve();
                  }
                ));

              // fetch richtext configuration
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_richtext_configuration)).get()
                .then(
                  async response => {
                    this.global.richtextConfiguration = await response.resolve();
                  }
                ));

              // fetch CTypes
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_ctypes)).get()
                .then(
                  async response => {
                    const result = await response.resolve();
                    this.global.ctypes = result.ctypes;
                  }
                ));

              // fetch field groups
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_field_groups)).get()
                .then(
                  async response => {
                    const result = await response.resolve();
                    this.groups = result.groups;
                  }
                ));

              // fetch elements
              promises.push(this.loadElements());

              // fetch tables
              promises.push(this.loadTables());

              // fetch backend layouts
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_backend_layouts)).get()
                .then(
                  async response => {
                    const backendLayouts = await response.resolve();
                    this.backendLayouts = backendLayouts['backendLayouts'];
                  }
                ));

              // fetch fontawesome icons
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_icons)).get()
                .then(
                  async response => {
                    this.faIcons = await response.resolve();
                  }
                ));

              // fetch possible missing files or folders
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_missing)).get()
                .then(
                  async response => {
                    this.missingFilesOrFolders = await response.resolve();
                  }
                ));

              // fetch optional extension status
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_optional_extension_status)).get()
                .then(
                  async response => {
                    this.optionalExtensionStatus = await response.resolve();
                  }
                ));

              // fetch migration status
              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_migrations_done)).get()
                .then(
                  async response => {
                    const migrationsDone = await response.resolve();
                    this.migrationsDone = migrationsDone.migrationsDone;
                  }
                ));

              promises.push((new AjaxRequest(TYPO3.settings.ajaxUrls.mask_restructuring_needed)).get()
                .then(
                  async response => {
                    const restructuringNeeded = await response.resolve();
                    this.restructuringNeeded = restructuringNeeded.restructuringNeeded;
                  }
                ));

              promises.push(Icons.getIcon('actions-edit-delete', Icons.sizes.small).then(icon => {
                this.icons.delete = icon;
              }));
              promises.push(Icons.getIcon('actions-move-move', Icons.sizes.small).then(icon => {
                this.icons.move = icon;
              }));
              promises.push(Icons.getIcon('actions-add', Icons.sizes.small).then(icon => {
                this.icons.add = icon;
              }));
              promises.push(Icons.getIcon('actions-edit-pick-date', Icons.sizes.small).then(icon => {
                this.icons.date = icon;
              }));
              promises.push(Icons.getIcon('actions-open', Icons.sizes.small).then(icon => {
                this.icons.edit = icon;
              }));
              promises.push(Icons.getIcon('actions-save', Icons.sizes.small).then(icon => {
                this.icons.save = icon;
              }));
              promises.push(Icons.getIcon('actions-close', Icons.sizes.small).then(icon => {
                this.icons.close = icon;
              }));
              promises.push(Icons.getIcon('spinner-circle-dark', Icons.sizes.small).then(icon => {
                this.icons.spinner = icon;
              }));
              promises.push(Icons.getIcon('actions-question-circle-alt', Icons.sizes.small).then(icon => {
                this.icons.questionCircle = icon;
              }));

              Promise.all(promises).then(() => {
                this.loaded = true;

                if (this.migrationsDone) {
                  Notification.info(
                    this.language.migrationsPerformedTitle,
                    this.language.migrationsPerformedMessage,
                    0,
                    [
                      {
                        label: this.language.updateMaskDefinition,
                        action: new DeferredAction(() => {
                          return new AjaxRequest(TYPO3.settings.ajaxUrls.mask_persist_definition)
                            .get()
                            .then(async response => {
                              const res = await response.resolve();
                              if (res.status === 'ok') {
                                Notification.success(res.title, res.message);
                              }
                              if (res.status === 'error') {
                                Notification.error(res.title, res.message);
                              }
                            });
                        })
                      }
                    ]
                  );
                }

                if (this.restructuringNeeded) {
                  Notification.warning(
                    this.language.restructuringNeededTitle,
                    this.language.restructuringNeededMessage,
                    0,
                    [
                      {
                        label: this.language.executeRestructuring,
                        action: new DeferredAction(() => {
                          return new AjaxRequest(TYPO3.settings.ajaxUrls.mask_execute_restructuring)
                            .get()
                            .then(async response => {
                              const res = await response.resolve();
                              if (res.status === 'ok') {
                                Notification.success(res.title, res.message);
                              }
                              if (res.status === 'error') {
                                Notification.error(res.title, res.message);
                              }
                            });
                        })
                      }
                    ]
                  );
                }
              });
            }
          );
      },
      save: function () {
        this.saving = true;
        this.validate();
        if (!this.hasErrors) {
          this.global.deletedFields = [];
          const payload = {
            element: this.getPostElement(),
            fields: JSON.stringify(this.getPostFields(this.fields)),
            type: this.type,
            isNew: this.mode === 'new' ? 1 : 0
          };
          new AjaxRequest(TYPO3.settings.ajaxUrls.mask_save).post(payload)
            .then(
              async response => {
                const res = await response.resolve();
                this.mode = 'edit';
                this.showMessages(res.messages);
                if (res.hasError) {
                  return;
                }
                this.loadElements();
                // load element fields
                new AjaxRequest(TYPO3.settings.ajaxUrls.mask_load_element)
                    .withQueryArguments({
                      type: payload.type,
                      key: payload.element.key
                    })
                    .get()
                    .then(
                        async response => {
                          const result = await response.resolve();
                          this.fields = result.fields;
                          this.addParentReferenceToFields({}, this.fields);
                          if (!this.isEmptyObject(this.global.activeField)) {
                            this.findActiveField(this.global.activeField, this.fields);
                          }
                          this.saving = false;
                        }
                    );
              }
            );
        } else {
          this.saving = false;
          Modal.confirm(
            this.language.alert || 'Alert',
            this.language.fieldsMissing,
            Severity.error,
            [
              {
                text: this.language.ok || 'OK',
                btnClass: 'btn-default',
                active: true,
                name: 'ok',
                trigger: () => {
                  Modal.dismiss();
                  this.getErrorFields().every(errorFields => {
                    if (errorFields.length > 0) {
                      this.global.activeField = errorFields[0];
                      return false;
                    }
                    return true;
                  });
                }
              }
            ]
          )
        }
      },
      getPostElement() {
        if (this.type === 'tt_content') {
          const postElement = {
            key: this.element.key,
            icon: this.$refs.iconPicker.iconPicker.currentIcon,
            label: this.element.label,
            shortLabel: this.element.shortLabel,
            description: this.element.description,
            color: this.element.color,
            colorOverlay: this.element.colorOverlay,
            hidden: this.element.hidden,
            sorting: this.element.sorting,
            saveAndClose: this.element.saveAndClose,
          };

          postElement.iconOverlay = this.$refs.iconOverlayPicker.iconPicker.currentIcon;
          return postElement;
        } else {
          return {
            key: this.element.key
          }
        }
      },
      getPostFields: function (fields) {
        const postFields = [];
        fields.forEach(item => {
          postFields.push({
            key: item.key,
            label: item.label,
            description: item.description,
            name: item.name,
            tca: Object.assign({}, item.tca),
            fields: this.getPostFields(item.fields),
            sql: item.sql
          });
        });
        return postFields;
      },
      closeEdit: function () {
        this.resetState();
        this.loadElements();
        this.mode = 'list';
      },
      /**
       * This method finds the last active field before saving and sets it again to active.
       * This is necessary, because the fields are loaded freshly after saving and the reference is gone.
       * @param activeField
       * @param fields
       */
      findActiveField: function (activeField, fields) {
        let found = false;
        fields.forEach(field => {
            if (field.key === activeField.key && (this.isEmptyObject(activeField.parent) && this.isEmptyObject(field.parent) || activeField.parent.key === field.parent.key)) {
              this.global.activeField = field;
              found = true;
            }
            if (!found) {
              this.findActiveField(activeField, field.fields);
            }
        });
      },
      loadElements: function () {
        return (new AjaxRequest(TYPO3.settings.ajaxUrls.mask_elements)).get()
            .then(
                async response => {
                  const result = await response.resolve();
                  this.elements = result.elements;
                }
            );
      },
      loadField: function () {
        if (this.canHaveMultiUsage(this.global.activeField)) {
          new AjaxRequest(TYPO3.settings.ajaxUrls.mask_load_field)
            .withQueryArguments({key: this.global.activeField.key, type: this.type})
            .get()
            .then(
              async response => {
                const result = await response.resolve();
                this.global.activeField.tca = result.field.tca;
                this.global.activeField.label = result.field.label;
                this.global.activeField.description = result.field.description;
                this.global.activeField.sql = result.field.sql;
              }
            );
          this.loadMultiUse();
        }
      },
      loadMultiUse: function () {
        // Check if field can have multi usage.
        if (!this.canHaveMultiUsage(this.global.activeField)) {
          return;
        }

        // If already cached, return.
        if (this.multiUseElements[this.global.activeField.key]) {
          return;
        }

        new AjaxRequest(TYPO3.settings.ajaxUrls.mask_multiuse)
            .withQueryArguments({key: this.global.activeField.key, elementKey: this.element.key, newField: this.global.activeField.newField ? 1 : 0})
            .get()
            .then(
                async response => {
                  const result = await response.resolve();
                  // We need to use $set here for reactivity to work, as keys are added dynamically.
                  this.$set(this.multiUseElements, this.global.activeField.key, result.multiUseElements);
                }
            );
      },
      loadTables: function () {
        return (new AjaxRequest(TYPO3.settings.ajaxUrls.mask_tables)).get()
          .then(
            async response => {
              const result = await response.resolve();
              this.global.foreignTables = result.foreignTables;
            }
          );
      },
      validateKey: function (field) {
        if (this.isEmptyObject(this.global.activeField)) {
          return false;
        }

        // Force mask prefix if not a core field
        if (!this.isActiveCoreField && !this.hasMaskPrefix(field.key)) {
          field.key = this.global.maskPrefix;
          return false;
        }

        // Force lowercase and remove special chars
        field.key = this.checkAllowedCharacters(field.key);

        // Skip empty fields (these are validated by empty validator)
        if (field.key === this.global.maskPrefix) {
          return false;
        }

        // Step 1: Check if key is in current fields array
        let fields = this.getFields(field);
        let keyExistsInFields = this.checkIfKeyExistsInFields(fields, this.global.activeField);
        if (keyExistsInFields) {
          this.fieldErrors.existingFieldKeyFields.push(this.global.activeField);
        } else {
          this.removeExistingKeyField(this.global.activeField);
        }

        // Step 2: Check if another field is now valid due to the change
        this.fieldErrors.existingFieldKeyFields.every(errorField => {
          if (errorField !== field && !this.checkIfKeyExistsInFields(this.getFields(errorField), errorField)) {
            this.removeExistingKeyField(errorField);
          }
          return true;
        });

        // Step 3: Check if key is in possible tca array and avoid ajax check if so (only root level).
        if (this.isRoot(field) && this.getAvailableTcaKeys()[field.name].includes(field.key)) {
          return false;
        }

        // If key already exists in current fields.
        if (keyExistsInFields) {
          return false;
        }

        // The field isn't new (must be valid).
        if (!field.newField) {
          return true;
        }

        // TCA check is also not needed for fields inside inline. Exception: type inline and content.
        if (!this.isRoot(field) && field.name !== 'inline' && field.name !== 'content') {
          return true;
        }

        // Check if key already exists in table
        let args = {
          table: this.type,
          key: field.key,
          type: field.name,
          elementKey: ''
        };
        if (this.mode === 'edit') {
          args.elementKey = this.element.key;
        }
        return (new AjaxRequest(TYPO3.settings.ajaxUrls.mask_check_field_key)
          .withQueryArguments(args)
          .get()
          .then(
            async response => {
              const result = await response.resolve();
              if (result.isAvailable) {
                this.removeExistingKeyField(this.global.activeField);
                return true;
              } else {
                this.fieldErrors.existingFieldKeyFields.push(this.global.activeField);
                return false;
              }
            }
          ));
      },
      hasMaskPrefix: function (key) {
        return key.substr(0, this.global.maskPrefix.length) === this.global.maskPrefix;
      },
      isRoot: function (field) {
        return this.isEmptyObject(field.parent) || field.parent.name === 'palette' && this.isEmptyObject(field.parent.parent);
      },
      canHaveMultiUsage(field) {
        return this.canBeShared(field) && this.isRoot(field) && this.isExistingMaskField(field);
      },
      canBeShared: function (field) {
        return !this.global.nonShareableFields.includes(field.name);
      },
      getFields: function (field) {
        let fields = this.fields;
        if (!this.isRoot(field)) {
          if (field.parent.name !== 'palette' || !this.isEmptyObject(field.parent.parent)) {
            fields = field.parent.fields;
          } else {
            fields = field.parent.parent.fields;
          }
        }
        return fields;
      },
      checkIfKeyExistsInFields: function (fields, checkField) {
        let error = false;
        fields.every(field => {
          if (field !== checkField) {
            if (checkField.key === field.key) {
              error = true;
            } else {
              if (!error && field.name === 'palette') {
                error = this.checkIfKeyExistsInFields(field.fields, checkField);
              }
            }
            return !error;
          }
          return true;
        });
        return error;
      },
      removeExistingKeyField: function (removedField) {
        this.fieldErrors.existingFieldKeyFields = this.fieldErrors.existingFieldKeyFields.filter(field => {
          return field !== removedField;
        });
      },
      openNew: function () {
        this.resetState();
        this.type = 'tt_content';
        this.element = this.getNewElement();

        const stepLabels = [
          this.language.multistep.chooseLabel,
          this.language.multistep.chooseKey
        ];

        /** Step 1: Choose element label */
        MultiStepWizard.addSlide('new-mask-element-step-1', this.language.multistep.chooseLabel, '', Severity.info, stepLabels[0], (slide) => {
          MultiStepWizard.blurCancelStep();
          MultiStepWizard.lockPrevStep();

          let html = '';
          html += '<p>' + this.language.multistep.text1 + '</p>';
          html += '<label class="control-label" for="mask-step-label">' + this.language.elementLabel + '</label>';
          html += '<input id="mask-step-label" class="form-control" placeholder="' + this.language.multistep.placeholder1 + '"/>';
          slide.html(html);

          const elementLabel = MultiStepWizard.setup.$carousel.closest('.modal').find('#mask-step-label');
          elementLabel.focus();
          MultiStepWizard.set('elementLabel', '');
          elementLabel.on('change', function () {
            MultiStepWizard.set('elementLabel', $(this).val());
          });

          // Go to next slide by hitting enter key
          elementLabel.on('keyup', (event) => {
            if (event.originalEvent.code === 'Enter') {
              MultiStepWizard.triggerStepButton('next');
            }
          });
        });

        /** Step 2: Choose element key. Generate suggestion from chosen label. */
        MultiStepWizard.addSlide('new-mask-element-step-2', this.language.multistep.chooseKey, '', Severity.info, stepLabels[1], (slide) => {
          MultiStepWizard.unlockPrevStep();
          let html = '';
          html += '<p>' + this.language.multistep.text2 + '</p>';
          html += '<label class="control-label" for="mask-step-key">' + this.language.elementKey + '</label>';
          html += '<input id="mask-step-key" class="form-control" placeholder="' + this.language.multistep.placeholder2 + '"/>';
          slide.html(html);

          const elementKey = MultiStepWizard.setup.$carousel.closest('.modal').find('#mask-step-key');
          elementKey.val(this.checkAllowedCharacters(MultiStepWizard.setup.settings['elementLabel']));
          MultiStepWizard.set('elementKey', elementKey.val());
          elementKey.on('change', function () {
            MultiStepWizard.set('elementKey', $(this).val());
          });

          // Go to next slide by hitting enter key
          elementKey.on('keyup', (event) => {
            if (event.originalEvent.code === 'Enter') {
              MultiStepWizard.triggerStepButton('next');
            }
          });

          let modal = MultiStepWizard.setup.$carousel.closest('.modal');
          let nextButton = modal.find('.modal-footer').find('button[name="next"]');
          nextButton.focus();
        });

        MultiStepWizard.addFinalProcessingSlide(() => {
          this.element.label = MultiStepWizard.setup.settings['elementLabel'];
          this.element.key = MultiStepWizard.setup.settings['elementKey'];
          this.loaded = false;
          this.mode = 'new';
          this.validate();

          Promise.resolve(this.loadTca()).then(() => {
            this.loaded = true;
            MultiStepWizard.dismiss();
          });
        }).then(() => {
          MultiStepWizard.show();
          MultiStepWizard.setup.forceSelection = false;
        });
      },
      openEdit: function (type, element) {
        this.loaded = false;
        this.mode = 'edit';
        this.type = type;
        this.element = element;
        this.searchString = '';
        let requests = [];

        // load element fields
        requests.push(this.loadTca().then(() => {
          new AjaxRequest(TYPO3.settings.ajaxUrls.mask_load_element)
              .withQueryArguments({
                type: type,
                key: element.key
              })
              .get()
              .then(
                  async response => {
                    const result = await response.resolve();
                    this.fields = result.fields;
                    this.updateParents();
                  }
              )
        }));

        requests.push(new AjaxRequest(TYPO3.settings.ajaxUrls.mask_all_multiuse)
            .withQueryArguments({
              table: this.type,
              elementKey: element.key
            })
            .get()
            .then(
                async response => {
                  const result = await response.resolve();
                  if (result.multiUseElements.length !== 0) {
                    this.multiUseElements = result.multiUseElements;
                  }
                }
            ));

        Promise.all(requests).then(() => {
          this.loaded = true;
        });
      },
      updateParents: function () {
        this.addParentReferenceToFields({}, this.fields);
      },
      addParentReferenceToFields: function (parent, fields) {
        fields.forEach(field => {
          field.parent = parent;
          this.addParentReferenceToFields(field, field.fields);
        });
      },
      loadTca: function () {
        // Fetch fieldtypes and available tca
        return (new AjaxRequest(TYPO3.settings.ajaxUrls.mask_fieldtypes)).get()
          .then(
            async response => {
              this.fieldTypes = await response.resolve();
              this.fieldTypes.forEach(item => {
                new AjaxRequest(TYPO3.settings.ajaxUrls.mask_existing_tca).withQueryArguments({table: this.type, type: item.name}).get()
                  .then(
                    async response => {
                      this.availableTca[item.name] = await response.resolve();
                    }
                  )
              });
            }
          );
      },
      deleteElement: function (item, purge) {
        new AjaxRequest(TYPO3.settings.ajaxUrls.mask_delete).post({key: item.key, purge: purge})
            .then(
                async response => {
                  const res = await response.resolve();
                  this.showMessages(res);
                  this.loadElements();
                }
            );
      },
      openDeleteDialog(item) {
        Modal.confirm(
            this.language.deleteModal.title + ': ' + item.label,
            this.language.deleteModal.content,
            Severity.warning,
            [
              {
                text: this.language.deleteModal.close,
                btnClass: 'btn-default',
                trigger: function () {
                  Modal.dismiss();
                }
              },
              {
                text: this.language.deleteModal.delete,
                active: true,
                btnClass: 'btn-warning',
                trigger: () => {
                  Modal.dismiss();
                  this.deleteElement(item, 0);
                }
              },
              {
                text: this.language.deleteModal.purge,
                btnClass: 'btn-danger',
                trigger: () => {
                  Modal.dismiss();
                  this.deleteElement(item, 1);
                }
              }
            ]);
      },
      showMissingFilesOrFolder() {
        let template = '';
        // isArray means empty array from json_encode.
        if (!Array.isArray(this.missingFilesOrFolders.missingFolders)) {
          template += this.language.missingFolders + ':\n';
          for (const [key, value] of Object.entries(this.missingFilesOrFolders.missingFolders)) {
            template += `${value} (${key})\n`;
          }
        }

        // isArray means empty array from json_encode.
        if (!Array.isArray(this.missingFilesOrFolders.missingTemplates)) {
          if (template !== '') {
            template += '\n';
          }
          template += this.language.missingTemplates + ':\n';
          for (const [key, value] of Object.entries(this.missingFilesOrFolders.missingTemplates)) {
            template += `${value} (${key}) \n`;
          }
        }

        Modal.confirm(
          this.language.createMissingFilesOrFolders,
          template,
          Severity.info,
          [
            {
              text: this.language.close,
              btnClass: 'btn btn-default',
              trigger: () => {
                Modal.dismiss();
              }
            },
            {
              text: this.language.create,
              btnClass: 'btn btn-info',
              trigger: () => {
                Modal.dismiss();
                this.fixMissing();
              }
            }
          ]
        );
      },
      fixMissing(showMessages = true) {
        (new AjaxRequest(TYPO3.settings.ajaxUrls.mask_fix_missing)).get()
          .then(
            async response => {
              const result = await response.resolve();
              if (showMessages) {
                this.showMessages(result.messages);
              }
              new AjaxRequest(TYPO3.settings.ajaxUrls.mask_missing).get()
                .then(
                  async response => {
                    this.missingFilesOrFolders = await response.resolve();
                    this.loadElements();
                  }
                );
            }
          )
      },
      showMessages: function (res) {
        Object.keys(res).forEach(key => {
          const item = res[key];
          if (item.severity === 0) {
            Notification.success(item.title, item.message);
          } else if (item.severity === 1) {
            Notification.warning(item.title, item.message);
          } else {
            Notification.error(item.title, item.message);
          }
        });
      },
      resetState: function () {
        this.type = '';
        this.element = {};
        this.fields = [];
        this.sidebar = 'fields';
        this.multiUseElements = {};
        this.global.deletedFields = [];
        this.global.activeField = {};
        this.global.clonedField = {};
        this.fieldErrors = {
          elementKeyAvailable: true,
          elementKey: false,
          elementLabel: false,
          emptyKeyFields: [],
          emptyTabLabels: [],
          emptyGroupAllowedFields: [],
          emptyRadioItems: [],
          existingFieldKeyFields: []
        };
      },
      fieldHasError: function (field) {
        if (!this.hasFieldErrors) {
          return false;
        }
        if (this.fieldErrors.emptyKeyFields.includes(field)) {
          return true;
        }
        if (this.fieldErrors.emptyTabLabels.includes(field)) {
          return true;
        }
        if (this.fieldErrors.existingFieldKeyFields.includes(field)) {
          return true;
        }
        if (this.fieldErrors.emptyGroupAllowedFields.includes(field)) {
          return true;
        }
        if (this.fieldErrors.emptyRadioItems.includes(field)) {
          return true;
        }
        return false;
      },
      validate: function () {
        this.fieldErrors.elementKey = this.element.key === '';
        this.fieldErrors.elementLabel = this.element.label === '';

        this.fieldErrors.emptyKeyFields = [];
        this.fieldErrors.emptyTabLabels = [];
        this.fieldErrors.emptyGroupAllowedFields = [];
        this.fieldErrors.emptyRadioItems = [];

        this.checkFieldKeyIsEmpty(this.fields);
        this.checkTabLabelIsEmpty(this.fields);
        this.checkEmptyGroupAllowed(this.fields);
        this.checkEmptyRadioItems(this.fields);
      },
      getErrorFields: function () {
        return [
          this.fieldErrors.emptyKeyFields,
          this.fieldErrors.emptyTabLabels,
          this.fieldErrors.emptyGroupAllowedFields,
          this.fieldErrors.emptyRadioItems
        ];
      },
      checkFieldKeyIsEmpty: function (fields) {
        fields.every(item => {
          if (item.key === this.global.maskPrefix) {
            this.fieldErrors.emptyKeyFields.push(item);
          }
          if (item.fields.length > 0) {
            this.checkFieldKeyIsEmpty(item.fields);
          }
          return true;
        });
      },
      checkTabLabelIsEmpty: function (fields) {
        fields.every(item => {
          if (item.name === 'tab' && item.label === '') {
            this.fieldErrors.emptyTabLabels.push(item);
          }
          if (item.fields.length > 0) {
            this.checkTabLabelIsEmpty(item.fields);
          }
          return true;
        });
      },
      checkEmptyGroupAllowed: function (fields) {
        fields.every(item => {
          if (this.isCoreField(item)) {
            return true;
          }
          if ((item.tca['config.internal_type'] === 'db' || this.global.typo3Version > 11) && item.tca['config.allowed'] === '') {
            this.fieldErrors.emptyGroupAllowedFields.push(item);
          }
          if (item.fields.length > 0) {
            this.checkEmptyGroupAllowed(item.fields);
          }
          return true;
        });
      },
      checkEmptyRadioItems: function (fields) {
        fields.every(item => {
          if (this.isCoreField(item)) {
            return true;
          }
          if (item.name === 'radio') {
            const items = item.tca['config.items'];
            if (items.length === 0) {
              this.fieldErrors.emptyRadioItems.push(item);
            } else {
              items.every(radioItem => {
                if (!this.isNumeric(this.global.typo3Version > 11 ? radioItem.value : radioItem[1])) {
                  this.fieldErrors.emptyRadioItems.push(item);
                  return false;
                }
                return true;
              });
            }
          }
          if (item.fields.length > 0) {
            this.checkEmptyRadioItems(item.fields);
          }
          return true;
        });
      },
      handleClone: function (item) {
        // Create a fresh copy of item
        let cloneMe = JSON.parse(JSON.stringify(item));
        this.$delete(cloneMe, 'uid');
        this.global.clonedField = cloneMe;
        return cloneMe;
      },
      /**
       * This adds a field by click on the field.
       * @param type
       */
      addField: function (type) {
        const newField = this.handleClone(type);
        const parent = this.global.activeField.parent;
        let parentKey = '';
        let parentName = '';
        let fields = this.fields;
        if (typeof parent === 'undefined' || parent.length === 0) {
          newField.parent = {};
        } else {
          parentName = parent.name;
          parentKey = parent.key;
          newField.parent = parent;
          if (typeof parent.fields !== 'undefined') {
            fields = parent.fields;
          }
        }
        if (this.validateMove(parentKey, parentName, newField)) {
          const index = fields.indexOf(this.global.activeField) + 1;
          fields.splice(index, 0, newField);
          this.global.activeField = newField;
          this.global.currentTab = 'general';
          this.validateKey(newField);
        }
      },
      onMove: function (e) {
        const draggedField = e.draggedContext.element;
        let parent = e.relatedContext.component.$parent;
        const depth = parent.depth;
        const index = parent.index;
        let parentName = '';
        let parentKey = '';

        if (depth > 0) {
          parentName = parent.$parent.list[index].name;
          parentKey = parent.$parent.list[index].key;
        }

        return this.validateMove(parentKey, parentName, draggedField);
      },
      onSort: function () {
        this.updateParents();
      },
      validateMove: function (newParentKey, newParentName, draggedField) {
        // Rule #1: Palette and tab fields are not allowed in palette.
        if (newParentName === 'palette' && ['palette', 'tab'].includes(draggedField.name)) {
          return false;
        }

        // Rule #2: Linebreaks are only allowed in palettes.
        if (draggedField.name === 'linebreak' && newParentName !== 'palette') {
          return false;
        }

        // The rule below only applies for existing fields.
        // As it is the last rule, return early true.
        // Exception: Core fields are always treated as existing.
        if (draggedField.newField && !this.isCoreField(draggedField)) {
          return true;
        }

        // Rule #3: Existing fields must never leave their inline parent.
        // Create a Set to store the keys of the fields into which the dragged field can be dragged.
        let allowedFields = new Set();
        let parentField = draggedField.parent;
        // Go up until inline parent is found.
        // Can logically only loop 2-times at max (inline <- palette) or (root <- palette).
        while (true) {
          // If inline field or root is found, add it to the allowed list and traverse
          // all sub-fields to find possible new palette parents.
          if (parentField.name === 'inline') {
            allowedFields = this.getAllowedFields(parentField.key, parentField.fields);
            break;
          }
          // root field has an empty string as key.
          if (typeof parentField.name === 'undefined') {
            allowedFields = this.getAllowedFields('', this.fields);
            break;
          }
          parentField = parentField.parent;
        }

        // Check if the newParent is in the allowed list
        return allowedFields.has(newParentKey);
      },
      getAllowedFields: function (key, fields) {
        const allowedFields = new Set();
        allowedFields.add(key);
        return new Set([...allowedFields, ...this.getPaletteKeysOfField(fields)]);
      },
      getPaletteKeysOfField: function (fields, keys = []) {
        for (let childField of fields) {
          if (childField.name === 'palette') {
            keys.push(childField.key);
          }
        }
        return keys;
      },
      getNewElement: function () {
        return {
          key: '',
          label: '',
          shortLabel: '',
          description: '',
          icon: '',
          color: '#000000',
          iconOverlay: '',
          colorOverlay: '#000000',
          saveAndClose: 0,
        };
      },
      isParentField: function (field) {
        return ['palette', 'inline'].includes(field.name);
      },
      resetActiveField: function () {
        this.global.activeField.tca = Object.assign({}, this.defaultTca[this.global.activeField.name]);
        Notification.success('', this.language.reset);
      },
      isEmptyObject: function (obj) {
        return Object.keys(obj).length === 0 && obj.constructor === Object;
      },
      isNumeric: function (str) {
        // we only process strings!
        if (typeof str != "string") {
          return false
        }
        return !isNaN(str) && // use type coercion to parse the _entirety_ of the string (`parseFloat` alone does not do this)...
            !isNaN(parseFloat(str)) // ...and ensure strings of whitespace fail
      },
      checkAllowedCharacters: function (key) {
        key = key.toLowerCase();
        key = key.replace(/\s/g, '_');
        key = key.replace(/[^a-z0-9_]/g, '');
        return key;
      },
      // todo merge isCoreField and isExistingMaskField into one method
      isCoreField: function (field) {
        if (this.isEmptyObject(field)) {
          return false;
        }
        let isExisting = false;
        this.availableTca[field.name].core.forEach(item => {
          if (item.field === field.key) {
            isExisting = true;
          }
        });
        return isExisting;
      },
      isExistingMaskField: function (field) {
        if (this.isEmptyObject(this.global.activeField)) {
          return false;
        }
        let isExisting = false;
        this.availableTca[field.name].mask.forEach(function (item) {
          if (item.field === field.key) {
            isExisting = true;
          }
        });
        return isExisting;
      },
      availableTcaForField: function (type, field) {
        if (this.isEmptyObject(this.availableTca) || this.isEmptyObject(field)) {
          return [];
        }
        return this.availableTca[field.name][type].filter(item => {
          return (!this.currentFieldKeys.includes(item.field) && !this.deletedFieldKeys.includes(item.field)) || field.key === item.field;
        });
      },
      getFieldKeys: function (fields) {
        const keys = [];
        fields.forEach(function (item) {
          if (item.name === 'palette') {
            item.fields.forEach(function (item) {
              if (!item.newField) {
                keys.push(item.key);
              }
            });
          }
          if (!item.newField) {
            keys.push(item.key);
          }
        });
        return keys;
      },
      getAvailableTcaKeys: function () {
        const keys = {};
        Object.keys(this.availableTca).forEach(key => {
          keys[key] = [];
          this.availableTca[key].core.forEach(item => {
            keys[key].push(item.field);
          });
          this.availableTca[key].mask.forEach(item => {
            keys[key].push(item.field);
          });
        });
        return keys;
      },
      openMultiUsageModal() {
        let template = '';
        this.activeMultiUseElements.forEach((item, index) => {
          template += `${index + 1}: ${item.label} (${item.key})\n`;
        });
        Modal.confirm(
            'Content elements with same field',
            template,
            Severity.info,
            [
              {
                text: this.language.close,
                btnClass: 'btn-default',
                trigger: () => {
                  Modal.dismiss();
                }
              },
            ]);
      },
      submitAutoConfiguration() {
        new AjaxRequest(TYPO3.settings.ajaxUrls.mask_setup_autoconfigure).post(this.setupConfiguration)
          .then(
            async response => {
              const resolvedResponse = await response.resolve();
              this.setupConfiguration.error = resolvedResponse.result.error;

              if (this.setupConfiguration.error === '') {
                Notification.success('', 'Successfully auto-configured Mask for ' + this.setupConfiguration.extension + '!');
                this.fixMissing(false);
                this.init();
              }
            }
          );
      },
      /**
       * Update tick to force rerender of date inputs
       */
      forceRenderer() {
        this.ticks += 1;
      },
      isAllowedToOverride(fieldKey) {
        // If feature "isOverrideSharedFields" is not active AND the active field is not a Core field.
        if (!this.isOverrideSharedFieldsEnabled && !this.isActiveCoreField) {
          return true;
        }
        const fieldKeyWithoutConfig = fieldKey.replace('config.', '');
        const fieldIsShared = this.activeMultiUseElements.length !== 0 || this.isActiveCoreField;
        if (!fieldIsShared) {
          return true;
        }
        const isOverridableField = !this.nonOverrideableOptions.includes(fieldKeyWithoutConfig);
        if (isOverridableField) {
          return true;
        }
        return false;
      },
      keyWithoutMask: function (key) {
        if (key.substr(0, 8) === this.global.maskPrefix) {
          return key.substr(8);
        } else {
          return key;
        }
      }
    },
    computed: {
      hasErrors: function () {
        return this.hasElementErrors || this.hasFieldErrors;
      },
      hasElementErrors: function () {
        return this.fieldErrors.elementKey || this.fieldErrors.elementLabel || !this.fieldErrors.elementKeyAvailable;
      },
      hasFieldErrors: function () {
        return this.fieldErrors.emptyKeyFields.length > 0
          || this.fieldErrors.emptyTabLabels.length > 0
          || this.fieldErrors.emptyGroupAllowedFields.length > 0
          || this.fieldErrors.emptyRadioItems.length > 0
          || this.fieldErrors.existingFieldKeyFields.length > 0;
      },
      maskBuilderOpen: function () {
        return this.mode === 'edit' || this.mode === 'new';
      },
      isActiveCoreField: function () {
        return this.isCoreField(this.global.activeField);
      },
      fieldTabs: function () {
        if (!this.global.activeField.name) {
          return [];
        }
        return this.tabs[this.global.activeField.name];
      },
      chooseFieldVisible: function () {
        if (this.isEmptyObject(this.global.activeField)) {
          return false;
        }
        if (!this.global.activeField.newField && !this.isActiveCoreField) {
          return false;
        }
        if (!this.canBeShared(this.global.activeField)) {
          return false;
        }
        if (!this.isRoot(this.global.activeField)) {
          return false;
        }
        return this.availableCoreTcaForActiveField.length > 0 || this.availableMaskTcaForActiveField.length > 0;
      },
      filteredElements() {
        if (this.lowerCaseSearchString === '') {
          return Object.values(this.elements);
        }
        return Object.values(this.elements).filter((element) => {
          return element.label.toLowerCase().includes(this.lowerCaseSearchString)
            || element.shortLabel.toLowerCase().includes(this.lowerCaseSearchString)
            || element.description.toLowerCase().includes(this.lowerCaseSearchString)
            || element.key.includes(this.lowerCaseSearchString);
        });
      },
      lowerCaseSearchString() {
        return this.searchString.toLowerCase();
      },
      keyFieldVisible: function () {
        return !this.global.structuralFields.includes(this.global.activeField.name) && this.maskFieldGeneralTabOpen;
      },
      maskFieldGeneralTabOpen: function () {
        return this.isGeneralTabOpen && !this.isActiveCoreField;
      },
      overrideLabelVisible: function () {
        return this.isGeneralTabOpen && this.isActiveCoreField;
      },
      overrideDescriptionVisible: function () {
        return this.isGeneralTabOpen && this.isActiveCoreField;
      },
      isGeneralTabOpen: function () {
        return this.global.currentTab === 'general';
      },
      availableCoreTcaForActiveField: function () {
        return this.availableTcaForField('core', this.global.activeField);
      },
      availableMaskTcaForActiveField: function () {
        return this.availableTcaForField('mask', this.global.activeField);
      },
      activeFieldHasKeyError: function () {
          return this.fieldErrors.emptyKeyFields.includes(this.global.activeField)
          || this.fieldErrors.existingFieldKeyFields.includes(this.global.activeField);
      },
      activeTabHasEmptyLabel: function () {
        return this.fieldErrors.emptyTabLabels.includes(this.global.activeField);
      },
      currentFieldKeys: function () {
        return this.getFieldKeys(this.fields);
      },
      deletedFieldKeys: function () {
        return this.getFieldKeys(this.global.deletedFields);
      },
      defaultTca: function () {
        if (this.isEmptyObject(this.fieldTypes)) {
          return [];
        }
        const defaults = {};
        this.fieldTypes.forEach(item => {
          defaults[item.name] = item.tca;
        });
        return defaults;
      },
      activeMultiUseElements: function () {
        if (this.isRoot(this.global.activeField) && typeof this.multiUseElements[this.global.activeField.key] !== 'undefined') {
          return this.multiUseElements[this.global.activeField.key]
        }
        return [];
      },
      isOverrideSharedFieldsEnabled: function () {
        return this.features?.overrideSharedFields?.state === 1;
      },
      metaVisible: function () {
        return this.sidebar === 'meta';
      },
      fieldsVisible: function () {
        return this.sidebar === 'fields';
      }
    }
  });
})();