Subversion Repositories web.active

Rev

Rev 1 | Blame | Compare with Previous | Last modification | View Log | Download

<?php namespace ProcessWire;

/**
 * ProcessWire FieldtypeFieldsetOpen and InputfieldFieldsetOpen
 *
 * Provides a Fieldtype and Inputfield for opening a Fieldset. 
 *
 * For documentation about the fields used in this class, please see:  
 * /wire/core/Fieldtype.php
 * 
 * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
 * https://processwire.com
 *
 *
 */

class InputfieldFieldsetOpen extends InputfieldWrapper { } 

class FieldtypeFieldsetOpen extends Fieldtype {

  /**
   * Appended to the name of a 'Close' version of a FieldsetOpen
   *
   */
  const fieldsetCloseIdentifier = '_END';

  public static function getModuleInfo() {
    return array(
      'title' => 'Fieldset (Open)',
      'version' => 101,
      'summary' => 'Open a fieldset to group fields. Should be followed by a Fieldset (Close) after one or more fields.',
      'permanent' => true, 
      );
  }

  /**
   * Setup hooks to minotor when Fields are added and deleted
   *
   * But only do it once so that we don't have the same hooks setup when there are multiple Fieldsets
   *
   */
  public function init() {
    if($this->process == 'ProcessField' || $this->process = 'ProcessTemplate') {
      $this->addProcessHooks();
    }
  }

  /**
   * Add hooks to ProcessField or ProcessTemplate, called only when applicable
   * 
   */
  protected function addProcessHooks() {
    $this->addHook('ProcessField::fieldAdded', $this, 'hookFieldAdded');
    $this->addHook('ProcessField::fieldDeleted', $this, 'hookFieldDeleted');
    $this->addHook('ProcessField::allowFieldInTemplate', $this, 'hookAllowFieldInTemplate');
    $this->addHook('ProcessTemplate::fieldAdded', $this, 'hookTemplateFieldAdded');
    $this->addHook('ProcessTemplate::fieldRemoved', $this, 'hookTemplateFieldRemoved'); 
  }

  public function sanitizeValue(Page $page, Field $field, $value) {
    return null;
  }

  public function getInputfield(Page $page, Field $field) {
    /** @var InputfieldFieldsetOpen $inputfield */
    $inputfield = $this->wire(new InputfieldFieldsetOpen());
    $inputfield->class = $this->className() . ' InputfieldFieldset';
    return $inputfield; 
  }

  public function savePageField(Page $page, Field $field) {
    return true; 
  }

  public function ___getConfigInputfields(Field $field) {
    $inputfields = parent::___getConfigInputfields($field);
    return $inputfields; 
  }

  public function ___getConfigAdvancedInputfields(Field $field) {
    $inputfields = parent::___getConfigAdvancedInputfields($field); 
    // these really aren't applicable with fieldsets, so remove them
    $inputfields->remove($inputfields->get('autojoin')); 
    $inputfields->remove($inputfields->get('global')); 
    return $inputfields; 
  }

  public function ___getCompatibleFieldtypes(Field $field) {
    $fieldtypes = $this->wire(new Fieldtypes());
    foreach($this->wire()->fieldtypes as $fieldtype) {
      if($fieldtype instanceof FieldtypeFieldsetOpen) $fieldtypes->add($fieldtype); 
    }
    return $fieldtypes; 
  }

  public function loadPageField(Page $page, Field $field) {
    return '';
  }

  public function getLoadQuery(Field $field, DatabaseQuerySelect $query) {
    return $query;
  }
  
  /**
   * Is given field a fieldset opener?
   * 
   * For hooks to share in determining if this is a field they want to operate on
   * 
   * @param Field $field
   * @return bool
   *
   */
  protected function isFieldset(Field $field) {
    return $field->type instanceof FieldtypeFieldsetOpen && !($field->type instanceof FieldtypeFieldsetClose);
  }

  /**
   * Check that Fieldgroup has matching open/close fieldsets and in correct order
   * 
   * @param Fieldgroup $fieldgroup
   * @return bool
   * @since 3.0.164
   * 
   */
  public function checkFieldgroupFieldsets(Fieldgroup $fieldgroup) {
    
    list($openers, $closers, $isChanged) = array(array(), array(), false);

    foreach($fieldgroup as $field) {
      /** @var Field $field */
      /** @var Fieldtype $fieldtype */
      $fieldtype = $field->type;
      if(!$fieldtype instanceof FieldtypeFieldsetOpen) continue;
      if($fieldtype instanceof FieldtypeFieldsetClose) {
        if(!strpos($field->name, self::fieldsetCloseIdentifier)) continue;
        $name = substr($field->name, 0, -1 * strlen(self::fieldsetCloseIdentifier));
        $closers[$name] = $field;
      } else if(!isset($closers[$field->name])) {
        // ensure opener comes before closer, otherwise only closer remains
        $openers[$field->name] = $field;
      }
    }

    if(count($openers) === count($closers)) return true;

    foreach($openers as $name => $opener) {
      /** @var Field $opener */
      if(isset($closers[$name])) continue;
      $closer = $this->getFieldsetCloseField($opener);
      if($closer) {
        $fieldgroup->insertAfter($closer, $opener);
      } else {
        $fieldgroup->remove($opener);
      }
      $isChanged = true;
    }
    
    foreach($closers as $name => $closer) {
      /** @var Field $closer */
      if(isset($openers[$name])) continue;
      $opener = $this->getFieldsetOpenField($closer); 
      if($opener) {
        $fieldgroup->insertBefore($opener, $closer); 
      } else {
        $fieldgroup->remove($closer);
      }
      $isChanged = true;
    }
    
    return $isChanged;
  }

  /**
   * Get the Field that closes/terminates the given Fieldset field
   * 
   * @param Field $field
   * @param bool $createIfNotExists Create it if it doesn't already exist?
   * @return null|Field of type FieldtypeFieldsetClose
   * 
   */
  public function getFieldsetCloseField(Field $field, $createIfNotExists = false) {
    
    if(!$this->isFieldset($field)) return null;

    $fields = $this->wire()->fields;
    
    $name = $field->name . self::fieldsetCloseIdentifier;
    $closer = $fields->get($name); 
    
    if(!$closer) {
      $closeFieldID = (int) $field->get('closeFieldID');
      if($closeFieldID) {
        $closer = $fields->get($closeFieldID); 
      }
    }
    
    if(!$closer && $createIfNotExists) {
      /** @var Field $closer */
      $closer = $this->wire(new Field());
      $closer->type = $this->wire(new FieldtypeFieldsetClose());
      $closer->name = $name;
      $closer->label = "Close an open fieldset";
      $closer->description = 
        "This field was added automatically to accompany fieldset '$field'.  " . 
        "It should be placed in your template/fieldgroup wherever you want the fieldset to end.";
      $closer->set('openFieldID', $field->id); 
      $closer->save();
    } 
    
    if($closer && !$field->get('closeFieldID')) {
      $field->set('closeFieldID', $closer->id); 
      $field->save();
    }
    
    return $closer; 
  }
  
  /**
   * Get the Field that opens the given FieldsetClose field
   *
   * @param Field $field
   * @return null|Field of type FieldtypeFieldsetOpen, if found
   * @since 3.0.164
   *
   */
  public function getFieldsetOpenField(Field $field) {

    if(!$field->type instanceof FieldtypeFieldsetClose) return null;
    if(!strpos($field->name, self::fieldsetCloseIdentifier)) return null;
    
    $fields = $this->wire()->fields;

    $name = substr($field->name, 0, -1 * strlen(self::fieldsetCloseIdentifier));
    $opener = $fields->get($name);

    if($opener) return $opener;

    foreach($fields as $f) {
      /** @var Field $f */
      if(!$f->type instanceof FieldtypeFieldsetOpen) continue;
      $closeFieldID = (int) $f->get('closeFieldID');
      if($closeFieldID != $field->id) continue;
      $opener = $f;
      break;
    }

    return $opener;
  }

  /**
   * Hook executed when field is added via ProcessField
   * 
   * @param HookEvent $event
   *
   */
  public function hookFieldAdded($event) {
    $field = $event->arguments[0]; 
    if(!$this->isFieldset($field)) return; 
    $closer = $this->getFieldsetCloseField($field, true); 
    $this->message("Also added field '$closer', to accompany '$field'"); 
  }

  /**
   * Hook executed when field is deleted via ProcessField
   * 
   * @param HookEvent $event
   *
   */
  public function hookFieldDeleted($event) {

    $field = $event->arguments[0]; 
    if(!$this->isFieldset($field)) return; 

    $closer = $this->getFieldsetCloseField($field); 
    
    if($closer) {
      $this->wire('fields')->delete($closer);
      $this->message("Delete also issued for field '$closer'");
    }
  }

  /**
   * Hook executed when field is added to a template via ProcessTemplate
   * 
   * @param HookEvent $event
   *
   */
  public function hookTemplateFieldAdded($event) {
    /** @var Field $field */
    /** @var Template $template */
    list($field, $template) = $event->arguments; 
    if(!$this->isFieldset($field)) return; 
    $closer = $this->getFieldsetCloseField($field); 
    if(!$closer) return; 
    if(!$template->fieldgroup->hasField($closer)) {
      $template->fieldgroup->add($closer);
      $this->message("Also added field '$closer', which should be placed where you want to close fieldset '$field'");
    }
  }

  /**
   * Hook executed when field is removed from a template via ProcessTemplate
   * 
   * @param HookEvent $event
   *
   */
  public function hookTemplateFieldRemoved($event) {
    list($field, $template) = $event->arguments; 
    if(!$this->isFieldset($field)) return; 
    $closer = $this->getFieldsetCloseField($field); 
    if(!$closer) return; 
    $template->fieldgroup->remove($closer); 
    $this->message("Also removed '$closer', which accompanies '$field'"); 
  }

  /**
   * Hook to ProcessField::allowFieldInTemplate
   * 
   * @param HookEvent $event
   * 
   */
  public function hookAllowFieldInTemplate(HookEvent $event) { 
    /** @var Field $field */
    // $field = $event->arguments(0); 
    /** @var Template $template */
    // $template = $event->arguments(1);
  }

  /**
   * Hook called by Fields::save() after a field using this type has been renamed
   *
   * Note that PW already takes care of renaming the field_[name] table.
   * Most Fieldtypes don't need to do anything here, but this exists just in case.
   *
   * #pw-internal
   *
   * @param Field $field
   * @param string $prevName Previous name (current name can be found in $field->name)
   *
   */
  public function ___renamedField(Field $field, $prevName) {
    // avoid ending up in infinite loop, since FieldtypeFieldsetClose extends this
    if($this instanceof FieldtypeFieldsetClose) return;
    // rename the _END field to match this one
    $fields = $this->wire()->fields;
    $oldName = $prevName . self::fieldsetCloseIdentifier;
    $newName = $field->name . self::fieldsetCloseIdentifier;
    $closer = $this->getFieldsetCloseField($field); 
    if(!$closer) $closer = $fields->get($oldName);
    if($closer && $closer->name != $newName) {
      $closer->name = $newName;
      $fields->save($closer);
    }
    parent::___renamedField($field, $prevName); 
  }
}