michael_hintbuble 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,572 @@
1
+ /**
2
+ * Michael Hintbuble creates pretty hint bubbles using prototype and
3
+ * scriptaculous. These functions work with ActionView helpers
4
+ * to provide hint bubble components using the syntax defined
5
+ * for rendering rails templates.
6
+ *
7
+ *
8
+ * Brought to you by the good folks at Coroutine. Hire us!
9
+ * http://coroutine.com
10
+ */
11
+ var MichaelHintbuble = {}
12
+
13
+
14
+ /**
15
+ * This property governs whether or not Michael bothers creating and
16
+ * managing a blocking iframe to accommodate ie6.
17
+ *
18
+ * Defaults to false, but override if you must.
19
+ */
20
+ MichaelHintbuble.SUPPORT_IE6_BULLSHIT = false;
21
+
22
+
23
+
24
+ //-----------------------------------------------------------------------------
25
+ // Bubble class
26
+ //-----------------------------------------------------------------------------
27
+
28
+ /**
29
+ * This function lets you come fly with Michael by defining
30
+ * the hint bubble class.
31
+ */
32
+ MichaelHintbuble.Bubble = function(target_id, content, options) {
33
+ this._target = $(target_id);
34
+ this._element = null;
35
+ this._positioner = null;
36
+ this._isShowing = null;
37
+
38
+ this._class = options["class"] || "michael_hintbuble_bubble";
39
+ this._style = options["style"] || "";
40
+ this._eventNames = options["eventNames"] || ["mouseover","resize","scroll"]
41
+ this._position = options["position"] || "right";
42
+ this._beforeShow = options["beforeShow"] || Prototype.emptyFunction
43
+ this._afterShow = options["afterShow"] || Prototype.emptyFunction
44
+ this._beforeHide = options["beforeHide"] || Prototype.emptyFunction
45
+ this._afterHide = options["afterHide"] || Prototype.emptyFunction
46
+
47
+ this._makeBubble();
48
+ this._makePositioner();
49
+ this._attachObservers();
50
+ this.setContent(content);
51
+ this.setPosition();
52
+
53
+ if (MichaelHintbuble.SUPPORT_IE6_BULLSHIT) {
54
+ this._makeFrame();
55
+ }
56
+ };
57
+
58
+
59
+ /**
60
+ * This hash maps the bubble id to the bubble object itself. It allows the Rails
61
+ * code a way to specify the js object it wishes to invoke.
62
+ */
63
+ MichaelHintbuble.Bubble.instances = {};
64
+
65
+
66
+ /**
67
+ * This method destroys the bubble with the corresponding target id.
68
+ *
69
+ * @param {String} id The target id value of the bubble element (also the key
70
+ * in the instances hash.)
71
+ */
72
+ MichaelHintbuble.Bubble.destroy = function(id) {
73
+ var bubble = this.instances[id];
74
+ if (bubble) {
75
+ bubble.finalize();
76
+ }
77
+ this.instances[id] = null;
78
+ };
79
+
80
+
81
+ /**
82
+ * This method hides the bubble with the corresponding target id.
83
+ *
84
+ * @param {String} id The target id value of the bubble element (also the key
85
+ * in the instances hash.)
86
+ *
87
+ * @return {Object} an instance of MichaelHintbuble.Bubble
88
+ *
89
+ */
90
+ MichaelHintbuble.Bubble.hide = function(id) {
91
+ var bubble = this.instances[id];
92
+ if (bubble) {
93
+ bubble.hide();
94
+ }
95
+ return bubble;
96
+ };
97
+
98
+
99
+ /**
100
+ * This method returns a boolean indiciating whether or not the
101
+ * bubble with the corresponding target id is showing.
102
+ *
103
+ * @param {String} id The target id value of the bubble element (also the key
104
+ * in the instances hash.)
105
+ *
106
+ * @return {Boolean} Whether or not the bubble with the corresponding
107
+ * id is showing.
108
+ *
109
+ */
110
+ MichaelHintbuble.Bubble.isShowing = function(id) {
111
+ var bubble = this.instances[id];
112
+ if (!bubble) {
113
+ throw "No bubble cound be found for the supplied id.";
114
+ }
115
+ return bubble.isShowing();
116
+ };
117
+
118
+
119
+ /**
120
+ * This method shows the bubble with the corresponding target id.
121
+ *
122
+ * @param {String} id The target id value of the bubble element (also the key
123
+ * in the instances hash.)
124
+ *
125
+ * @return {Object} an instance of MichaelHintbuble.Bubble
126
+ *
127
+ */
128
+ MichaelHintbuble.Bubble.show = function(id) {
129
+ var bubble = this.instances[id];
130
+ if (bubble) {
131
+ bubble.show();
132
+ }
133
+ return bubble;
134
+ };
135
+
136
+
137
+ /**
138
+ * This function establishes all of the observations specified in the options.
139
+ */
140
+ MichaelHintbuble.Bubble.prototype._attachObservers = function() {
141
+ if (this._eventNames.indexOf("focus") > -1) {
142
+ this._target.observe("focus", function() {
143
+ this.show();
144
+ }.bind(this));
145
+ this._target.observe("blur", function() {
146
+ this.hide();
147
+ }.bind(this));
148
+ }
149
+ if (this._eventNames.indexOf("mouseover") > -1) {
150
+ this._target.observe("mouseover", function() {
151
+ this.show();
152
+ }.bind(this));
153
+ this._target.observe("mouseout", function() {
154
+ this.hide();
155
+ }.bind(this));
156
+ }
157
+ if (this._eventNames.indexOf("resize") > -1) {
158
+ Event.observe(window, "resize", function() {
159
+ if (this.isShowing()) {
160
+ this.setPosition();
161
+ }
162
+ }.bind(this));
163
+ }
164
+ if (this._eventNames.indexOf("scroll") > -1) {
165
+ Event.observe(window, "scroll", function() {
166
+ if (this.isShowing()) {
167
+ this.setPosition();
168
+ }
169
+ }.bind(this));
170
+ }
171
+ };
172
+
173
+
174
+ /**
175
+ * This function creates the bubble element and hides it by default.
176
+ */
177
+ MichaelHintbuble.Bubble.prototype._makeBubble = function() {
178
+ if (!this._element) {
179
+ this._container = new Element("DIV");
180
+ this._container.className = "container";
181
+
182
+ this._element = new Element("DIV");
183
+ this._element.className = this._class;
184
+ this._element.writeAttribute("style", this._style);
185
+ this._element.update(this._container);
186
+ this._element.hide();
187
+ document.body.insert(this._element);
188
+ }
189
+ };
190
+
191
+
192
+ /**
193
+ * This function creates the blocking frame element and hides it by default.
194
+ */
195
+ MichaelHintbuble.Bubble.prototype._makeFrame = function() {
196
+ if (!this._frame) {
197
+ this._frame = new Element("IFRAME");
198
+ this._frame.className = this._class + "_frame";
199
+ this._frame.setAttribute("src", "about:blank");
200
+ this._frame.hide();
201
+ }
202
+ };
203
+
204
+
205
+ /**
206
+ * This function creates the bubble positioner object.
207
+ */
208
+ MichaelHintbuble.Bubble.prototype._makePositioner = function() {
209
+ if (!this._positioner) {
210
+ this._positioner = new MichaelHintbuble.BubblePositioner(this._target, this._element, this._position);
211
+ }
212
+ };
213
+
214
+
215
+ /**
216
+ * This method updates the container element by applying an additional style
217
+ * class representing the relative position of the bubble to the target.
218
+ */
219
+ MichaelHintbuble.Bubble.prototype._updateContainerClass = function() {
220
+ this._container.className = "container";
221
+ this._container.addClassName(this._positioner.styleClassForPosition());
222
+ };
223
+
224
+
225
+ /**
226
+ * This function allows the bubble object to be destroyed without
227
+ * creating memory leaks.
228
+ */
229
+ MichaelHintbuble.Bubble.prototype.finalize = function() {
230
+ this._positioner.finalize();
231
+ this._container.remove();
232
+ this._element.remove();
233
+
234
+ this._target = null;
235
+ this._element = null;
236
+ this._container = null;
237
+ this._positioner = null;
238
+
239
+ if (MichaelHintbuble.SUPPORT_IE6_BULLSHIT) {
240
+ this._frame.remove();
241
+ this._frame = null;
242
+ }
243
+ };
244
+
245
+
246
+ /**
247
+ * This function shows the hint bubble container (and the blocking frame, if
248
+ * required).
249
+ */
250
+ MichaelHintbuble.Bubble.prototype.hide = function() {
251
+ new Effect.Fade(this._element, {
252
+ duration: 0.2,
253
+ beforeStart: this._beforeHide,
254
+ afterFinish: function() {
255
+ this._isShowing = false;
256
+ this._afterHide();
257
+ }.bind(this)
258
+ });
259
+
260
+ if (this._frame) {
261
+ new Effect.Fade(this._frame, {
262
+ duration: 0.2
263
+ });
264
+ }
265
+ };
266
+
267
+
268
+ /**
269
+ * This function returns a boolean indicating whether or not the bubble is
270
+ * showing.
271
+ *
272
+ * @returns {Boolean} Whether or not the bubble is showing.
273
+ */
274
+ MichaelHintbuble.Bubble.prototype.isShowing = function() {
275
+ return this._isShowing;
276
+ };
277
+
278
+
279
+ /**
280
+ * This function sets the content of the hint bubble container.
281
+ *
282
+ * @param {String} content A string representation of the content to be added
283
+ * to the hint bubble container.
284
+ */
285
+ MichaelHintbuble.Bubble.prototype.setContent = function(content) {
286
+ var content_container = new Element("DIV");
287
+ content_container.className = "content";
288
+ content_container.update(content);
289
+
290
+ this._container.update(content_container);
291
+ };
292
+
293
+
294
+ /**
295
+ * This method sets the position of the hint bubble. It should be noted that the
296
+ * position simply states a preferred location for the bubble within the viewport.
297
+ * If the supplied position results in the bubble overrunning the viewport,
298
+ * the bubble will be repositioned to the opposite side to avoid viewport
299
+ * overrun.
300
+ *
301
+ * @param {String} position A string representation of the preferred position of
302
+ * the bubble element.
303
+ */
304
+ MichaelHintbuble.Bubble.prototype.setPosition = function(position) {
305
+ if (position) {
306
+ this._position = position.toLowerCase();
307
+ }
308
+ this._positioner.setPosition(this._position);
309
+ this._updateContainerClass();
310
+ };
311
+
312
+
313
+ /**
314
+ * This function shows the hint bubble container (and the blocking frame, if
315
+ * required).
316
+ */
317
+ MichaelHintbuble.Bubble.prototype.show = function() {
318
+ this.setPosition();
319
+
320
+ if (this._frame) {
321
+ var layout = new Element.Layout(this._element);
322
+ this._frame.style.top = layout.get("top") + "px";
323
+ this._frame.style.left = layout.get("left") + "px";
324
+ this._frame.style.width = layout.get("width") + "px";
325
+ this._frame.style.height = layout.get("height") + "px";
326
+
327
+ new Effect.Appear(this._frame, {
328
+ duration: 0.2
329
+ });
330
+ }
331
+
332
+ new Effect.Appear(this._element, {
333
+ duration: 0.2,
334
+ beforeStart: this._beforeShow,
335
+ afterFinish: function() {
336
+ this._isShowing = true;
337
+ this._afterShow();
338
+ }.bind(this)
339
+ });
340
+ };
341
+
342
+
343
+
344
+
345
+ //-----------------------------------------------------------------------------
346
+ // BubblePositioner class
347
+ //-----------------------------------------------------------------------------
348
+
349
+ /**
350
+ * This class encapsulates the positioning logic for bubble classes.
351
+ *
352
+ * @param {Element} target the dom element to which the bubble is anchored.
353
+ * @param {Element} element the bubble element itself.
354
+ */
355
+ MichaelHintbuble.BubblePositioner = function(target, element, position) {
356
+ this._target = target;
357
+ this._element = element;
358
+ this._position = position;
359
+ this._axis = null
360
+ };
361
+
362
+
363
+ /**
364
+ * These properties establish numeric values for the x and y axes.
365
+ */
366
+ MichaelHintbuble.BubblePositioner.X_AXIS = 1;
367
+ MichaelHintbuble.BubblePositioner.Y_AXIS = 2;
368
+
369
+
370
+ /**
371
+ * This property maps position values to one or the other axis.
372
+ */
373
+ MichaelHintbuble.BubblePositioner.AXIS_MAP = {
374
+ left: MichaelHintbuble.BubblePositioner.X_AXIS,
375
+ right: MichaelHintbuble.BubblePositioner.X_AXIS,
376
+ top: MichaelHintbuble.BubblePositioner.Y_AXIS,
377
+ bottom: MichaelHintbuble.BubblePositioner.Y_AXIS
378
+ };
379
+
380
+
381
+ /**
382
+ * This property maps position values to their opposite value.
383
+ */
384
+ MichaelHintbuble.BubblePositioner.COMPLEMENTS = {
385
+ left: "right",
386
+ right: "left",
387
+ top: "bottom",
388
+ bottom: "top"
389
+ };
390
+
391
+
392
+ /**
393
+ * This hash is a convenience that allows us to write slightly denser code when
394
+ * calculating the bubble's position.
395
+ */
396
+ MichaelHintbuble.BubblePositioner.POSITION_FN_MAP = {
397
+ left: "getWidth",
398
+ top: "getHeight"
399
+ };
400
+
401
+
402
+
403
+ /**
404
+ * This function positions the element below the target.
405
+ */
406
+ MichaelHintbuble.BubblePositioner.prototype._bottom = function() {
407
+ var to = this._targetAdjustedOffset();
408
+ var tl = new Element.Layout(this._target);
409
+
410
+ this._element.style.top = (to.top + tl.get("border-box-height")) + "px";
411
+ };
412
+
413
+
414
+ /**
415
+ * This function centers the positioning of the element for whichever
416
+ * axis it is on.
417
+ */
418
+ MichaelHintbuble.BubblePositioner.prototype._center = function() {
419
+ var to = this._targetAdjustedOffset();
420
+ var tl = new Element.Layout(this._target);
421
+ var el = new Element.Layout(this._element);
422
+
423
+ if (this._axis === MichaelHintbuble.BubblePositioner.X_AXIS) {
424
+ this._element.style.top = (to.top + Math.ceil(tl.get("border-box-height")/2) - Math.ceil(el.get("padding-box-height")/2)) + "px";
425
+ }
426
+ else if (this._axis === MichaelHintbuble.BubblePositioner.Y_AXIS) {
427
+ this._element.style.left = (to.left + Math.ceil(tl.get("border-box-width")/2) - Math.ceil(el.get("padding-box-width")/2)) + "px";
428
+ }
429
+ };
430
+
431
+
432
+ /**
433
+ * This function returns a boolean indicating whether or not the element is
434
+ * contained within the viewport.
435
+ *
436
+ * @returns {Boolean} whether or not the element is contained within the viewport.
437
+ */
438
+ MichaelHintbuble.BubblePositioner.prototype._isElementWithinViewport = function() {
439
+ var isWithinViewport = true;
440
+ var fnMap = MichaelHintbuble.BubblePositioner.POSITION_FN_MAP;
441
+ var method = null;
442
+ var viewPortMinEdge = null;
443
+ var viewPortMaxEdge = null;
444
+ var elementMinEdge = null;
445
+ var elementMaxEdge = null;
446
+
447
+ for (var prop in fnMap) {
448
+ method = fnMap[prop];
449
+ viewportMinEdge = document.viewport.getScrollOffsets()[prop];
450
+ viewportMaxEdge = viewportMinEdge + document.viewport[method]();
451
+ elementMinEdge = parseInt(this._element.style[prop] || 0);
452
+ elementMaxEdge = elementMinEdge + this._element[method]();
453
+
454
+ if ((elementMaxEdge > viewportMaxEdge) || (elementMinEdge < viewportMinEdge)) {
455
+ isWithinViewport = false;
456
+ break;
457
+ }
458
+ }
459
+
460
+ return isWithinViewport;
461
+ };
462
+
463
+
464
+ /**
465
+ * This function positions the element to the left of the target.
466
+ */
467
+ MichaelHintbuble.BubblePositioner.prototype._left = function() {
468
+ var to = this._targetAdjustedOffset();
469
+ var el = new Element.Layout(this._element);
470
+
471
+ this._element.style.left = (to.left - el.get("padding-box-width")) + "px";
472
+ };
473
+
474
+
475
+ /**
476
+ * This function positions the element to the right of the target.
477
+ */
478
+ MichaelHintbuble.BubblePositioner.prototype._right = function() {
479
+ var to = this._targetAdjustedOffset();
480
+ var tl = new Element.Layout(this._target);
481
+
482
+ this._element.style.left = (to.left + tl.get("border-box-width")) + "px";
483
+ };
484
+
485
+
486
+ /**
487
+ * This function positions the element relative to the target according to the
488
+ * position value supplied. Because this function is private, it assumes a
489
+ * safe position value.
490
+ *
491
+ * @param {String} position the desired relative position of the element to the
492
+ * target.
493
+ */
494
+ MichaelHintbuble.BubblePositioner.prototype._setPosition = function(position) {
495
+ this._axis = MichaelHintbuble.BubblePositioner.AXIS_MAP[position];
496
+ this._position = position;
497
+ this["_" + position]();
498
+ this._center();
499
+ };
500
+
501
+
502
+ /**
503
+ * This function returns a hash with the adjusted offset positions for the target
504
+ * element.
505
+ */
506
+ MichaelHintbuble.BubblePositioner.prototype._targetAdjustedOffset = function() {
507
+ var bs = $$("body").first().cumulativeScrollOffset();
508
+ var to = this._target.cumulativeOffset();
509
+ var ts = this._target.cumulativeScrollOffset();
510
+
511
+ return {
512
+ "top": to.top - ts.top + bs.top,
513
+ "left": to.left - ts.left + bs.left
514
+ }
515
+ };
516
+
517
+
518
+ /**
519
+ * This function positions the element above the target.
520
+ */
521
+ MichaelHintbuble.BubblePositioner.prototype._top = function() {
522
+ var to = this._targetAdjustedOffset();
523
+ var el = new Element.Layout(this._element);
524
+
525
+ this._element.style.top = (to.top - el.get("padding-box-height")) + "px";
526
+ };
527
+
528
+
529
+ /**
530
+ * This function allows the bubble positioner object to be destroyed without
531
+ * creating memory leaks.
532
+ */
533
+ MichaelHintbuble.BubblePositioner.prototype.finalize = function() {
534
+ this._target = null;
535
+ this._element = null;
536
+ this._axis = null;
537
+ this._position = null;
538
+ };
539
+
540
+
541
+ /**
542
+ * This function positions the element relative to the target according to the
543
+ * position value supplied. Invalid position values are ignored. If the new
544
+ * position runs off the viewport, the complement is tried. If that fails too,
545
+ * it gives up and does what was asked.
546
+ *
547
+ * @param {String} position the desired relative position of the element to the
548
+ * target.
549
+ */
550
+ MichaelHintbuble.BubblePositioner.prototype.setPosition = function(position) {
551
+ var axis = MichaelHintbuble.BubblePositioner.AXIS_MAP[position];
552
+ if (axis) {
553
+ this._setPosition(position);
554
+ if (!this._isElementWithinViewport()) {
555
+ this._setPosition(MichaelHintbuble.BubblePositioner.COMPLEMENTS[position]);
556
+ if (!this._isElementWithinViewport()) {
557
+ this._setPosition(position);
558
+ }
559
+ }
560
+ }
561
+ };
562
+
563
+
564
+ /**
565
+ * This function returns a string representation of the current logical positioning that
566
+ * can be used as a stylesheet class for physical positioning.
567
+ *
568
+ * @returns {String} a styleclass name appropriate for the current position.
569
+ */
570
+ MichaelHintbuble.BubblePositioner.prototype.styleClassForPosition = function() {
571
+ return this._position.toLowerCase();
572
+ };