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