diff --git a/components/email.inc b/components/email.inc
index 3118cc2..64dff47 100644
--- a/components/email.inc
+++ b/components/email.inc
@@ -337,13 +337,6 @@ function _webform_table_email($component, $value) {
return check_plain(empty($value[0]) ? '' : $value[0]);
}
-/**
- * Implements _webform_action_set_component().
- */
-function _webform_action_set_email($component, &$element, &$form_state, $value) {
- $element['#value'] = $value;
- form_set_value($element, $value, $form_state);
-}
/**
* Implements _webform_csv_headers_component().
diff --git a/components/select.inc b/components/select.inc
index e57f614..33d3724 100644
--- a/components/select.inc
+++ b/components/select.inc
@@ -816,9 +816,7 @@ function _webform_csv_data_select($component, $export_options, $value) {
if ($component['extra']['multiple']) {
foreach ($options as $key => $item) {
- // Strict search is needed to avoid a key of 0 from matching an empty
- // value.
- $index = array_search((string)$key, (array)$value, TRUE);
+ $index = array_search($key, (array) $value);
if ($index !== FALSE) {
if ($export_options['select_format'] == 'separate') {
$return[] = 'X';
diff --git a/src/Annotation/Component.php b/src/Annotation/Component.php
new file mode 100644
index 0000000..d92f46b
--- /dev/null
+++ b/src/Annotation/Component.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Annotation\Component.
+ */
+
+namespace Drupal\webform\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Component item annotation object.
+ *
+ * Plugin Namespace: Plugin\webform\component
+ *
+ * @see \Drupal\webform\Plugin\ComponentManager
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class Component extends Plugin {
+
+ /**
+ * The plugin ID.
+ *
+ * @var string
+ */
+ public $id;
+
+ /**
+ * The name of the component.
+ *
+ * @var \Drupal\Core\Annotation\Translation
+ *
+ * @ingroup plugin_translatable
+ */
+ public $label;
+
+ /**
+ * The description of the component.
+ *
+ * @var \Drupal\Core\Annotation\Translation
+ *
+ * @ingroup plugin_translatable
+ */
+ public $description;
+
+}
diff --git a/src/ComponentBase.php b/src/ComponentBase.php
new file mode 100644
index 0000000..068fdfa
--- /dev/null
+++ b/src/ComponentBase.php
@@ -0,0 +1,223 @@
+<?php
+
+/**
+ * @file
+ * Provides Drupal\webform\ComponentBase.
+ */
+
+namespace Drupal\webform;
+
+use Drupal\Component\Plugin\PluginBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\node\Entity\Node;
+
+class ComponentBase extends PluginBase implements ComponentInterface {
+
+ public function getLabel() {
+ return $this->pluginDefinition['label'];
+ }
+
+ public function getDescription() {
+ return $this->pluginDefinition['description'];
+ }
+
+ public function getNodeId() {
+ return $this->configuration['nid'];
+ }
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $form['#node'] = Node::load($node);
+
+ $component = $this->configuration;
+ if (\Drupal::request()->query->get('type')) {
+ $component['type'] = \Drupal::request()->query->get('type');
+ }
+ if (\Drupal::request()->query->get('pid')) {
+ $component['pid'] = \Drupal::request()->query->get('pid');
+ } else {
+ $component['pid'] = 0;
+ }
+ if (\Drupal::request()->query->get('weight')) {
+ $component['weight'] = \Drupal::request()->query->get('weight');
+ } else {
+ $component['weight'] = 0;
+ }
+ if (\Drupal::request()->query->get('name')) {
+ $component['name'] = \Drupal::request()->query->get('name');
+ }
+ if (\Drupal::request()->query->get('required')) {
+ $component['required'] = \Drupal::request()->query->get('required');
+ }
+
+ $form['nid'] = array(
+ '#type' => 'value',
+ '#value' => $node,
+ );
+ if ($component['cid']) {
+ $form['cid'] = array(
+ '#type' => 'value',
+ '#value' => $component['cid'],
+ );
+ }
+
+ // Load basic component information from query string
+ $form['type'] = array(
+ '#type' => 'value',
+ '#value' => $component['type'],
+ );
+ $form['pid'] = array(
+ '#type' => 'value',
+ '#value' => $component['pid'],
+ );
+ $form['weight'] = array(
+ '#type' => 'value',
+ '#value' => $component['weight'],
+ );
+
+ $form['name'] = [
+ '#type' => 'textfield',
+ '#title' => t('Name'),
+ '#description' => t('This is used as a descriptive label when displaying this form element.'),
+ '#size' => 60,
+ '#maxlength' => NULL,
+ '#required' => TRUE,
+ '#default_value' => $component['name'],
+ '#weight' => -20,
+ '#attributes' => [
+ 'required' => TRUE,
+ 'class' => ['webform-component-name'],
+ ],
+ ];
+ // @TODO The machine name isn't properly 'slugifying' a field name with spaces. Is this a core-issue?
+ $form['form_key'] = [
+ '#type' => 'machine_name',
+ '#title' => t('Field key'),
+ '#description' => t('Enter a machine readable key for this form element. May contain only alphanumeric characters and underscores. This key will be used as the name attribute of the form element. This value has no effect on the way data is saved, but may be helpful if doing custom form processing.'),
+ '#size' => 60,
+ '#machine_name' => [
+ 'exists' => array($this, 'componentKeyExists'),
+ 'source' => array('name'),
+ ],
+ '#maxlength' => NULL,
+ '#required' => TRUE,
+ '#weight' => -19,
+ '#attributes' => [
+ 'required' => TRUE,
+ 'class' => ['webform-component-form-key'],
+ ],
+ ];
+ $form['description'] = [
+ '#type' => 'textarea',
+ '#title' => t('Description'),
+ '#description' => t('A short description of the field used as help for the user when he/she uses the form.') . ' ' . theme_webform_token_help(),
+ '#required' => FALSE,
+ '#default_value' => $component['extra']['description'],
+ '#weight' => 1,
+ '#attributes' => [
+ 'class' => ['webform-component-description'],
+ ],
+ ];
+ $form['extra'] = [
+ '#weight' => 5,
+ ];
+ $form['validation'] = [
+ '#type' => 'details',
+ '#title' => t('Validation'),
+ '#collapsible' => TRUE,
+ '#open' => TRUE,
+ '#weight' => 10,
+ '#attributes' => [
+ 'class' => ['webform-component-validation'],
+ ],
+ ];
+ $form['validation']['required'] = [
+ '#type' => 'checkbox',
+ '#title' => t('Required'),
+ '#description' => t('Check this option if the user must enter a value.'),
+ '#default_value' => $component['required'],
+ '#attributes' => [
+ 'class' => ['webform-component-required'],
+ ],
+ ];
+ $form['display'] = [
+ '#type' => 'details',
+ '#title' => t('Display'),
+ '#collapsible' => TRUE,
+ '#open' => TRUE,
+ '#weight' => 11,
+ '#attributes' => [
+ 'class' => ['webform-component-display'],
+ ],
+ ];
+
+ if (webform_component_feature($component['type'], 'title_display')) {
+ if (webform_component_feature($component['type'], 'title_inline')) {
+ $form['display']['title_display'] = array(
+ '#type' => 'select',
+ '#title' => t('Label display'),
+ '#default_value' => !empty($component['extra']['title_display']) ? $component['extra']['title_display'] : 'before',
+ '#options' => array(
+ 'before' => t('Above'),
+ 'inline' => t('Inline'),
+ 'none' => t('None'),
+ ),
+ '#description' => t('Determines the placement of the component\'s label.'),
+ );
+ }
+ else {
+ $form['display']['title_display'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Hide label'),
+ '#default_value' => strcmp($component['extra']['title_display'], 'none') === 0,
+ '#return_value' => 'none',
+ '#description' => t('Do not display the label of this component.'),
+ );
+ }
+ $form['display']['title_display']['#weight'] = 8;
+ $form['display']['title_display']['#parents'] = array('extra', 'title_display');
+ }
+
+ $form['display']['wrapper_css'] = [
+ '#type' => 'textfield',
+ '#title' => t('Wrapper CSS classes'),
+ '#description' => t('Apply a class to the wrapper around both the field and its label. Separate multiple classes by spaces.'),
+ '#size' => 60,
+ '#maxlength' => NULL,
+ '#weight' => 20,
+ '#parents' => array('extra', 'wrapper_css'),
+ '#default_value' => $component['extra']['wrapper_css'],
+ '#attributes' => [
+ 'class' => ['webform-component-wrapper-css'],
+ ],
+ ];
+ $form['display']['field_css'] = [
+ '#type' => 'textfield',
+ '#title' => t('Field CSS classes'),
+ '#description' => t('Apply a class to the field. Separate multiple by spaces.'),
+ '#size' => 60,
+ '#maxlength' => NULL,
+ '#weight' => 22,
+ '#parents' => array('extra', 'field_css'),
+ '#default_value' => $component['extra']['field_css'],
+ '#attributes' => [
+ 'class' => ['webform-component-field-css'],
+ ],
+ ];
+ $form['actions'] = [
+ '#weight' => 20,
+ ];
+ $form['actions']['submit'] = [
+ '#type' => 'submit',
+ '#value' => t('Save component'),
+ '#validate' => ['::validateComponentEditForm'],
+ ];
+
+ return $form;
+ }
+
+ public function componentKeyExists($value, $element, \Drupal\Core\Form\FormStateInterface $form_state) {
+ // @TODO Actually check whether the field key has been used on the current
+ // webform.
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/src/ComponentInterface.php b/src/ComponentInterface.php
new file mode 100644
index 0000000..53f6f51
--- /dev/null
+++ b/src/ComponentInterface.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * @file
+ * Provides Drupal\webform\ComponentInterface
+ */
+
+namespace Drupal\webform;
+
+use Drupal\Component\Plugin\PluginInspectionInterface;
+
+/**
+ * Defines an interface for webform component plugins.
+ */
+interface ComponentInterface extends PluginInspectionInterface {
+ /**
+ * Return the label of the component.
+ *
+ * @return string
+ */
+ public function getLabel();
+
+ /**
+ * Return the description of the component.
+ *
+ * @return string
+ */
+ public function getDescription();
+
+}
diff --git a/src/ComponentManager.php b/src/ComponentManager.php
new file mode 100644
index 0000000..2e2dc6b
--- /dev/null
+++ b/src/ComponentManager.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * @file
+ * Contains ComponentManager.
+ */
+
+namespace Drupal\webform;
+
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Plugin\DefaultPluginManager;
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+
+/**
+ * Component plugin manager.
+ */
+class ComponentManager extends DefaultPluginManager {
+
+ /**
+ * Constructs the ComponentManager object.
+ *
+ * @param \Traversable $namespaces
+ * An object that implements \Traversable which contains the root paths
+ * keyed by the corresponding namespace to look for plugin implementations.
+ * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+ * Cache backend instance to use.
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+ * The module handler to invoke the alter hook with.
+ */
+ public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+ parent::__construct('Plugin/Component', $namespaces, $module_handler, 'Drupal\webform\ComponentInterface', 'Drupal\webform\Annotation\Component');
+
+ $this->alterInfo('webform_component_info');
+ $this->setCacheBackend($cache_backend, 'webform_component');
+ }
+
+ /**
+ * Returns a list of component plugins.
+ *
+ * @return array
+ * List of component plugins keyed by ID.
+ */
+ public function componentList() {
+ $component_list = array();
+ $component_types = $this->getDefinitions();
+ foreach ($component_types as $key => $component_type) {
+ $component = $this->createInstance($component_type['id']);
+ $component_list[$key] = $component->getLabel();
+ }
+
+ return $component_list;
+ }
+
+ /**
+ * Loads a specific component, based on a cid
+ *
+ * @return Component Object
+ * An instance of a component.
+ */
+ public function loadComponent($cid) {
+ $result = db_select('webform_component', 'c')
+ ->fields('c')
+ ->condition('cid', $cid, '=')
+ ->execute()
+ ->fetchAssoc();
+ $result['extra'] = unserialize($result['extra']);
+
+ $component = $this->createInstance($result['type'], $result);
+
+ return $component;
+ }
+}
diff --git a/src/Form/WebformClientForm.php b/src/Form/WebformClientForm.php
index 545b043..554d03b 100644
--- a/src/Form/WebformClientForm.php
+++ b/src/Form/WebformClientForm.php
@@ -10,6 +10,8 @@ namespace Drupal\webform\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\node\NodeInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\webform\ComponentManager;
/**
* Provides a webform client form.
@@ -17,6 +19,32 @@ use Drupal\node\NodeInterface;
class WebformClientForm extends FormBase {
/**
+ * The component manager service.
+ *
+ * @var \Drupal\webform\ComponentManager
+ */
+ protected $componentManager;
+
+ /**
+ * Constructs a WebformComponentAddForm object.
+ *
+ * @param ComponentManager $component_manager
+ * The component manager service.
+ */
+ // public function __construct(ComponentManager $component_manager) {
+ // $this->componentManager = $component_manager;
+ // }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.webform.component')
+ );
+ }
+
+ /**
* The node the client form belongs to.
*
* @var \Drupal\node\NodeInterface
@@ -68,6 +96,7 @@ class WebformClientForm extends FormBase {
$this->submission = $submission;
$this->resume_draft = $resume_draft;
$this->filter = $filter;
+ $this->componentManager = $component_manager;
}
/**
@@ -257,6 +286,7 @@ class WebformClientForm extends FormBase {
// Recursively add components to the form. The unfiltered version of the
// form (typically used in Form Builder), includes all components.
foreach ($component_tree['children'] as $cid => $component) {
+ // $componentObject = $this->componentManager->createInstance($cid);
if ($component['type'] == 'pagebreak') {
$next_page_labels[$component['page_num'] - 1] = !empty($component['extra']['next_page_label']) ? $component['extra']['next_page_label'] : $this->t('Next Page >');
$prev_page_labels[$component['page_num']] = !empty($component['extra']['prev_page_label']) ? $component['extra']['prev_page_label'] : $this->t('< Previous Page');
diff --git a/src/Form/WebformComponentAddForm.php b/src/Form/WebformComponentAddForm.php
new file mode 100644
index 0000000..e3b9a15
--- /dev/null
+++ b/src/Form/WebformComponentAddForm.php
@@ -0,0 +1,163 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Form\WebformComponentAddForm.
+ */
+
+namespace Drupal\webform\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Database\Connection;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\webform\ComponentManager;
+
+/**
+ * Add / edit components on a webform.
+ */
+class WebformComponentAddForm extends FormBase {
+
+ /**
+ * The component manager service.
+ *
+ * @var \Drupal\webform\ComponentManager
+ */
+ protected $componentManager;
+
+ /**
+ * Constructs a WebformComponentAddForm object.
+ *
+ * @param ComponentManager $component_manager
+ * The component manager service.
+ */
+ public function __construct(ComponentManager $component_manager) {
+ $this->componentManager = $component_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.webform.component')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'webform_component_add';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, $node = NULL, $component = NULL) {
+ // Load the component plugin.
+ $component = $this->componentManager->createInstance($component);
+
+ // Get the form from the component plugin.
+ $form = $component->buildForm($form, $form_state, $node);
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValues();
+
+ //parent::submitForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateComponentAddForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValues();
+
+ //parent::submitForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $node = $form['#node'];
+ $values = $form_state->getValues();
+
+ $component = array(
+ 'nid' => $values['nid'],
+ 'pid' => $values['pid'],
+ 'form_key' => $values['form_key'],
+ 'name' => $values['name'],
+ 'type' => $values['type'],
+ 'value' => $values['value'],
+ 'extra' => $values['extra'],
+ 'required' => $values['required'],
+ 'weight' => $values['weight'],
+ );
+
+ // @TODO Figure out why the description element doesn't get bundled up
+ // with [extra] automatically.
+ $component['extra']['description'] = $values['description'];
+
+ $form['cid'] = array(
+ '#type' => 'value',
+ '#value' => $this->insertComponent($component),
+ );
+
+ $form_state->setRedirect( 'webform.components', array( 'node' => $node->id()));
+
+ // parent::submitForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ // parent::save($form, $form_state);
+ drupal_set_message($this->t('Changes to the webform have been saved.'));
+ }
+
+ private function insertComponent(&$component) {
+ // @TODO Allow components/modules to modify the component before saving.
+
+ if (!isset($component['cid'])) {
+ $lock = \Drupal::lock();
+ if ($lock->acquire('webform_component_insert_' . $component['nid'], 5)) {
+ $next_id_query = db_select('webform_component')->condition('nid', $component['nid']);
+ $next_id_query->addExpression('MAX(cid) + 1', 'cid');
+ $component['cid'] = $next_id_query->execute()->fetchField();
+ if ($component['cid'] == NULL) {
+ $component['cid'] = 1;
+ }
+ $lock->release('webform_component_insert_' . $component['nid']);
+ }
+ else {
+ watchdog('webform', 'A Webform component could not be saved because a timeout occurred while trying to acquire a lock for the node. Details: <pre>@component</pre>', array('@component' => print_r($component, TRUE)));
+ return FALSE;
+ }
+ }
+
+ $query = db_insert('webform_component')
+ ->fields(array(
+ 'nid' => $component['nid'],
+ 'cid' => $component['cid'],
+ 'pid' => $component['pid'],
+ 'form_key' => $component['form_key'],
+ 'name' => $component['name'],
+ 'type' => $component['type'],
+ 'value' => (string) $component['value'],
+ 'extra' => serialize($component['extra']),
+ 'required' => $component['required'],
+ 'weight' => $component['weight'],
+ ))
+ ->execute();
+
+ return $component['cid'];
+ }
+}
diff --git a/src/Form/WebformComponentDeleteForm.php b/src/Form/WebformComponentDeleteForm.php
new file mode 100644
index 0000000..1a16e92
--- /dev/null
+++ b/src/Form/WebformComponentDeleteForm.php
@@ -0,0 +1,233 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Form\WebformComponentDeleteForm.
+ */
+
+namespace Drupal\webform\Form;
+
+use Drupal\node\Entity\Node;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\ConfirmFormBase;
+use Drupal\Core\Database\Connection;
+use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\webform\ComponentManager;
+
+/**
+ * Add / edit components on a webform.
+ */
+class WebformComponentDeleteForm extends ConfirmFormBase {
+
+ /**
+ * The component manager service.
+ *
+ * @var \Drupal\webform\ComponentManager
+ */
+ protected $componentManager;
+
+ /**
+ * Constructs a WebformComponentDeleteForm object.
+ *
+ * @param ComponentManager $component_manager
+ * The component manager service.
+ */
+ public function __construct(ComponentManager $component_manager) {
+ $this->componentManager = $component_manager;
+ }
+
+ /**
+ * The ID of the item to delete.
+ *
+ * @var string
+ */
+ protected $id;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ $component = $this->componentManager->loadComponent($this->id);
+
+ if ($component->type == 'fieldset') {
+ $question = t('Delete the %name fieldset?', array('%name' => $component->getLabel()));
+ } else {
+ $question = t('Delete the %name component?', array('%name' => $component->getLabel()));
+ }
+
+ return $question;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ $component = $this->componentManager->loadComponent($this->id);
+ return new Url('webform.components', ['node' => $component->getNodeId()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ $component = $this->componentManager->loadComponent($this->id);
+ $node = Node::load($component->getNodeId());
+
+ if ($component->type == 'fieldset') {
+ $description = t('This will immediately delete the %name fieldset and all children elements within %name from the %webform webform. This cannot be undone.', array('%name' => $component->getLabel(), '%webform' => $node->title));
+ } else {
+ $description = "This cannot be undone.";
+ // $description = t('This will immediately delete the %name component from the %webform webform. This cannot be undone.', array('%name' => $component->getLabel(), '%webform' => $node->title));
+ }
+
+ return $description;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return t('Delete it!');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelText() {
+ return t('Nevermind');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.webform.component')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'webform_component_delete';
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @param int $node
+ * The id of the node to which the component belongs.
+ * @param int $cid
+ * The id of the component to be deleted.
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, $node = NULL, $cid = NULL) {
+ $this->id = $cid;
+ // Load the component plugin.
+ $component = $this->componentManager->loadComponent($cid);
+ $node = Node::load($component->getNodeId());
+
+ $form = parent::buildForm($form, $form_state, $node);
+ $form['node'] = array(
+ '#type' => 'value',
+ '#value' => $node,
+ );
+ $form['component'] = array(
+ '#type' => 'value',
+ '#value' => $component,
+ );
+
+ return $form;
+ // return confirm_form($form, $question, 'node/' . $node->nid . '/webform/components', $description, t('Delete'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $component = $this->componentManager->loadComponent($this->id);
+ $nid = $component->getNodeId();
+
+ $component = array(
+ 'cid' => $this->id,
+ 'nid' => $nid,
+ );
+
+ $this->deleteComponent($component);
+
+ $form_state->setRedirect('webform.components', ['node' => $component['nid']]);
+
+ // parent::submitForm($form, $form_state);
+ }
+
+ private function deleteComponent(&$component) {
+ // @TODO Do we need to check for a delete function? Shouldn't that be
+ // required?
+
+ $query = db_delete('webform_component')
+ ->condition('nid', $component['nid'], '=')
+ ->condition('cid', $component['cid'], '=')
+ ->execute();
+
+ // Delete all elements under this element.
+ // @TODO Components should use the individual component delete method.
+ /*
+ $children = db_select('webform_component', 'c')
+ ->fields('c')
+ ->condition('nid', $component['nid'])
+ ->condition('pid', $component['cid'])
+ ->execute();
+ foreach ($children as $row) {
+ $child_component = $node->webform['components'][$row->cid];
+ webform_component_delete($node, $child_component);
+ }
+ */
+
+ // @TODO Delete submission_data for this component. Perhaps should this be
+ // handled by the component delete function?
+
+ // @TODO Delete conditionals that depend on this component
+ /*
+ module_load_include('inc', 'webform', 'includes/webform.conditionals');
+ foreach ($node->webform['conditionals'] as $rgid => $conditional) {
+ $delete_conditional = FALSE;
+ $specs = array(
+ array(
+ 'field' => 'rules',
+ 'table' => 'webform_conditional_rules',
+ 'component' => 'source',
+ 'component_type' => 'source_type',
+ 'index' => 'rid',
+ ),
+ array(
+ 'field' => 'actions',
+ 'table' => 'webform_conditional_actions',
+ 'component' => 'target',
+ 'component_type' => 'target_type',
+ 'index' => 'aid',
+ ),
+ );
+ foreach ($specs as $spec) {
+ $count = count($conditional[$spec['field']]);
+ foreach ($conditional[$spec['field']] as $key => $thing) {
+ if ($thing[$spec['component_type']] === 'component' && $thing[$spec['component']] == $component['cid']) {
+ if ($count == 1) {
+ // Delete the conditional if this component is the only source or target.
+ webform_conditional_delete($node, $conditional);
+ break 2;
+ }
+ db_delete($spec['table'])
+ ->condition('nid', $node->nid)
+ ->condition('rgid', $rgid)
+ ->condition($spec['index'], $key)
+ ->execute();
+ $count--;
+ }
+ }
+ }
+ }
+ */
+ return;
+ }
+}
diff --git a/src/Form/WebformComponentEditForm.php b/src/Form/WebformComponentEditForm.php
new file mode 100644
index 0000000..7eabc89
--- /dev/null
+++ b/src/Form/WebformComponentEditForm.php
@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Form\WebformComponentEditForm.
+ */
+
+namespace Drupal\webform\Form;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Form\FormBase;
+use Drupal\Core\Database\Connection;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\webform\ComponentManager;
+
+/**
+ * Add / edit components on a webform.
+ */
+class WebformComponentEditForm extends FormBase {
+
+ /**
+ * The component manager service.
+ *
+ * @var \Drupal\webform\ComponentManager
+ */
+ protected $componentManager;
+
+ /**
+ * Constructs a WebformComponentEditForm object.
+ *
+ * @param ComponentManager $component_manager
+ * The component manager service.
+ */
+ public function __construct(ComponentManager $component_manager) {
+ $this->componentManager = $component_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.webform.component')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'webform_component_edit';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, $node = NULL, $cid = NULL) {
+ // Load the component plugin.
+ $component = $this->componentManager->loadComponent($cid);
+
+ // Get the form from the component plugin.
+ $form = $component->buildForm($form, $form_state, $node);
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValues();
+
+ //parent::submitForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateComponentEditForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValues();
+
+ //parent::submitForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $node = $form['#node'];
+ $values = $form_state->getValues();
+
+ $component = array(
+ 'cid' => $values['cid'],
+ 'nid' => $values['nid'],
+ 'pid' => $values['pid'],
+ 'form_key' => $values['form_key'],
+ 'name' => $values['name'],
+ 'type' => $values['type'],
+ 'value' => $values['value'],
+ 'extra' => $values['extra'],
+ 'required' => $values['required'],
+ 'weight' => $values['weight'],
+ );
+
+ $component['extra']['description'] = $values['description'];
+
+ $this->updateComponent($component);
+
+ $form_state->setRedirect( 'webform.components', array( 'node' => $node->id()));
+
+ // parent::submitForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ // parent::save($form, $form_state);
+ drupal_set_message($this->t('Changes to the webform have been saved.'));
+ }
+
+ private function updateComponent(&$component) {
+ // @TODO Allow components/modules to modify the component before saving.
+
+ //@TODO Compare component configuration to saved data, to make sure we only update altered configuration.
+
+ $query = db_update('webform_component')
+ ->fields(array(
+ 'pid' => $component['pid'],
+ 'form_key' => $component['form_key'],
+ 'name' => $component['name'],
+ 'type' => $component['type'],
+ 'value' => (string) $component['value'],
+ 'extra' => serialize($component['extra']),
+ 'required' => $component['required'],
+ 'weight' => $component['weight'],
+ ))
+ ->condition('nid', $component['nid'], '=')
+ ->condition('cid', $component['cid'], '=')
+ ->execute();
+
+ return;
+ }
+}
diff --git a/src/Form/WebformComponentsForm.php b/src/Form/WebformComponentsForm.php
index e12df2a..01a8318 100644
--- a/src/Form/WebformComponentsForm.php
+++ b/src/Form/WebformComponentsForm.php
@@ -13,8 +13,10 @@ use Drupal\Component\Utility\Xss;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\node\Entity\Node;
use Drupal\node\NodeInterface;
+use Drupal\webform\ComponentManager;
/**
* Provides a table-based listing of all components for a webform.
@@ -22,6 +24,32 @@ use Drupal\node\NodeInterface;
class WebformComponentsForm extends FormBase {
/**
+ * The component manager service.
+ *
+ * @var \Drupal\webform\ComponentManager
+ */
+ protected $componentManager;
+
+ /**
+ * Constructs a WebformComponentsForm object.
+ *
+ * @param ComponentManager $component_manager
+ * The component manager service.
+ */
+ public function __construct(ComponentManager $component_manager) {
+ $this->componentManager = $component_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('plugin.manager.webform.component')
+ );
+ }
+
+ /**
* {@inheritdoc}
*/
public function getFormId() {
@@ -35,7 +63,7 @@ class WebformComponentsForm extends FormBase {
$form = [
'#tree' => TRUE,
'#node' => $node,
- '#component_options' => webform_component_options(),
+ '#component_options' => $this->WebformComponentOptions(),
'#component_weights' => [],
'components' => [],
];
@@ -321,17 +349,26 @@ class WebformComponentsForm extends FormBase {
'required' => $component['required'],
'pid' => $component['pid'],
'weight' => $component['weight'],
+ 'type' => $component['type'],
];
- /* @todo Set the destination when the route exists.
+ /* @TODO Maintain destination query string between forms.
// Forward the "destination" query string value to the next form.
if (isset($_GET['destination'])) {
$query['destination'] = $_GET['destination'];
unset($_GET['destination']);
drupal_static_reset('drupal_get_destination');
}
- $form_state['redirect'] = ['node/' . $node->id() . '/webform/components/new/' . $component['type'], ['query' => $query]];
*/
+
+ $form_state->setRedirect(
+ 'webform.component_add_form',
+ array(
+ 'node' => $form_state->getValue('nid'),
+ 'component' => $component['type'],
+ ),
+ array( 'query' => $query )
+ );
}
/**
@@ -449,7 +486,7 @@ class WebformComponentsForm extends FormBase {
// @todo Fix these links once the routes exist.
$form['components'][$cid]['operations']['#links']['edit'] = [
'title' => $this->t('Edit'),
- 'url' => Url::fromRoute('entity.node.webform', ['node' => $node->id()]),
+ 'url' => Url::fromRoute('webform.component_edit_form', ['node' => $node->id(), 'cid' => $cid]),
];
$form['components'][$cid]['operations']['#links']['clone'] = [
'title' => $this->t('Clone'),
@@ -457,7 +494,7 @@ class WebformComponentsForm extends FormBase {
];
$form['components'][$cid]['operations']['#links']['delete'] = [
'title' => $this->t('Delete'),
- 'url' => Url::fromRoute('entity.node.webform', ['node' => $node->id()]),
+ 'url' => Url::fromRoute('webform.component_delete_form', ['node' => $node->id(), 'cid' => $cid]),
];
if (isset($component['children']) && is_array($component['children'])) {
@@ -474,4 +511,15 @@ class WebformComponentsForm extends FormBase {
}
}
+ private function WebformComponentOptions() {
+ $components = $this->componentManager->getDefinitions();
+ $options = array();
+
+ foreach($components as $type => $component) {
+ $options[$type] = $component['label'];
+ }
+ ksort($options);
+
+ return $options;
+ }
}
diff --git a/src/Form/WebformSettingsForm.php b/src/Form/WebformSettingsForm.php
index 585b473..4391f50 100644
--- a/src/Form/WebformSettingsForm.php
+++ b/src/Form/WebformSettingsForm.php
@@ -47,14 +47,16 @@ class WebformSettingsForm extends ConfigFormBase {
);
// Add each component to the form.
- $component_types = webform_components(TRUE);
- foreach ($component_types as $key => $component) {
+ $manager = \Drupal::service('plugin.manager.webform.component');
+ $component_types = $manager->getDefinitions();
+ foreach ($component_types as $key => $component_type) {
+ $component = $manager->createInstance($component_type['id']);
$form['components'][$key] = array(
'#type' => 'checkbox',
- '#title' => $component['label'],
- '#description' => $component['description'],
+ '#title' => $component->getLabel(),
+ '#description' => $component->getDescription(),
'#return_value' => 1,
- '#default_value' => $component['enabled'],
+ '#default_value' => 1,//$component['enabled'],
);
}
diff --git a/src/Plugin/Component/Date.php b/src/Plugin/Component/Date.php
new file mode 100644
index 0000000..0fe8d19
--- /dev/null
+++ b/src/Plugin/Component/Date.php
@@ -0,0 +1,98 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\Date.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'date' component.
+ *
+ * @Component(
+ * id = "date",
+ * label = @Translation("Date"),
+ * description = @Translation("Presents month, day, and year fields.")
+ * )
+ */
+class Date extends ComponentBase {
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ $form['value'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Default value'),
+ '#default_value' => $component['value'],
+ '#description' => t('The default value of the field.') . '<br />' . t('Accepts any date in any <a href="http://www.gnu.org/software/tar/manual/html_chapter/Date-input-formats.html">GNU Date Input Format</a>. Strings such as today, +2 months, and Dec 9 2004 are all valid.'),
+ '#size' => 60,
+ '#maxlength' => 127,
+ '#weight' => 0,
+ );
+ $form['extra']['timezone'] = array(
+ '#type' => 'radios',
+ '#title' => t('Default value timezone'),
+ '#default_value' => empty($component['extra']['timezone']) ? 'user' : $component['extra']['timezone'],
+ '#description' => t('If using relative dates for a default value (e.g. "today") base the current day on this timezone.'),
+ '#options' => array('user' => t('User timezone'), 'site' => t('Website timezone')),
+ '#access' => \Drupal::config('system.date')->get('timezone.user.configurable'),
+ );
+
+ $form['extra']['exclude'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Hide'),
+ '#default_value' => $component['extra']['exclude'],
+ '#options' => array(
+ 'day' => t('Day'),
+ 'month' => t('Month'),
+ 'year' => t('Year'),
+ ),
+ '#description' => t('A hidden day or month will be set to 1. A hidden year will be set to the year of the default value.'),
+ );
+
+ $form['display']['datepicker'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Enable popup calendar'),
+ '#default_value' => $component['extra']['datepicker'],
+ '#description' => t('Enable a JavaScript date picker next to the date field.'),
+ '#weight' => 2,
+ '#parents' => array('extra', 'datepicker'),
+ );
+
+ $form['display']['year_textfield'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Use a textfield for year'),
+ '#default_value' => $component['extra']['year_textfield'],
+ '#description' => t('If checked, the generated date field will use a textfield for the year. Otherwise it will use a select list.'),
+ '#weight' => 5,
+ '#parents' => array('extra', 'year_textfield'),
+ );
+
+ $form['validation']['start_date'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Start date'),
+ '#default_value' => $component['extra']['start_date'],
+ '#description' => t('The earliest date that may be entered into the field.') . ' ' . t('Accepts any date in any <a href="http://www.gnu.org/software/tar/manual/html_chapter/Date-input-formats.html">GNU Date Input Format</a>.'),
+ '#size' => 30,
+ '#weight' => 3,
+ '#parents' => array('extra', 'start_date'),
+ );
+ $form['validation']['end_date'] = array(
+ '#type' => 'textfield',
+ '#title' => t('End date'),
+ '#default_value' => $component['extra']['end_date'],
+ '#description' => t('The latest date that may be entered into the field.') . ' ' . t('Accepts any date in any <a href="http://www.gnu.org/software/tar/manual/html_chapter/Date-input-formats.html">GNU Date Input Format</a>.'),
+ '#size' => 30,
+ '#weight' => 4,
+ '#parents' => array('extra', 'end_date'),
+ );
+
+ $form['#validate'] = array('_webform_edit_date_validate');
+ return $form;
+ }
+}
diff --git a/src/Plugin/Component/Email.php b/src/Plugin/Component/Email.php
new file mode 100644
index 0000000..47ecdfa
--- /dev/null
+++ b/src/Plugin/Component/Email.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\Email.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'email' component.
+ *
+ * @Component(
+ * id = "email",
+ * label = @Translation("E-mail"),
+ * description = @Translation("A special textfield that accepts e-mail addresses.")
+ * )
+ */
+class Email extends ComponentBase {
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ $form['value'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Default value'),
+ '#default_value' => $component['value'],
+ '#description' => t('The default value of the field.') . ' ' . theme_webform_token_help(),
+ '#size' => 60,
+ '#maxlength' => 127,
+ '#weight' => 0,
+ '#attributes' => ($component['value'] == '[current-user:mail]' && count(form_get_errors()) == 0) ? array('disabled' => TRUE) : array(),
+ '#id' => 'email-value',
+ );
+ $form['user_email'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('User email as default'),
+ '#default_value' => $component['value'] == '[current-user:mail]' ? 1 : 0,
+ '#description' => t('Set the default value of this field to the user email, if he/she is logged in.'),
+ '#attributes' => array('onclick' => 'getElementById("email-value").value = (this.checked ? "[current-user:mail]" : ""); getElementById("email-value").disabled = this.checked;'),
+ '#weight' => 0,
+ '#element_validate' => array('_webform_edit_email_validate'),
+ );
+ $form['validation']['multiple'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Multiple'),
+ '#default_value' => $component['extra']['multiple'],
+ '#description' => t('Allow multiple e-mail addresses, separated by commas.'),
+ '#weight' => 0,
+ );
+ if (\Drupal::config('webform.settings')->get('advanced.email_address_format') == 'long') {
+ $form['validation']['format'] = array(
+ '#type' => 'radios',
+ '#title' => t('Format'),
+ '#options' => array(
+ 'long' => t('Allow long format: "Example Name" <[email protected]>'),
+ 'short' => t('Short format only: [email protected]'),
+ ),
+ '#default_value' => $component['extra']['format'],
+ '#description' => t('Not all servers support the "long" format.'),
+ );
+ }
+ $form['display']['width'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Width'),
+ '#default_value' => $component['extra']['width'],
+ '#description' => t('Width of the textfield.') . ' ' . t('Leaving blank will use the default size.'),
+ '#size' => 5,
+ '#maxlength' => 10,
+ '#parents' => array('extra', 'width'),
+ );
+ $form['display']['placeholder'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Placeholder'),
+ '#default_value' => $component['extra']['placeholder'],
+ '#description' => t('The placeholder will be shown in the field until the user starts entering a value.') . ' ' . t('Often used for example values, such as "[email protected]".'),
+ '#parents' => array('extra', 'placeholder'),
+ );
+ $form['display']['disabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Disabled'),
+ '#return_value' => 1,
+ '#description' => t('Make this field non-editable. Useful for setting an unchangeable default value.'),
+ '#weight' => 11,
+ '#default_value' => $component['extra']['disabled'],
+ '#parents' => array('extra', 'disabled'),
+ );
+ $form['validation']['unique'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Unique'),
+ '#return_value' => 1,
+ '#description' => t('Check that all entered values for this field are unique. The same value is not allowed to be used twice.'),
+ '#weight' => 1,
+ '#default_value' => $component['extra']['unique'],
+ '#parents' => array('extra', 'unique'),
+ );
+ return $form;
+ }
+}
diff --git a/src/Plugin/Component/Fieldset.php b/src/Plugin/Component/Fieldset.php
new file mode 100644
index 0000000..69e10c8
--- /dev/null
+++ b/src/Plugin/Component/Fieldset.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\Fieldset.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'fieldset' component.
+ *
+ * @Component(
+ * id = "fieldset",
+ * label = @Translation("Fieldset"),
+ * description = @Translation("A special component that allows grouping of fields.")
+ * )
+ */
+class Fieldset extends ComponentBase {
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ $form['display']['collapsible'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Collapsible'),
+ '#default_value' => $component['extra']['collapsible'],
+ '#description' => t('If this fieldset is collapsible, the user may open or close the fieldset.'),
+ '#weight' => 0,
+ '#parents' => array('extra', 'collapsible'),
+ );
+ $form['display']['collapsed'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Collapsed by Default'),
+ '#default_value' => $component['extra']['collapsed'],
+ '#description' => t('Collapsible fieldsets are "open" by default. Select this option to default the fieldset to "closed."'),
+ '#weight' => 3,
+ '#parents' => array('extra', 'collapsed'),
+ );
+
+ return $form;
+ }
+}
diff --git a/src/Plugin/Component/File.php b/src/Plugin/Component/File.php
new file mode 100644
index 0000000..b5d8da6
--- /dev/null
+++ b/src/Plugin/Component/File.php
@@ -0,0 +1,203 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\File.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'file' component.
+ *
+ * @Component(
+ * id = "file",
+ * label = @Translation("File"),
+ * description = @Translation("A validated field that allows both public or private file uploads.")
+ * )
+ */
+class File extends ComponentBase {
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ // @ TODO Figure out if/why this is necessary.
+ // $form['#after_build'] = array('_webform_edit_file_check_directory');
+
+ $form['validation']['size'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Max upload size'),
+ '#default_value' => $component['extra']['filtering']['size'],
+ '#description' => t('Enter the max file size a user may upload such as 2 MB or 800 KB. Your server has a max upload size of @size.', array('@size' => format_size(file_upload_max_size()))),
+ '#size' => 10,
+ '#parents' => array('extra', 'filtering', 'size'),
+ '#element_validate' => array('_webform_edit_file_size_validate'),
+ '#weight' => 1,
+ );
+
+ $form['validation']['extensions'] = array(
+ '#element_validate' => array('_webform_edit_file_extensions_validate'),
+ '#parents' => array('extra', 'filtering'),
+ '#theme' => 'webform_edit_file_extensions',
+ '#theme_wrappers' => array('form_element'),
+ '#title' => t('Allowed file extensions'),
+ '#attached' => array(
+ 'library' => array(
+ 'webform/webform.admin',
+ ),
+ ),
+ '#type' => 'webform_file_extensions',
+ '#weight' => 2,
+ );
+
+ // Find the list of all currently valid extensions.
+ $current_types = isset($component['extra']['filtering']['types']) ? $component['extra']['filtering']['types'] : array();
+
+ $types = array('gif', 'jpg', 'png');
+ $form['validation']['extensions']['types']['webimages'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Web images'),
+ '#options' => array_combine($types, $types),
+ '#default_value' => array_intersect($current_types, $types),
+ );
+
+ $types = array('bmp', 'eps', 'tif', 'pict', 'psd');
+ $form['validation']['extensions']['types']['desktopimages'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Desktop images'),
+ '#options' => array_combine($types, $types),
+ '#default_value' => array_intersect($current_types, $types),
+ );
+
+ $types = array('txt', 'rtf', 'html', 'pdf', 'doc', 'docx', 'odt', 'ppt', 'pptx', 'odp', 'xls', 'xlsx', 'ods', 'xml');
+ $form['validation']['extensions']['types']['documents'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Documents'),
+ '#options' => array_combine($types, $types),
+ '#default_value' => array_intersect($current_types, $types),
+ );
+
+ $types = array('avi', 'mov', 'mp3', 'ogg', 'wav');
+ $form['validation']['extensions']['types']['media'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Media'),
+ '#options' => array_combine($types, $types),
+ '#default_value' => array_intersect($current_types, $types),
+ );
+
+ $types = array('bz2', 'dmg', 'gz', 'jar', 'rar', 'sit', 'tar', 'zip');
+ $form['validation']['extensions']['types']['archives'] = array(
+ '#type' => 'checkboxes',
+ '#title' => t('Archives'),
+ '#options' => array_combine($types, $types),
+ '#default_value' => array_intersect($current_types, $types),
+ );
+
+ $form['validation']['extensions']['addextensions'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Additional extensions'),
+ '#default_value' => $component['extra']['filtering']['addextensions'],
+ '#description' => t('Enter a list of additional file extensions for this upload field, separated by commas.<br /> Entered extensions will be appended to checked items above.'),
+ '#size' => 20,
+ '#weight' => 3,
+ );
+
+ // @TODO Provide the option to select which stream wrapper the field should use to store files.
+ /*
+ $scheme_options = array();
+ foreach (file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE) as $scheme => $stream_wrapper) {
+ $scheme_options[$scheme] = $stream_wrapper['name'];
+ }
+ $form['extra']['scheme'] = array(
+ '#type' => 'radios',
+ '#title' => t('Upload destination'),
+ '#options' => $scheme_options,
+ '#default_value' => $component['extra']['scheme'],
+ '#description' => t('Private file storage has significantly more overhead than public files, but restricts file access to users who can view submissions.'),
+ '#weight' => 4,
+ '#access' => count($scheme_options) > 1,
+ );
+ */
+ $form['extra']['directory'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Upload directory'),
+ '#default_value' => $component['extra']['directory'],
+ '#description' => t('You may optionally specify a sub-directory to store your files.') . ' ' . theme_webform_token_help(),
+ '#weight' => 5,
+ '#field_prefix' => 'webform/',
+ '#element_validate' => array('_webform_edit_file_check_directory'),
+ );
+
+ $form['extra']['rename'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Rename files'),
+ '#default_value' => $component['extra']['rename'],
+ '#description' => t('You may optionally use tokens to create a pattern used to rename files upon submission. Omit the extension; it will be added automatically.') . ' ' . theme_webform_token_help(array('groups' => array('node', 'submission'))),
+ '#weight' => 6,
+ '#element_validate' => array('_webform_edit_file_rename_validate'),
+ );
+
+ $form['display']['progress_indicator'] = array(
+ '#type' => 'radios',
+ '#title' => t('Progress indicator'),
+ '#options' => array(
+ 'throbber' => t('Throbber'),
+ 'bar' => t('Bar with progress meter'),
+ ),
+ '#default_value' => $component['extra']['progress_indicator'],
+ '#description' => t('The throbber display does not show the status of uploads but takes up less space. The progress bar is helpful for monitoring progress on large uploads.'),
+ '#weight' => 16,
+ '#access' => file_progress_implementation(),
+ '#parents' => array('extra', 'progress_indicator'),
+ );
+
+ // TODO: Make managed_file respect the "size" parameter.
+ /*
+ $form['display']['width'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Width'),
+ '#default_value' => $component['extra']['width'],
+ '#description' => t('Width of the file field.') . ' ' . t('Leaving blank will use the default size.'),
+ '#size' => 5,
+ '#maxlength' => 10,
+ '#weight' => 4,
+ '#parents' => array('extra', 'width')
+ );
+ */
+
+ return $form;
+ }
+
+ /**
+ * A Form API after build and validate function.
+ *
+ * Ensure that the destination directory exists and is writable.
+ */
+ private function _webform_edit_file_check_directory($element) {
+ /*
+ $scheme = $element['extra']['scheme']['#value'];
+ $directory = $element['extra']['directory']['#value'];
+
+ $destination_dir = file_stream_wrapper_uri_normalize($scheme . '://webform/' . $directory);
+ $tokenized_dir = Unicode::strtolower(webform_replace_tokens($destination_dir, $element['#node']));
+
+ // Sanity check input to prevent use parent (../) directories.
+ if (preg_match('/\.\.[\/\\\]/', $tokenized_dir . '/')) {
+ form_error($element['extra']['directory'], t('The save directory %directory is not valid.', array('%directory' => $tokenized_dir)));
+ }
+ else {
+ if (!file_prepare_directory($tokenized_dir, FILE_CREATE_DIRECTORY)) {
+ form_error($element['extra']['directory'], t('The save directory %directory could not be created. Check that the webform files directory is writable.', array('%directory' => $tokenized_dir)));
+ }
+ }
+
+*/
+ return $element;
+ }
+
+}
diff --git a/src/Plugin/Component/Grid.php b/src/Plugin/Component/Grid.php
new file mode 100644
index 0000000..9ab200a
--- /dev/null
+++ b/src/Plugin/Component/Grid.php
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\Grid.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'grid' component.
+ *
+ * @Component(
+ * id = "grid",
+ * label = @Translation("Grid"),
+ * description = @Translation("A tabular display of questions and radio button
+ * options.")
+ * )
+ */
+class Grid extends ComponentBase {
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ if (\Drupal::moduleHandler()->moduleExists('options_element')) {
+ $form['options'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Options'),
+ '#collapsible' => TRUE,
+ '#description' => t('Options to select across the top. Usually these are ratings such as "poor" through "excellent" or "strongly disagree" through "strongly agree".'),
+ '#attributes' => array('class' => array('webform-options-element')),
+ '#element_validate' => array('_webform_edit_validate_options'),
+ );
+ $form['options']['options'] = array(
+ '#type' => 'options',
+ '#options' => _webform_select_options_from_text($component['extra']['options'], TRUE),
+ '#default_value' => $component['value'],
+ '#default_value_allowed' => TRUE,
+ '#optgroups' => FALSE,
+ '#key_type' => 'mixed',
+ '#key_type_toggle' => t('Customize option keys (Advanced)'),
+ '#key_type_toggled' => $component['extra']['custom_option_keys'],
+ '#default_value_pattern' => '^%.+\[.+\]$',
+ );
+
+ $form['questions'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Questions'),
+ '#collapsible' => TRUE,
+ '#description' => t('Questions list down the side of the grid.'),
+ '#attributes' => array('class' => array('webform-options-element')),
+ '#element_validate' => array('_webform_edit_validate_options'),
+ );
+ $form['questions']['options'] = array(
+ '#type' => 'options',
+ '#options' => _webform_select_options_from_text($component['extra']['questions'], TRUE),
+ '#optgroups' => FALSE,
+ '#default_value' => FALSE,
+ '#default_value_allowed' => FALSE,
+ '#key_type' => 'mixed',
+ '#key_type_toggle' => t('Customize question keys (Advanced)'),
+ '#key_type_toggled' => $component['extra']['custom_question_keys'],
+ );
+ }
+ else {
+ $form['extra']['options'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Options'),
+ '#default_value' => $component['extra']['options'],
+ '#description' => t('Options to select across the top. One option per line. <strong>Key-value pairs MUST be specified as "safe_key|Some readable option"</strong>. Use of only alphanumeric characters and underscores is recommended in keys.') . ' ' . theme_webform_token_help(),
+ '#cols' => 60,
+ '#rows' => 5,
+ '#weight' => -3,
+ '#required' => TRUE,
+ '#wysiwyg' => FALSE,
+ '#element_validate' => array('_webform_edit_validate_select'),
+ );
+ $form['extra']['questions'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Questions'),
+ '#default_value' => $component['extra']['questions'],
+ '#description' => t('Questions list down the side of the grid. One question per line. <strong>Key-value pairs MUST be specified as "safe_key|Some readable question"</strong>. For a heading column on the right, append "|" and the right-side heading. Use of only alphanumeric characters and underscores is recommended in keys.') . ' ' . theme_webform_token_help(),
+ '#cols' => 60,
+ '#rows' => 5,
+ '#weight' => -2,
+ '#required' => TRUE,
+ '#wysiwyg' => FALSE,
+ '#element_validate' => array('_webform_edit_validate_select'),
+ );
+ $form['value'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Default value'),
+ '#default_value' => $component['value'],
+ '#description' => t('The default option of the grid identified by its key.') . ' ' . theme_webform_token_help(),
+ '#size' => 60,
+ '#maxlength' => 1024,
+ '#weight' => 1,
+ );
+ }
+
+ $form['display']['optrand'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Randomize Options'),
+ '#default_value' => $component['extra']['optrand'],
+ '#description' => t('Randomizes the order of options on the top when they are displayed in the form.'),
+ '#parents' => array('extra', 'optrand')
+ );
+ $form['display']['qrand'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Randomize Questions'),
+ '#default_value' => $component['extra']['qrand'],
+ '#description' => t('Randomize the order of the questions on the side when they are displayed in the form.'),
+ '#parents' => array('extra', 'qrand')
+ );
+ $form['display']['sticky'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Sticky table header'),
+ '#default_value' => $component['extra']['sticky'],
+ '#description' => t('Use a sticky (non-scrolling) table header.'),
+ '#parents' => array('extra', 'sticky')
+ );
+
+ $form['validation']['unique'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Unique'),
+ '#return_value' => 1,
+ '#description' => t('Check that all entered values for this field are unique. The same value is not allowed to be used twice.'),
+ '#weight' => 1,
+ '#default_value' => $component['extra']['unique'],
+ '#parents' => array('extra', 'unique'),
+ );
+
+ return $form;
+ }
+}
diff --git a/src/Plugin/Component/Hidden.php b/src/Plugin/Component/Hidden.php
new file mode 100644
index 0000000..048c33b
--- /dev/null
+++ b/src/Plugin/Component/Hidden.php
@@ -0,0 +1,53 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\Hidden.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'hidden' component.
+ *
+ * @Component(
+ * id = "hidden",
+ * label = @Translation("Hidden"),
+ * description = @Translation("A form field that is not shown to end-users.")
+ * )
+ */
+class Hidden extends ComponentBase {
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ $form['value'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Default value'),
+ '#default_value' => $component['value'],
+ '#description' => t('The default value of the field.') . ' ' . theme_webform_token_help(),
+ '#cols' => 60,
+ '#rows' => 5,
+ '#weight' => 0,
+ );
+
+ $form['display']['hidden_type'] = array(
+ '#type' => 'radios',
+ '#options' => array(
+ 'value' => t('Secure value (allows use of all tokens)'),
+ 'hidden' => t('Hidden element (less secure, changeable via JavaScript)'),
+ ),
+ '#title' => t('Hidden type'),
+ '#description' => t('Both types of hidden fields are not shown to end-users. Using a <em>Secure value</em> allows the use of <em>all tokens</em>, even for anonymous users.'),
+ '#default_value' => $component['extra']['hidden_type'],
+ '#parents' => array('extra', 'hidden_type'),
+ );
+
+ return $form;
+ }
+}
diff --git a/src/Plugin/Component/Markup.php b/src/Plugin/Component/Markup.php
new file mode 100644
index 0000000..d405c96
--- /dev/null
+++ b/src/Plugin/Component/Markup.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\Markup.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'markup' component.
+ *
+ * @Component(
+ * id = "markup",
+ * label = @Translation("Markup"),
+ * description = @Translation("A field to display custom HTML on a form.")
+ * )
+ */
+class Markup extends ComponentBase {
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ $form['value'] = array(
+ '#type' => 'text_format',
+ '#title' => t('Value'),
+ '#default_value' => $component['value'],
+ '#description' => t('Markup allows you to enter custom HTML into your form.') . ' ' . theme('webform_token_help', array('groups' => array('node', 'submission'))),
+ '#weight' => -1,
+ '#format' => $component['extra']['format'],
+ '#element_validate' => array('_webform_edit_markup_validate'),
+ );
+
+ $form['display']['display_on'] = array(
+ '#type' => 'select',
+ '#title' => t('Display on'),
+ '#default_value' => $component['extra']['display_on'],
+ '#options' => array(
+ 'form' => t('form only'),
+ 'display' => t('viewed submission only'),
+ 'both' => t('both form and viewed submission'),
+ ),
+ '#weight' => 1,
+ '#parents' => array('extra', 'display_on'),
+ );
+
+ return $form;
+ }
+}
diff --git a/src/Plugin/Component/Number.php b/src/Plugin/Component/Number.php
new file mode 100644
index 0000000..3734c2d
--- /dev/null
+++ b/src/Plugin/Component/Number.php
@@ -0,0 +1,187 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\Number.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'number' component.
+ *
+ * @Component(
+ * id = "number",
+ * label = @Translation("Number"),
+ * description = @Translation("A field that accepts numerical values only.")
+ * )
+ */
+class Number extends ComponentBase {
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ $form['value'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Default value'),
+ '#default_value' => $component['value'],
+ '#description' => t('The default value of the field.') . ' ' . theme_webform_token_help(),
+ '#size' => 60,
+ '#maxlength' => 1024,
+ '#weight' => 0,
+ );
+ $form['display']['type'] = array(
+ '#type' => 'radios',
+ '#title' => t('Element type'),
+ '#options' => array(
+ 'textfield' => t('Text field'),
+ 'select' => t('Select list'),
+ ),
+ '#default_value' => $component['extra']['type'],
+ '#description' => t('A minimum and maximum value are required if displaying as a select.'),
+ '#weight' => -1,
+ '#parents' => array('extra', 'type'),
+ );
+ $form['display']['field_prefix'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Prefix text placed to the left of the field'),
+ '#default_value' => $component['extra']['field_prefix'],
+ '#description' => t('Examples: $, #, -.'),
+ '#size' => 20,
+ '#maxlength' => 127,
+ '#weight' => 1.1,
+ '#parents' => array('extra', 'field_prefix'),
+ );
+ $form['display']['field_suffix'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Postfix text placed to the right of the field'),
+ '#default_value' => $component['extra']['field_suffix'],
+ '#description' => t('Examples: lb, kg, %.'),
+ '#size' => 20,
+ '#maxlength' => 127,
+ '#weight' => 1.2,
+ '#parents' => array('extra', 'field_suffix'),
+ );
+ $form['display']['disabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Disabled'),
+ '#return_value' => 1,
+ '#description' => t('Make this field non-editable. Useful for setting an unchangeable default value.'),
+ '#weight' => 11,
+ '#default_value' => $component['extra']['disabled'],
+ '#parents' => array('extra', 'disabled'),
+ );
+ $decimal_options = array('' => t('Automatic')) + range(0, 10);
+ $form['display']['decimals'] = array(
+ '#type' => 'select',
+ '#title' => t('Decimal places'),
+ '#options' => array_combine($decimal_options, $decimal_options),
+ '#description' => t('Automatic will display up to @count decimals places if needed. A value of "2" is common to format currency amounts.', array('@count' => '4')),
+ '#default_value' => $component['extra']['decimals'],
+ '#weight' => 2,
+ '#parents' => array('extra', 'decimals'),
+ '#element_validate' => array('_webform_edit_number_validate'),
+ );
+ $form['display']['separator'] = array(
+ '#type' => 'select',
+ '#title' => t('Thousands separator'),
+ '#options' => array(
+ ',' => t('Comma (,)'),
+ '.' => t('Period (.)'),
+ ' ' => t('Space ( )'),
+ '' => t('None'),
+ ),
+ '#default_value' => $component['extra']['separator'],
+ '#weight' => 3,
+ '#parents' => array('extra', 'separator'),
+ '#element_validate' => array('_webform_edit_number_validate'),
+ );
+ $form['display']['point'] = array(
+ '#type' => 'select',
+ '#title' => t('Decimal point'),
+ '#options' => array(
+ ',' => t('Comma (,)'),
+ '.' => t('Period (.)'),
+ ),
+ '#default_value' => $component['extra']['point'],
+ '#weight' => 4,
+ '#parents' => array('extra', 'point'),
+ '#element_validate' => array('_webform_edit_number_validate'),
+ );
+ $form['validation']['unique'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Unique'),
+ '#return_value' => 1,
+ '#description' => t('Check that all entered values for this field are unique. The same value is not allowed to be used twice.'),
+ '#weight' => 1,
+ '#default_value' => $component['extra']['unique'],
+ '#parents' => array('extra', 'unique'),
+ );
+ $form['validation']['integer'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Integer'),
+ '#return_value' => 1,
+ '#description' => t('Permit only integer values as input. e.g. 12.34 would be invalid.'),
+ '#weight' => 1.5,
+ '#default_value' => $component['extra']['integer'],
+ '#parents' => array('extra', 'integer'),
+ );
+ $form['validation']['min'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Minimum'),
+ '#default_value' => $component['extra']['min'],
+ '#description' => t('Minimum numeric value. e.g. 0 would ensure positive numbers.'),
+ '#size' => 5,
+ '#maxlength' => 10,
+ '#weight' => 2.1,
+ '#parents' => array('extra', 'min'),
+ '#element_validate' => array('_webform_edit_number_validate'),
+ );
+ $form['validation']['max'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Maximum'),
+ '#default_value' => $component['extra']['max'],
+ '#description' => t('Maximum numeric value. This may also determine the display width of your field.'),
+ '#size' => 5,
+ '#maxlength' => 10,
+ '#weight' => 2.2,
+ '#parents' => array('extra', 'max'),
+ '#element_validate' => array('_webform_edit_number_validate'),
+ );
+ $form['validation']['step'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Step'),
+ '#default_value' => $component['extra']['step'],
+ '#description' => t('Limit options to a specific increment. e.g. a step of "5" would allow values 5, 10, 15, etc.'),
+ '#size' => 5,
+ '#maxlength' => 10,
+ '#weight' => 3,
+ '#parents' => array('extra', 'step'),
+ '#element_validate' => array('_webform_edit_number_validate'),
+ );
+ // Analysis settings.
+ $form['analysis'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Analysis'),
+ '#collapsible' => TRUE,
+ '#collapsed' => FALSE,
+ '#weight' => 10,
+ );
+ $form['analysis']['excludezero'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Exclude zero'),
+ '#return_value' => 1,
+ '#description' => t('Exclude entries of zero (or blank) when counting submissions to calculate average and standard deviation.'),
+ '#weight' => 1.5,
+ '#default_value' => $component['extra']['excludezero'],
+ '#parents' => array('extra', 'excludezero'),
+ );
+
+ return $form;
+ }
+}
diff --git a/src/Plugin/Component/PageBreak.php b/src/Plugin/Component/PageBreak.php
new file mode 100644
index 0000000..d59db42
--- /dev/null
+++ b/src/Plugin/Component/PageBreak.php
@@ -0,0 +1,52 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\PageBreak.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'pagebreak' component.
+ *
+ * @Component(
+ * id = "pagebreak",
+ * label = @Translation("Page Break"),
+ * description = @Translation("Creates a way to have multi-page forms.")
+ * )
+ */
+class PageBreak extends ComponentBase {
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ // Force the parent to always be root.
+ $form['position']['pid'] = array(
+ '#type' => 'hidden',
+ '#value' => '0',
+ );
+
+ $form['extra']['next_page_label'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Next page button label'),
+ '#description' => t('This is used for the <em>Next Page</em> button on the page before this page break. Default: <em>Next Page ></em>'),
+ '#default_value' => $component['extra']['next_page_label'],
+ '#size' => 30,
+ );
+ $form['extra']['prev_page_label'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Previous page button label'),
+ '#description' => t('This is used for the <em>Previous Page</em> button on the page after this page break. Default: <em>< Prev Page</em>'),
+ '#default_value' => $component['extra']['prev_page_label'],
+ '#size' => 30,
+ );
+
+ return $form;
+ }
+}
diff --git a/src/Plugin/Component/SelectOptions.php b/src/Plugin/Component/SelectOptions.php
new file mode 100644
index 0000000..f780900
--- /dev/null
+++ b/src/Plugin/Component/SelectOptions.php
@@ -0,0 +1,201 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\SelectOptions.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'selectoptions' component.
+ *
+ * @Component(
+ * id = "selectoptions",
+ * label = @Translation("Select Options"),
+ * description = @Translation("A field that displays options in a select box.")
+ * )
+ */
+class SelectOptions extends ComponentBase {
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ // @TODO Figure out the appropriate way to attach JS to a form.
+ // $form['#attached'] = array(
+ // 'js' => array(
+ // drupal_get_path('module', 'webform') . '/js/select-admin.js' => array('preprocess' => FALSE),
+ // array('data' => array('webform' => array('selectOptionsUrl' => \Drupal\Core\Url::fromUri('base://webform/ajax/options/' . $component['nid']))), 'type' => 'setting'),
+ // ),
+ // );
+
+ $other = array();
+ if ($info = $this->WebformSelectOptionsInfo()) {
+ $options = array('' => t('None'));
+ foreach ($info as $name => $source) {
+ $options[$name] = $source['title'];
+ }
+
+ $other['options_source'] = array(
+ '#title' => t('Load a pre-built option list'),
+ '#type' => 'select',
+ '#options' => $options,
+ '#default_value' => $component['extra']['options_source'],
+ '#weight' => 1,
+ '#description' => t('Use a pre-built list of options rather than entering options manually. Options will not be editable if using pre-built list.'),
+ '#parents' => array('extra', 'options_source'),
+ '#weight' => 5,
+ );
+ }
+
+ if (\Drupal::moduleHandler()->moduleExists('select_or_other')) {
+ $other['other_option'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Allow "Other..." option'),
+ '#default_value' => $component['extra']['other_option'],
+ '#description' => t('Check this option if you want to allow users to enter an option not on the list.'),
+ '#parents' => array('extra', 'other_option'),
+ '#attributes' => array('class' => array('other-option-checkbox')),
+ '#weight' => 2,
+ );
+ $other['other_text'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Text for "Other..." option'),
+ '#default_value' => $component['extra']['other_text'],
+ '#description' => t('If allowing other options, enter text to be used for other-enabling option.'),
+ '#parents' => array('extra', 'other_text'),
+ '#weight' => 3,
+ '#states' => array(
+ 'visible' => array(
+ ':input.other-option-checkbox' => array('checked' => TRUE),
+ ),
+ ),
+ );
+ }
+
+ if (\Drupal::moduleHandler()->moduleExists('options_element')) {
+ $options = _webform_select_options($component, FALSE, FALSE);
+
+ $form['items'] = array(
+ '#type' => 'fieldset',
+ '#title' => t('Options'),
+ '#collapsible' => TRUE,
+ '#attributes' => array('class' => array('webform-options-element')),
+ '#element_validate' => array('_webform_edit_validate_options'),
+ '#weight' => 2,
+ );
+
+ $form['items']['options'] = array(
+ '#type' => 'options',
+ '#limit' => 500,
+ '#optgroups' => TRUE,
+ '#multiple' => $component['extra']['multiple'],
+ '#multiple_toggle' => t('Multiple'),
+ '#default_value' => $component['value'],
+ '#options' => $options,
+ '#options_readonly' => !empty($component['extra']['options_source']),
+ '#key_type' => 'mixed',
+ '#key_type_toggle' => t('Customize keys (Advanced)'),
+ '#key_type_toggled' => $component['extra']['custom_keys'],
+ '#default_value_pattern' => '^%.+\[.+\]$',
+ '#weight' => 1,
+ );
+
+ $form['items']['options']['option_settings'] = $other;
+ }
+ else {
+ $form['extra']['items'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Options'),
+ '#default_value' => $component['extra']['items'],
+ '#description' => t('<strong>Key-value pairs MUST be specified as "safe_key|Some readable option"</strong>. Use of only alphanumeric characters and underscores is recommended in keys. One option per line. Option groups may be specified with <Group Name>. <> can be used to insert items at the root of the menu after specifying a group.') . ' ' . theme_webform_token_help(),
+ '#cols' => 60,
+ '#rows' => 5,
+ '#weight' => 0,
+ '#required' => TRUE,
+ '#wysiwyg' => FALSE,
+ '#element_validate' => array('_webform_edit_validate_select'),
+ );
+
+ if (!empty($component['extra']['options_source'])) {
+ $form['extra']['items']['#attributes'] = array('readonly' => 'readonly');
+ }
+
+ $form['extra'] = array_merge($form['extra'], $other);
+ $form['value'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Default value'),
+ '#default_value' => $component['value'],
+ '#description' => t('The default value of the field identified by its key. For multiple selects use commas to separate multiple defaults.') . ' ' . theme_webform_token_help(),
+ '#size' => 60,
+ '#maxlength' => 1024,
+ '#weight' => 0,
+ );
+ $form['extra']['multiple'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Multiple'),
+ '#default_value' => $component['extra']['multiple'],
+ '#description' => t('Check this option if the user should be allowed to choose multiple values.'),
+ '#weight' => 0,
+ );
+ }
+
+ $form['display']['aslist'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Listbox'),
+ '#default_value' => $component['extra']['aslist'],
+ '#description' => t('Check this option if you want the select component to be displayed as a select list box instead of radio buttons or checkboxes. Option groups (nested options) are only supported with listbox components.'),
+ '#parents' => array('extra', 'aslist'),
+ );
+ $form['display']['empty_option'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Empty option'),
+ '#default_value' => $component['extra']['empty_option'],
+ '#size' => 60,
+ '#maxlength' => 255,
+ '#description' => t('The list item to show when no default is provided. Leave blank for "- None -" or "- Select -".'),
+ '#parents' => array('extra', 'empty_option'),
+ '#states' => array(
+ 'visible' => array(
+ ':input[name="extra[aslist]"]' => array('checked' => TRUE),
+ ':input[name="extra[multiple]"]' => array('checked' => FALSE),
+ ':input[name="value"]' => array('filled' => FALSE),
+ ),
+ ),
+ );
+ $form['display']['optrand'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Randomize options'),
+ '#default_value' => $component['extra']['optrand'],
+ '#description' => t('Randomizes the order of the options when they are displayed in the form.'),
+ '#parents' => array('extra', 'optrand'),
+ );
+
+ return $form;
+ }
+ /**
+ * Load Webform select option info from 3rd party modules.
+ */
+ private function WebformSelectOptionsInfo() {
+ static $info;
+ if (!isset($info)) {
+ $info = array();
+
+ foreach (\Drupal::moduleHandler()->getImplementations('webform_select_options_info') as $module) {
+ $additions = \Drupal::moduleHandler()->invoke($module, 'webform_select_options_info');
+ foreach ($additions as $key => $addition) {
+ $additions[$key]['module'] = $module;
+ }
+ $info = array_merge($info, $additions);
+ }
+ \Drupal::moduleHandler()->alter('webform_select_options_info', $info);
+ }
+ return $info;
+ }
+
+}
diff --git a/src/Plugin/Component/TextArea.php b/src/Plugin/Component/TextArea.php
new file mode 100644
index 0000000..4ce49ed
--- /dev/null
+++ b/src/Plugin/Component/TextArea.php
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\Textarea.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'textarea' component.
+ *
+ * @Component(
+ * id = "textarea",
+ * label = @Translation("Textarea"),
+ * description = @Translation("A textarea field.")
+ * )
+ */
+class Textarea extends ComponentBase {
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ $form['value'] = array(
+ '#type' => 'textarea',
+ '#title' => t('Default value'),
+ '#default_value' => $component['value'],
+ '#description' => t('The default value of the field.') . ' ' . theme_webform_token_help(),
+ '#cols' => 60,
+ '#rows' => 5,
+ '#weight' => 0,
+ );
+ $form['display']['cols'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Width'),
+ '#default_value' => $component['extra']['cols'],
+ '#description' => t('Width of the textarea in columns. This property might not have a visual impact depending on the CSS of your site.') . ' ' . t('Leaving blank will use the default size.'),
+ '#size' => 5,
+ '#maxlength' => 10,
+ '#parents' => array('extra', 'cols'),
+ );
+ $form['display']['rows'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Height'),
+ '#default_value' => $component['extra']['rows'],
+ '#description' => t('Height of the textarea in rows.') . ' ' . t('Leaving blank will use the default size.'),
+ '#size' => 5,
+ '#maxlength' => 10,
+ '#parents' => array('extra', 'rows'),
+ );
+ $form['display']['resizable'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Resizable'),
+ '#description' => t('Make this field resizable by the user.'),
+ '#weight' => 2,
+ '#default_value' => $component['extra']['resizable'],
+ '#parents' => array('extra', 'resizable'),
+ );
+ $form['display']['placeholder'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Placeholder'),
+ '#default_value' => $component['extra']['placeholder'],
+ '#description' => t('The placeholder will be shown in the field until the user starts entering a value.'),
+ '#parents' => array('extra', 'placeholder'),
+ );
+ $form['display']['disabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Disabled'),
+ '#return_value' => 1,
+ '#description' => t('Make this field non-editable. Useful for setting an unchangeable default value.'),
+ '#weight' => 11,
+ '#default_value' => $component['extra']['disabled'],
+ '#parents' => array('extra', 'disabled'),
+ );
+
+ return $form;
+ }
+}
diff --git a/src/Plugin/Component/TextField.php b/src/Plugin/Component/TextField.php
new file mode 100644
index 0000000..bab52df
--- /dev/null
+++ b/src/Plugin/Component/TextField.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\TextField.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'textfield' component.
+ *
+ * @Component(
+ * id = "textfield",
+ * label = @Translation("Text Field"),
+ * description = @Translation("A textfield field.")
+ * )
+ */
+class TextField extends ComponentBase {
+
+ public function render($component, $value = NULL, $filter = TRUE, $submission = NULL) {
+ $element = array(
+ '#type' => 'textfield',
+ '#title' => $filter ? webform_filter_xss($component['name']) : $component['name'],
+ '#title_display' => $component['extra']['title_display'] ? $component['extra']['title_display'] : 'before',
+ '#default_value' => $filter ? webform_replace_tokens($component['value'], $node) : $component['value'],
+ '#required' => $component['required'],
+ '#weight' => $component['weight'],
+ '#field_prefix' => empty($component['extra']['field_prefix']) ? NULL : ($filter ? webform_filter_xss($component['extra']['field_prefix']) : $component['extra']['field_prefix']),
+ '#field_suffix' => empty($component['extra']['field_suffix']) ? NULL : ($filter ? webform_filter_xss($component['extra']['field_suffix']) : $component['extra']['field_suffix']),
+ '#description' => $filter ? webform_filter_descriptions($component['extra']['description'], $node) : $component['extra']['description'],
+ '#attributes' => $component['extra']['attributes'],
+ '#theme_wrappers' => array('webform_element'),
+ '#translatable' => array('title', 'description', 'field_prefix', 'field_suffix'),
+ );
+ }
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ $form['value'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Default value'),
+ '#default_value' => $component['value'],
+ '#description' => t('The default value of the field.') . ' ' . theme_webform_token_help(),
+ '#size' => 60,
+ '#maxlength' => 1024,
+ '#weight' => 0,
+ );
+ $form['display']['width'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Width'),
+ '#default_value' => $component['extra']['width'],
+ '#description' => t('Width of the textfield.') . ' ' . t('Leaving blank will use the default size.'),
+ '#size' => 5,
+ '#maxlength' => 10,
+ '#weight' => 0,
+ '#parents' => array('extra', 'width'),
+ );
+ $form['display']['placeholder'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Placeholder'),
+ '#default_value' => $component['extra']['placeholder'],
+ '#description' => t('The placeholder will be shown in the field until the user starts entering a value.'),
+ '#weight' => 1,
+ '#parents' => array('extra', 'placeholder'),
+ );
+ $form['display']['field_prefix'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Prefix text placed to the left of the textfield'),
+ '#default_value' => $component['extra']['field_prefix'],
+ '#description' => t('Examples: $, #, -.'),
+ '#size' => 20,
+ '#maxlength' => 127,
+ '#weight' => 2.1,
+ '#parents' => array('extra', 'field_prefix'),
+ );
+ $form['display']['field_suffix'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Postfix text placed to the right of the textfield'),
+ '#default_value' => $component['extra']['field_suffix'],
+ '#description' => t('Examples: lb, kg, %.'),
+ '#size' => 20,
+ '#maxlength' => 127,
+ '#weight' => 2.2,
+ '#parents' => array('extra', 'field_suffix'),
+ );
+ $form['display']['disabled'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Disabled'),
+ '#return_value' => 1,
+ '#description' => t('Make this field non-editable. Useful for setting an unchangeable default value.'),
+ '#weight' => 11,
+ '#default_value' => $component['extra']['disabled'],
+ '#parents' => array('extra', 'disabled'),
+ );
+ $form['validation']['unique'] = array(
+ '#type' => 'checkbox',
+ '#title' => t('Unique'),
+ '#return_value' => 1,
+ '#description' => t('Check that all entered values for this field are unique. The same value is not allowed to be used twice.'),
+ '#weight' => 1,
+ '#default_value' => $component['extra']['unique'],
+ '#parents' => array('extra', 'unique'),
+ );
+ $form['validation']['maxlength'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Maxlength'),
+ '#default_value' => $component['extra']['maxlength'],
+ '#description' => t('Maximum length of the textfield value.'),
+ '#size' => 5,
+ '#maxlength' => 10,
+ '#weight' => 2,
+ '#parents' => array('extra', 'maxlength'),
+ );
+
+ return $form;
+ }
+}
diff --git a/src/Plugin/Component/Time.php b/src/Plugin/Component/Time.php
new file mode 100644
index 0000000..d383cb3
--- /dev/null
+++ b/src/Plugin/Component/Time.php
@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\webform\Plugin\Component\Time.
+ */
+
+namespace Drupal\webform\Plugin\Component;
+
+use Drupal\webform\ComponentBase;
+
+/**
+ * Provides a 'time' component.
+ *
+ * @Component(
+ * id = "time",
+ * label = @Translation("Time"),
+ * description = @Translation("A special textfield that accepts time entries.")
+ * )
+ */
+class Time extends ComponentBase {
+
+ public function buildForm(array $form, \Drupal\Core\Form\FormStateInterface $form_state, $node = NULL) {
+ $component = $this->configuration;
+
+ // Begin with the parent object's basic form.
+ $form = parent::buildForm($form, $form_state, $node);
+
+ $form['value'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Default value'),
+ '#default_value' => $component['value'],
+ '#description' => t('The default value of the field.') . '<br />' . t('Accepts a time in any <a href="http://www.gnu.org/software/tar/manual/html_chapter/Date-input-formats.html">GNU Date Input Format</a>. Strings such as now, +2 hours, and 10:30pm are all valid.'),
+ '#size' => 60,
+ '#maxlength' => 127,
+ '#weight' => 0,
+ );
+ $form['validation']['start_time'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Start time'),
+ '#default_value' => $component['extra']['start_time'],
+ '#description' => t('The earliest time that may be entered into the field.'),
+ '#size' => 10,
+ '#weight' => 3,
+ '#parents' => array('extra', 'start_time'),
+ );
+ $form['validation']['end_time'] = array(
+ '#type' => 'textfield',
+ '#title' => t('End time'),
+ '#default_value' => $component['extra']['end_time'],
+ '#description' => t('The latest time that may be entered into the field.'),
+ '#size' => 10,
+ '#weight' => 4,
+ '#parents' => array('extra', 'end_time'),
+ );
+ $form['extra']['timezone'] = array(
+ '#type' => 'radios',
+ '#title' => t('Default value timezone'),
+ '#default_value' => $component['extra']['timezone'],
+ '#description' => t('If using relative dates for a default value (e.g. "now") base the current time on this timezone.'),
+ '#options' => array('user' => t('User timezone'), 'site' => t('Website timezone')),
+ '#weight' => 2,
+ '#access' => \Drupal::config('system.date')->get('timezone.user.configurable'),
+ );
+ $form['display']['hourformat'] = array(
+ '#type' => 'radios',
+ '#title' => t('Time format'),
+ '#default_value' => $component['extra']['hourformat'],
+ '#options' => array('12-hour' => t('12-hour (am/pm)'), '24-hour' => t('24-hour')),
+ '#weight' => 2,
+ '#parents' => array('extra', 'hourformat'),
+ );
+ $form['display']['minuteincrements'] = array(
+ '#type' => 'select',
+ '#title' => t('Minute increments'),
+ '#default_value' => $component['extra']['minuteincrements'],
+ '#options' => array(
+ 1 => t('1 minute'),
+ 5 => t('5 minute'),
+ 10 => t('10 minute'),
+ 15 => t('15 minute'),
+ 30 => t('30 minute'),
+ ),
+ '#weight' => 3,
+ '#parents' => array('extra', 'minuteincrements'),
+ );
+ $form['#validate'] = array('_webform_edit_time_validate');
+
+ return $form;
+ }
+}
diff --git a/webform.links.task.yml b/webform.links.task.yml
index 813fc72..c1cbeca 100644
--- a/webform.links.task.yml
+++ b/webform.links.task.yml
@@ -3,6 +3,13 @@ webform.content_overview:
route_name: webform.content_overview
base_route: system.admin_content
+
+webform.components:
+ route_name: webform.components
+ base_route: entity.node.canonical
+ title: 'Webform'
+ weight: 50
+
entity.node.webform:
title: Webform
route_name: entity.node.webform
diff --git a/webform.module b/webform.module
index e320e7a..ff65a9d 100644
--- a/webform.module
+++ b/webform.module
@@ -22,6 +22,7 @@ use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\webform\Form\WebformClientForm;
+use Drupal\webform\ComponentManager;
/**
* Constants used in conditional logic.
@@ -602,7 +603,7 @@ function webform_submission_access($node, $submission, $op = 'view', $account =
webform_disable_page_cache();
}
- $module_access = count(array_filter(\Drupal::moduleHandler()->invokeAll('webform_submission_access', $node, $submission, $op, $account))) > 0;
+ $module_access = count(array_filter(\Drupal::moduleHandler()->invokeAll('webform_submission_access', array($node, $submission, $op, $account)))) > 0;
switch ($op) {
case 'view':
@@ -1845,7 +1846,7 @@ function webform_node_view(array &$build, EntityInterface $node, EntityViewDispl
// special exception is made for this view mode only.
if ($view_mode == 'teaser' && !isset($node->webform['action'])) {
$query = array_diff_key($_GET, array('q' => ''));
- $node->webform['action'] = url('node/' . $node->id(), array('query' => $query));
+ $node->webform['action'] = \Drupal\Core\Url::fromUri('base://node/' . $node->id(), array('query' => $query))->toString();
}
// When logging in using a form on the same page as a webform node, suppress
@@ -1939,14 +1940,14 @@ function webform_node_view(array &$build, EntityInterface $node, EntityViewDispl
if (empty($node->in_preview) && !isset($node->webform_block) && !$logging_in) {
$build['webform_view_messages'] = [
'#theme' => 'webform_view_messages',
- 'node' => $node,
- 'page' => $page,
- 'submission_count' => $submission_count,
- 'user_limit_exceeded' => $user_limit_exceeded,
- 'total_limit_exceeded' => $total_limit_exceeded,
- 'allowed_roles' => $allowed_roles,
- 'closed' => $closed,
- 'cached' => $cached,
+ '#node' => $node,
+ '#page' => $page,
+ '#submission_count' => $submission_count,
+ '#user_limit_exceeded' => $user_limit_exceeded,
+ '#total_limit_exceeded' => $total_limit_exceeded,
+ '#allowed_roles' => $allowed_roles,
+ '#closed' => $closed,
+ '#cached' => $cached,
];
}
diff --git a/webform.routing.yml b/webform.routing.yml
index ad1307e..95cf305 100644
--- a/webform.routing.yml
+++ b/webform.routing.yml
@@ -13,6 +13,37 @@ webform.settings:
requirements:
_permission: 'administer site configuration'
+
+# @TODO: These probably need different permissions.
+# @TODO: This needs to be restricted to only show on webform node types.
+webform.components:
+ path: '/node/{node}/webform'
+ defaults:
+ _form: '\Drupal\webform\Form\WebformComponentsForm'
+ requirements:
+ _permission: 'administer site configuration'
+
+webform.component_add_form:
+ path: '/node/{node}/webform/components/new/{component}'
+ defaults:
+ _form: '\Drupal\webform\Form\WebformComponentAddForm'
+ requirements:
+ _permission: 'administer site configuration'
+
+webform.component_edit_form:
+ path: '/node/{node}/webform/components/edit/{cid}'
+ defaults:
+ _form: '\Drupal\webform\Form\WebformComponentEditForm'
+ requirements:
+ _permission: 'administer site configuration'
+
+webform.component_delete_form:
+ path: '/node/{node}/webform/components/delete/{cid}'
+ defaults:
+ _form: '\Drupal\webform\Form\WebformComponentDeleteForm'
+ requirements:
+ _permission: 'administer site configuration'
+
# @todo Only show on webform enabled node types
entity.node.webform:
path: '/node/{node}/webform'
diff --git a/webform.services.yml b/webform.services.yml
new file mode 100644
index 0000000..afcbfa2
--- /dev/null
+++ b/webform.services.yml
@@ -0,0 +1,4 @@
+services:
+ plugin.manager.webform.component:
+ class: Drupal\webform\ComponentManager
+ parent: default_plugin_manager
Comments