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