1007225e5Sgerardnico<?php 2007225e5Sgerardnico 3007225e5Sgerardnico 421913ab3SNickeauuse ComboStrap\Bootstrap; 5*85e82846SNickeauuse ComboStrap\Call; 6*85e82846SNickeauuse ComboStrap\CallStack; 7*85e82846SNickeauuse ComboStrap\LogUtility; 8007225e5Sgerardnicouse ComboStrap\PluginUtility; 9*85e82846SNickeauuse ComboStrap\TagAttributes; 10007225e5Sgerardnico 11007225e5Sgerardnicoif (!defined('DOKU_INC')) die(); 12007225e5Sgerardnico 13007225e5Sgerardnico/** 14007225e5Sgerardnico * Class syntax_plugin_combo_tooltip 15007225e5Sgerardnico * Implementation of a tooltip 16*85e82846SNickeau * 17*85e82846SNickeau * A tooltip is implemented as a super title attribute 18*85e82846SNickeau * on a HTML element such as a link or a button 19*85e82846SNickeau * 20*85e82846SNickeau * The implementation pass the information that there is 21*85e82846SNickeau * a tooltip on the container which makes the output of {@link TagAttributes::toHtmlEnterTag()} 22*85e82846SNickeau * to print all attributes until the title and not closing. 23*85e82846SNickeau * 24*85e82846SNickeau * Bootstrap generate the <a href="https://getbootstrap.com/docs/5.0/components/tooltips/#markup">markup tooltip</a> 25*85e82846SNickeau * on the fly. It's possible to generate a bootstrap markup like and use popper directly 26*85e82846SNickeau * but this is far more difficult 27*85e82846SNickeau * 28*85e82846SNickeau * 29*85e82846SNickeau * https://material.io/components/tooltips 30*85e82846SNickeau * [[https://getbootstrap.com/docs/4.0/components/tooltips/|Tooltip Boostrap version 4]] 31*85e82846SNickeau * [[https://getbootstrap.com/docs/5.0/components/tooltips/|Tooltip Boostrap version 5]] 32007225e5Sgerardnico */ 33007225e5Sgerardnicoclass syntax_plugin_combo_tooltip extends DokuWiki_Syntax_Plugin 34007225e5Sgerardnico{ 35007225e5Sgerardnico 36007225e5Sgerardnico const TAG = "tooltip"; 37007225e5Sgerardnico const TEXT_ATTRIBUTE = "text"; 38007225e5Sgerardnico const POSITION_ATTRIBUTE = "position"; 39007225e5Sgerardnico 40*85e82846SNickeau /** 41*85e82846SNickeau * An attribute that hold the 42*85e82846SNickeau * information that a tooltip was found 43*85e82846SNickeau */ 44*85e82846SNickeau const TOOLTIP_FOUND = "tooltipFound"; 45*85e82846SNickeau 46*85e82846SNickeau /** 47*85e82846SNickeau * Class added to the parent 48*85e82846SNickeau */ 49*85e82846SNickeau const CANONICAL = "tooltip"; 50*85e82846SNickeau 51*85e82846SNickeau /** 52*85e82846SNickeau * @var string 53*85e82846SNickeau */ 54*85e82846SNickeau private $docCapture; 55*85e82846SNickeau 56007225e5Sgerardnico 575f891b7eSNickeau /** 585f891b7eSNickeau * tooltip is used also in page protection 595f891b7eSNickeau */ 605f891b7eSNickeau public static function addToolTipSnippetIfNeeded() 6119b0880dSgerardnico { 62*85e82846SNickeau PluginUtility::getSnippetManager()->attachJavascriptSnippetForBar(self::TAG); 63*85e82846SNickeau PluginUtility::getSnippetManager()->attachCssSnippetForBar(self::TAG); 6419b0880dSgerardnico } 6519b0880dSgerardnico 66007225e5Sgerardnico 67007225e5Sgerardnico /** 68007225e5Sgerardnico * Syntax Type. 69007225e5Sgerardnico * 70007225e5Sgerardnico * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 71007225e5Sgerardnico * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 72007225e5Sgerardnico * @see DokuWiki_Syntax_Plugin::getType() 73007225e5Sgerardnico */ 74007225e5Sgerardnico function getType() 75007225e5Sgerardnico { 76*85e82846SNickeau /** 77*85e82846SNickeau * You could add a tooltip to a {@link syntax_plugin_combo_itext} 78*85e82846SNickeau */ 79*85e82846SNickeau return 'formatting'; 80007225e5Sgerardnico } 81007225e5Sgerardnico 82007225e5Sgerardnico /** 83007225e5Sgerardnico * How Dokuwiki will add P element 84007225e5Sgerardnico * 85007225e5Sgerardnico * * 'normal' - The plugin can be used inside paragraphs (inline) 86007225e5Sgerardnico * * 'block' - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs 87007225e5Sgerardnico * * 'stack' - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs 88007225e5Sgerardnico * 89007225e5Sgerardnico * @see DokuWiki_Syntax_Plugin::getPType() 90007225e5Sgerardnico * @see https://www.dokuwiki.org/devel:syntax_plugins#ptype 91007225e5Sgerardnico */ 92007225e5Sgerardnico function getPType() 93007225e5Sgerardnico { 94007225e5Sgerardnico return 'normal'; 95007225e5Sgerardnico } 96007225e5Sgerardnico 97007225e5Sgerardnico /** 98007225e5Sgerardnico * @return array 99007225e5Sgerardnico * Allow which kind of plugin inside 100007225e5Sgerardnico * 101007225e5Sgerardnico * No one of array('baseonly','container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 102007225e5Sgerardnico * because we manage self the content and we call self the parser 103007225e5Sgerardnico * 104007225e5Sgerardnico * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php 105007225e5Sgerardnico */ 106007225e5Sgerardnico function getAllowedTypes() 107007225e5Sgerardnico { 108007225e5Sgerardnico return array('baseonly', 'container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs'); 109007225e5Sgerardnico } 110007225e5Sgerardnico 111007225e5Sgerardnico function getSort() 112007225e5Sgerardnico { 113007225e5Sgerardnico return 201; 114007225e5Sgerardnico } 115007225e5Sgerardnico 116007225e5Sgerardnico 117007225e5Sgerardnico function connectTo($mode) 118007225e5Sgerardnico { 119007225e5Sgerardnico 120007225e5Sgerardnico $pattern = PluginUtility::getContainerTagPattern(self::TAG); 1219337a630SNickeau $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 122007225e5Sgerardnico 123007225e5Sgerardnico } 124007225e5Sgerardnico 125007225e5Sgerardnico function postConnect() 126007225e5Sgerardnico { 127007225e5Sgerardnico 1289337a630SNickeau $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeFromTag($this->getPluginComponent())); 129007225e5Sgerardnico 130007225e5Sgerardnico } 131007225e5Sgerardnico 132007225e5Sgerardnico /** 133007225e5Sgerardnico * 134007225e5Sgerardnico * The handle function goal is to parse the matched syntax through the pattern function 135007225e5Sgerardnico * and to return the result for use in the renderer 136007225e5Sgerardnico * This result is always cached until the page is modified. 137007225e5Sgerardnico * @param string $match 138007225e5Sgerardnico * @param int $state 139007225e5Sgerardnico * @param int $pos - byte position in the original source file 140007225e5Sgerardnico * @param Doku_Handler $handler 141007225e5Sgerardnico * @return array|bool 142007225e5Sgerardnico * @see DokuWiki_Syntax_Plugin::handle() 143007225e5Sgerardnico * 144007225e5Sgerardnico */ 145007225e5Sgerardnico function handle($match, $state, $pos, Doku_Handler $handler) 146007225e5Sgerardnico { 147007225e5Sgerardnico 148007225e5Sgerardnico switch ($state) { 149007225e5Sgerardnico 150007225e5Sgerardnico case DOKU_LEXER_ENTER : 151*85e82846SNickeau $tagAttributes = TagAttributes::createFromTagMatch($match); 152*85e82846SNickeau 153*85e82846SNickeau /** 154*85e82846SNickeau * Old Syntax 155*85e82846SNickeau */ 156*85e82846SNickeau if ($tagAttributes->hasComponentAttribute(self::TEXT_ATTRIBUTE)) { 157*85e82846SNickeau return array( 158*85e82846SNickeau PluginUtility::STATE => $state, 159*85e82846SNickeau PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray() 160*85e82846SNickeau ); 161*85e82846SNickeau } 162*85e82846SNickeau 163*85e82846SNickeau 164*85e82846SNickeau /** 165*85e82846SNickeau * New Syntax, the tooltip attribute 166*85e82846SNickeau * are applied to the parent and is seen as an advanced attribute 167*85e82846SNickeau */ 168*85e82846SNickeau 169*85e82846SNickeau /** 170*85e82846SNickeau * Advertise that we got a tooltip 171*85e82846SNickeau * to start the {@link action_plugin_combo_tooltippostprocessing postprocessing} 172*85e82846SNickeau * or not 173*85e82846SNickeau */ 174*85e82846SNickeau $handler->setStatus(self::TOOLTIP_FOUND, true); 175*85e82846SNickeau 176*85e82846SNickeau /** 177*85e82846SNickeau * Callstack manipulation 178*85e82846SNickeau */ 179*85e82846SNickeau $callStack = CallStack::createFromHandler($handler); 180*85e82846SNickeau 181*85e82846SNickeau /** 182*85e82846SNickeau * Processing 183*85e82846SNickeau * We should have one parent 184*85e82846SNickeau * and no Sibling 185*85e82846SNickeau */ 186*85e82846SNickeau $parent = false; 187*85e82846SNickeau $sibling = false; 188*85e82846SNickeau while ($actualCall = $callStack->previous()) { 189*85e82846SNickeau if ($actualCall->getState() == DOKU_LEXER_ENTER) { 190*85e82846SNickeau $parent = $actualCall; 191*85e82846SNickeau $context = $parent->getTagName(); 192*85e82846SNickeau 193*85e82846SNickeau /** 194*85e82846SNickeau * If this is an svg icon, the title attribute is not existing on a svg 195*85e82846SNickeau * the icon should be wrapped up in a span (ie {@link syntax_plugin_combo_itext}) 196*85e82846SNickeau */ 197*85e82846SNickeau if ($parent->getTagName() == syntax_plugin_combo_icon::TAG) { 198*85e82846SNickeau $parent->setContext(self::TAG); 199*85e82846SNickeau } else { 200*85e82846SNickeau 201*85e82846SNickeau /** 202*85e82846SNickeau * Do not close the tag 203*85e82846SNickeau */ 204*85e82846SNickeau $parent->addAttribute(TagAttributes::OPEN_TAG, true); 205*85e82846SNickeau /** 206*85e82846SNickeau * Do not output the title 207*85e82846SNickeau */ 208*85e82846SNickeau $parent->addAttribute(TagAttributes::TITLE_KEY, TagAttributes::UN_SET); 209*85e82846SNickeau 210*85e82846SNickeau } 2115f891b7eSNickeau 212007225e5Sgerardnico return array( 213007225e5Sgerardnico PluginUtility::STATE => $state, 214*85e82846SNickeau PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray(), 215*85e82846SNickeau PluginUtility::CONTEXT => $context 216*85e82846SNickeau 217007225e5Sgerardnico ); 218*85e82846SNickeau } else { 219*85e82846SNickeau if ($actualCall->getTagName() == "eol") { 220*85e82846SNickeau $callStack->deleteActualCallAndPrevious(); 221*85e82846SNickeau $callStack->next(); 222*85e82846SNickeau } else { 223*85e82846SNickeau // sibling ? 224*85e82846SNickeau // In a link, we would get the separator 225*85e82846SNickeau if ($actualCall->getState() != DOKU_LEXER_UNMATCHED) { 226*85e82846SNickeau $sibling = $actualCall; 227*85e82846SNickeau break; 228*85e82846SNickeau } 229*85e82846SNickeau } 230*85e82846SNickeau } 231*85e82846SNickeau } 232*85e82846SNickeau 233*85e82846SNickeau 234*85e82846SNickeau /** 235*85e82846SNickeau * Error 236*85e82846SNickeau */ 237*85e82846SNickeau $errorMessage = "Error: "; 238*85e82846SNickeau if ($parent == false) { 239*85e82846SNickeau $errorMessage .= "A tooltip was found without parent and this is mandatory."; 240*85e82846SNickeau } else { 241*85e82846SNickeau if ($sibling != false) { 242*85e82846SNickeau $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*85e82846SNickeau } 244*85e82846SNickeau } 245*85e82846SNickeau return array( 246*85e82846SNickeau PluginUtility::STATE => $state, 247*85e82846SNickeau PluginUtility::ERROR_MESSAGE => $errorMessage 248*85e82846SNickeau ); 249*85e82846SNickeau 250007225e5Sgerardnico 251007225e5Sgerardnico case DOKU_LEXER_UNMATCHED : 25232b85071SNickeau return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 253007225e5Sgerardnico 254007225e5Sgerardnico case DOKU_LEXER_EXIT : 255007225e5Sgerardnico 256*85e82846SNickeau $callStack = CallStack::createFromHandler($handler); 257*85e82846SNickeau $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 2585f891b7eSNickeau 259007225e5Sgerardnico return array( 260007225e5Sgerardnico PluginUtility::STATE => $state, 261*85e82846SNickeau PluginUtility::ATTRIBUTES => $openingTag->getAttributes() 262007225e5Sgerardnico ); 263007225e5Sgerardnico 264007225e5Sgerardnico 265007225e5Sgerardnico } 266007225e5Sgerardnico return array(); 267007225e5Sgerardnico 268007225e5Sgerardnico } 269007225e5Sgerardnico 270007225e5Sgerardnico /** 271007225e5Sgerardnico * Render the output 272007225e5Sgerardnico * @param string $format 273007225e5Sgerardnico * @param Doku_Renderer $renderer 274007225e5Sgerardnico * @param array $data - what the function handle() return'ed 275007225e5Sgerardnico * @return boolean - rendered correctly? (however, returned value is not used at the moment) 276007225e5Sgerardnico * @see DokuWiki_Syntax_Plugin::render() 277007225e5Sgerardnico * 278007225e5Sgerardnico * 279007225e5Sgerardnico */ 280007225e5Sgerardnico function render($format, Doku_Renderer $renderer, $data) 281007225e5Sgerardnico { 282007225e5Sgerardnico if ($format == 'xhtml') { 283007225e5Sgerardnico 284007225e5Sgerardnico /** @var Doku_Renderer_xhtml $renderer */ 285007225e5Sgerardnico $state = $data[PluginUtility::STATE]; 286007225e5Sgerardnico switch ($state) { 287007225e5Sgerardnico 288*85e82846SNickeau case DOKU_LEXER_ENTER : 289*85e82846SNickeau if (isset($data[PluginUtility::ERROR_MESSAGE])) { 290*85e82846SNickeau LogUtility::msg($data[PluginUtility::ERROR_MESSAGE], LogUtility::LVL_MSG_ERROR, self::CANONICAL); 291*85e82846SNickeau return false; 292*85e82846SNickeau } 293*85e82846SNickeau 294*85e82846SNickeau $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 295*85e82846SNickeau 296*85e82846SNickeau /** 297*85e82846SNickeau * Snippet 298*85e82846SNickeau */ 299*85e82846SNickeau self::addToolTipSnippetIfNeeded(); 300*85e82846SNickeau 301*85e82846SNickeau /** 302*85e82846SNickeau * Tooltip 303*85e82846SNickeau */ 304*85e82846SNickeau $dataAttributeNamespace = Bootstrap::getDataNamespace(); 305*85e82846SNickeau $tagAttributes->addHtmlAttributeValue("data{$dataAttributeNamespace}-toggle", "tooltip"); 306*85e82846SNickeau 307*85e82846SNickeau /** 308*85e82846SNickeau * Position 309*85e82846SNickeau */ 310*85e82846SNickeau $position = $tagAttributes->getValueAndRemove(self::POSITION_ATTRIBUTE, "top"); 311*85e82846SNickeau $tagAttributes->addHtmlAttributeValue("data{$dataAttributeNamespace}-placement", "${position}"); 312*85e82846SNickeau 313*85e82846SNickeau 314*85e82846SNickeau /** 315*85e82846SNickeau * Old tooltip syntax 316*85e82846SNickeau */ 317*85e82846SNickeau if ($tagAttributes->hasComponentAttribute(self::TEXT_ATTRIBUTE)) { 318*85e82846SNickeau $tagAttributes->addHtmlAttributeValue("title", $tagAttributes->getValueAndRemove(self::TEXT_ATTRIBUTE)); 319*85e82846SNickeau $tagAttributes->addClassName("d-inline-block"); 320*85e82846SNickeau 321*85e82846SNickeau // Arbitrary HTML elements (such as <span>s) can be made focusable by adding the tabindex="0" attribute 322*85e82846SNickeau $tagAttributes->addHtmlAttributeValue("tabindex", "0"); 323*85e82846SNickeau 324*85e82846SNickeau $renderer->doc .= $tagAttributes->toHtmlEnterTag("span"); 325*85e82846SNickeau } else { 326*85e82846SNickeau /** 327*85e82846SNickeau * New Syntax 328*85e82846SNickeau * (The new syntax add the attributes to the previous element 329*85e82846SNickeau */ 330*85e82846SNickeau $tagAttributes->addHtmlAttributeValue("data{$dataAttributeNamespace}-html", "true"); 331*85e82846SNickeau 332*85e82846SNickeau /** 333*85e82846SNickeau * Keyboard user and assistive technology users 334*85e82846SNickeau * If not button or link (ie span), add tabindex to make the element focusable 335*85e82846SNickeau * in order to see the tooltip 336*85e82846SNickeau * Not sure, if this is a good idea 337*85e82846SNickeau */ 338*85e82846SNickeau if (!in_array($data[PluginUtility::CONTEXT], [syntax_plugin_combo_link::TAG, syntax_plugin_combo_button::TAG])) { 339*85e82846SNickeau $tagAttributes->addHtmlAttributeValue("tabindex", "0"); 340*85e82846SNickeau } 341*85e82846SNickeau 342*85e82846SNickeau $renderer->doc = rtrim($renderer->doc) . " {$tagAttributes->toHTMLAttributeString()} title=\""; 343*85e82846SNickeau $this->docCapture = $renderer->doc; 344*85e82846SNickeau $renderer->doc = ""; 345*85e82846SNickeau } 346*85e82846SNickeau 347*85e82846SNickeau break; 348*85e82846SNickeau 349007225e5Sgerardnico case DOKU_LEXER_UNMATCHED: 35032b85071SNickeau $renderer->doc .= PluginUtility::renderUnmatched($data); 351007225e5Sgerardnico break; 352007225e5Sgerardnico 353007225e5Sgerardnico case DOKU_LEXER_EXIT: 354*85e82846SNickeau 355*85e82846SNickeau if (isset($data[PluginUtility::ERROR_MESSAGE])) { 356*85e82846SNickeau return false; 357*85e82846SNickeau } 358*85e82846SNickeau 3595f891b7eSNickeau if (isset($data[PluginUtility::ATTRIBUTES][self::TEXT_ATTRIBUTE])) { 36019b0880dSgerardnico 3615f891b7eSNickeau $text = $data[PluginUtility::ATTRIBUTES][self::TEXT_ATTRIBUTE]; 3625f891b7eSNickeau if (!empty($text)) { 3635f891b7eSNickeau $renderer->doc .= "</span>"; 364007225e5Sgerardnico } 3655f891b7eSNickeau 366*85e82846SNickeau } else { 367*85e82846SNickeau /** 368*85e82846SNickeau * We get the doc created since the enter 369*85e82846SNickeau * We replace the " by ' to be able to add it in the title attribute 370*85e82846SNickeau */ 371*85e82846SNickeau $renderer->doc = PluginUtility::htmlEncode(preg_replace("/\r|\n/", "", $renderer->doc)); 372*85e82846SNickeau 373*85e82846SNickeau /** 374*85e82846SNickeau * We recreate the whole document 375*85e82846SNickeau */ 376*85e82846SNickeau $renderer->doc = $this->docCapture . $renderer->doc . "\">"; 377*85e82846SNickeau $this->docCapture = null; 3785f891b7eSNickeau } 3795f891b7eSNickeau break; 3805f891b7eSNickeau 381007225e5Sgerardnico 382007225e5Sgerardnico } 383007225e5Sgerardnico return true; 384007225e5Sgerardnico } 385007225e5Sgerardnico 386007225e5Sgerardnico // unsupported $mode 387007225e5Sgerardnico return false; 388007225e5Sgerardnico } 389007225e5Sgerardnico 390007225e5Sgerardnico 391007225e5Sgerardnico} 392007225e5Sgerardnico 393