MDL 38508 JavaScript: Split out AJAX and non-AJAX help
[moodle.git] / lib / yui / src / tooltip / js / tooltip.js
CommitLineData
627a4391
SH
1/**
2 * Provides the base tooltip class.
3 *
4 * @module moodle-core-tooltip
5 */
6
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 */
15function TOOLTIP(config) {
16 if (!config) {
17 config = {};
238b8bc9
ARN
18 }
19
627a4391
SH
20 // Override the default options provided by the parent class.
21 if (typeof config.draggable === 'undefined') {
22 config.draggable = true;
23 }
238b8bc9 24
627a4391
SH
25 if (typeof config.constrain === 'undefined') {
26 config.constrain = true;
27 }
238b8bc9 28
627a4391
SH
29 if (typeof config.lightbox === 'undefined') {
30 config.lightbox = false;
31 }
238b8bc9 32
627a4391
SH
33 TOOLTIP.superclass.constructor.apply(this, [config]);
34}
238b8bc9 35
627a4391
SH
36var SELECTORS = {
37 CLOSEBUTTON: '.closebutton'
38 },
238b8bc9 39
627a4391
SH
40 CSS = {
41 PANELTEXT: 'tooltiptext'
42 },
43 RESOURCES = {
44 WAITICON: {
45 pix: 'i/loading_small',
46 component: 'moodle'
47 }
48 },
49 ATTRS = {};
50
51/**
52 * Static property provides a string to identify the JavaScript class.
53 *
54 * @property NAME
55 * @type String
56 * @static
57 */
58TOOLTIP.NAME = 'moodle-core-tooltip';
59
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 */
67TOOLTIP.CSS_PREFIX = 'moodle-dialogue';
68
69/**
70 * Static property used to define the default attribute configuration for the Tooltip.
71 *
72 * @property ATTRS
73 * @type String
74 * @static
75 */
76TOOLTIP.ATTRS = ATTRS;
77
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 */
86ATTRS.initialheadertext = {
87 value: ''
88};
89
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 */
101ATTRS.initialbodytext = {
102 value: '',
103 setter: function(content) {
104 var parentnode,
105 spinner;
106 parentnode = Y.Node.create('<div />')
107 .addClass(CSS.PANELTEXT);
108
109 spinner = Y.Node.create('<img />')
110 .setAttribute('src', M.util.image_url(RESOURCES.WAITICON.pix, RESOURCES.WAITICON.component))
111 .addClass('spinner');
112
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');
238b8bc9 121 }
238b8bc9 122
627a4391
SH
123 parentnode.append(spinner);
124 return parentnode;
125 }
126};
127
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 */
138ATTRS.initialfootertext = {
139 value: null,
140 setter: function(content) {
141 if (content) {
142 return Y.Node.create('<div />')
143 .set('text', content);
144 }
145 }
146};
147
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 */
158ATTRS.headerhandler = {
159 value: 'set_header_content'
160};
161
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 */
173ATTRS.bodyhandler = {
174 value: 'set_body_content'
175};
176
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 */
187ATTRS.footerhandler = {
188 value: null
189};
190
56d465b2
ARN
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 */
200ATTRS.urlmodifier = {
201 value: null
202};
203
627a4391
SH
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 */
216ATTRS.textcache = {
217 value: null
218};
219
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 */
229ATTRS.textcachesize = {
230 value: 10
231};
232
233Y.extend(TOOLTIP, M.core.dialogue, {
234 // The bounding box.
235 bb: null,
236
237 // Any event listeners we may need to cancel later.
238 listenevents: [],
239
240 // Cache of objects we've already retrieved.
241 textcache: null,
242
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 ],
248
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() {});
238b8bc9 260 }
56d465b2
ARN
261 if (!this.get('urlmodifier')) {
262 this.set('urlmodifier', this.modify_url);
263 }
238b8bc9 264
627a4391
SH
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 });
272
273 // Hide and then render the dialogue.
274 this.hide();
275 this.render();
276
277 // Hook into a few useful areas.
278 this.bb = this.get('boundingBox');
279
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 }
238b8bc9 287
627a4391
SH
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 }
238b8bc9 295
627a4391
SH
296 // Disable the textcache when in developerdebug.
297 if (M.cfg.developerdebug) {
298 this.get('textcache').set('max', 0);
299 }
238b8bc9 300
627a4391
SH
301 return this;
302 },
238b8bc9
ARN
303
304 /**
627a4391 305 * Display the tooltip for the clicked link.
238b8bc9 306 *
56d465b2 307 * The anchor for the clicked link is used.
238b8bc9 308 *
627a4391
SH
309 * @method display_panel
310 * @param {EventFacade} e The event from the clicked link. This is used to determine the clicked URL.
238b8bc9 311 */
627a4391
SH
312 display_panel: function(e) {
313 var clickedlink, thisevent, ajaxurl, config, cacheentry;
314
315 // Prevent the default click action and prevent the event triggering anything else.
316 e.preventDefault();
317 e.stopPropagation();
318
319 // Cancel any existing listeners and close the panel if it's already open.
320 this.cancel_events();
321
322 // Grab the clickedlink - this contains the URL we fetch and we align the panel to it.
323 clickedlink = e.target.ancestor('a', true);
324
325 // Align with the link that was clicked.
326 this.align(clickedlink, this.alignpoints);
327
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 });
334
335 // Now that initial setup has begun, show the panel.
336 this.show();
337
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);
341
342 thisevent = Y.one('body').on('key', this.close_panel, 'esc', this);
343 this.listenevents.push(thisevent);
344
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);
348
56d465b2
ARN
349 // Modify the URL as required.
350 ajaxurl = Y.bind(this.get('urlmodifier'), this, clickedlink.get('href'))();
627a4391
SH
351
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,
627a4391
SH
362 on: {
363 complete: function(tid, response) {
364 this._set_panel_contents(response.responseText, ajaxurl);
238b8bc9 365 }
627a4391
SH
366 }
367 };
238b8bc9 368
56d465b2 369 Y.io(ajaxurl, config);
627a4391
SH
370 }
371 },
238b8bc9 372
627a4391
SH
373 _set_panel_contents: function(response, ajaxurl) {
374 var responseobject;
238b8bc9 375
627a4391
SH
376 // Attempt to parse the response into an object.
377 try {
378 responseobject = Y.JSON.parse(response);
379 if (responseobject.error) {
238b8bc9 380 this.close_panel();
627a4391 381 return new M.core.ajaxException(responseobject);
238b8bc9 382 }
627a4391
SH
383 } catch (error) {
384 this.close_panel();
56d465b2 385 return new M.core.exception(error);
627a4391 386 }
238b8bc9 387
627a4391
SH
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)();
238b8bc9 393
627a4391
SH
394 if (ajaxurl) {
395 // Ensure that this data is added to the cache.
396 this.get('textcache').add(ajaxurl, response);
397 }
238b8bc9 398
627a4391
SH
399 this.get('buttons').header[0].focus();
400 },
238b8bc9 401
627a4391
SH
402 set_header_content: function(responseobject) {
403 this.set('headerContent', responseobject.heading);
404 },
238b8bc9 405
627a4391
SH
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 },
238b8bc9 413
56d465b2
ARN
414 modify_url: function(url) {
415 return url.replace(/\.php\?/, '_ajax.php?');
416 },
417
627a4391
SH
418 close_panel: function(e) {
419 // Hide the panel first.
420 this.hide();
238b8bc9 421
627a4391
SH
422 // Cancel the listeners that we added in display_panel.
423 this.cancel_events();
238b8bc9 424
627a4391
SH
425 // Prevent any default click that the close button may have.
426 if (e) {
427 e.preventDefault();
428 }
429 },
430
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();
238b8bc9 437 }
627a4391
SH
438 }
439});
440M.core = M.core || {};
441M.core.tooltip = M.core.tooltip = TOOLTIP;