MDL 38508 JavaScript: Split out AJAX and non-AJAX help
[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  * The function which handles modifying the URL that was clicked on.
193  *
194  * The default function rewrites '.php' to '_ajax.php'.
195  *
196  * @attribute urlmodifier
197  * @type Function|String|null
198  * @default null
199  */
200 ATTRS.urlmodifier = {
201     value: null
202 };
204 /**
205  * Set the Y.Cache object to use.
206  *
207  * By default a new Y.Cache object will be created for each instance of the tooltip.
208  *
209  * In certain situations, where multiple tooltips may share the same cache, it may be preferable to
210  * seed this cache from the calling method.
211  *
212  * @attribute textcache
213  * @type Y.Cache|null
214  * @default null
215  */
216 ATTRS.textcache = {
217     value: null
218 };
220 /**
221  * Set the default size of the Y.Cache object.
222  *
223  * This is only used if no textcache is specified.
224  *
225  * @attribute textcachesize
226  * @type Number
227  * @default 10
228  */
229 ATTRS.textcachesize = {
230     value: 10
231 };
233 Y.extend(TOOLTIP, M.core.dialogue, {
234     // The bounding box.
235     bb: null,
237     // Any event listeners we may need to cancel later.
238     listenevents: [],
240     // Cache of objects we've already retrieved.
241     textcache: null,
243     // The align position. This differs for RTL languages so we calculate once and store.
244     alignpoints: [
245         Y.WidgetPositionAlign.TL,
246         Y.WidgetPositionAlign.RC
247     ],
249     initializer: function() {
250         // Set the initial values for the handlers.
251         // These cannot be set in the attributes section as context isn't present at that time.
252         if (!this.get('headerhandler')) {
253             this.set('headerhandler', this.set_header_content);
254         }
255         if (!this.get('bodyhandler')) {
256             this.set('bodyhandler', this.set_body_content);
257         }
258         if (!this.get('footerhandler')) {
259             this.set('footerhandler', function() {});
260         }
261         if (!this.get('urlmodifier')) {
262             this.set('urlmodifier', this.modify_url);
263         }
265         // Set up the dialogue with initial content.
266         this.setAttrs({
267             headerContent: this.get('initialheadertext'),
268             bodyContent: this.get('initialbodytext'),
269             footerContent: this.get('initialfootertext'),
270             zIndex: 150
271         });
273         // Hide and then render the dialogue.
274         this.hide();
275         this.render();
277         // Hook into a few useful areas.
278         this.bb = this.get('boundingBox');
280         // Change the alignment if this is an RTL language.
281         if (right_to_left()) {
282             this.alignpoints = [
283                 Y.WidgetPositionAlign.TR,
284                 Y.WidgetPositionAlign.LC
285             ];
286         }
288         // Set up the text cache if it's not set up already.
289         if (!this.get('textcache')) {
290             this.set('textcache', new Y.Cache({
291                 // Set a reasonable maximum cache size to prevent memory growth.
292                 max: this.get('textcachesize')
293             }));
294         }
296         // Disable the textcache when in developerdebug.
297         if (M.cfg.developerdebug) {
298             this.get('textcache').set('max', 0);
299         }
301         return this;
302     },
304     /**
305      * Display the tooltip for the clicked link.
306      *
307      * The anchor for the clicked link is used.
308      *
309      * @method display_panel
310      * @param {EventFacade} e The event from the clicked link. This is used to determine the clicked URL.
311      */
312     display_panel: function(e) {
313         var clickedlink, thisevent, ajaxurl, config, cacheentry;
315         // Prevent the default click action and prevent the event triggering anything else.
316         e.preventDefault();
317         e.stopPropagation();
319         // Cancel any existing listeners and close the panel if it's already open.
320         this.cancel_events();
322         // Grab the clickedlink - this contains the URL we fetch and we align the panel to it.
323         clickedlink = e.target.ancestor('a', true);
325         // Align with the link that was clicked.
326         this.align(clickedlink, this.alignpoints);
328         // Reset the initial text to a spinner while we retrieve the text.
329         this.setAttrs({
330             headerContent: this.get('initialheadertext'),
331             bodyContent: this.get('initialbodytext'),
332             footerContent: this.get('initialfootertext')
333         });
335         // Now that initial setup has begun, show the panel.
336         this.show();
338         // Add some listen events to close on.
339         thisevent = this.bb.delegate('click', this.close_panel, SELECTORS.CLOSEBUTTON, this);
340         this.listenevents.push(thisevent);
342         thisevent = Y.one('body').on('key', this.close_panel, 'esc', this);
343         this.listenevents.push(thisevent);
345         // Listen for mousedownoutside events - clickoutside is broken on IE.
346         thisevent = this.bb.on('mousedownoutside', this.close_panel, this);
347         this.listenevents.push(thisevent);
349         // Modify the URL as required.
350         ajaxurl = Y.bind(this.get('urlmodifier'), this, clickedlink.get('href'))();
352         cacheentry = this.get('textcache').retrieve(ajaxurl);
353         if (cacheentry) {
354             // The data from this help call was already cached so use that and avoid an AJAX call.
355             this._set_panel_contents(cacheentry.response);
356         } else {
357             // Retrieve the actual help text we should use.
358             config = {
359                 method: 'get',
360                 context: this,
361                 sync: false,
362                 on: {
363                     complete: function(tid, response) {
364                         this._set_panel_contents(response.responseText, ajaxurl);
365                     }
366                 }
367             };
369             Y.io(ajaxurl, config);
370         }
371     },
373     _set_panel_contents: function(response, ajaxurl) {
374         var responseobject;
376         // Attempt to parse the response into an object.
377         try {
378             responseobject = Y.JSON.parse(response);
379             if (responseobject.error) {
380                 this.close_panel();
381                 return new M.core.ajaxException(responseobject);
382             }
383         } catch (error) {
384             this.close_panel();
385             return new M.core.exception(error);
386         }
388         // Set the contents using various handlers.
389         // We must use Y.bind to ensure that the correct context is used when the default handlers are overridden.
390         Y.bind(this.get('headerhandler'), this, responseobject)();
391         Y.bind(this.get('bodyhandler'), this, responseobject)();
392         Y.bind(this.get('footerhandler'), this, responseobject)();
394         if (ajaxurl) {
395             // Ensure that this data is added to the cache.
396             this.get('textcache').add(ajaxurl, response);
397         }
399         this.get('buttons').header[0].focus();
400     },
402     set_header_content: function(responseobject) {
403         this.set('headerContent', responseobject.heading);
404     },
406     set_body_content: function(responseobject) {
407         var bodycontent = Y.Node.create('<div />')
408             .set('innerHTML', responseobject.text)
409             .setAttribute('role', 'alert')
410             .addClass(CSS.PANELTEXT);
411         this.set('bodyContent', bodycontent);
412     },
414     modify_url: function(url) {
415         return url.replace(/\.php\?/, '_ajax.php?');
416     },
418     close_panel: function(e) {
419         // Hide the panel first.
420         this.hide();
422         // Cancel the listeners that we added in display_panel.
423         this.cancel_events();
425         // Prevent any default click that the close button may have.
426         if (e) {
427             e.preventDefault();
428         }
429     },
431     cancel_events: function() {
432         // Detach all listen events to prevent duplicate triggers.
433         var thisevent;
434         while (this.listenevents.length) {
435             thisevent = this.listenevents.shift();
436             thisevent.detach();
437         }
438     }
439 });
440 M.core = M.core || {};
441 M.core.tooltip = M.core.tooltip = TOOLTIP;