Manual sorting of plucked tags

I’m using several tags on my “product” pages. I display the tags of these pages on the parent page like this:

$tags = $page->children()->visible()->pluck('tag', ';', true);
// sort($tags);

As you can see I previously sorted the tags alphabetically with the sort() function. That worked great until my client wanted to sort them manually. I really want to keep the flexibility of adding or removing tags in the panel, but I can’t think of a solution how to sort them after adding them.
Ideally, the parent page would display a live updated list of all the tags as draggable items in the panel. Since Kirby 2.3.0 we can get the content of a single tag field by options: field, but I

  1. want to get the content from multiple fields. Just like the ->pluck() method
  2. can’t “import” the tags into a structure field. These are draggable but not supported by the options: field function

Would this be possible with a custom field somehow? Or is there a simpler solution I’ve previously overlooked?

Thank you!

1 Like

How about using a hook to append newly added tags to the parent structure field?

That’s actually a really good idea, I’ve never used hooks aside from testing the functionality. I’m going to try that right now, thanks @texnixe!

For anyone having the same problem/idea:

Here’s my hook, I added comments so you can understand it better and adapt it to your needs/fields:

kirby()->hook('panel.page.update', function($page) {

  // Is there a "tag" field?
  if(isset($page->content()->data['tag'])) {

    // Parent page
    $parent = $page->parent();

    // Split tags
    $tags = $page->content()->get('tag')->split(";");

    // Is there a "tags" field on the parent page?
    if(isset($parent->content()->data['tags'])) {

      // Existing tags on the parent page
      $oldtags = $parent->content()->get('tags')->yaml();

      // For every tag on the updated page
      foreach($tags as $tag) {

        $n = 0;
        
        // For every existing tag on the parent page
        foreach($oldtags as $oldtag) {

          // If the tag already exists
           if($oldtag["name"] == $tag) $n++;

        }

        // If the tag doesn't exist yet
        if($n == 0) {

          // Add it to the tags array
          $oldtags[] = ['name' => $tag];
          
        }

      }

      // Update parent page
      $parent->update(array(
        'tags' => yaml::encode($oldtags)
      ));

    }
  }
});

The structure field responsible for the sorting looks like this in the blueprint:

label: Tag Order
type:  structure
fields:
  name:
    label:  Name
    type:   text

I added the following to my “panel.css” to hide the “Add” button and the “Edit” and “Delete” buttons of the structure field:

.field-name-tags .structure-entry-options, .field-name-tags .structure-add-button {
  display: none;
}
.field-name-tags .structure-entry-content {
  border-bottom: 0;
}

Everything works fine but the “loading bar” in the panel moves very slowly, for about 20 seconds. The field is updated in less than a second, but the panel seems to wait for something. If you click on something or reload the panel, the loading bar disappears. It seems like a similar issue like this one. I’m going to disable each my plugins/fields later today and see if one of them causes this “snail loading bar”.

I’d try to use an if statement at the very beginning to exit the hook when the parent page is updated in order to prevent a loop, don’t know if that helps, though, because you are already checking if the tag field exists …

I’ve added if($page->intendedTemplate() == "single") { to the very beginning. The parent has a different template, but it still hangs. I’ve also searched for panel.page.update across all of my plugins and fields. No match.

Maybe it’s a different issue/bug.

Probably, I just tested your hook on a fresh starterkit, it works perfectly fine without any issues.

I think the code could make use of array_diff() instead of going through the loop with an if-statement:

Edit: Code deleted because it somehow duplicated the tags …

The only problem that currently remains is that the hook does not update the parent tags in case a tag is removed. So maybe it would be better to always compare all tags of all children to the tags in the parent page?

Oh man, this whole “hooks” thing opened a can of worms. I can’t figure out why the loading bar is stuck every time I hit save.

I removed everything from my blueprints except for the tag field and the tags structure field. No change.

I changed the $parent variable to the $site and added the structure field there because I thought it may trigger the child to “update” when the parent is updated. But if I then hit save nothing happens.

I also tried to pluck all of the siblings tags in the $tags variable to compare all tags of all children to the tags in the parent page, just how you suggested. But that didn’t work at all. I’m not sure if the pluck() function is even possible in a hook, probably not.

$tags = $page->siblings()->visible()->content()->pluck('tag', ';', true)->split(";");

Maybe I should just sort it alphabetically or hardcode the tags and tell the client it’s not possible. It’s a disproportionate effort.

EDIT:

Yes!! I’ve figured it out. After a lot of trial and error I finally found the bug. @texnixe If you add c::set('debug', true); to your config.php the loading bar bug should appear. Could you confirm that on your installation? Thank you!

Yes, you are right, as soon as I enable debug, I have the same problem.

BTW, I finally came up with this code, but using the list field by @timoetting instead of the structure field:

kirby()->hook('panel.page.update', function($page) {
    // exit the hook if the page is the parent page (in this example, blog) to prevent an update loop
    if($page->is(page('blog'))) { exit; }

  // Is there a "tag" field?
  if(isset($page->content()->data['tag'])) {

    // Parent page
    $parent = $page->parent();

    // find all tags
    $alltags = $parent->children()->pluck('tag', ',', true);

    // Is there a "tags" field on the parent page?
    if(isset($parent->content()->data['tags'])) {
      // Existing tags on the parent page
      $oldtags = $parent->content()->get('tags')->yaml();

      // find the elements to remove
      $remove = array_diff($oldtags, $alltags);

      // remove them
      foreach($remove as $r) {
        $key = array_search($r,$oldtags);
        if($key!==false){
          unset($oldtags[$key]);
        }
      }

      //find the elements in $alltags that are not in $oldtags
      $difference = array_diff($alltags, $oldtags);
      // Loop through these tags
      foreach($difference as $tag) {

        // and add them to the tags array
        $oldtags[] = $tag;

      }

    }

    // Update parent page
    $parent->update(array(
      'tags' => yaml::encode($oldtags)
    ));

  }
});

I added this bit on the first line of the hook to prevent the loading bar issue, when debug is active:

  if($page->is(page('blog'))) { exit; }

Awesome! I think there are two issues, tough:

  1. The list field is editable. If you edit a tag, the relevant children will not be updated.
  2. On every update, the order is reset.

…harder than it seems :sweat:

I think there’s no way around the inelegant if’s and foreach’s I used in the initial hook. I’m trying to add a “delete tag if nowhere to be found” function.

Yes, unfortunately.

The first can be solved by modifying the list field itself. Replace line 40 of list.php

// remove this line
$input->addClass('input-is-readonly');
// replace with this
$input->attr('readonly', 'readonly');

and set field to readonly: true. This then still allows for the field to be sorted but it cannot be updated anymore.

The second is the more important, though, because manual sort and preserving that sort order, is the whole purpose of having this field. I guess there is a solutions as well …

Ah, that’s an idea. Better than the structure field CSS solution, it felt a bit hacky.

I updated the hook to delete tags that don’t exist anymore:

kirby()->hook('panel.page.update', function($page) {

  // Is there a "tag" field?
  if(isset($page->content()->data['tag'])) {

    // Parent page
    $parent = $page->parent();

    // find all tags
    $tags = $parent->children()->visible()->pluck('tag', ';', true);

    // Is there a "tags" field on the parent page?
    if(isset($parent->content()->data['tags'])) {

      // Existing tags on the parent page
      $oldtags = $parent->content()->get('tags')->yaml();

      // For every tag on the updated page
      foreach($tags as $tag) {

        $n = 0;

        // For every existing tag on the parent page
        foreach($oldtags as $oldtag) {

          // If the tag already exists
           if($oldtag == $tag) $n++;

        }

        // If the tag doesn't exist yet
        if($n == 0) {

          // Add it to the tags array
          $oldtags[] = $tag;

        }

      }

      // For every tag in the new taglist
      foreach($oldtags as $key => $oldtag) {

        $n = 0;
        // For every found tag
        foreach($tags as $tag) {

          // If the tag exists
          if($oldtag == $tag) $n++;

        }

        // If tag exist
        if($n == 0) {

          // Remove it from the tags array
          unset($oldtags[$key]);

        }

      }

      // Update parent page
      $parent->update(array(
        'tags' => yaml::encode($oldtags)
      ));

    }

  }

});

It’s a lot of stuff, and can probably be written in half the lines and way more efficient. But it works for me.

Thanks again @texnixe!

1 Like

I removed a leftover-from-old-code-error from my code above; now for me it seems to be working without changing the order of the items in the parent page … And it’s much shorter.