Drupal: Parent-child draggable table - drupal

So I've been going at this one for a while now. I'm trying to create a draggable table that has a parent-child relationship, but where the children cannot be moved out of the parent group, and all of the parents are sortable among each other. I've modeled my form and theme off of the admin menu code, and I have it duplicating that functionality. The problem is that I can move the children to another parent, or let it become a parent. As an illustration:
Category 1
|
|--Item 1
|--Item 2
Category 2
|
|--Item 3
|--Item 4
|--Item 5
I would like to be able to sort Item 1 and Item 2 with each other, and Item 3, Item 4, and Item 5 with each other, but not move them between Category 1 and Category 2. I also need to be able to sort Category 1 and Category 2 with one another, taking the children with them. I've went through so many combinations of $action, $group, $subgroup settings mixed with $class settings for the categories and items that I've lost track. Nothing I have tried so far has produced the desired result. Here's the relevant bits of my code as it is currently:
In my form:
$form['#tree'] = true;
foreach($categories as $cat) {
if(!isset($form['categories'][$cat->cid])){
$form['categories'][$cat->cid] = array(
'weight' => array(
'#type' => 'weight',
'#delta' => 25,
'#attributes' => array('class' => array('item-weight', 'item-weight-' . $cat->cid)),
),
'cid' => array(
'#type' => 'hidden',
'#value' => $cat->cid,
'#attributes' => array('class' => array('cid')),
),
);
foreach($cats[$cat->cid] as $item) {
$form['categories'][$cat->cid]['items'][$item->id] = array(
'weight' => array(
'#type' => 'weight',
'#delta' => 25,
'#default_value'=> $item->weight,
'#attributes' => array('class' => array('item-weight', 'item-weight-' . $cat->cid)),
),
'cid' => array(
'#type' => 'hidden',
'#value' => $cat->cid,
'#attributes' => array('class' => array('cid')),
),
);
}
}
}
In my theme:
$children = element_children($form['categories']);
$rows = array();
if(count($children) > 0) {
foreach($children as $cid) {
$row = array(
drupal_render($form['categories'][$cid]['weight']) .
drupal_render($form['categories'][$cid]['cid']),
);
$rows[] = array(
'data' => $row,
'class' => array('draggable', 'tabledrag-root'),
);
foreach(element_children($form['categories'][$cid]['items']) as $id) {
$row = array(
theme('indentation', array('size' => 1)) . drupal_render($form['categories'][$cid]['items'][$id]['name']),
drupal_render($form['categories'][$cid]['items'][$id]['weight']) .
drupal_render($form['categories'][$cid]['items'][$id]['cid']),
);
$rows[] = array(
'data' => $row,
'class' => array('draggable', 'tabledrag-leaf'),
);
}
drupal_add_tabledrag('cat-table', 'order', 'sibling', 'item-weight', 'item-weight-' . $cid);
}
}
drupal_add_tabledrag('cat-table', 'match', 'parent', 'cid', 'cid', 'cid', true, 1);
$output = theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => 'cat-table')));
$output .= drupal_render_children($form);
return $output;
I've read over the documentation for drupal_add_tabledrag(), looked at the code, looked at example code, and searched around drupal.org and Google, but haven't come up with anything.
My only solution so far is to copy and modify the tabledrag.js file to just eliminate those capabilities, but while stopping the indent problem with the items (meaning, not letting them be on the same as the categories), keeping them in the same category has been Not Fun.
I suppose the most important question is, using standard Drupal is this possible?

I know you've already done a lot of coding so you might not want to give it up at this point, but DraggableViews is great to accomplish this. You can set up a normal view and add this draggableviews filter, it adds a weight and optionally a parent reference. The view itself uses the same drag-n-drop system as the rest of Drupal's backend tables.
Alternatively you can use a term reference and tie taxonomy terms to nodes, and just use that drag-n-drop.
If I'm missing something in your needs, my apologies, just thought I'd offer this simpler solution as it has definitely served me well in the past. Best of luck either way.

Just finished adding this functionality to my module
https://github.com/player259/ajax_table
There is no help, demo is outdated, but I'm working on it from time to time
Sections support was achieved by overriding tabledrag.js functions
Use this snippet to insert table
$form['map'] = array(
'#type' => 'ajax_table',
'#header' => array(t('Element'), t('Settings'), t('Weight')),
'rows' => array(),
'#draggable' => array(
// drupal_add_tabledrag will be called in theme layer
// NULL first arg to apply to this table
array(NULL, 'match', 'parent', 'perfect-form-parent', 'perfect-form-parent', 'perfect-form-index'),
array(NULL, 'depth', 'group', 'perfect-form-depth', NULL, NULL, FALSE),
array(NULL, 'order', 'sibling', 'perfect-form-weight'),
),
'#draggable_groups' => array(),
);
foreach ($map as $i => $element) {
// ... some logic
$form['map']['rows'][$i] = array(
'data' => array(
'element' => array(),
'settings' => array(),
'tabledrag' => array(
'index' => array(
'#type' => 'hidden',
'#value' => $element['data']['tabledrag']['index'],
'#attributes' => array('class' => array('perfect-form-index')),
),
'parent' => array(
'#type' => 'hidden',
'#default_value' => $element['data']['tabledrag']['parent'],
'#attributes' => array('class' => array('perfect-form-parent')),
),
'depth' => array(
'#type' => 'hidden',
'#default_value' => $element['data']['tabledrag']['depth'],
'#attributes' => array('class' => array('perfect-form-depth')),
),
'weight' => array(
'#type' => 'weight',
'#delta' => $max_weight,
'#default_value' => $weight,
'#attributes' => array('class' => array('perfect-form-weight')),
),
),
),
'#attributes' => array('class' => array($row_class_current, $row_class_child)),
);
// This means that row with $row_class_child class could have as parent
// only row with $row_class_parent class
// NULL means root - there are no parents
$form['map']['#draggable_groups'][$row_class_child] =
$depth ? $row_class_parent : NULL;
}

I had a similar problem at work so posting here my solution since none i found worked correctly in all situation. It is done 100% in javascript, on the php side you just have to set tabledrag in match with parent on pid and sort with siblings on weight.
The current code work on the example module (tabledrag parent/child) to adapt it to your need, change the .example-item-pid by your class for the PID input field. You just need to add it to the example code to have it working and see if it corresponds to your need.
First function invalidate any attempt to drop elements that don't have the same parent (PID) than the target element.
Second Function bypass the dragRow function to drop the element in the correct place (= the last children of the target row) and at the right depth ( = same depth than the target row).
/**
* Invalidate swap check if the row target is not of the same parent
* So we can only sort elements under the same parent and not move them to another parent
*
* #override Drupal.tableDrag.row.isValidSwap
*/
// Keep the original implementation - we still need it.
Drupal.tableDrag.prototype.row.prototype._isValidSwap = Drupal.tableDrag.prototype.row.prototype.isValidSwap;
Drupal.tableDrag.prototype.row.prototype.isValidSwap = function(row) {
if (this.indentEnabled) {
if (row && $('.example-item-pid', this.element).val() !== $('.example-item-pid', row).val()) {
return false;
}
}
// Return the original result.
return this._isValidSwap(row);
}
/**
* Position the dragged element under the last children of the element target for swapping when moving down our dragged element.
* Removed the indentation, since we can not change parent.
* #override Drupal.tableDrag.row.dragRow
*/
Drupal.tableDrag.prototype.dragRow = function (event, self) {
if (self.dragObject) {
self.currentMouseCoords = self.mouseCoords(event);
var y = self.currentMouseCoords.y - self.dragObject.initMouseOffset.y;
var x = self.currentMouseCoords.x - self.dragObject.initMouseOffset.x;
// Check for row swapping and vertical scrolling.
if (y != self.oldY) {
self.rowObject.direction = y > self.oldY ? 'down' : 'up';
self.oldY = y; // Update the old value.
// Check if the window should be scrolled (and how fast).
var scrollAmount = self.checkScroll(self.currentMouseCoords.y);
// Stop any current scrolling.
clearInterval(self.scrollInterval);
// Continue scrolling if the mouse has moved in the scroll direction.
if (scrollAmount > 0 && self.rowObject.direction == 'down' || scrollAmount < 0 && self.rowObject.direction == 'up') {
self.setScroll(scrollAmount);
}
// If we have a valid target, perform the swap and restripe the table.
var currentRow = self.findDropTargetRow(x, y);
if (currentRow) {
if (self.rowObject.direction == 'down') {
/**
* When going down we want to position the element after the last children and not right under the currentRow
*/
// create a new row prototype with currentRow
var rowObject = new self.row(currentRow, 'mouse', self.indentEnabled, self.maxDepth, false);
// extract all children
var childrenRows = rowObject.findChildren();
// if we have children
if (childrenRows.length > 0) {
// we change the row to swap with the last children
currentRow = childrenRows[childrenRows.length - 1];
}
self.rowObject.swap('after', currentRow, self);
}
else {
self.rowObject.swap('before', currentRow, self);
}
self.restripeTable();
}
}
/**
* We have disabled the indentation changes since it is not possible to change parent.
*/
return false;
}
};

Related

Drupal 6: Checkboxes table not rendering properly

I'm working on a Drupal 6 module which I want to generate a table with checkboxes in each row from data I have saved in a database. The table is being generated fine, but the checkboxes are not rendering in the table but are instead having their node id's put below the table. See the screenshot below:
"21" is the node id of "Test Question 01", and "19" is the node id of "Test Question 02".
The code I'm using (yes, it is all in a theme function which isn't ideal. I'm planning on moving stuff around once the checkboxes problem is resolved):
function theme_qt_assignment_questions_table($form) {
// Get the questions from the database
$db_results = db_query('SELECT {qt_questions}.nid, title, lesson, unit FROM node INNER JOIN {qt_questions} on {node}.nid = {qt_questions}.nid WHERE lesson = %d AND unit = %d',
$form['#lesson'], $form['#unit']);
// Define the headers for the table
$headers = array(
theme('table_select_header_cell'),
array('data' => t('Title'), 'field' => 'title'/*, 'sort' => 'asc'*/),
array('data' => t('Lesson'), 'field' => 'lesson'),
array('data' => t('Unit'), 'field' => 'unit'),
);
while($row = db_fetch_object($db_results)) {
$checkboxes[$row->nid] = '';
$form['nid'][$row->nid] = array(
'#value' => $row->nid
);
$form['title'][$row->nid] = array(
'#value' => $row->title
);
$form['lesson'][$row->nid] = array(
'#value' => $row->lesson
);
$form['unit'][$row->nid] = array(
'#value' => $row->unit
);
}
$form['checkboxes'] = array(
'#type' => 'checkboxes',
'#options' => $checkboxes,
);
// Add the questions to the table
if(!empty($form['checkboxes']['#options'])) {
foreach(element_children($form['nid']) as $nid) {
$questions[] = array(
drupal_render($form['checkboxes'][$nid]),
drupal_render($form['title'][$nid]),
drupal_render($form['lesson'][$nid]),
drupal_render($form['unit'][$nid]),
);
}
} else {
// If no query results, show as such in the table
$questions[] = array(array('data' => '<div class="error">No questions available for selected lesson and unit.</div>', 'colspan' => 4));
}
// Render the table and return the result
$output = theme('table', $headers, $questions);
$output .= drupal_render($form);
return $output;
}
Turns out my attempt at simplification of the problem was, in fact, the problem. Namely, doing everything in hook_theme isn't correct. Rather, I defined a function that pulls the info from database and creates the checkboxes array and call it in hook_form as such:
$form['questions_wrapper']['questions'] = _qt_get_questions_table($node->lesson, $node->unit);
At the end of this function (_qt_get_questions_table()), I specify the theme function which put everything into the table as such:
$form['#theme'] = 'qt_assignment_questions_table';
I'm still very new to Drupal so this explanation may not be the best to someone having the same problem, but hopefully it will help.

cannot get checkboxes value using drupal form api

i have form in drupal which uploads images and has got few checkboxes in it.
Here is the form:
$form['checklist_fieldset'] = array(
'#type' => 'fieldset',
'#title' => t('Check List'),
'#collapsible' => FALSE,
'#collapsed' => FALSE,
);
$form['checklist_fieldset']['heating'] = array(
'#type' => 'checkboxes',
'#title' => t('Heating options'),
'#options' => array(
'0' => t('Yes'),
'1' => t('No')
),
'#description' => t('Heating details.')
);
and here is my submit function where i am processing image upload and grabbing the checkboxes value as well. I am getting the success message and image is getting uploaded but not getting the value of check boxes.
function property_add_view_submit($form,&$form_state){
$validators = array();
if($file = file_save_upload('p_file1',$validators,file_direcotry_path)){
$heating = array_keys($form_state['values']['heating']);
drupal_set_message(t('Property Saved! '.$heating));
dpm( $form_state['values']['heating']);
}
When you use #options on a FAPI element the value passed to the $form_state is the array key, so you don't need to use array_keys().
I'm not sure why you're using checkboxes for a yes/no, usually one would use a simple checkbox element. However if that's really what you want to do:
Your #options can't contain on option with 0 as the array key, it will be automatically filtered out and you'll never know if that option has been checked.
You should use $heating_options_chosen = array_filter($form_state['values']['heating'] to get the selected checkbox options.
I honestly think your code should look like this though:
$form['checklist_fieldset']['heating'] = array(
'#type' => 'checkbox',
'#title' => t('Heating options'),
'#options' => array(
'1' => t('Yes'),
'0' => t('No')
),
'#description' => t('Heating details.')
);
$heating_checked = $form_state['values']['heating'] == 1;
If I have checkbox Friends and options are like
[ ] abc
[ ] def
[ ] ghi
[ ] jkl
And I want to know which options user have marked, then use below function.
if ($form_state->getValue('friends') != NULL) {
foreach ($form_state->getValue('friends') as $key => $value) {
if ($value != 0) {
$friends = $friends . ", " . $key;
$friends = substr_replace($friends, "", 0, 1);
}
}
}
If user has chosen abc and ghi then you will get 1,3 as result in $friends
If you wanted to know the value then use $friends = $friends.", ".$value;
it worked for me..hope it will help you as well :)

Drupal Block not showing on the page

$blocks['onemore'] = array(
'info' => t('onemore'),
'status' => TRUE,
'region' => 'content',
'weight' => 0,
'cache' => DRUPAL_NO_CACHE,
'visibility' => BLOCK_VISIBILITY_LISTED,
'pages' => 'admin/structure/nodequeue/1/view/1',
);
Problem - The above block shows up and works perfectly and as expected at 'admin/structure/nodequeue/1/view/1'
My problem is that I need to declare dynamic amounts of blocks based on the users inputs. So I wrote a db fetch and for each loop.
If I do this then the block shows up in 'admin/modules' but the it is not in 'content' region for the seven theme. As I want to show it there.
I have double checked the values and even the admin/structure/block/manage/xdmp/onemore/configure has the value but the region is not selected.
I am assuming there is some conflict in the for each loop or the db query. Please advice your thoughts on it.
function xdmp_block_info() {
$blocks = array();
// Here we are going to do a db query so that I can get a list of
// block ids to declare
$resultxdmp = db_query("
SELECT * FROM xdmp_container_list ");
foreach($resultxdmp as $resultRecords)
{
$xdmp_nodeque_id_to_display =(int)$resultRecords->xdmp_nodequeue_id;
$xdmp_nodeque_id_to_display = intval($xdmp_nodeque_id_to_display);
$xdmp_path_to_show_block = 'admin/structure/nodequeue/'.$xdmp_nodeque_id_to_display.'
/view/'.$xdmp_nodeque_id_to_display.'';
$xdmp_machinenameofblock=(string)$resultRecords->xdmp_container_machine_name;
$xdmp_nameofblock=(string)$resultRecords->xdmp_container_name;
$blocks[$xdmp_machinenameofblock] = array(
'info' => t($xdmp_nameofblock),
'status' => TRUE,
'region' => 'content',
'weight' => 0,
'cache' => DRUPAL_NO_CACHE,
'visibility' => BLOCK_VISIBILITY_LISTED,
'pages' => $xdmp_path_to_show_block,
);
} // end for for each
return $blocks;
}
cheers,
Vishal
Are you sure the 'content' region is valid? If it's not, it of course can't show up :)

How do I keep a Drupal form "markup" element from rendering inside the wrapper of the "submit" element?

I have programatically added a "markup" element to a Drupal form. When it renders on the page, the element appears in the wrapper for the submit button. Clarification: It should appear between the 'field_school_name_value' element and the 'distance' element. I have set the weights of every element in hopes that that would force the layout to be right, but it doesn't seems to help. What am I doing wrong?
<?php
function abq_misc_form_alter(&$form, &$form_state, $form_id) {
if ($form_id == 'views_exposed_form') {
$state_select = array(
'#attributes' => array(
'style' => 'width:10em;',
),
'#default_value' => 'All',
'#multiple' => FALSE,
'#options' => array_merge(array('All' => 'State'), location_get_provinces()),
'#title' => NULL,
'#type' => 'select',
'#weight' => 0,
);
$form['province'] = $state_select;
$school = &$form['field_school_name_value'];
$school['#attributes'] = array(
'size' => 15,
);
$school['#weight'] = 1;
// THIS GUY
$form['divider'] = array(
'#type' => 'item',
'#markup' => '<div>–or–</div>',
'#weight' => 2,
);
$form['distance']['#weight'] = 3;
$search_distance = &$form['distance']['search_distance'];
$search_distance['#attributes'] = array(
'placeholder' => 'miles',
'size' => '5',
);
$search_distance['#prefix'] = 'Within';
$search_distance['#suffix'] = 'of';
unset($search_distance['#title']);
$search_distance['#weight'] = 0;
$postal_code = &$form['distance']['postal_code'];
unset($postal_code['#title']);
$postal_code['#attributes'] = array(
'placeholder' => 'Zip Code',
'size' => '5',
);
$postal_code['#weight'] = 1;
hide($form['distance']['search_units']);
$form['submit']['#weight'] = 4;
}
}
I'm not sure why that's happening, there's no good reason for your element to be rendered inside the wrapper for the submit button.
An easy fix, though, would be to use the #prefix attribute on the submit button which would guarantee that your markup was rendered immediately before the wrapper for the submit button:
$form['submit']['#prefix'] = '<div>–or–</div>';
UPDATE
Just to address your edit, I think the same solution can apply if you set the #prefix of the distance element instead:
$form['distance']['#prefix'] = '<div>–or–</div>';
It's possible that there's some extra formatting done by another module that implements hook_form_alter which happens to run after yours, messing up your good work. By applying the prefix to the distance element you'll be ensuring it comes immediately before the field_school_name_value element.
I should mention that I've had problems with exactly this when taking a reference to an array member of the provided $form (which you're doing with $school = &$form['field_school_name_value'];). As an extra sanity check I'd recommend changing that bit of code to this and see if it helps (try this before the other suggestion above as it might just fix it):
$form['field_school_name_value']['#attributes'] = array('size' => 15);
$form['field_school_name_value']['#weight'] = 1;
instead of
$school = &$form['field_school_name_value'];
$school['#attributes'] = array(
'size' => 15,
);
$school['#weight'] = 1;
Although this is an almost 7 years old issue, I just run this problem.
Here is the solution: https://www.drupal.org/project/views/issues/2070533
First, you should declare your markup in your form_alter hook but the '#printed' key must be declared in your defining array. Then you should implement the template_preprocess_views_exposed_form() function in your module or in theme. Here you can render the required form element by drupal_render and add it to $variables. And finally you can print out the variable in the theme (as custom variable or among widgets).
But the simpliest version: clone the views-exposed-form.tpl.php to the template directory of your theme, name it as is suggested (eg. views-exposed-form--news.tpl.php) and print your linebreak where you want.

Drupal filter_form form input

This drupal form snippet will give me a textarea with user able to change filter to full html/wysiwyg mode.
My Questions: How can I default to to full html mode?
function MY_MODULE_admin() {
$form = array();
$form['format'] = filter_form($form->format);
// MY_MODULE - ** Image 1 **
$form['MY_MODULE_image_1'] = array(
'#type' => 'textarea',
'#title' => t('Image 1'),
'#default_value' => variable_get('setup_image_1', 'image_1.jpg'),
'#description' => "Current value =" .variable_get('setup_image_1', 'image_1.jpg'),
'#required' => TRUE,
);
This did the trick.
$form = array();
$form['carousel_setup_image_1']['accepted_text_1'] = array(
'#type' => 'textarea',
'#title' => t('Image 1 - Carousel '),
'#default_value' => variable_get('carousel_setup_image_1', 'carousel_image_1.jpg'),
'#description' => "Current value =" .variable_get('carousel_setup_image_1', 'carousel_image_1.jpg'),
);
$form['carousel_setup_image_1']['format'] = filter_form(2, NULL, array('accepted_text_1_format'));
Well there are two ways.
One, you can set that role's default format to Full HTML under Input Formats.
Two, you can say $form['format']['#default_value'] = 2 (I think Full HTML is 2). This will preselect Full HTML.
however, I am not sure why $edit['carousel_setup_image_1']['accepted_text_1']) does not contain entered value.
function carousel_setup_block($op = 'list', $delta = 0, $edit = array())
{
switch($op)
{
// case save (save configuration values)
case 'save':
variable_set('carousel_setup_image_1',
$edit['carousel_setup_image_1']['accepted_text_1']);
break;
}
}
For Drupal 7: The best way to handle (or leverage) the Drupal text filter system is to use not 'textarea', but the 'text_format' field type, like this, which will render a textarea with the filter select below:
$form['holycrap'] = array(
'#type' => 'text_format',
'#format' => NULL, // <- Let Drupal handle the default, based upon user role
'#title' => t('The HTML for the Holy Crap block above the Main Menu.'),
'#default_value' => variable_get('caplogin_holycrap', _caplogin_holycrap_default_html()),
);
return $form;

Resources