1<?php 2 3 4use ComboStrap\Bootstrap; 5use ComboStrap\CallStack; 6use ComboStrap\LogUtility; 7use ComboStrap\PluginUtility; 8use ComboStrap\TagAttributes; 9 10if (!defined('DOKU_INC')) die(); 11 12/** 13 * Class syntax_plugin_combo_tooltip 14 * Implementation of a tooltip 15 * 16 * A tooltip is implemented as a super title attribute 17 * on a HTML element such as a link or a button 18 * 19 * The implementation pass the information that there is 20 * a tooltip on the container which makes the output of {@link TagAttributes::toHtmlEnterTag()} 21 * to print all attributes until the title and not closing. 22 * 23 * Bootstrap generate the <a href="https://getbootstrap.com/docs/5.0/components/tooltips/#markup">markup tooltip</a> 24 * on the fly. It's possible to generate a bootstrap markup like and use popper directly 25 * but this is far more difficult 26 * 27 * 28 * https://material.io/components/tooltips 29 * [[https://getbootstrap.com/docs/4.0/components/tooltips/|Tooltip Boostrap version 4]] 30 * [[https://getbootstrap.com/docs/5.0/components/tooltips/|Tooltip Boostrap version 5]] 31 */ 32class syntax_plugin_combo_tooltip extends DokuWiki_Syntax_Plugin 33{ 34 35 const TAG = "tooltip"; 36 const TEXT_ATTRIBUTE = "text"; 37 const POSITION_ATTRIBUTE = "position"; 38 39 /** 40 * An attribute that hold the 41 * information that a tooltip was found 42 */ 43 const TOOLTIP_FOUND = "tooltipFound"; 44 45 /** 46 * Class added to the parent 47 */ 48 const CANONICAL = "tooltip"; 49 50 /** 51 * @var string 52 */ 53 private $docCapture; 54 55 56 /** 57 * tooltip is used also in page protection 58 */ 59 public static function addToolTipSnippetIfNeeded() 60 { 61 PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar(self::TAG); 62 PluginUtility::getSnippetManager()->attachCssSnippetForBar(self::TAG); 63 } 64 65 66 /** 67 * Syntax Type. 68 * 69 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 70 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 71 * @see DokuWiki_Syntax_Plugin::getType() 72 */ 73 function getType() 74 { 75 /** 76 * You could add a tooltip to a {@link syntax_plugin_combo_itext} 77 */ 78 return 'formatting'; 79 } 80 81 /** 82 * How Dokuwiki will add P element 83 * 84 * * 'normal' - The plugin can be used inside paragraphs (inline) 85 * * 'block' - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs 86 * * 'stack' - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs 87 * 88 * @see DokuWiki_Syntax_Plugin::getPType() 89 * @see https://www.dokuwiki.org/devel:syntax_plugins#ptype 90 */ 91 function getPType() 92 { 93 return 'normal'; 94 } 95 96 /** 97 * @return array 98 * Allow which kind of plugin inside 99 * 100 * No one of array('baseonly','container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 101 * because we manage self the content and we call self the parser 102 * 103 * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php 104 */ 105 function getAllowedTypes() 106 { 107 return array('baseonly', 'container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); 108 } 109 110 function getSort() 111 { 112 return 201; 113 } 114 115 116 function connectTo($mode) 117 { 118 119 $pattern = PluginUtility::getContainerTagPattern(self::TAG); 120 $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 121 122 } 123 124 function postConnect() 125 { 126 127 $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeFromTag($this->getPluginComponent())); 128 129 } 130 131 /** 132 * 133 * The handle function goal is to parse the matched syntax through the pattern function 134 * and to return the result for use in the renderer 135 * This result is always cached until the page is modified. 136 * @param string $match 137 * @param int $state 138 * @param int $pos - byte position in the original source file 139 * @param Doku_Handler $handler 140 * @return array|bool 141 * @see DokuWiki_Syntax_Plugin::handle() 142 * 143 */ 144 function handle($match, $state, $pos, Doku_Handler $handler) 145 { 146 147 switch ($state) { 148 149 case DOKU_LEXER_ENTER : 150 $tagAttributes = TagAttributes::createFromTagMatch($match); 151 152 /** 153 * Old Syntax 154 */ 155 if ($tagAttributes->hasComponentAttribute(self::TEXT_ATTRIBUTE)) { 156 return array( 157 PluginUtility::STATE => $state, 158 PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray() 159 ); 160 } 161 162 163 /** 164 * New Syntax, the tooltip attribute 165 * are applied to the parent and is seen as an advanced attribute 166 */ 167 168 /** 169 * Advertise that we got a tooltip 170 * to start the {@link action_plugin_combo_tooltippostprocessing postprocessing} 171 * or not 172 */ 173 $handler->setStatus(self::TOOLTIP_FOUND, true); 174 175 /** 176 * Callstack manipulation 177 */ 178 $callStack = CallStack::createFromHandler($handler); 179 180 /** 181 * Processing 182 * We should have one parent 183 * and no Sibling 184 */ 185 $parent = false; 186 $sibling = false; 187 while ($actualCall = $callStack->previous()) { 188 if ($actualCall->getState() == DOKU_LEXER_ENTER) { 189 $parent = $actualCall; 190 $context = $parent->getTagName(); 191 192 /** 193 * If this is an svg icon, the title attribute is not existing on a svg 194 * the icon should be wrapped up in a span (ie {@link syntax_plugin_combo_itext}) 195 */ 196 if ($parent->getTagName() == syntax_plugin_combo_icon::TAG) { 197 $parent->setContext(self::TAG); 198 } else { 199 200 /** 201 * Do not close the tag 202 */ 203 $parent->addAttribute(TagAttributes::OPEN_TAG, true); 204 /** 205 * Do not output the title 206 */ 207 $parent->addAttribute(TagAttributes::TITLE_KEY, TagAttributes::UN_SET); 208 209 } 210 211 return array( 212 PluginUtility::STATE => $state, 213 PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray(), 214 PluginUtility::CONTEXT => $context 215 216 ); 217 } else { 218 if ($actualCall->getTagName() == "eol") { 219 $callStack->deleteActualCallAndPrevious(); 220 $callStack->next(); 221 } else { 222 // sibling ? 223 // In a link, we would get the separator 224 if ($actualCall->getState() != DOKU_LEXER_UNMATCHED) { 225 $sibling = $actualCall; 226 break; 227 } 228 } 229 } 230 } 231 232 233 /** 234 * Error 235 */ 236 $errorMessage = "Error: "; 237 if ($parent == false) { 238 $errorMessage .= "A tooltip was found without parent and this is mandatory."; 239 } else { 240 if ($sibling != false) { 241 $errorMessage .= "A tooltip should be just below its parent. We found a tooltip next to the other sibling component ($sibling) and this will not work"; 242 } 243 } 244 return array( 245 PluginUtility::STATE => $state, 246 PluginUtility::ERROR_MESSAGE => $errorMessage 247 ); 248 249 250 case DOKU_LEXER_UNMATCHED : 251 return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 252 253 case DOKU_LEXER_EXIT : 254 255 $callStack = CallStack::createFromHandler($handler); 256 $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 257 258 return array( 259 PluginUtility::STATE => $state, 260 PluginUtility::ATTRIBUTES => $openingTag->getAttributes() 261 ); 262 263 264 } 265 return array(); 266 267 } 268 269 /** 270 * Render the output 271 * @param string $format 272 * @param Doku_Renderer $renderer 273 * @param array $data - what the function handle() return'ed 274 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 275 * @see DokuWiki_Syntax_Plugin::render() 276 * 277 * 278 */ 279 function render($format, Doku_Renderer $renderer, $data) 280 { 281 if ($format == 'xhtml') { 282 283 /** @var Doku_Renderer_xhtml $renderer */ 284 $state = $data[PluginUtility::STATE]; 285 switch ($state) { 286 287 case DOKU_LEXER_ENTER : 288 if (isset($data[PluginUtility::ERROR_MESSAGE])) { 289 LogUtility::msg($data[PluginUtility::ERROR_MESSAGE], LogUtility::LVL_MSG_ERROR, self::CANONICAL); 290 return false; 291 } 292 293 $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 294 295 /** 296 * Snippet 297 */ 298 self::addToolTipSnippetIfNeeded(); 299 300 /** 301 * Tooltip 302 */ 303 $dataAttributeNamespace = Bootstrap::getDataNamespace(); 304 $tagAttributes->addHtmlAttributeValue("data{$dataAttributeNamespace}-toggle", "tooltip"); 305 306 /** 307 * Position 308 */ 309 $position = $tagAttributes->getValueAndRemove(self::POSITION_ATTRIBUTE, "top"); 310 $tagAttributes->addHtmlAttributeValue("data{$dataAttributeNamespace}-placement", "${position}"); 311 312 313 /** 314 * Old tooltip syntax 315 */ 316 if ($tagAttributes->hasComponentAttribute(self::TEXT_ATTRIBUTE)) { 317 $tagAttributes->addHtmlAttributeValue("title", $tagAttributes->getValueAndRemove(self::TEXT_ATTRIBUTE)); 318 $tagAttributes->addClassName("d-inline-block"); 319 320 // Arbitrary HTML elements (such as <span>s) can be made focusable by adding the tabindex="0" attribute 321 $tagAttributes->addHtmlAttributeValue("tabindex", "0"); 322 323 $renderer->doc .= $tagAttributes->toHtmlEnterTag("span"); 324 } else { 325 /** 326 * New Syntax 327 * (The new syntax add the attributes to the previous element 328 */ 329 $tagAttributes->addHtmlAttributeValue("data{$dataAttributeNamespace}-html", "true"); 330 331 /** 332 * Keyboard user and assistive technology users 333 * If not button or link (ie span), add tabindex to make the element focusable 334 * in order to see the tooltip 335 * Not sure, if this is a good idea 336 */ 337 if (!in_array($data[PluginUtility::CONTEXT], [syntax_plugin_combo_link::TAG, syntax_plugin_combo_button::TAG])) { 338 $tagAttributes->addHtmlAttributeValue("tabindex", "0"); 339 } 340 341 $renderer->doc = rtrim($renderer->doc) . " {$tagAttributes->toHTMLAttributeString()} title=\""; 342 $this->docCapture = $renderer->doc; 343 $renderer->doc = ""; 344 } 345 346 break; 347 348 case DOKU_LEXER_UNMATCHED: 349 $renderer->doc .= PluginUtility::renderUnmatched($data); 350 break; 351 352 case DOKU_LEXER_EXIT: 353 354 if (isset($data[PluginUtility::ERROR_MESSAGE])) { 355 return false; 356 } 357 358 if (isset($data[PluginUtility::ATTRIBUTES][self::TEXT_ATTRIBUTE])) { 359 360 $text = $data[PluginUtility::ATTRIBUTES][self::TEXT_ATTRIBUTE]; 361 if (!empty($text)) { 362 $renderer->doc .= "</span>"; 363 } 364 365 } else { 366 /** 367 * We get the doc created since the enter 368 * We replace the " by ' to be able to add it in the title attribute 369 */ 370 $renderer->doc = PluginUtility::htmlEncode(preg_replace("/\r|\n/", "", $renderer->doc)); 371 372 /** 373 * We recreate the whole document 374 */ 375 $renderer->doc = $this->docCapture . $renderer->doc . "\">"; 376 $this->docCapture = null; 377 } 378 break; 379 380 381 } 382 return true; 383 } 384 385 // unsupported $mode 386 return false; 387 } 388 389 390} 391 392