[BUGFIX] Make JavaScript of SlugElement a real independent instance 73/58273/4
authorAndreas Fernandez <a.fernandez@scripting-base.de>
Fri, 14 Sep 2018 08:17:23 +0000 (10:17 +0200)
committerFrank Naegler <frank.naegler@typo3.org>
Fri, 14 Sep 2018 09:47:46 +0000 (11:47 +0200)
The JavaScript for the SlugElement is partially designed to be a
independent instance for each slug field. However, this does not work for
mass editing the slug fields, as the `initialize()` method is always
called on the same instance.

This patch does multiple things:

- The JavaScript is converted to a TypeScript basis, which allows better
  handling of each instance
- The "Regenerate slug" button is disabled in case the "feeder" fields
  are not available to avoid getting bogus values

Resolves: #86221
Releases: master
Change-Id: I458247beb597c77407e94ce633314ae2e2d7095a
Reviewed-on: https://review.typo3.org/58273
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
Reviewed-by: Frank Naegler <frank.naegler@typo3.org>
Tested-by: Frank Naegler <frank.naegler@typo3.org>
typo3/sysext/backend/Classes/Form/Element/InputSlugElement.php
typo3/sysext/backend/Resources/Private/TypeScript/FormEngine/Element/SlugElement.ts [new file with mode: 0644]
typo3/sysext/backend/Resources/Public/JavaScript/FormEngine/Element/SlugElement.js

index a0ab76b..ff9a5f9 100644 (file)
@@ -149,7 +149,7 @@ class InputSlugElement extends AbstractFormElement
 
         $resultArray['html'] = implode(LF, $mainFieldHtml);
 
-        list($commonElementPrefix) = GeneralUtility::revExplode('[', $parameterArray['itemFormElName'], 2);
+        [$commonElementPrefix] = GeneralUtility::revExplode('[', $parameterArray['itemFormElName'], 2);
         $validInputNamesToListenTo = [];
         foreach ($config['generatorOptions']['fields'] ?? [] as $listenerFieldName) {
             $validInputNamesToListenTo[$listenerFieldName] = $commonElementPrefix . '[' . htmlspecialchars($listenerFieldName) . ']';
@@ -185,7 +185,7 @@ class InputSlugElement extends AbstractFormElement
         ];
         $resultArray['requireJsModules'][] = ['TYPO3/CMS/Backend/FormEngine/Element/SlugElement' => '
             function(SlugElement) {
-                SlugElement.initialize(' . GeneralUtility::quoteJSvalue('#' . $thisSlugId) . ', ' . json_encode($optionsForModule) . ');
+                new SlugElement(' . GeneralUtility::quoteJSvalue('#' . $thisSlugId) . ', ' . json_encode($optionsForModule) . ');
             }'
         ];
         return $resultArray;
diff --git a/typo3/sysext/backend/Resources/Private/TypeScript/FormEngine/Element/SlugElement.ts b/typo3/sysext/backend/Resources/Private/TypeScript/FormEngine/Element/SlugElement.ts
new file mode 100644 (file)
index 0000000..26297de
--- /dev/null
@@ -0,0 +1,207 @@
+/*
+ * 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!
+ */
+
+import * as $ from 'jquery';
+
+interface FieldOptions {
+  pageId: number;
+  recordId: number;
+  tableName: string;
+  fieldName: string;
+  config: { [key: string]: any };
+  listenerFieldNames: string[];
+  language: number;
+  originalValue: string;
+  signature: string;
+  command: string;
+  parentPageId: number;
+}
+
+interface Response {
+  hasConflicts: boolean;
+  manual: string;
+  proposal: ProposalModes;
+}
+
+enum Selectors {
+  toggleButton = '.t3js-form-field-slug-toggle',
+  recreateButton = '.t3js-form-field-slug-recreate',
+  inputField = '.t3js-form-field-slug-input',
+  readOnlyField = '.t3js-form-field-slug-readonly',
+  hiddenField = '.t3js-form-field-slug-hidden',
+}
+
+enum ProposalModes {
+  AUTO = 'auto',
+  RECREATE = 'recreate',
+  MANUAL = 'manual'
+}
+
+/**
+ * Module: TYPO3/CMS/Backend/FormEngine/Element/SlugElement
+ * Logic for a TCA type "slug"
+ *
+ * For new records, changes on the other fields of the record (typically the record title) are listened
+ * on as well and the response is put in as "placeholder" into the input field.
+ *
+ * For new and existing records, the toggle switch will allow editors to modify the slug
+ *  - for new records, we only need to see if that is already in use or not (uniqueInSite), if it is taken, show a message.
+ *  - for existing records, we also check for conflicts, and check if we have subpges, or if we want to add a redirect (todo)
+ */
+class SlugElement {
+  private options: FieldOptions = null;
+  private $fullElement: JQuery = null;
+  private manuallyChanged: boolean = false;
+  private $readOnlyField: JQuery = null;
+  private $inputField: JQuery = null;
+  private $hiddenField: JQuery = null;
+  private readonly fieldsToListenOn: { [key: string]: string } = {};
+
+  constructor(selector: string, options: FieldOptions) {
+    this.options = options;
+    this.fieldsToListenOn = this.options.listenerFieldNames || {};
+
+    $((): void => {
+      this.$fullElement = $(selector);
+      this.$inputField = this.$fullElement.find(Selectors.inputField);
+      this.$readOnlyField = this.$fullElement.find(Selectors.readOnlyField);
+      this.$hiddenField = this.$fullElement.find(Selectors.hiddenField);
+
+      this.registerEvents();
+    });
+  }
+
+  private registerEvents(): void {
+    const fieldsToListenOnList = Object.keys(this.getAvailableFieldsForProposalGeneration()).map(k => this.fieldsToListenOn[k]);
+
+    // Listen on 'listenerFieldNames' for new pages. This is typically the 'title' field
+    // of a page to create slugs from the title when title is set / changed.
+    if (fieldsToListenOnList.length > 0) {
+      if (this.options.command === 'new') {
+        $(this.$fullElement).on('keyup', fieldsToListenOnList.join(','), (): void => {
+          if (!this.manuallyChanged) {
+            this.sendSlugProposal(ProposalModes.AUTO);
+          }
+        });
+      }
+
+      // Clicking the recreate button makes new slug proposal created from 'title' field
+      $(this.$fullElement).on('click', Selectors.recreateButton, (e): void => {
+        e.preventDefault();
+        if (this.$readOnlyField.hasClass('hidden')) {
+          // Switch to readonly version - similar to 'new' page where field is
+          // written on the fly with title change
+          this.$readOnlyField.toggleClass('hidden', false);
+          this.$inputField.toggleClass('hidden', true);
+        }
+        this.sendSlugProposal(ProposalModes.RECREATE);
+      });
+    } else {
+      $(this.$fullElement).find(Selectors.recreateButton).addClass('disabled').prop('disabled', true);
+    }
+
+    // Scenario for new pages: Usually, slug is created from the page title. However, if user toggles the
+    // input field and feeds an own slug, and then changes title again, the slug should stay. manuallyChanged
+    // is used to track this.
+    $(this.$inputField).on('keyup', (): void => {
+      this.manuallyChanged = true;
+      this.sendSlugProposal(ProposalModes.MANUAL);
+    });
+
+    // Clicking the toggle button toggles the read only field and the input field.
+    // Also set the value of either the read only or the input field to the hidden field.
+    $(this.$fullElement).on('click', Selectors.toggleButton, (e): void => {
+      e.preventDefault();
+      const showReadOnlyField = this.$readOnlyField.hasClass('hidden');
+      this.$readOnlyField.toggleClass('hidden', !showReadOnlyField);
+      this.$inputField.toggleClass('hidden', showReadOnlyField);
+      if (showReadOnlyField) {
+        this.manuallyChanged = false;
+        this.$hiddenField.val(this.$readOnlyField.val());
+        this.$fullElement.find('.t3js-form-proposal-accepted').addClass('hidden');
+        this.$fullElement.find('.t3js-form-proposal-different').addClass('hidden');
+      } else {
+        this.$hiddenField.val(this.$inputField.val());
+      }
+    });
+  }
+
+  /**
+   * @param {ProposalModes} mode
+   */
+  private sendSlugProposal(mode: ProposalModes): void {
+    const input: { [key: string]: string } = {};
+    if (mode === ProposalModes.AUTO || mode === ProposalModes.RECREATE) {
+      $.each(this.getAvailableFieldsForProposalGeneration(), (fieldName: string, field: string): void => {
+        input[fieldName] = $('[data-formengine-input-name="' + field + '"]').val();
+      });
+    } else {
+      input.manual = this.$inputField.val();
+    }
+    $.post(
+      TYPO3.settings.ajaxUrls.record_slug_suggest,
+      {
+        values: input,
+        mode: mode,
+        tableName: this.options.tableName,
+        pageId: this.options.pageId,
+        parentPageId: this.options.parentPageId,
+        recordId: this.options.recordId,
+        language: this.options.language,
+        fieldName: this.options.fieldName,
+        command: this.options.command,
+        signature: this.options.signature
+      },
+      (response: Response): void => {
+        if (response.hasConflicts) {
+          this.$fullElement.find('.t3js-form-proposal-accepted').addClass('hidden');
+          this.$fullElement.find('.t3js-form-proposal-different').removeClass('hidden').find('span').text(response.proposal);
+        } else {
+          this.$fullElement.find('.t3js-form-proposal-accepted').removeClass('hidden').find('span').text(response.proposal);
+          this.$fullElement.find('.t3js-form-proposal-different').addClass('hidden');
+        }
+        const isChanged = this.$hiddenField.val() !== response.proposal;
+        if (isChanged) {
+          this.$fullElement.find('input').trigger('change');
+        }
+        if (mode === ProposalModes.AUTO || mode === ProposalModes.RECREATE) {
+          this.$readOnlyField.val(response.proposal);
+          this.$hiddenField.val(response.proposal);
+        } else {
+          this.$hiddenField.val(response.proposal);
+        }
+      },
+      'json'
+    );
+  }
+
+  /**
+   * Gets a list of all available fields that can be used for slug generation
+   *
+   * @return { [key: string]: string }
+   */
+  private getAvailableFieldsForProposalGeneration(): { [key: string]: string } {
+    const availableFields: { [key: string]: string } = {};
+
+    $.each(this.fieldsToListenOn, (fieldName: string, field: string): void => {
+      const $selector = $('[data-formengine-input-name="' + field + '"]');
+      if ($selector.length > 0) {
+        availableFields[fieldName] = field;
+      }
+    });
+
+    return availableFields;
+  }
+}
+
+export = SlugElement;
index a9b1d6a..b58cf7a 100644 (file)
  *
  * The TYPO3 project - inspiring people to share!
  */
-
-/**
- * Module: TYPO3/CMS/Backend/FormEngine/Element/SlugElement
- * Logic for a TCA type "slug"
- *
- * For new records, changes on the other fields of the record (typically the record title) are listened
- * on as well and the response is put in as "placeholder" into the input field.
- *
- * For new and existing records, the toggle switch will allow editors to modify the slug
- *  - for new records, we only need to see if that is already in use or not (uniqueInSite), if it is taken, show a message.
- *  - for existing records, we also check for conflicts, and check if we have subpges, or if we want to add a redirect (todo)
- */
-define(['jquery'], function ($) {
-
-  /**
-   *
-   * @type {{}}
-   * @exports TYPO3/CMS/Backend/FormEngine/Element/SlugElement
-   */
-  var SlugElement = {
-    options: {},
-    manuallyChanged: false, // This flag is set to true as soon as user submitted data to the input field
-    $fullElement: null,
-    $readOnlyField: null,
-    $inputField: null,
-    $hiddenField: null
-  };
-
-  /**
-   * Initializes the SlugElement
-   *
-   * @param {String} selector
-   * @param {Object} options
-   */
-  SlugElement.initialize = function (selector, options) {
-    var self = this;
-    var toggleButtonClass = '.t3js-form-field-slug-toggle';
-    var recreateButtonClass = '.t3js-form-field-slug-recreate';
-    var inputFieldClass = '.t3js-form-field-slug-input';
-    var readOnlyFieldClass = '.t3js-form-field-slug-readonly';
-    var hiddenFieldClass = '.t3js-form-field-slug-hidden';
-
-    this.options = options;
-    this.$fullElement = $(selector);
-    this.$inputField = this.$fullElement.find(inputFieldClass);
-    this.$readOnlyField = this.$fullElement.find(readOnlyFieldClass);
-    this.$hiddenField = this.$fullElement.find(hiddenFieldClass);
-
-    var fieldsToListenOn = this.options.listenerFieldNames || {};
-
-    if (options.command === 'new') {
-      // Listen on 'listenerFieldNames' for new pages. This is typically the 'title' field
-      // of a page to create slugs from the title when title is set / changed.
-
-      $.each(fieldsToListenOn, function (fieldName, field) {
-        $(document).on('keyup', '[data-formengine-input-name="' + field + '"]', function (e) {
-          if (!self.manuallyChanged) {
-            // manuallyChanged = true stops slug generation as soon as editor set a slug manually
-            SlugElement.sendSlugProposal('auto');
-          }
-        });
-      });
-    }
-
-    // Clicking the recreate button makes new slug proposal created from 'title' field
-    $(document).on('click', recreateButtonClass, function (e) {
-      e.preventDefault();
-      if (self.$readOnlyField.hasClass('hidden')) {
-        // Switch to readonly version - similar to 'new' page where field is
-        // written on the fly with title change
-        self.$readOnlyField.toggleClass('hidden', false);
-        self.$inputField.toggleClass('hidden', true);
-      }
-      SlugElement.sendSlugProposal('recreate');
-    });
-
-    // Scenario for new pages: Usually, slug is created from the page title. However, if user toggles the
-    // input field and feeds an own slug, and then changes title again, the slug should stay. manuallyChanged
-    // is used to track this.
-    $(this.$inputField).on('keyup', function (e) {
-      self.manuallyChanged = true;
-      SlugElement.sendSlugProposal('manual');
-    });
-
-    // Clicking the toggle button toggles the read only field and the input field.
-    // Also set the value of either the read only or the input field to the hidden field.
-    $(document).on('click', toggleButtonClass, function (e) {
-      e.preventDefault();
-      var showReadOnlyField = self.$readOnlyField.hasClass('hidden');
-      self.$readOnlyField.toggleClass('hidden', !showReadOnlyField);
-      self.$inputField.toggleClass('hidden', showReadOnlyField);
-      if (showReadOnlyField) {
-        self.manuallyChanged = false;
-        self.$hiddenField.val(self.$readOnlyField.val());
-        self.$fullElement.find('.t3js-form-proposal-accepted').addClass('hidden');
-        self.$fullElement.find('.t3js-form-proposal-different').addClass('hidden');
-      } else {
-        self.$hiddenField.val(self.$inputField.val());
-      }
-    });
-  };
-
-  SlugElement.sendSlugProposal = function (mode) {
-    var input = {};
-    if (mode === 'auto' || mode === 'recreate') {
-      var fieldsToListenOn = SlugElement.options.listenerFieldNames || {};
-      $.each(fieldsToListenOn, function (fieldName, field) {
-        input[fieldName] = $('[data-formengine-input-name="' + field + '"]').val();
-      });
-    } else {
-      input['manual'] = SlugElement.$inputField.val();
-    }
-    $.post(
-      TYPO3.settings.ajaxUrls['record_slug_suggest'],
-      {
-        values: input,
-        mode: mode,
-        tableName: SlugElement.options.tableName,
-        pageId: SlugElement.options.pageId,
-        parentPageId: SlugElement.options.parentPageId,
-        recordId: SlugElement.options.recordId,
-        language: SlugElement.options.language,
-        fieldName: SlugElement.options.fieldName,
-        command: SlugElement.options.command,
-        signature: SlugElement.options.signature
-      }, function (response) {
-        if (response.hasConflicts) {
-          SlugElement.$fullElement.find('.t3js-form-proposal-accepted').addClass('hidden');
-          SlugElement.$fullElement.find('.t3js-form-proposal-different').removeClass('hidden').find('span').text(response.proposal);
-        } else {
-          SlugElement.$fullElement.find('.t3js-form-proposal-accepted').removeClass('hidden').find('span').text(response.proposal);
-          SlugElement.$fullElement.find('.t3js-form-proposal-different').addClass('hidden');
-        }
-        const isChanged = SlugElement.$hiddenField.val() !== response.proposal;
-        if (isChanged) {
-          SlugElement.$fullElement.find('input').trigger('change');
-        }
-        if (mode === 'auto' || mode === 'recreate') {
-          SlugElement.$readOnlyField.val(response.proposal);
-          SlugElement.$hiddenField.val(response.proposal);
-        } else {
-          SlugElement.$hiddenField.val(response.proposal);
-        }
-      },
-      'json'
-    );
-  };
-
-  return SlugElement;
-});
+define(["require","exports","jquery"],function(e,l,n){"use strict";var i,t,d,s;return(t=i||(i={})).toggleButton=".t3js-form-field-slug-toggle",t.recreateButton=".t3js-form-field-slug-recreate",t.inputField=".t3js-form-field-slug-input",t.readOnlyField=".t3js-form-field-slug-readonly",t.hiddenField=".t3js-form-field-slug-hidden",(s=d||(d={})).AUTO="auto",s.RECREATE="recreate",s.MANUAL="manual",function(){function e(e,l){var t=this;this.options=null,this.$fullElement=null,this.manuallyChanged=!1,this.$readOnlyField=null,this.$inputField=null,this.$hiddenField=null,this.fieldsToListenOn={},this.options=l,this.fieldsToListenOn=this.options.listenerFieldNames||{},n(function(){t.$fullElement=n(e),t.$inputField=t.$fullElement.find(i.inputField),t.$readOnlyField=t.$fullElement.find(i.readOnlyField),t.$hiddenField=t.$fullElement.find(i.hiddenField),t.registerEvents()})}return e.prototype.registerEvents=function(){var e=this,l=Object.keys(this.getAvailableFieldsForProposalGeneration()).map(function(l){return e.fieldsToListenOn[l]});l.length>0?("new"===this.options.command&&n(this.$fullElement).on("keyup",l.join(","),function(){e.manuallyChanged||e.sendSlugProposal(d.AUTO)}),n(this.$fullElement).on("click",i.recreateButton,function(l){l.preventDefault(),e.$readOnlyField.hasClass("hidden")&&(e.$readOnlyField.toggleClass("hidden",!1),e.$inputField.toggleClass("hidden",!0)),e.sendSlugProposal(d.RECREATE)})):n(this.$fullElement).find(i.recreateButton).addClass("disabled").prop("disabled",!0),n(this.$inputField).on("keyup",function(){e.manuallyChanged=!0,e.sendSlugProposal(d.MANUAL)}),n(this.$fullElement).on("click",i.toggleButton,function(l){l.preventDefault();var n=e.$readOnlyField.hasClass("hidden");e.$readOnlyField.toggleClass("hidden",!n),e.$inputField.toggleClass("hidden",n),n?(e.manuallyChanged=!1,e.$hiddenField.val(e.$readOnlyField.val()),e.$fullElement.find(".t3js-form-proposal-accepted").addClass("hidden"),e.$fullElement.find(".t3js-form-proposal-different").addClass("hidden")):e.$hiddenField.val(e.$inputField.val())})},e.prototype.sendSlugProposal=function(e){var l=this,i={};e===d.AUTO||e===d.RECREATE?n.each(this.getAvailableFieldsForProposalGeneration(),function(e,l){i[e]=n('[data-formengine-input-name="'+l+'"]').val()}):i.manual=this.$inputField.val(),n.post(TYPO3.settings.ajaxUrls.record_slug_suggest,{values:i,mode:e,tableName:this.options.tableName,pageId:this.options.pageId,parentPageId:this.options.parentPageId,recordId:this.options.recordId,language:this.options.language,fieldName:this.options.fieldName,command:this.options.command,signature:this.options.signature},function(n){n.hasConflicts?(l.$fullElement.find(".t3js-form-proposal-accepted").addClass("hidden"),l.$fullElement.find(".t3js-form-proposal-different").removeClass("hidden").find("span").text(n.proposal)):(l.$fullElement.find(".t3js-form-proposal-accepted").removeClass("hidden").find("span").text(n.proposal),l.$fullElement.find(".t3js-form-proposal-different").addClass("hidden")),l.$hiddenField.val()!==n.proposal&&l.$fullElement.find("input").trigger("change"),e===d.AUTO||e===d.RECREATE?(l.$readOnlyField.val(n.proposal),l.$hiddenField.val(n.proposal)):l.$hiddenField.val(n.proposal)},"json")},e.prototype.getAvailableFieldsForProposalGeneration=function(){var e={};return n.each(this.fieldsToListenOn,function(l,i){n('[data-formengine-input-name="'+i+'"]').length>0&&(e[l]=i)}),e},e}()});
\ No newline at end of file