[FEATURE] Add dependencies between classes in RTE 43/29643/5
authorStanislas Rolland <typo3@sjbr.ca>
Wed, 5 Nov 2014 15:45:29 +0000 (10:45 -0500)
committerStanislas Rolland <typo3@sjbr.ca>
Wed, 5 Nov 2014 18:48:19 +0000 (19:48 +0100)
To comply with complex CSS frameworks like Twitter Bootstrap, we need
to add multiple classes on the same element. This adds complexity for
authors. With this new dependency feature, users just have to select
one style instead of many styles.

The syntax of this new property is:
    RTE.classes.[ *classname* ] {
        .requires = list of class names; list of classes that are
             required by the class;
             if this property, in combination with others, produces a
             circular relationship, it is ignored;
             when a class is added on an element, the classes it
             requires are also added, possibly recursively;
             when a class is removed from an element, any
             non-selectable class that is not required by any of the
             classes remaining on the element is also removed.
    }

Releases: master
Resolves: #51905
Change-Id: I890e6786647a4b31e759e8a3086b3bd8c7e5dd4e
Reviewed-on: http://review.typo3.org/29643
Reviewed-by: Stanislas Rolland <typo3@sjbr.ca>
Tested-by: Stanislas Rolland <typo3@sjbr.ca>
typo3/sysext/core/Documentation/Changelog/master/Feature-51905-AddDependenciesBetweenClassesInRte.rst [new file with mode: 0644]
typo3/sysext/rtehtmlarea/Classes/RteHtmlAreaBase.php
typo3/sysext/rtehtmlarea/Documentation/Configuration/PageTsconfig/classes/Index.rst
typo3/sysext/rtehtmlarea/htmlarea/DOM/HTMLArea.DOM.js

diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-51905-AddDependenciesBetweenClassesInRte.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-51905-AddDependenciesBetweenClassesInRte.rst
new file mode 100644 (file)
index 0000000..9975022
--- /dev/null
@@ -0,0 +1,24 @@
+==========================================================================
+Feature: #51905 - Add dependencies between classes in the Rich Text Editor
+==========================================================================
+
+Description
+===========
+
+It is now possible to configure a class as requiring other classes.
+
+The syntax of this new property is
+       ::
+
+       RTE.classes.[ *classname* ] {
+               .requires = list of class names; list of classes that are required by the class;
+                       if this property, in combination with others, produces a circular relationship, it is ignored;
+                       when a class is added on an element, the classes it requires are also added, possibly recursively;
+                       when a class is removed from an element, any non-selectable class that is not required by any of the classes remaining on the element is also removed.
+       }
+
+
+Impact
+======
+
+There is no impact on previous configurations.
index efa783a..d8597b0 100644 (file)
@@ -1063,13 +1063,24 @@ class RteHtmlAreaBase extends \TYPO3\CMS\Backend\Rte\AbstractRte {
                } else {
                        $RTEProperties = $this->RTEsetup['properties'];
                }
-               $classesArray = array('labels' => array(), 'values' => array(), 'noShow' => array(), 'alternating' => array(), 'counting' => array(), 'selectable' => array(), 'XOR' => array());
+               // Declare sub-arrays
+               $classesArray = array(
+                       'labels' => array(),
+                       'values' => array(),
+                       'noShow' => array(),
+                       'alternating' => array(),
+                       'counting' => array(),
+                       'selectable' => array(),
+                       'requires' => array(),
+                       'requiredBy' => array(),
+                       'XOR' => array()
+               );
                $JSClassesArray = '';
                // Scanning the list of classes if specified in the RTE config
                if (is_array($RTEProperties['classes.'])) {
                        foreach ($RTEProperties['classes.'] as $className => $conf) {
                                $className = rtrim($className, '.');
-                               $classesArray['labels'][$className] = $this->getPageConfigLabel($conf['name'], FALSE);
+                               $classesArray['labels'][$className] = trim($conf['name']) ? $this->getPageConfigLabel($conf['name'], FALSE) : '';
                                $classesArray['values'][$className] = str_replace('\\\'', '\'', $conf['value']);
                                if (isset($conf['noShow'])) {
                                        $classesArray['noShow'][$className] = $conf['noShow'];
@@ -1083,6 +1094,28 @@ class RteHtmlAreaBase extends \TYPO3\CMS\Backend\Rte\AbstractRte {
                                if (isset($conf['selectable'])) {
                                        $classesArray['selectable'][$className] = $conf['selectable'];
                                }
+                               if (isset($conf['requires'])) {
+                                       $classesArray['requires'][$className] = explode(',', GeneralUtility::rmFromList($className, $this->cleanList($conf['requires'])));
+                               }
+                       }
+                       // Remove circularities from classes dependencies
+                       $requiringClasses = array_keys($classesArray['requires']);
+                       foreach ($requiringClasses as $requiringClass) {
+                               if ($this->hasCircularDependency($classesArray, $requiringClass, $requiringClass)) {
+                                       unset($classesArray['requires'][$requiringClass]);
+                               }
+                       }
+                       // Reverse relationship for the dependency checks when removing styles
+                       $requiringClasses = array_keys($classesArray['requires']);
+                       foreach ($requiringClasses as $className) {
+                               foreach ($classesArray['requires'][$className] as $requiredClass) {
+                                       if (!is_array($classesArray['requiredBy'][$requiredClass])) {
+                                               $classesArray['requiredBy'][$requiredClass] = array();
+                                       }
+                                       if (!in_array($className, $classesArray['requiredBy'][$requiredClass])) {
+                                               $classesArray['requiredBy'][$requiredClass][] = $className;
+                                       }
+                               }
                        }
                }
                // Scanning the list of sets of mutually exclusives classes if specified in the RTE config
@@ -1102,6 +1135,34 @@ class RteHtmlAreaBase extends \TYPO3\CMS\Backend\Rte\AbstractRte {
        }
 
        /**
+        * Check for possible circularity in classes dependencies
+        *
+        * @param array $classesArray: reference to the array of classes dependencies
+        * @param string $requiringClass: class requiring at some iteration level from the initial requiring class
+        * @param string $initialClass: initial class from which a circular relationship is being searched
+        * @param integer $recursionLevel: depth of recursive call
+        * @return boolean TRUE, if a circular relationship is found
+        */
+       protected function hasCircularDependency(&$classesArray, $requiringClass, $initialClass, $recursionLevel = 0) {
+               if (is_array($classesArray['requires'][$requiringClass])) {
+                       if (in_array($initialClass, $classesArray['requires'][$requiringClass])) {
+                               return TRUE;
+                       } else {
+                               if ($recursionLevel++ < 20) {
+                                       foreach ($classesArray['requires'][$requiringClass] as $requiringClass2) {
+                                               if ($this->hasCircularDependency($classesArray, $requiringClass2, $initialClass, $recursionLevel)) {
+                                                       return TRUE;
+                                               }
+                                       }
+                               }
+                               return FALSE;
+                       }
+               } else {
+                       return FALSE;
+               }
+       }
+
+       /**
         * Translate Page TS Config array in JS nested array definition
         * Replace 0 values with false
         * Unquote regular expression values
index c0603ab..1ecb96d 100644 (file)
@@ -38,7 +38,12 @@ classes.[ *classname* ]
             .name = label of the class (may be a reference to an entry in a localization file of the form LLL:EXT:[fileref]:[labelkey])
             .value = the style for the class
             .noShow = boolean; if set, the style of the class is not used to render it in the pop-up selector.
-            .selectable = boolean; if set to 0, the class is not selectable in the style selectors; if the property is omitted, or set to 1, the class is selectable in the style selectors
+            .selectable = boolean; if set to 0, the class is not selectable in the style selectors; 
+                       if the property is omitted, or set to 1, the class is selectable in the style selectors.
+            .requires = list of class names; list of classes that are required by the class;
+                       if this property, in combination with others, produces a circular relationship, it is ignored;
+                       when a class is added on an element, the classes it requires are also added, possibly recursively;
+                       when a class is removed from an element, any non-selectable class that is not required by any of the classes remaining on the element is also removed.
             
             # specification of alternating classes for rows and/or columns of a table
             .alternating { 
index bf805ac..2152c97 100644 (file)
@@ -94,33 +94,52 @@ HTMLArea.DOM = function () {
                        }
                        return found;
                },
-               /*
+               /**
                 * Add a class name to the class attribute of a node
                 *
-                * @param       object          node: the node
-                * @param       string          className: the name of the class to be added
-                * @return      void
+                * @param object node: the node
+                * @param string className: the name of the class to be added
+                * @param integer recursionLevel: recursion level of current call
+                * @return void
                 */
-               addClass: function (node, className) {
+               addClass: function (node, className, recursionLevel) {
                        if (node) {
-                               HTMLArea.DOM.removeClass(node, className);
+                               var classNames = HTMLArea.DOM.getClassNames(node);
+                               if (classNames.indexOf(className) === -1) {
                                        // Remove classes configured to be incompatible with the class to be added
-                               if (node.className && HTMLArea.classesXOR && HTMLArea.classesXOR[className] && typeof HTMLArea.classesXOR[className].test === 'function') {
-                                       var classNames = node.className.trim().split(' ');
-                                       for (var i = classNames.length; --i >= 0;) {
-                                               if (HTMLArea.classesXOR[className].test(classNames[i])) {
-                                                       HTMLArea.DOM.removeClass(node, classNames[i]);
+                                       if (node.className && HTMLArea.classesXOR && HTMLArea.classesXOR[className] && Ext.isFunction(HTMLArea.classesXOR[className].test)) {
+                                               for (var i = classNames.length; --i >= 0;) {
+                                                       if (HTMLArea.classesXOR[className].test(classNames[i])) {
+                                                               HTMLArea.DOM.removeClass(node, classNames[i]);
+                                                       }
                                                }
                                        }
-                               }
-                               if (node.className) {
-                                       node.className += ' ' + className;
-                               } else {
-                                       node.className = className;
+                                       // Check dependencies to add required classes recursively
+                                       if (typeof HTMLArea.classesRequires !== 'undefined' && typeof HTMLArea.classesRequires[className] !== 'undefined') {
+                                               if (typeof recursionLevel === 'undefined') {
+                                                       var recursionLevel = 1;
+                                               } else {
+                                                       recursionLevel++;
+                                               }
+                                               if (recursionLevel < 20) {
+                                                       for (var i = 0, n = HTMLArea.classesRequires[className].length; i < n; i++) { 
+                                                               var classNames = HTMLArea.DOM.getClassNames(node);
+                                                               if (classNames.indexOf(HTMLArea.classesRequires[className][i]) === -1) {
+                                                                       HTMLArea.DOM.addClass(node, HTMLArea.classesRequires[className][i], recursionLevel);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                                       if (node.className) {
+                                               node.className += ' ' + className;
+                                       } else {
+                                               node.className = className;
+                                       }
                                }
                        }
                },
-               /*
+
+               /**
                 * Remove a class name from the class attribute of a node
                 *
                 * @param       object          node: the node
@@ -140,18 +159,48 @@ HTMLArea.DOM = function () {
                                if (newClasses.length) {
                                        node.className = newClasses.join(' ');
                                } else {
-                                       if (!HTMLArea.UserAgent.isOpera) {
+                                       if (!Ext.isOpera) {
                                                node.removeAttribute('class');
-                                               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                                               if (HTMLArea.isIEBeforeIE9) {
                                                        node.removeAttribute('className');
                                                }
                                        } else {
                                                node.className = '';
                                        }
                                }
+                               // Remove the first unselectable class that is no more required, the following ones being removed by recursive calls
+                               if (node.className && typeof HTMLArea.classesSelectable !== 'undefined') {
+                                       classes = HTMLArea.DOM.getClassNames(node);
+                                       for (var i = classes.length; --i >= 0;) {
+                                               if (typeof HTMLArea.classesSelectable[classes[i]] !== 'undefined' && !HTMLArea.classesSelectable[classes[i]] && !HTMLArea.DOM.isRequiredClass(node, classes[i])) {
+                                                       HTMLArea.DOM.removeClass(node, classes[i]);
+                                                       break;
+                                               }
+                                       }
+                               }
                        }
                },
-               /*
+
+               /**
+                * Check if the class is required by another class assigned to the node
+                * 
+                * @param object node: the node
+                * @param string className: the class name to check
+                * @return boolean 
+                */
+               isRequiredClass: function (node, className) {
+                       if (typeof HTMLArea.classesRequiredBy !== 'undefined') {
+                               var classes = HTMLArea.DOM.getClassNames(node);
+                               for (var i = classes.length; --i >= 0;) {
+                                       if (typeof HTMLArea.classesRequiredBy[classes[i]] !== 'undefined' && HTMLArea.classesRequiredBy[classes[i]].indexOf(className) !== -1) {
+                                               return true;
+                                       }
+                               }
+                       }
+                       return false;
+               },
+
+               /**
                 * Get the innerText of a given node
                 *
                 * @param       object          node: the node
@@ -159,7 +208,7 @@ HTMLArea.DOM = function () {
                 * @return      string          the text inside the node
                 */
                getInnerText: function (node) {
-                       return HTMLArea.UserAgent.isIEBeforeIE9 ? node.innerText : node.textContent;;
+                       return HTMLArea.isIEBeforeIE9 ? node.innerText : node.textContent;;
                },
                /*
                 * Get the block ancestors of a node within a given block
@@ -185,15 +234,15 @@ HTMLArea.DOM = function () {
                 * Get the deepest element ancestor of a given node that is of one of the specified types
                 *
                 * @param       object          node: the given node
-                * @param       mixed           types: an array of nodeNames, or a nodeName
+                * @param       array           types: an array of nodeNames
                 *
                 * @return      object          the found ancestor of one of the given types or null
                 */
                getFirstAncestorOfType: function (node, types) {
                        var ancestor = null,
                                parent = node;
-                       if (typeof types !== 'undefined' && types.length > 0) {
-                               if (typeof types === 'string') {
+                       if (!Ext.isEmpty(types)) {
+                               if (Ext.isString(types)) {
                                        var types = [types];
                                }
                                types = new RegExp( '^(' + types.join('|') + ')$', 'i');
@@ -243,7 +292,7 @@ HTMLArea.DOM = function () {
                 hasAllowedAttributes: function (node, allowedAttributes) {
                        var value,
                                hasAllowedAttributes = false;
-                       if (typeof allowedAttributes === 'string') {
+                       if (Ext.isString(allowedAttributes)) {
                                allowedAttributes = [allowedAttributes];
                        }
                        allowedAttributes = allowedAttributes || [];
@@ -310,7 +359,7 @@ HTMLArea.DOM = function () {
                        var rangeIntersectsNode = false,
                                ownerDocument = node.ownerDocument;
                        if (ownerDocument) {
-                               if (HTMLArea.UserAgent.isIEBeforeIE9) {
+                               if (HTMLArea.isIEBeforeIE9) {
                                        var nodeRange = ownerDocument.body.createTextRange();
                                        nodeRange.moveToElementText(node);
                                        rangeIntersectsNode = (range.compareEndPoints('EndToStart', nodeRange) == -1 && range.compareEndPoints('StartToEnd', nodeRange) == 1) ||
@@ -320,7 +369,7 @@ HTMLArea.DOM = function () {
                                        try {
                                                nodeRange.selectNode(node);
                                        } catch (e) {
-                                               if (HTMLArea.UserAgent.isWebKit) {
+                                               if (Ext.isWebKit) {
                                                        nodeRange.setStart(node, 0);
                                                        if (node.nodeType === HTMLArea.DOM.TEXT_NODE || node.nodeType === HTMLArea.DOM.COMMENT_NODE || node.nodeType === HTMLArea.DOM.CDATA_SECTION_NODE) {
                                                                nodeRange.setEnd(node, node.textContent.length);