1<?php 2/** 3 * Table editor 4 * 5 * @author Adrian Lang <lang@cosmocode.de> 6 * @author Andreas Gohr <gohr@cosmocode.de> 7 */ 8 9use dokuwiki\Form\Form; 10use dokuwiki\Utf8; 11 12/** 13 * handles all the editor related things 14 * 15 * like displaying the editor and adding custom edit buttons 16 */ 17class action_plugin_edittable_editor extends DokuWiki_Action_Plugin 18{ 19 /** 20 * Register its handlers with the DokuWiki's event controller 21 */ 22 public function register(Doku_Event_Handler $controller) 23 { 24 // register custom edit buttons 25 $controller->register_hook('HTML_SECEDIT_BUTTON', 'BEFORE', $this, 'secedit_button'); 26 27 // register our editor 28 $controller->register_hook('EDIT_FORM_ADDTEXTAREA', 'BEFORE', $this, 'editform'); 29 $controller->register_hook('HTML_EDIT_FORMSELECTION', 'BEFORE', $this, 'editform'); 30 31 // register preprocessing for accepting editor data 32 // $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_table_post'); 33 $controller->register_hook('PLUGIN_EDITTABLE_PREPROCESS_EDITOR', 'BEFORE', $this, 'handle_table_post'); 34 } 35 36 /** 37 * Add a custom edit button under each table 38 * 39 * The target 'table' is provided by DokuWiki's XHTML core renderer in the table_close() method 40 * 41 * @param Doku_Event $event 42 */ 43 public function secedit_button(Doku_Event $event) 44 { 45 if ($event->data['target'] !== 'table') return; 46 47 $event->data['name'] = $this->getLang('secedit_name'); 48 } 49 50 /** 51 * Creates the actual Table Editor form 52 * 53 * @param Doku_Event $event 54 */ 55 public function editform(Doku_Event $event) 56 { 57 global $TEXT; 58 global $RANGE; 59 global $INPUT; 60 61 if ($event->data['target'] !== 'table') return; 62 if (!$RANGE){ 63 // section editing failed, use default editor instead 64 $event->data['target'] = 'section'; 65 return; 66 } 67 68 $event->stopPropagation(); 69 $event->preventDefault(); 70 71 /** @var renderer_plugin_edittable_json $Renderer our own renderer to convert table to array */ 72 $Renderer = plugin_load('renderer', 'edittable_json', true); 73 $instructions = p_get_instructions($TEXT); 74 75 // Loop through the instructions 76 foreach ($instructions as $instruction) { 77 // Execute the callback against the Renderer 78 call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1]); 79 } 80 81 // output data and editor field 82 83 /** @var Doku_Form $form */ 84 $form =& $event->data['form']; 85 86 if (is_a($form, Form::class)) { // $event->name is EDIT_FORM_ADDTEXTAREA 87 // data for handsontable 88 $form->setHiddenField('edittable_data', $Renderer->getDataJSON()); 89 $form->setHiddenField('edittable_meta', $Renderer->getMetaJSON()); 90 $form->addHTML('<div id="edittable__editor"></div>'); 91 92 // set data from action asigned to "New Table" button in the toolbar 93 foreach ($INPUT->post->arr('edittable__new', []) as $k => $v) { 94 $form->setHiddenField("edittable__new[$k]", $v); 95 } 96 97 // set target and range to keep track during previews 98 $form->setHiddenField('target', 'table'); 99 $form->setHiddenField('range', $RANGE); 100 101 } else { // $event->name is HTML_EDIT_FORMSELECTION 102 // data for handsontable 103 $form->addHidden('edittable_data', $Renderer->getDataJSON()); 104 $form->addHidden('edittable_meta', $Renderer->getMetaJSON()); 105 $form->addElement('<div id="edittable__editor"></div>'); 106 107 // set data from action asigned to "New Table" button in the toolbar 108 foreach ($INPUT->post->arr('edittable__new', []) as $k => $v) { 109 $form->addHidden("edittable__new[$k]", $v); 110 } 111 112 // set target and range to keep track during previews 113 $form->addHidden('target', 'table'); 114 $form->addHidden('range', $RANGE); 115 } 116 } 117 118 /** 119 * Handles a POST from the table editor 120 * 121 * This function preprocesses a POST from the table editor and converts it to plain DokuWiki markup 122 * 123 * @author Andreas Gohr <gohr@cosmocode,de> 124 */ 125 public function handle_table_post(Doku_Event $event) 126 { 127 global $TEXT; 128 global $INPUT; 129 if (!$INPUT->post->has('edittable_data')) return; 130 131 $data = json_decode($INPUT->post->str('edittable_data'), true); 132 $meta = json_decode($INPUT->post->str('edittable_meta'), true); 133 134 $TEXT = $this->build_table($data, $meta); 135 } 136 137 /** 138 * Create a DokuWiki table 139 * 140 * converts the table array to plain wiki markup text. pads the table so the markup is easy to read 141 * 142 * @param array $data table content for each cell 143 * @param array $meta meta data for each cell 144 * @return string 145 */ 146 public function build_table($data, $meta) 147 { 148 $table = ''; 149 $rows = count($data); 150 $cols = $rows ? count($data[0]) : 0; 151 152 $colmax = $cols ? array_fill(0, $cols, 0) : array(); 153 154 // find maximum column widths 155 for ($row = 0; $row < $rows; $row++) { 156 for ($col = 0; $col < $cols; $col++) { 157 $len = $this->strWidth($data[$row][$col]); 158 159 // alignment adds padding 160 if (isset($meta[$row][$col]['align']) && $meta[$row][$col]['align'] == 'center') { 161 $len += 4; 162 } else { 163 $len += 3; 164 } 165 166 // remember lenght 167 $meta[$row][$col]['length'] = $len; 168 169 if ($len > $colmax[$col]) $colmax[$col] = $len; 170 } 171 } 172 173 $last = '|'; // used to close the last cell 174 for ($row = 0; $row < $rows; $row++) { 175 for ($col = 0; $col < $cols; $col++) { 176 177 // minimum padding according to alignment 178 if (isset($meta[$row][$col]['align']) && $meta[$row][$col]['align'] == 'center') { 179 $lpad = 2; 180 $rpad = 2; 181 } elseif (isset($meta[$row][$col]['align']) && $meta[$row][$col]['align'] == 'right') { 182 $lpad = 2; 183 $rpad = 1; 184 } else { 185 $lpad = 1; 186 $rpad = 2; 187 } 188 189 // target width of this column 190 $target = $colmax[$col]; 191 192 // colspanned columns span all the cells 193 for ($i = 1; $i < $meta[$row][$col]['colspan']; $i++) { 194 $target += $colmax[$col + $i]; 195 } 196 197 // copy colspans to rowspans below if any 198 if ($meta[$row][$col]['colspan'] > 1) { 199 for ($i = 1; $i < $meta[$row][$col]['rowspan']; $i++) { 200 $meta[$row + $i][$col]['colspan'] = $meta[$row][$col]['colspan']; 201 } 202 } 203 204 // how much padding needs to be added? 205 $length = $meta[$row][$col]['length']; 206 $addpad = $target - $length; 207 208 // decide which side needs padding 209 if (isset($meta[$row][$col]['align']) && $meta[$row][$col]['align'] == 'right') { 210 $lpad += $addpad; 211 } else { 212 $rpad += $addpad; 213 } 214 215 // add the padding 216 $cdata = $data[$row][$col]; 217 if (!(isset($meta[$row][$col]['hide']) && $meta[$row][$col]['hide']) || $cdata) { 218 $cdata = str_pad('', $lpad).$cdata.str_pad('', $rpad); 219 } 220 221 // finally add the cell 222 $last = (isset($meta[$row][$col]['tag']) && $meta[$row][$col]['tag'] == 'th') ? '^' : '|'; 223 $table .= $last; 224 $table .= $cdata; 225 } 226 227 // close the row 228 $table .= "$last\n"; 229 } 230 $table = rtrim($table, "\n"); 231 232 return $table; 233 } 234 235 /** 236 * Return width of string 237 * 238 * @param string $str 239 * @return int 240 */ 241 public function strWidth($str) 242 { 243 static $callable; 244 245 if (isset($callable)) { 246 return $callable($str); 247 } else { 248 if (UTF8_MBSTRING) { 249 // count fullwidth characters as 2, halfwidth characters as 1 250 $callable = 'mb_strwidth'; 251 } elseif (method_exists(Utf8\PhpString::class, 'strlen')) { 252 // count any characters as 1 253 $callable = [Utf8\PhpString::class, 'strlen']; 254 } else { 255 // fallback deprecated utf8_strlen since 2019-06-09 256 $callable = 'utf8_strlen'; 257 } 258 return $this->strWidth($str); 259 } 260 } 261 262} 263