xref: /plugin/combo/syntax/tooltip.php (revision 85e82846b0a214bc35e62864fa49d9cad0723d0e)
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