Jump to content

How can I add <optgroup>s to a InputfieldSelectMultiple inside the page editor?


eelkenet
 Share

Recommended Posts

I have a very long list of pages inside a InputfieldSelectMultiple (InputfieldAsmSelect) dropdown, and I want to group the pages inside <optgroup>s, according to their template and some other features. But I can't get it to work.

First of all, I think I can't get this done by the available options inside the Inputfield configuration (would love to be wrong here!) so I have taken the route of hooking into the Inputfield's render option. There, I would need to loop through all the <option>s, wrap these in their relevant <optgroup>s and return the result.

1. So, I have done some testing, and made a proof of concept for myself on how to edit incoming HTML with a  <select>  in order to group its options inside <optgroup> tags.
This stand-alone bit of code works perfectly fine:

<?php

// Create a snippet of HTML
$html = "<p>
  <select>
    <option value='1'>Option #1</option>
    <option value='2'>Option #2</option>
    <option value='3'>Option #3</option>
    <option value='4'>Option #4</option>
  </select>
</p>";

// Create a DOMDocument object, and load the snippet
$dom = new \DOMDocument;
$dom->loadHTML($html);

// Select the first <select> element from the HTML
$select = $dom->getElementsByTagName("select")->item(0);

// Create two option groups
$optgroupEven = $dom->createElement("optgroup");
$optgroupEven->setAttribute("label", "This is the even group");

$optgroupOdd = $dom->createElement("optgroup");
$optgroupOdd->setAttribute("label", "This is the odd group");

// List through all the childnodes of the select
$options = $select->childNodes;
for ($i = $options->length - 1; $i >= 0; $i--) {

  // Get the current <option> inside the <select>
  $option = $options->item($i);

  // Some validation
  if ($option && $option->attributes->getNamedItem('value')) {

    // Remove from <select>
    $select->removeChild($option);

    // Prepend to odd or even optgroup
    // (Prepend because we list through the children in reverse)
    if ((int) $option->attributes->getNamedItem("value")->nodeValue % 2 === 0) {
      $optgroupEven->prepend($option);
    } else {
      $optgroupOdd->prepend($option);
    }
  }
}

// Add the groups to the <select>
$select->appendChild($optgroupOdd);
$select->appendChild($optgroupEven);

// Add another option to the <select>
$opt = $dom->createElement("option");
$opt->nodeValue = "Option B";
$select->appendChild($opt);

echo "<h2>Original:</h2>";
echo $html;

echo "<h2>Grouped:</h2>";
echo $dom->saveHTML();

This results in what I would expect:

310776944_Schermafbeelding2022-12-20om13_19_49.png.841b024b562632672d1f9159ed6d95b9.png

1670791916_Schermafbeelding2022-12-20om13_19_55.png.8859d2343bca94e5568c146712d9c1a1.png

Which proves the idea.

2. I also know I can edit the output inside the page editor by hooking into the Inputfield's render function:

<?php
$wire->addHookAfter("InputfieldAsmSelect(name=linked)::render", function ($event) {
  $html = $event->return;
  $event->return = $html . "<p>This gets added!</p>";
});

1193839022_Schermafbeelding2022-12-20om12_47_35.png.2e9a7fd9c0faeac8cac2c84ae1a3b51d.png

3. Trying to combine the two in a simple way without using <optgroup> also works. With the following bit of code I can easily add a new option to the Inputfield's <select>:

<?php

namespace ProcessWire;

$wire->addHookAfter("InputfieldAsmSelect(name=linked)::render", function ($event) {
  $html = $event->return;

  // Get the input field in order to sanitize new values
  /** @var Inputfield $input */
  $input = $event->object;

  // Create a DOMDocument object and load the html from the Inputfield
  $dom = new \DOMDocument;
  $dom->loadHTML($html);

  // Get the <select>
  $select = $dom->getElementsByTagName("select")->item(0);

  // Create a new <option>, set label and value
  $opt = $dom->createElement("option");
  $opt->nodeValue = $input->entityEncode("This is a test, let's add this `option` to the `select`");
  $opt->setAttribute("value", 1);

  // Append to the <select>
  $select->appendChild($opt);

  // Return the new HTML and add a paragraph to double prove this bit of code do its work
  $event->return = $dom->saveHTML() . "<p>This gets added!</p>";
});

This yields the full <select> with my new <option> as the last element:

1158663217_Schermafbeelding2022-12-20om13_04_56.jpg.9e8743fab0ee3dee852c70688cdb9b79.jpg

 

4. Now here is my issue: I can't get <optgroup>'s to show up in the select. When I adapt my code above just a little bit, to incorperate <optgroup>, it just does not show up.
The final paragraph does till get added though!

<?php

namespace ProcessWire;

$wire->addHookAfter("InputfieldAsmSelect(name=linked)::render", function ($event) {
  $html = $event->return;

  // Get the input field in order to sanitize new values
  /** @var Inputfield $input */
  $input = $event->object;

  // Create a DOMDocument object and load the html from the Inputfield
  $dom = new \DOMDocument;
  $dom->loadHTML($html);

  // Get the <select>
  $select = $dom->getElementsByTagName("select")->item(0);

  // Create a new <option>, set label and value
  $opt = $dom->createElement("option");
  $opt->nodeValue = $input->entityEncode("This is a test, let's add this `option` to the `select`");
  $opt->setAttribute("value", 1);

  // Create an <optgroup>
  $optgroup = $dom->createElement("optgroup");
  $optgroup->setAttribute("label", "This is the optgroup");
  
  // Add the option to the optgroup
  $optgroup->prepend($opt);

  // Add the optgroup to the select
  $select->appendChild($optgroup);

  // Return the new HTML and add a paragraph to double prove this bit of code do its work
  $event->return = $dom->saveHTML() . "<p>This gets added!</p>";
});

A final screenshot would be useless, the whole optgroup and the option itself are both invisible now.

Does anybody have an idea on how to get this to work?

(Please note: I don't want to render a flat list with labels like {template} - {title}, that works technically but is problematic from a UX point of view).

 

 

 

Link to comment
Share on other sites

9 hours ago, eelkenet said:

2. I also know I can edit the output inside the page editor by hooking into the Inputfield's render function:

<?php
$wire->addHookAfter("InputfieldAsmSelect(name=linked)::render", function ($event) {
  $html = $event->return;
  $event->return = $html . "<p>This gets added!</p>";
});

I'd have to look into your issue in detail but as a quick solution (maybe neither the best nor the most beautiful 😄 ) have you thought of just replacing the whole markup with the desired one instead of appending something? Should be quite easy to do and should not really hurt I guess.

Link to comment
Share on other sites

@eelkenet, unfortunately I think you have a difficult road ahead if the optgroups are really important to your use case. Probably you would need to create your own custom inputfield using something like Selectize or Select2.

It's a shame, but AsmSelect doesn't support optgroups. It's not adding the optgroups to the underlying select markup that's the big problem, it's that AsmSelect doesn't account for optgroups when it builds the "fake" select from the underlying hidden select element. There was a pull request to add optgroup support the standalone jquery-asmSelect back in 2015 but it was never merged into either the standalone version or the InputfieldAsmSelect included in the PW core.

You might think that Selectize would be a simple solution because it supports optgroups out of the box and PW already provides an API for Selectize inputfields via InputfieldTextTags. But unfortunately no optgroup support was included in InputfieldTextTags because the addTag() and addOption() methods only support individual options without the ability to add options in a group as per InputfieldSelect::addOption(). And as far as I know you can't do something like supply the entire select markup to InputfieldTextTags.

P.S. the title of the topic is bit confusing because InputfieldSelectMultiple is a different thing to InputfieldAsmSelect. InputfieldSelectMultiple does support optgroups - you can add them via InputfieldSelectMultiple::addOption().

Link to comment
Share on other sites

Thanks for your replies! After @bernhard's suggestion I tried returning a completely custom bit of <select><optgroup><option ...></option><optgroup></select>.. but I would not render at all. So after all of this, it seems that asmSelect completely ignores the returned <select> from the function and replaces it with its own version. So there I bumped into what @Robin S wrote:

12 hours ago, Robin S said:

It's a shame, but AsmSelect doesn't support optgroups. It's not adding the optgroups to the underlying select markup that's the big problem, it's that AsmSelect doesn't account for optgroups when it builds the "fake" select from the underlying hidden select element. There was a pull request to add optgroup support the standalone jquery-asmSelect back in 2015 but it was never merged into either the standalone version or the InputfieldAsmSelect included in the PW core.

So yeah.. clearly my idea will never work, unless Ryan decides to suddenly merge a request from 7 years ago on a project which hasn't been updated in 10 years.

For now I've decided to leave it, and use InputfieldTextTags instead, which seems a bit more friendly visually. No optgroups there either, and no idea on how I would implement that either.

Bummer!

Link to comment
Share on other sites

Update: I have, in a way, accomplished my goals! I have not been successful at adding any <optgroup>s, BUT I did figure out a way around my issue.
Instead of adding the unsupported optgroups, I just inject extra, disabled <option>s. These options can get a label, so I can use them as sort of dividers between the rows of pages

Currently I group them by template name, as the selector for the field has that as primary sorting anyway.

Pretty simple.. as always with these things, once you figure it out
 

<?php

$wire->addHookAfter("InputfieldAsmSelect(name=linked)::render", function ($event) {
  $html = $event->return;

  // Get the input field in order to sanitize new values
  /** @var Inputfield $input */
  $input = $event->object;

  // Create a DOMDocument object and load the html from the Inputfield
  $dom = new \DOMDocument;
  $dom->loadHTML($html);

  // Get the <select> and its childnodes
  $select = $dom->getElementsByTagName("select")->item(0);
  $options = $select->childNodes;

  // Keep track of the previous template name..
  $prevTemplate = '';

  //Loop through the childnodes
  for ($i = 0; $i < $options->length - 1; $i++) {
    $option = $options->item($i);

    if ($option && $option->attributes->getNamedItem('value')) {

      // Get the page onject
      $pageId = (int) $option->attributes->getNamedItem('value')->nodeValue;
      $page = $this->pages->get($pageId);

      // If a new template label is found..
      if ($prevTemplate !== $page->template->label) {

        // Create a new <option>
        $newOption = $dom->createElement("option");
        $newOption->setAttribute("disabled", true);
        $newOption->nodeValue = $input->entityEncode(mb_strtoupper($page->template->label));

        // And simply insert it above the current <option>
        $select->insertBefore($newOption, $option);
        $prevTemplate = $page->template->label;
      }
    }
  }

  // Return the new HTML and add a paragraph to double prove this bit of code do its work
  $event->return = $dom->saveHTML();
});

I'd love to be able to add things like icons, and some more formatting.. but for now: pretty decent.

 

  • Like 2
Link to comment
Share on other sites

@eelkenet, cool that you were able to find a solution!

Here's another way you could add disabled section items into the Page Reference AsmSelect using the PW API. It avoids getting each page individually in a loop so probably a bit more efficient.

// Add dummy items to the selectable pages to act as section headings
$wire->addHookAfter('InputfieldPage::getSelectablePages', function(HookEvent $event) {
	/** @var InputfieldPage $inputfield */
	$inputfield = $event->object;
	$field = $inputfield->hasField;
	$selectable = $event->return;
	$t = null;
	$i = 1;
	if($field && $field->name === 'my_page_reference') {
		foreach($selectable as $p) {
			if($p->template->name !== $t) {
				$section = new NullPage();
				$section->id = "section{$i}";
				$section->title = mb_strtoupper($p->template->label) . ' ––––––––––––––––––––';
				$selectable->insertBefore($section, $p);
				$t = $p->template->name;
				$i++;
			}
		}
		$event->return = $selectable;
	}
});

// Set the dummy options to disabled
$wire->addHookAfter('InputfieldAsmSelect::renderReadyHook', function(HookEvent $event) {
	/** @var InputfieldAsmSelect $inputfield */
	$inputfield = $event->object;
	$field = $inputfield->hasField;
	if($field && $field->name === 'my_page_reference') {
		foreach($inputfield->options as $value => $label) {
			if(substr($value, 0, 7) === 'section') {
				$inputfield->addOptionAttributes($value, ['disabled' => 'disabled']);
			}
		}
	}
});

 

  • Like 2
  • Thanks 1
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...