10e91218e2ed4623a6ba263e9e83f25782f3c95d
[moodle.git] / lib / yui / src / tooltip / js / tooltip.js
1 /**
2  * Provides the base tooltip class.
3  *
4  * @module moodle-core-tooltip
5  */
7 /**
8  * A base class for a tooltip.
9  *
10  * @param {Object} config Object literal specifying tooltip configuration properties.
11  * @class M.core.tooltip
12  * @constructor
13  * @extends M.core.dialogue
14  */
15 function TOOLTIP(config) {
16     if (!config) {
17         config = {};
18     }
20     // Override the default options provided by the parent class.
21     if (typeof config.draggable === 'undefined') {
22         config.draggable = true;
23     }
25     if (typeof config.constrain === 'undefined') {
26         config.constrain = true;
27     }
29     if (typeof config.lightbox === 'undefined') {
30         config.lightbox = false;
31     }
33     TOOLTIP.superclass.constructor.apply(this, [config]);
34 }
36 var SELECTORS = {
37         CLOSEBUTTON: '.closebutton'
38     },
40     CSS = {
41         PANELTEXT: 'tooltiptext'
42     },
43     RESOURCES = {
44         WAITICON: {
45             pix: 'i/loading_small',
46             component: 'moodle'
47         }
48     },
49     ATTRS = {};
51 /**
52  * Static property provides a string to identify the JavaScript class.
53  *
54  * @property NAME
55  * @type String
56  * @static
57  */
58 TOOLTIP.NAME = 'moodle-core-tooltip';
60 /**
61  * Static property used to define the CSS prefix applied to tooltip dialogues.
62  *
63  * @property CSS_PREFIX
64  * @type String
65  * @static
66  */
67 TOOLTIP.CSS_PREFIX = 'moodle-dialogue';
69 /**
70  * Static property used to define the default attribute configuration for the Tooltip.
71  *
72  * @property ATTRS
73  * @type String
74  * @static
75  */
76 TOOLTIP.ATTRS = ATTRS;
78 /**
79  * The initial value of the header region before the content finishes loading.
80  *
81  * @attribute initialheadertext
82  * @type String
83  * @default ''
84  * @writeOnce
85  */
86 ATTRS.initialheadertext = {
87     value: ''
88 };
90 /**
91   * The initial value of the body region before the content finishes loading.
92   *
93   * The supplid string will be wrapped in a div with the CSS.PANELTEXT class and a standard Moodle spinner
94   * appended.
95   *
96   * @attribute initialbodytext
97   * @type String
98   * @default ''
99   * @writeOnce
100   */
101 ATTRS.initialbodytext = {
102     value: '',
103     setter: function(content) {
104         var parentnode,
105             spinner;
106         parentnode = Y.Node.create('<div />')
107             .addClass(CSS.PANELTEXT);
109         spinner = Y.Node.create('<img />')
110             .setAttribute('src', M.util.image_url(RESOURCES.WAITICON.pix, RESOURCES.WAITICON.component))
111             .addClass('spinner');
113         if (content) {
114             // If we have been provided with content, add it to the parent and make
115             // the spinner appear correctly inline
116             parentnode.set('text', content);
117             spinner.addClass('iconsmall');
118         } else {
119             // If there is no loading message, just make the parent node a lightbox
120             parentnode.addClass('content-lightbox');
121         }
123         parentnode.append(spinner);
124         return parentnode;
125     }
126 };
128 /**
129  * The initial value of the footer region before the content finishes loading.
130  *
131  * If a value is supplied, it will be wrapped in a <div> first.
132  *
133  * @attribute initialfootertext
134  * @type String
135  * @default ''
136  * @writeOnce
137  */
138 ATTRS.initialfootertext = {
139     value: null,
140     setter: function(content) {
141         if (content) {
142             return Y.Node.create('<div />')
143                 .set('text', content);
144         }
145     }
146 };
148 /**
149  * The function which handles setting the content of the title region.
150  * The specified function will be called with a context of the tooltip instance.
151  *
152  * The default function will simply set the value of the title to object.heading as returned by the AJAX call.
153  *
154  * @attribute headerhandler
155  * @type Function|String|null
156  * @default set_header_content
157  */
158 ATTRS.headerhandler = {
159     value: 'set_header_content'
160 };
162 /**
163  * The function which handles setting the content of the body region.
164  * The specified function will be called with a context of the tooltip instance.
165  *
166  * The default function will simply set the value of the body area to a div containing object.text as returned
167  * by the AJAX call.
168  *
169  * @attribute bodyhandler
170  * @type Function|String|null
171  * @default set_body_content
172  */
173 ATTRS.bodyhandler = {
174     value: 'set_body_content'
175 };
177 /**
178  * The function which handles setting the content of the footer region.
179  * The specified function will be called with a context of the tooltip instance.
180  *
181  * By default, the footer is not set.
182  *
183  * @attribute footerhandler
184  * @type Function|String|null
185  * @default null
186  */
187 ATTRS.footerhandler = {
188     value: null
189 };
191 /**
192  * Set the Y.Cache object to use.
193  *
194  * By default a new Y.Cache object will be created for each instance of the tooltip.
195  *
196  * In certain situations, where multiple tooltips may share the same cache, it may be preferable to
197  * seed this cache from the calling method.
198  *
199  * @attribute textcache
200  * @type Y.Cache|null
201  * @default null
202  */
203 ATTRS.textcache = {
204     value: null
205 };
207 /**
208  * Set the default size of the Y.Cache object.
209  *
210  * This is only used if no textcache is specified.
211  *
212  * @attribute textcachesize
213  * @type Number
214  * @default 10
215  */
216 ATTRS.textcachesize = {
217     value: 10
218 };
220 Y.extend(TOOLTIP, M.core.dialogue, {
221     // The bounding box.
222     bb: null,
224     // Any event listeners we may need to cancel later.
225     listenevents: [],
227     // Cache of objects we've already retrieved.
228     textcache: null,
230     // The align position. This differs for RTL languages so we calculate once and store.
231     alignpoints: [
232         Y.WidgetPositionAlign.TL,
233         Y.WidgetPositionAlign.RC
234     ],
236     initializer: function() {
237         // Set the initial values for the handlers.
238         // These cannot be set in the attributes section as context isn't present at that time.
239         if (!this.get('headerhandler')) {
240             this.set('headerhandler', this.set_header_content);
241         }
242         if (!this.get('bodyhandler')) {
243             this.set('bodyhandler', this.set_body_content);
244         }
245         if (!this.get('footerhandler')) {
246             this.set('footerhandler', function() {});
247         }
249         // Set up the dialogue with initial content.
250         this.setAttrs({
251             headerContent: this.get('initialheadertext'),
252             bodyContent: this.get('initialbodytext'),
253             footerContent: this.get('initialfootertext'),
254             zIndex: 150
255         });
257         // Hide and then render the dialogue.
258         this.hide();
259         this.render();
261         // Hook into a few useful areas.
262         this.bb = this.get('boundingBox');
264         // Change the alignment if this is an RTL language.
265         if (right_to_left()) {
266             this.alignpoints = [
267                 Y.WidgetPositionAlign.TR,
268                 Y.WidgetPositionAlign.LC
269             ];
270         }
272         // Set up the text cache if it's not set up already.
273         if (!this.get('textcache')) {
274             this.set('textcache', new Y.Cache({
275                 // Set a reasonable maximum cache size to prevent memory growth.
276                 max: this.get('textcachesize')
277             }));
278         }
280         // Disable the textcache when in developerdebug.
281         if (M.cfg.developerdebug) {
282             this.get('textcache').set('max', 0);
283         }
285         return this;
286     },
288     /**
289      * Display the tooltip for the clicked link.
290      *
291      * The anchor for the clicked link is used, additionally appending ajax=1 to the parameters.
292      *
293      * @method display_panel
294      * @param {EventFacade} e The event from the clicked link. This is used to determine the clicked URL.
295      */
296     display_panel: function(e) {
297         var clickedlink, thisevent, ajaxurl, config, cacheentry;
299         // Prevent the default click action and prevent the event triggering anything else.
300         e.preventDefault();
301         e.stopPropagation();
303         // Cancel any existing listeners and close the panel if it's already open.
304         this.cancel_events();
306         // Grab the clickedlink - this contains the URL we fetch and we align the panel to it.
307         clickedlink = e.target.ancestor('a', true);
309         // Align with the link that was clicked.
310         this.align(clickedlink, this.alignpoints);
312         // Reset the initial text to a spinner while we retrieve the text.
313         this.setAttrs({
314             headerContent: this.get('initialheadertext'),
315             bodyContent: this.get('initialbodytext'),
316             footerContent: this.get('initialfootertext')
317         });
319         // Now that initial setup has begun, show the panel.
320         this.show();
322         // Add some listen events to close on.
323         thisevent = this.bb.delegate('click', this.close_panel, SELECTORS.CLOSEBUTTON, this);
324         this.listenevents.push(thisevent);
326         thisevent = Y.one('body').on('key', this.close_panel, 'esc', this);
327         this.listenevents.push(thisevent);
329         // Listen for mousedownoutside events - clickoutside is broken on IE.
330         thisevent = this.bb.on('mousedownoutside', this.close_panel, this);
331         this.listenevents.push(thisevent);
333         ajaxurl = clickedlink.get('href');
335         cacheentry = this.get('textcache').retrieve(ajaxurl);
336         if (cacheentry) {
337             // The data from this help call was already cached so use that and avoid an AJAX call.
338             this._set_panel_contents(cacheentry.response);
339         } else {
340             // Retrieve the actual help text we should use.
341             config = {
342                 method: 'get',
343                 context: this,
344                 sync: false,
345                 data: {
346                     // We use a slightly different AJAX URL to the one on the anchor to allow non-JS fallback.
347                     ajax: 1
348                 },
349                 on: {
350                     complete: function(tid, response) {
351                         this._set_panel_contents(response.responseText, ajaxurl);
352                     }
353                 }
354             };
356             Y.io(clickedlink.get('href'), config);
357         }
358     },
360     _set_panel_contents: function(response, ajaxurl) {
361         var responseobject;
363         // Attempt to parse the response into an object.
364         try {
365             responseobject = Y.JSON.parse(response);
366             if (responseobject.error) {
367                 this.close_panel();
368                 return new M.core.ajaxException(responseobject);
369             }
370         } catch (error) {
371             this.close_panel();
372             return new M.core.exception({
373                 name: error.name,
374                 message: "Unable to retrieve the requested content. The following error was returned: " + error.message
375             });
376         }
378         // Set the contents using various handlers.
379         // We must use Y.bind to ensure that the correct context is used when the default handlers are overridden.
380         Y.bind(this.get('headerhandler'), this, responseobject)();
381         Y.bind(this.get('bodyhandler'), this, responseobject)();
382         Y.bind(this.get('footerhandler'), this, responseobject)();
384         if (ajaxurl) {
385             // Ensure that this data is added to the cache.
386             this.get('textcache').add(ajaxurl, response);
387         }
389         this.get('buttons').header[0].focus();
390     },
392     set_header_content: function(responseobject) {
393         this.set('headerContent', responseobject.heading);
394     },
396     set_body_content: function(responseobject) {
397         var bodycontent = Y.Node.create('<div />')
398             .set('innerHTML', responseobject.text)
399             .setAttribute('role', 'alert')
400             .addClass(CSS.PANELTEXT);
401         this.set('bodyContent', bodycontent);
402     },
404     close_panel: function(e) {
405         // Hide the panel first.
406         this.hide();
408         // Cancel the listeners that we added in display_panel.
409         this.cancel_events();
411         // Prevent any default click that the close button may have.
412         if (e) {
413             e.preventDefault();
414         }
415     },
417     cancel_events: function() {
418         // Detach all listen events to prevent duplicate triggers.
419         var thisevent;
420         while (this.listenevents.length) {
421             thisevent = this.listenevents.shift();
422             thisevent.detach();
423         }
424     }
425 });
426 M.core = M.core || {};
427 M.core.tooltip = M.core.tooltip = TOOLTIP;