Skip to content
Snippets Groups Projects
class.dynamic_forms.php 61.2 KiB
Newer Older
  • Learn to ignore specific revisions
  •         return $this->_form;
        }
    
        function getDynamicFields() {
            return $this->form->fields;
    
        function getMedia() {
            return $this->getForm()->getMedia();
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    
        function getFields() {
    
            if (!isset($this->_fields)) {
    
    Jared Hancock's avatar
    Jared Hancock committed
                $this->_fields = array();
    
                // Get all dynamic fields associated with the form
    
                // even when stored elsewhere -- important during validation
                foreach ($this->getDynamicFields() as $f) {
                    $f = $f->getImpl($f);
                    $this->_fields[$f->get('id')] = $f;
                    $f->isnew = true;
    
                // Include any other answers included in this entry, which may
                // be for fields which have since been deleted
    
                foreach ($this->getAnswers() as $a) {
    
                    $f = $a->getField();
                    $id = $f->get('id');
                    if (!isset($this->_fields[$id])) {
                        // This field is not currently on the associated form
                        $a->deleted = true;
                    }
                    $this->_fields[$id] = $f;
                    // This field has an answer, so it isn't new (to this entry)
                    $f->isnew = false;
    
    Jared Hancock's avatar
    Jared Hancock committed
            }
            return $this->_fields;
        }
    
    
        function filterFields($filter) {
            $this->getFields();
            foreach ($this->_fields as $i=>$f) {
                if ($filter($f))
                    unset($this->_fields[$i]);
            }
        }
    
    
        function getSource() {
            return $this->_source ?: (isset($this->id) ? false : $_POST);
        }
        function setSource($source) {
            $this->_source = $source;
    
            // Ensure the field is connected to this data source
            foreach ($this->getFields() as $F)
                if (!$F->getForm())
                    $F->setForm($this);
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getField($name) {
            foreach ($this->getFields() as $field)
                if (!strcasecmp($field->get('name'), $name))
                    return $field;
    
            return null;
        }
    
    
        /**
         * Validate the form and indicate if there no errors.
         *
         * Parameters:
         * $filter - (callback) function to receive each field and return
         *      boolean true if the field's errors are significant
    
         * $options - options to pass to form and fields.
         *
    
        function isValid($filter=false, $options=array()) {
    
    
    Jared Hancock's avatar
    Jared Hancock committed
            if (!is_array($this->_errors)) {
    
                $form = $this->getForm(false, $options);
                $form->isValid($filter);
                $this->_errors = $form->errors();
    
    Jared Hancock's avatar
    Jared Hancock committed
            return !$this->_errors;
        }
    
    
        function isValidForClient() {
            $filter = function($f) {
    
                return $f->isVisibleToUsers();
    
            return $this->isValid($filter);
        }
    
        function isValidForStaff() {
            $filter = function($f) {
    
                return $f->isVisibleToStaff();
    
            return $this->isValid($filter);
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getClean() {
    
            return $this->getForm()->getClean();
    
        /**
         * Compile a list of data used by the filtering system to match dynamic
         * content in this entry. This returs an array of `field.<id>` =>
         * <value> pairs where the <id> is the field id and the <value> is the
         * toString() value for the entered data.
         *
         * If the field returns an array for its ::getFilterData() method, the
         * data will be added in the array with the keys prefixed with
         * `field.<id>`. This is useful for properties on custom lists, for
         * instance, which can contain properties usefule for matching and
         * filtering.
         */
        function getFilterData() {
            $vars = array();
            foreach ($this->getFields() as $f) {
                $tag = 'field.'.$f->get('id');
                if ($d = $f->getFilterData()) {
                    if (is_array($d)) {
                        foreach ($d as $k=>$v) {
                            if (is_string($k))
                                $vars["$tag$k"] = $v;
                            else
                                $vars[$tag] = $v;
                        }
                    }
                    else {
                        $vars[$tag] = $d;
                    }
                }
            }
            return $vars;
        }
    
    
        function forTicket($ticket_id, $force=false) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            static $entries = array();
    
            if (!isset($entries[$ticket_id]) || $force) {
                $stuff = DynamicFormEntry::objects()
    
                    ->filter(array('object_id'=>$ticket_id, 'object_type'=>'T'));
    
                // If forced, don't cache the result
                if ($force)
                    return $stuff;
                $entries[$ticket_id] = &$stuff;
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
            return $entries[$ticket_id];
        }
    
        function setTicketId($ticket_id) {
            $this->object_type = 'T';
            $this->object_id = $ticket_id;
        }
    
        function setClientId($user_id) {
            $this->object_type = 'U';
            $this->object_id = $user_id;
        }
    
        function setObjectId($object_id) {
            $this->object_id = $object_id;
        }
    
    
        function forObject($object_id, $object_type) {
    
            return DynamicFormEntry::objects()
    
                ->filter(array('object_id'=>$object_id, 'object_type'=>$object_type));
    
        function render($staff=true, $title=false, $options=array()) {
            return $this->getForm()->render($staff, $title, $options);
    
        function getChanges() {
            $fields = array();
            foreach ($this->getAnswers() as $a) {
                $field = $a->getField();
                if (!$field->hasData() || $field->isPresentationOnly())
                    continue;
    
                $after = $field->to_database($field->getClean());
    
                $before = $field->to_database($a->getValue());
    
                if ($before == $after)
                    continue;
                $fields[$field->get('id')] = array($before, $after);
    
    Jared Hancock's avatar
    Jared Hancock committed
        /**
         * addMissingFields
         *
    
         * Adds fields that have been added to the linked form (field set) since
         * this entry was originally created. If fields are added to the form,
         * the method will automatically add the fields and null answers to the
         * entry.
    
    Jared Hancock's avatar
    Jared Hancock committed
         */
        function addMissingFields() {
    
            foreach ($this->getFields() as $field) {
                if ($field->isnew && $field->isEnabled()
    
                    && !$field->isPresentationOnly()
                    && $field->hasData()
                    && $field->isStorable()
                ) {
    
                    $a = new DynamicFormEntryAnswer(
    
                        array('field_id'=>$field->get('id'), 'entry'=>$this));
    
    Jared Hancock's avatar
    Jared Hancock committed
                    // Add to list of answers
    
                    $this->answers->add($a);
    
                    // Omit fields without data and non-storable fields.
                    if (!$field->hasData() || !$field->isStorable())
    
            }
    
            // Sort the form the way it is declared to be sorted
            if ($this->_fields) {
                uasort($this->_fields,
                    function($a, $b) {
                        return $a->get('sort') - $b->get('sort');
                });
    
        /**
         * Save the form entry and all associated answers.
         *
         * Returns:
         * (mixed) FALSE if updated failed, otherwise the number of dirty answers
         * which were save is returned (which may be ZERO).
         */
    
        function save($refetch=false) {
    
    Jared Hancock's avatar
    Jared Hancock committed
            if (count($this->dirty))
                $this->set('updated', new SqlFunction('NOW'));
    
            if (!parent::save($refetch || count($this->dirty)))
                return false;
    
    
            foreach ($this->getAnswers() as $a) {
                $field = $a->getField();
    
                // Don't save answers for presentation-only fields or fields
                // which are stored elsewhere
                if (!$field->hasData() || !$field->isStorable()
                    || $field->isPresentationOnly()
                ) {
    
                // Set the entry here so that $field->getClean() can use the
    
                // entry-id if necessary
    
                $a->entry = $this;
    
                    $field->setForm($this);
    
                    $val = $field->to_database($field->getClean());
                }
                catch (FieldUnchanged $e) {
                    // Don't update the answer.
                    continue;
                }
    
                if (is_array($val)) {
                    $a->set('value', $val[0]);
                    $a->set('value_id', $val[1]);
                }
    
                    $a->set('value', $val);
    
                $a->save($refetch);
    
            if (!parent::delete())
                return false;
    
    
            foreach ($this->getAnswers() as $a)
                $a->delete();
    
        static function create($ht=false, $data=null) {
    
            $inst = new static($ht);
    
    Jared Hancock's avatar
    Jared Hancock committed
            $inst->set('created', new SqlFunction('NOW'));
    
                $inst->setSource($data);
    
            foreach ($inst->getDynamicFields() as $field) {
    
                if (!($impl = $field->getImpl($field)))
                    continue;
                if (!$impl->hasData() || !$impl->isStorable())
    
                $a = new DynamicFormEntryAnswer(
    
                    array('field'=>$field, 'entry'=>$inst));
    
                $a->field->setAnswer($a);
    
                $inst->answers->add($a);
    
     * Represents a single answer to a single field on a dynamic form. The
     * data / answer to the field is linked back to the form and field which was
     * originally used for the submission.
    
    Jared Hancock's avatar
    Jared Hancock committed
     */
    class DynamicFormEntryAnswer extends VerySimpleModel {
    
        static $meta = array(
            'table' => FORM_ANSWER_TABLE,
            'ordering' => array('field__sort'),
            'pk' => array('entry_id', 'field_id'),
    
            'select_related' => array('field'),
            'fields' => array('entry_id', 'field_id', 'value', 'value_id'),
    
    Jared Hancock's avatar
    Jared Hancock committed
            'joins' => array(
                'field' => array(
                    'constraint' => array('field_id' => 'DynamicFormField.id'),
                ),
                'entry' => array(
                    'constraint' => array('entry_id' => 'DynamicFormEntry.id'),
                ),
            ),
        );
    
    
        var $deleted = false;
    
    Jared Hancock's avatar
    Jared Hancock committed
        var $_value;
    
        function getEntry() {
            return $this->entry;
        }
    
        function getForm() {
    
            return $this->getEntry()->getForm();
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function getField() {
    
            if (!isset($this->_field)) {
                $this->_field = $this->field->getImpl($this->field);
                $this->_field->setAnswer($this);
    
            return $this->_field;
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function getValue() {
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if (!isset($this->_value)) {
    
                //XXX: We're settting the value here to avoid infinite loop
                $this->_value = false;
    
    Peter Rotich's avatar
    Peter Rotich committed
                if (isset($this->value))
                    $this->_value = $this->getField()->to_php(
                            $this->get('value'), $this->get('value_id'));
    
            return $this->_value;
    
    Peter Rotich's avatar
    Peter Rotich committed
        function setValue($value, $id=false) {
            $this->getField()->reset();
            $this->_value = null;
            $this->set('value', $value);
            if ($id !== false)
                $this->set('value_id', $id);
        }
    
    
        function getLocal($tag) {
            return $this->field->getLocal($tag);
        }
    
    
        function getIdValue() {
            return $this->get('value_id');
        }
    
    
        function isDeleted() {
            return $this->deleted;
        }
    
    
    Jared Hancock's avatar
    Jared Hancock committed
        function toString() {
            return $this->getField()->toString($this->getValue());
        }
    
        function display() {
            return $this->getField()->display($this->getValue());
        }
    
    
        function getSearchable($include_label=false) {
            if ($include_label)
                $label = Format::searchable($this->getField()->getLabel()) . " ";
            return sprintf("%s%s", $label,
                $this->getField()->searchable($this->getValue())
            );
        }
    
    
            return implode(',', (array) $this->getField()->getKeys($this->getValue()));
    
        function asVar() {
    
            return $this->getField()->asVar(
                $this->get('value'), $this->get('value_id')
            );
    
        }
    
        function getVar($tag) {
    
            if (is_object($var = $this->asVar()) && method_exists($var, 'getVar'))
                return $var->getVar($tag);
    
        function __toString() {
    
            $v = $this->toString();
            return is_string($v) ? $v : (string) $this->getValue();
    
    
        function delete() {
            if (!parent::delete())
                return false;
    
            // Allow the field to cleanup anything else in the database
            $this->getField()->db_cleanup();
            return true;
        }
    
        function save($refetch=false) {
    
            if ($this->dirty)
                unset($this->_value);
            return parent::save($refetch);
        }
    
    Jared Hancock's avatar
    Jared Hancock committed
    }
    
    class SelectionField extends FormField {
    
        static $widget = 'ChoicesWidget';
    
        function getListId() {
            list(,$list_id) = explode('-', $this->get('type'));
    
            return $list_id ?: $this->get('list_id');
    
    Jared Hancock's avatar
    Jared Hancock committed
        function getList() {
    
            if (!$this->_list)
                $this->_list = DynamicList::lookup($this->getListId());
    
    Jared Hancock's avatar
    Jared Hancock committed
            return $this->_list;
        }
    
    
        function getWidget($widgetClass=false) {
    
            $config = $this->getConfiguration();
    
            if ($config['widget'] == 'typeahead' && $config['multiselect'] == false)
    
                $widgetClass = 'TypeaheadSelectionWidget';
    
            elseif ($config['widget'] == 'textbox')
                $widgetClass = 'TextboxSelectionWidget';
    
    
            return parent::getWidget($widgetClass);
        }
    
    
    Peter Rotich's avatar
    Peter Rotich committed
        function display($value) {
            global $thisstaff;
    
            if (!is_array($value)
                    || !$thisstaff // Only agents can preview for now
                    || !($list=$this->getList()))
                return parent::display($value);
    
            $display = array();
            foreach ($value as $k => $v) {
                if (is_numeric($k)
                        && ($i=$list->getItem((int) $k))
                        && $i->hasProperties())
                    $display[] = $i->display();
                else // Perhaps deleted  entry
                    $display[] = $v;
            }
    
            return implode(',', $display);
    
        }
    
    
        function parse($value) {
    
    Peter Rotich's avatar
    Peter Rotich committed
    
            if (!($list=$this->getList()))
                return null;
    
    
            $config = $this->getConfiguration();
    
    Peter Rotich's avatar
    Peter Rotich committed
            $choices = $this->getChoices();
            $selection = array();
    
    
            if ($value && !is_array($value))
                $value = array($value);
    
    
            if ($value && is_array($value)) {
                foreach ($value as $k=>$v) {
    
                    if ($k && ($i=$list->getItem((int) $k)))
    
    Peter Rotich's avatar
    Peter Rotich committed
                        $selection[$i->getId()] = $i->getValue();
    
                    elseif (isset($choices[$k]))
                        $selection[$k] = $choices[$k];
    
    Peter Rotich's avatar
    Peter Rotich committed
                    elseif (isset($choices[$v]))
                        $selection[$v] = $choices[$v];
    
                    elseif (($i=$list->getItem($v, true)))
                        $selection[$i->getId()] = $i->getValue();
    
            } elseif($value) {
                //Assume invalid textbox input to be validated
                $selection[] = $value;
    
            // Don't return an empty array
            return $selection ?: null;
    
    Peter Rotich's avatar
    Peter Rotich committed
        }
    
        function to_database($value) {
    
            if (is_array($value)) {
                reset($value);
            }
    
    Peter Rotich's avatar
    Peter Rotich committed
            if ($value && is_array($value))
                $value = JsonDataEncoder::encode($value);
    
    
        function to_php($value, $id=false) {
    
            if (is_string($value))
                $value = JsonDataParser::parse($value) ?: $value;
    
    
            if (!is_array($value)) {
    
                $values = array();
    
                $choices = $this->getChoices();
    
                foreach (explode(',', $value) as $V) {
                    if (isset($choices[$V]))
                        $values[$V] = $choices[$V];
                }
                if ($id && isset($choices[$id]))
                    $values[$id] = $choices[$id];
    
                if ($values)
                    return $values;
                // else return $value unchanged
    
            // Don't set the ID here as multiselect prevents using exactly one
            // ID value. Instead, stick with the JSON value only.
    
        function getKeys($value) {
            if (!is_array($value))
                $value = $this->getChoice($value);
            if (is_array($value))
                return implode(', ', array_keys($value));
            return (string) $value;
        }
    
    
        // PHP 5.4 Move this to a trait
        function whatChanged($before, $after) {
            $before = (array) $before;
            $after = (array) $after;
            $added = array_diff($after, $before);
            $deleted = array_diff($before, $after);
            $added = array_map(array($this, 'display'), $added);
            $deleted = array_map(array($this, 'display'), $deleted);
    
            if ($added && $deleted) {
                $desc = sprintf(
                    __('added <strong>%1$s</strong> and removed <strong>%2$s</strong>'),
                    implode(', ', $added), implode(', ', $deleted));
            }
            elseif ($added) {
                $desc = sprintf(
                    __('added <strong>%1$s</strong>'),
                    implode(', ', $added));
            }
            elseif ($deleted) {
                $desc = sprintf(
                    __('removed <strong>%1$s</strong>'),
                    implode(', ', $deleted));
            }
            else {
                $desc = sprintf(
                    __('changed to <strong>%1$s</strong>'),
                    $this->display($after));
            }
            return $desc;
        }
    
    
        function asVar($value, $id=false) {
            $values = $this->to_php($value, $id);
            if (is_array($values)) {
                return new PlaceholderList($this->getList()->getAllItems()
                    ->filter(array('id__in' => array_keys($values)))
                );
            }
        }
    
    
        function hasSubFields() {
    
            return $this->getList()->getForm();
    
        }
        function getSubFields() {
    
            $fields = new ListObject(array(
                new TextboxField(array(
                    // XXX: i18n: Change to a better word when the UI changes
                    'label' => '['.__('Abbrev').']',
                    'id' => 'abb',
                ))
            ));
    
            $form = $this->getList()->getForm();
    
            if ($form && ($F = $form->getFields()))
                $fields->extend($F);
            return $fields;
    
    Peter Rotich's avatar
    Peter Rotich committed
        function toString($items) {
    
            return is_array($items)
    
                ? implode(', ', $items) : (string) $items;
    
    Peter Rotich's avatar
    Peter Rotich committed
        function validateEntry($entry) {
            parent::validateEntry($entry);
            if (!$this->errors()) {
                $config = $this->getConfiguration();
    
                if ($config['widget'] == 'textbox') {
    
                    if ($entry && (
                            !($k=key($entry))
                         || !($i=$this->getList()->getItem((int) $k))
                     )) {
                        $config = $this->getConfiguration();
                        $this->_errors[] = $this->getLocal('validator-error', $config['validator-error'])
                            ?: __('Unknown or invalid input');
                    }
    
                } elseif ($config['typeahead']
    
                        && ($entered = $this->getWidget()->getEnteredValue())
    
                        && !in_array($entered, $entry)
                        && $entered != $entry) {
    
                    $this->_errors[] = __('Select a value from the list');
    
    Jared Hancock's avatar
    Jared Hancock committed
        }
    
        function getConfigurationOptions() {
            return array(
    
                'multiselect' => new BooleanField(array(
                    'id'=>2,
                    'label'=>__(/* Type of widget allowing multiple selections */ 'Multiselect'),
                    'required'=>false, 'default'=>false,
                    'configuration'=>array(
                        'desc'=>__('Allow multiple selections')),
                )),
    
    Peter Rotich's avatar
    Peter Rotich committed
                'widget' => new ChoiceField(array(
    
                    'id'=>1,
                    'label'=>__('Widget'),
                    'required'=>false, 'default' => 'dropdown',
    
    Peter Rotich's avatar
    Peter Rotich committed
                    'choices'=>array(
    
                        'dropdown' => __('Drop Down'),
    
                        'typeahead' => __('Typeahead'),
                        'textbox' => __('Text Input'),
    
    Peter Rotich's avatar
    Peter Rotich committed
                    ),
                    'configuration'=>array(
                        'multiselect' => false,
                    ),
    
                    'visibility' => new VisibilityConstraint(
    
                        new Q(array('multiselect__eq'=>false)),
    
                        VisibilityConstraint::HIDDEN
                    ),
    
                    'hint'=>__('Typeahead will work better for large lists')
    
                'validator-error' => new TextboxField(array(
                    'id'=>5, 'label'=>__('Validation Error'), 'default'=>'',
                    'configuration'=>array('size'=>40, 'length'=>80,
                        'translatable'=>$this->getTranslateTag('validator-error')
                    ),
    
                    'visibility' => new VisibilityConstraint(
    
                        new Q(array('widget__eq'=>'textbox')),
    
                        VisibilityConstraint::HIDDEN
                    ),
    
                    'hint'=>__('Message shown to user if the item entered is not in the list')
    
    Peter Rotich's avatar
    Peter Rotich committed
                )),
    
                'prompt' => new TextboxField(array(
    
                    'id'=>3,
                    'label'=>__('Prompt'), 'required'=>false, 'default'=>'',
    
                    'hint'=>__('Leading text shown before a value is selected'),
    
                    'configuration'=>array('size'=>40, 'length'=>40,
                        'translatable'=>$this->getTranslateTag('prompt'),
                    ),
    
                'default' => new SelectionField(array(
                    'id'=>4, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
                    'list_id'=>$this->getListId(),
                    'configuration' => array('prompt'=>__('Select a Default')),
                )),
    
    Peter Rotich's avatar
    Peter Rotich committed
        function getConfiguration() {
    
            $config = parent::getConfiguration();
            if ($config['widget'])
    
                $config['typeahead'] = $config['widget'] == 'typeahead';
    
            // Drop down list does not support multiple selections
    
    Peter Rotich's avatar
    Peter Rotich committed
            if ($config['typeahead'])
                $config['multiselect'] = false;
    
            return $config;
        }
    
        function getChoices($verbose=false) {
            if (!$this->_choices || $verbose) {
    
                $choices = array();
    
                foreach ($this->getList()->getItems() as $i)
    
                    $choices[$i->getId()] = $i->getValue();
    
    Peter Rotich's avatar
    Peter Rotich committed
    
                // Retired old selections
                $values = ($a=$this->getAnswer()) ? $a->getValue() : array();
                if ($values && is_array($values)) {
                    foreach ($values as $k => $v) {
    
                        if (!isset($choices[$k])) {
    
                            if ($verbose) $v .= ' '.__('(retired)');
    
                            $choices[$k] = $v;
    
    
                if ($verbose) // Don't cache
                    return $choices;
    
                $this->_choices = $choices;
    
            return $this->_choices;
        }
    
        function getChoice($value) {
            $choices = $this->getChoices();
            if ($value && is_array($value)) {
                $selection = $value;
            } elseif (isset($choices[$value]))
                $selection[] = $choices[$value];
            elseif ($this->get('default'))
                $selection[] = $choices[$this->get('default')];
    
            return $selection;
        }
    
    
        function lookupChoice($value) {
    
            // See if it's in the choices.
            $choices = $this->getChoices();
            if ($choices && ($i=array_search($value, $choices)))
                return array($i=>$choices[$i]);
    
            // Query the store by value or extra (abbrv.)
    
            if (!($list=$this->getList()))
                return null;
    
            if ($i = $list->getItem($value))
                return array($i->getId() => $i->getValue());
    
            if ($i = $list->getItem($value, true))
    
                return array($i->getId() => $i->getValue());
    
            return null;
        }
    
    
    
        function getFilterData() {
    
            // Start with the filter data for the list item as the [0] index
    
            $data = array(parent::getFilterData());
    
            if (($v = $this->getClean())) {
                // Add in the properties for all selected list items in sub
                // labeled by their field id
                foreach ($v as $id=>$L) {
                    if (!($li = DynamicListItem::lookup($id)))
                        continue;
                    foreach ($li->getFilterData() as $prop=>$value) {
                        if (!isset($data[$prop]))
                            $data[$prop] = $value;
                        else
                            $data[$prop] .= " $value";
                    }
                }
    
    
        function getSearchMethods() {
            return array(
                'set' =>        __('has a value'),
                'notset' =>     __('does not have a value'),
                'includes' =>   __('includes'),
                '!includes' =>  __('does not include'),
            );
        }
    
        function getSearchMethodWidgets() {
            return array(
                'set' => null,
                'notset' => null,
                'includes' => array('ChoiceField', array(
                    'choices' => $this->getChoices(),
                    'configuration' => array('multiselect' => true),
                )),
                '!includes' => array('ChoiceField', array(
                    'choices' => $this->getChoices(),
                    'configuration' => array('multiselect' => true),
                )),
            );
        }
    
        function getSearchQ($method, $value, $name=false) {
            $name = $name ?: $this->get('name');
            switch ($method) {
            case '!includes':
    
                return Q::not(array("{$name}__intersect" => array_keys($value)));
    
                return new Q(array("{$name}__intersect" => array_keys($value)));
    
            default:
                return parent::getSearchQ($method, $value, $name);
            }
        }
    
    class TypeaheadSelectionWidget extends ChoicesWidget {
    
        function render($options=array()) {
    
            if ($options['mode'] == 'search')
                return parent::render($options);
    
            $name = $this->getEnteredValue();
    
            $config = $this->field->getConfiguration();
    
            if (is_array($this->value)) {
                $name = $name ?: current($this->value);
                $value = key($this->value);
    
            else {
                // Pull configured default (if configured)
                $def_key = $this->field->get('default');
                if (!$def_key && $config['default'])
                    $def_key = $config['default'];
                if (is_array($def_key))
                    $name = current($def_key);
            }
    
    Jared Hancock's avatar
    Jared Hancock committed
            $source = array();
            foreach ($this->field->getList()->getItems() as $i)
                $source[] = array(
    
    Peter Rotich's avatar
    Peter Rotich committed
                    'value' => $i->getValue(), 'id' => $i->getId(),
    
                    'info' => sprintf('%s%s',
    
    Peter Rotich's avatar
    Peter Rotich committed
                        $i->getValue(),
    
                        (($extra= $i->getAbbrev()) ? " — $extra" : '')),
    
    Jared Hancock's avatar
    Jared Hancock committed
            ?>
            <span style="display:inline-block">
    
            <input type="text" size="30" name="<?php echo $this->name; ?>_name"
                id="<?php echo $this->name; ?>" value="<?php echo Format::htmlchars($name); ?>"
    
                placeholder="<?php echo $config['prompt'];
                ?>" autocomplete="off" />
    
            <input type="hidden" name="<?php echo $this->name;
    
                ?>_id" id="<?php echo $this->name;
                ?>_id" value="<?php echo Format::htmlchars($value); ?>"/>
    
    Jared Hancock's avatar
    Jared Hancock committed
            <script type="text/javascript">
            $(function() {
    
                $('input#<?php echo $this->name; ?>').typeahead({
    
    Jared Hancock's avatar
    Jared Hancock committed
                    source: <?php echo JsonDataEncoder::encode($source); ?>,
    
                    property: 'info',
    
    Jared Hancock's avatar
    Jared Hancock committed
                    onselect: function(item) {
    
                        $('input#<?php echo $this->name; ?>_name').val(item['value'])
                        $('input#<?php echo $this->name; ?>_id')
                          .attr('name', '<?php echo $this->name; ?>[' + item['id'] + ']')
                          .val(item['value']);
    
    Jared Hancock's avatar
    Jared Hancock committed
                    }
                });
            });
            </script>
            </span>
            <?php
        }
    
        function parsedValue() {
            return array($this->getValue() => $this->getEnteredValue());
        }
    
    
        function getValue() {
            $data = $this->field->getSource();
    
            $name = $this->field->get('name');
    
            if (isset($data["{$this->name}_id"]) && is_numeric($data["{$this->name}_id"])) {
                return array($data["{$this->name}_id"] => $data["{$this->name}_name"]);
            }
            elseif (isset($data[$name])) {
                return $data[$name];
            }
            // Attempt to lookup typed value (usually from a default)
            elseif ($val = $this->getEnteredValue()) {
                return $this->field->lookupChoice($val);
            }
    
        function getEnteredValue() {
            // Used to verify typeahead fields
    
            $data = $this->field->getSource();
    
            if (isset($data[$this->name.'_name'])) {
                // Drop the extra part, if any
                $v = $data[$this->name.'_name'];
    
                $pos = strrpos($v, ' — ');
                if ($pos !== false)
                    $v = substr($v, 0, $pos);
    
    
            return parent::getValue();
        }