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