diff --git a/README.txt b/README.txt index 770986a..b92f791 100644 --- a/README.txt +++ b/README.txt @@ -19,13 +19,15 @@ gets triggered on the accordion element. INSTALLATION: -------- -1. Install & Enable the module -2. Open Administration > Configuration > Content authoring > +1. Download external library from https://github.com/smillart/WAI-ARIA-Patterns-And-Widgets. +2. Place the library in the root libraries folder (/libraries). +3. Install & Enable the module +4. Open Administration > Configuration > Content authoring > Text formats and editors (admin/config/content/formats) -3. Edit a text format's settings (usually Basic HTML) -4. Drag n Drop the Add Accordion -button to the toolbar to show it to the users -5. Scroll down to the bottom to the input Allowed HTML tags -6. Find and replace
with
+5. Edit a text format's settings (usually Basic HTML) +6. Drag n Drop the Add Accordion -button to the toolbar to show it to the users +7. Scroll down to the bottom to the input Allowed HTML tags +8. Find and replace
with
This ensures CKEditor doesn't remove the class name that the accordion uses. -7. If you would like the Accordion tabs to be closed by default, +9. If you would like the Accordion tabs to be closed by default, you can change this setting at: /admin/config/content/ckeditor-accordion diff --git a/ckeditor_accordion.api.php b/ckeditor_accordion.api.php new file mode 100644 index 0000000..1b30644 --- /dev/null +++ b/ckeditor_accordion.api.php @@ -0,0 +1,32 @@ +get('collapse_all'); diff --git a/ckeditor_accordion.services.yml b/ckeditor_accordion.services.yml new file mode 100644 index 0000000..58fd86e --- /dev/null +++ b/ckeditor_accordion.services.yml @@ -0,0 +1,4 @@ +services: + plugin.manager.ckeditor_accordion_variant: + class: Drupal\ckeditor_accordion\Plugin\CkeditorAccordionVariantManager + parent: default_plugin_manager diff --git a/config/install/ckeditor_accordion.settings.yml b/config/install/ckeditor_accordion.settings.yml new file mode 100644 index 0000000..6b1304f --- /dev/null +++ b/config/install/ckeditor_accordion.settings.yml @@ -0,0 +1,3 @@ +collapse_all: 0 +variant: 'ckeditor_accordion_variant_default' +langcode: 'en' diff --git a/css/ckeditor-accessible-accordion.css b/css/ckeditor-accessible-accordion.css new file mode 100644 index 0000000..25c28e7 --- /dev/null +++ b/css/ckeditor-accessible-accordion.css @@ -0,0 +1,105 @@ +/* + Accessible Accordion style definition. +*/ + +.aria-accordion { + border: 1px solid #0065a3; +} +.aria-accordion .aria-accordion__heading { + margin: 0; + padding: 0; +} +.aria-accordion .aria-accordion__heading button { + display: block; + width: 100%; + color: #ffffff; + text-align: left; + margin: 0; + padding: 10px 15px 10px 35px; + cursor: pointer; + background: #007bb2; + border: 0; + border-bottom: 1px solid #0072a5; + border-radius: 0; + appearance: none; + box-shadow: 0 0 0 transparent; + transition: background-color 0.3s; + position: relative; + z-index: 1; + font-size: 1rem; +} +.aria-accordion .aria-accordion__heading:last-of-type button { + border-bottom: 0; +} +.aria-accordion .aria-accordion__heading button:before, +.aria-accordion .aria-accordion__heading button:after { + content: ''; + display: block; + width: 2px; + height: 10px; + margin: -5px 0 0 5px; + position: absolute; + top: 50%; + background: #ffffff; + transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} +.aria-accordion .aria-accordion__heading button:before { + left: 7px; + transform: rotate(135deg); +} +.aria-accordion .aria-accordion__heading button:after { + left: 14px; + transform: rotate(-135deg); +} +.aria-accordion .aria-accordion__heading button:hover, +.aria-accordion .aria-accordion__heading button:focus { + background-color: #0073a7; +} +.aria-accordion .aria-accordion__heading button[aria-expanded="true"] { + background-color: #004772; +} +.aria-accordion .aria-accordion__heading button[aria-expanded="true"]:before { + left: 7px; + transform: rotate(45deg); +} +.aria-accordion .aria-accordion__heading button[aria-expanded="true"]:after { + left: 14px; + transform: rotate(-45deg); +} +.aria-accordion .aria-accordion__panel { + margin: 0; + padding: 0 15px; +} +.aria-accordion .aria-accordion__panel--transition[hidden] { + display: block; + height: auto; + max-height: 0; + overflow: hidden; + -webkit-transition: max-height 800ms ease-out; + -moz-transition: max-height 800ms ease-out; + -ms-transition: max-height 800ms ease-out; + -o-transition: max-height 800ms ease-out; + transition: max-height 800ms ease-out; + visibility: visible; + /* persist visibility value from animation */ + animation: 800ms delay-visibility; +} +.aria-accordion .aria-accordion__panel--transition { + display: block; + max-height: 1000px; + visibility: visible; + -webkit-transition: max-height 800ms ease-in; + -moz-transition: max-height 800ms ease-in; + -ms-transition: max-height 800ms ease-in; + -o-transition: max-height 800ms ease-in; + transition: max-height 800ms ease-in; + overflow: visible; + /* persist overflow value from animation */ + animation: 800ms delay-overflow; +} +@keyframes delay-visibility { + from { visibility: visible; } +} +@keyframes delay-overflow { + from { overflow: hidden; } +} diff --git a/src/Annotation/CkeditorAccordionVariant.php b/src/Annotation/CkeditorAccordionVariant.php new file mode 100644 index 0000000..0f35659 --- /dev/null +++ b/src/Annotation/CkeditorAccordionVariant.php @@ -0,0 +1,43 @@ + $config->get('keep_rows_open') ?: 0, ]; + $variant_options = []; + foreach (\Drupal::service('plugin.manager.ckeditor_accordion_variant')->getDefinitions() as $variant_id => $values) { + $variant_options[$values['id']] = Html::escape($values['label']->render() . ': ' . $values['description']->render()); + } + + $form['variant'] = [ + '#type' => 'radios', + '#title' => $this->t('Variant'), + '#description' => $this->t('Select the variant to be used.'), + '#options' => $variant_options, + '#default_value' => $config->get('variant'), + ]; + return parent::buildForm($form, $form_state); } @@ -58,6 +72,7 @@ class CkeditorAccordionSettingsForm extends ConfigFormBase { $config->set('collapse_all', $values['collapse_all']); $config->set('keep_rows_open', $values['keep_rows_open']); + $config->set('variant', $values['variant']); $config->save(); parent::submitForm($form, $form_state); diff --git a/src/Plugin/CkeditorAccordionVariant/CkeditorAccordionVariantDefault.php b/src/Plugin/CkeditorAccordionVariant/CkeditorAccordionVariantDefault.php new file mode 100644 index 0000000..33ec0d9 --- /dev/null +++ b/src/Plugin/CkeditorAccordionVariant/CkeditorAccordionVariantDefault.php @@ -0,0 +1,41 @@ +

style tags."), + * ) + */ +class CkeditorAccordionVariantDefault extends CkeditorAccordionVariantBase{ + + /** + * {@inheritdoc} + */ + protected $list_tag = [ + 'tag' => 'div', + 'attributes' => [], + ]; + + /** + * {@inheritdoc} + */ + protected $title_tag = [ + 'tag' => 'h2', + 'attributes' => [], + ]; + + /** + * {@inheritdoc} + */ + protected $description_tag = [ + 'tag' => 'div', + 'attributes' => [], + ]; + +} diff --git a/src/Plugin/CkeditorAccordionVariantBase.php b/src/Plugin/CkeditorAccordionVariantBase.php new file mode 100644 index 0000000..5442a8c --- /dev/null +++ b/src/Plugin/CkeditorAccordionVariantBase.php @@ -0,0 +1,79 @@ + '', + 'attributes' => [], + ]; + + /** + * The title tag to be used. + * + * @var array + */ + protected $title_tag = [ + 'tag' => '', + 'attributes' => [], + ]; + + /** + * The description tag to be used. + * + * @var array + */ + protected $description_tag = [ + 'tag' => '', + 'attributes' => [], + ]; + + /** + * Retrieves the CKEditor Accordion variant definition. + * + * @return array + * An associative array containing tag and optional attributes for each type + * of accordion's element (list, title, description): + * - list + * - tag + * - attributes + * - class + * - class_name_1 + * - class_name_2 + * - ... + * - ... + * - title + * - tag + * - attributes + * - description + * - tag + * - attributes + */ + public function getVariantDefinition() { + $definition = [ + 'list' => $this->list_tag, + 'title' => $this->title_tag, + 'description' => $this->description_tag, + ]; + + // Allow other modules to alter the variant definition. + $context = [ + 'variant_id' => $this->getBaseId(), + ]; + \Drupal::moduleHandler()->alter('ckeditor_accordion_variant', $definition, $context); + + return $definition; + } + +} diff --git a/src/Plugin/CkeditorAccordionVariantInterface.php b/src/Plugin/CkeditorAccordionVariantInterface.php new file mode 100644 index 0000000..7d76c89 --- /dev/null +++ b/src/Plugin/CkeditorAccordionVariantInterface.php @@ -0,0 +1,15 @@ +alterInfo('ckeditor_accordion_ckeditor_accordion_variant_info'); + $this->setCacheBackend($cache_backend, 'ckeditor_accordion_ckeditor_accordion_variant_plugins'); + } + +} diff --git a/src/Plugin/Filter/CKEditorAccordion.php b/src/Plugin/Filter/CKEditorAccordion.php new file mode 100644 index 0000000..49d4965 --- /dev/null +++ b/src/Plugin/Filter/CKEditorAccordion.php @@ -0,0 +1,145 @@ +createInstance($config->get('variant')); + $defintion = $variant->getVariantDefinition(); + + $tags = [ + 'intial' => [ + 'list' => [ + 'tag' => 'dl', + ], + 'title' => [ + 'tag' => 'dt', + ], + 'description' => [ + 'tag' => 'dd', + ], + ], + 'target' => $defintion, + ]; + + // Add the needed attributes for the accessible script. + $tags['target']['list']['attributes']['data-aria-accordion'] = 'data-aria-accordion'; + $tags['target']['list']['attributes']['data-aria-accordion-allow-toggle'] = 'data-aria-accordion-allow-toggle'; + $tags['target']['list']['attributes']['data-aria-accordion-panel-transition'] = 'data-aria-accordion-panel-transition'; + $tags['target']['title']['attributes']['data-aria-accordion-heading'] = 'data-aria-accordion-heading'; + $tags['target']['description']['attributes']['data-aria-accordion-panel'] = 'data-aria-accordion-panel'; + + if (!$config->get('collapse_all')) { + $tags['target']['list']['attributes']['data-aria-accordion-open-default'] = ''; + } + if ($config->get('keep_rows_open')) { + $tags['target']['list']['attributes']['data-aria-accordion-allow-multiple'] = 'data-aria-accordion-allow-multiple'; + } + + $initial_class_name = 'ckeditor-accordion'; + + // Load the text into a dom object. + $dom = new \DOMDocument(); + $dom->loadHTML(mb_convert_encoding($text, 'HTML-ENTITIES', 'UTF-8')); + $xpath = new \DOMXPath($dom); + + // Find and replace title tags. + foreach ($xpath->query("//" . $tags['intial']['list']['tag'] . "[contains(@class, '$initial_class_name')]/" . $tags['intial']['title']['tag']) as $title) { + // Create the new title tag. + $this->replaceElementTag($dom, $title, $tags['target']['title']['tag'], $tags['target']['title']['attributes']); + } + + // Find and replace description tags. + foreach ($xpath->query("//" . $tags['intial']['list']['tag'] . "[contains(@class, '$initial_class_name')]/" . $tags['intial']['description']['tag']) as $description) { + // Create the new description tag. + $this->replaceElementTag($dom, $description, $tags['target']['description']['tag'], $tags['target']['description']['attributes']); + } + + // Find and replace list tags. + foreach ($xpath->query("//" . $tags['intial']['list']['tag'] . "[contains(@class, '$initial_class_name')]") as $accordion) { + // Remove the legacy class to avoid conflicts. + $classes = explode(' ', $accordion->getAttribute('class')); + if (($key = array_search($initial_class_name, $classes)) !== FALSE) { + unset($classes[$key]); + } + empty($classes) ? $accordion->removeAttribute('class') : $accordion->setAttribute('class', implode(' ', $classes)); + + // Create the new list tag. + $this->replaceElementTag($dom, $accordion, $tags['target']['list']['tag'], $tags['target']['list']['attributes']); + } + + $new_text = $dom->saveXml($dom->documentElement); + + return new FilterProcessResult($new_text); + } + + /** + * Replaces an element's tag with a new one. + * + * @param \DOMDocument $dom + * The DOMDocument the element is attached to. + * + * @param \DOMElement $element + * The element for which the tag needs to be replaced. + * + * @param string $new_tag + * The new tag to be used. + * + * @param array $new_attributes + * The attributes to be added to the new tag as key => value. + * + * @param bool $preserve_attributes + * Wether to keep attributes from the given element in the new one. + */ + protected function replaceElementTag(\DOMDocument &$dom, \DOMElement $element, string $new_tag, array $new_attributes = [], $preserve_attributes = TRUE) { + $new_element = $dom->createElement($new_tag); + + // Add new attributes to the new element. + foreach ($new_attributes as $attribute => $value) { + if (is_array($value)) { + $value = implode(' ', $value); + } + $new_element->setAttribute($attribute, $value); + } + + if ($preserve_attributes) { + // Add all attributes from the intial element. + foreach ($element->attributes as $attribute) { + $value = $attribute->nodeValue; + + // Make sure we keep the intial attribute value if one exists. + if (!empty($new_element->getAttribute($attribute->nodeName))) { + $value = implode(' ', [$new_element->getAttribute($attribute->nodeName), $attribute->nodeValue]); + } + + $new_element->setAttribute($attribute->nodeName, $value); + } + } + + // Move the children of the current node to the new one + while ($element->hasChildNodes()) { + $new_element->appendChild($element->firstChild); + } + + // Replace the current element with the new one. + $element->parentNode->replaceChild($new_element, $element); + } +}