ballonizer 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/examples/ballonizer_app/config.ru +59 -0
- data/examples/ballonizer_app/index.html +159 -0
- data/examples/ballonizer_js_module/index.html +196 -0
- data/lib/assets/javascripts/ballonizer.js +482 -0
- data/lib/assets/stylesheets/ballonizer.css +78 -0
- data/lib/ballonizer.rb +201 -36
- data/spec/ballonizer_spec.rb +153 -2
- data/spec/javascripts/ballonizer_spec.js +568 -0
- data/spec/javascripts/fixtures/ballonized-xkcd-with-anchor-in-image.html +163 -0
- data/spec/javascripts/fixtures/ballonized-xkcd-with-ballons.html +163 -0
- data/spec/javascripts/fixtures/ballonized-xkcd-without-ballons.html +163 -0
- data/spec/javascripts/fixtures/xkcd.css +191 -0
- data/spec/javascripts/helpers/jasmine-jquery.js +660 -0
- data/spec/javascripts/helpers/jquery.simulate-ext.js +32 -0
- data/spec/javascripts/helpers/jquery.simulate.drag-n-drop.js +583 -0
- data/spec/javascripts/helpers/jquery.simulate.js +328 -0
- data/spec/javascripts/support/jasmine.yml +99 -0
- data/vendor/assets/javascripts/jquery-2.0.1.js +8837 -0
- data/vendor/assets/javascripts/jquery-ui-1.10.3.custom.min.js +6 -0
- data/vendor/assets/javascripts/jquery.json-2.4.min.js +24 -0
- data/vendor/assets/stylesheets/ui-lightness/images/animated-overlay.gif +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-bg_diagonals-thick_18_b81900_40x40.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-bg_diagonals-thick_20_666666_40x40.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-bg_flat_10_000000_40x100.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-bg_glass_100_f6f6f6_1x400.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-bg_glass_100_fdf5ce_1x400.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-bg_gloss-wave_35_f6a828_500x100.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-bg_highlight-soft_100_eeeeee_1x100.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-bg_highlight-soft_75_ffe45c_1x100.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-icons_222222_256x240.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-icons_228ef1_256x240.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-icons_ef8c08_256x240.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-icons_ffd27a_256x240.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/images/ui-icons_ffffff_256x240.png +0 -0
- data/vendor/assets/stylesheets/ui-lightness/jquery-ui-1.10.3.custom.min.css +5 -0
- metadata +51 -3
@@ -0,0 +1,482 @@
|
|
1
|
+
// Module pattern
|
2
|
+
(function (global) {
|
3
|
+
"use strict";
|
4
|
+
var Ballonizer = (function () {
|
5
|
+
|
6
|
+
var htmlEscape = function (str) {
|
7
|
+
return (str)
|
8
|
+
.replace(/&/g, "&")
|
9
|
+
.replace(/"/g, """)
|
10
|
+
.replace(/'/g, "'")
|
11
|
+
.replace(/</g, "<")
|
12
|
+
.replace(/>/g, ">");
|
13
|
+
};
|
14
|
+
|
15
|
+
var objectIsEmpty = function (obj) {
|
16
|
+
for (var p in obj) {
|
17
|
+
if (obj.hasOwnProperty(p)) {
|
18
|
+
return false;
|
19
|
+
}
|
20
|
+
}
|
21
|
+
return true;
|
22
|
+
};
|
23
|
+
|
24
|
+
// Classes used by the module
|
25
|
+
var Ballonizer;
|
26
|
+
var BallonizedImageContainer;
|
27
|
+
var InterfaceBallon;
|
28
|
+
|
29
|
+
/**
|
30
|
+
* @constructor Construct a Ballonizer object. This includes
|
31
|
+
* the BallonizedImageContainer objects for any images who match
|
32
|
+
* the ballonizerContainerSelector in the context. Add form for the
|
33
|
+
* submit of the ballonized image changes.
|
34
|
+
* @param actionFormURL A string with the value of the action
|
35
|
+
* attribute of the ballonizer form.
|
36
|
+
* @param ballonizerContainerSelector A css selector string who
|
37
|
+
* match the container wrapped around each image to be ballonized.
|
38
|
+
* @param context A node or JQuery object. The Ballonizer will
|
39
|
+
* affect and see only inside of this node.
|
40
|
+
* @param config A hash with the config to be used in some
|
41
|
+
* situations:
|
42
|
+
* ballonInitialText: ballon text when the ballon is created (do
|
43
|
+
* not use a empty string or only spaces). Defaults
|
44
|
+
* to "double click to edit ballon text";
|
45
|
+
* submitButtonValue: text of the button who submits the changes
|
46
|
+
* in the ballons. Defaults to "Submit ballon changes";
|
47
|
+
* @return Ballonizer object
|
48
|
+
*/
|
49
|
+
Ballonizer = function (actionFormURL,
|
50
|
+
ballonizerContainerSelector,
|
51
|
+
context,
|
52
|
+
config) {
|
53
|
+
|
54
|
+
// Implicit constructor pattern
|
55
|
+
if (!(this instanceof Ballonizer)) {
|
56
|
+
return new Ballonizer(actionFormURL,
|
57
|
+
ballonizerContainerSelector,
|
58
|
+
context,
|
59
|
+
config);
|
60
|
+
}
|
61
|
+
|
62
|
+
if (null == config) {
|
63
|
+
this.config = {
|
64
|
+
ballonInitialText: "double click to edit ballon text",
|
65
|
+
submitButtonValue: "Submit ballon changes"
|
66
|
+
};
|
67
|
+
} else {
|
68
|
+
this.config = config;
|
69
|
+
}
|
70
|
+
|
71
|
+
this.actionFormURL = actionFormURL;
|
72
|
+
this.context = $(context);
|
73
|
+
|
74
|
+
this.ballonizedImages = {};
|
75
|
+
|
76
|
+
var imageContainers = $(ballonizerContainerSelector, this.context);
|
77
|
+
imageContainers.each($.proxy(function (ix, element) {
|
78
|
+
var container = $(element);
|
79
|
+
var ballonizedContainer = new BallonizedImageContainer(this, container);
|
80
|
+
var imgSrc = $("img", container).prop("src");
|
81
|
+
|
82
|
+
this.ballonizedImages[imgSrc] = ballonizedContainer;
|
83
|
+
|
84
|
+
}, this));
|
85
|
+
|
86
|
+
this.formNode = null;
|
87
|
+
if (!objectIsEmpty(this.ballonizedImages)) {
|
88
|
+
this.formNode = this.generateBallonizerFormNode();
|
89
|
+
this.context.children().last().after(this.formNode);
|
90
|
+
}
|
91
|
+
|
92
|
+
return this;
|
93
|
+
};
|
94
|
+
|
95
|
+
Ballonizer.Error = function (message) {
|
96
|
+
this.message = message;
|
97
|
+
};
|
98
|
+
|
99
|
+
Ballonizer.Error.prototype = new Error();
|
100
|
+
|
101
|
+
// Instance methods
|
102
|
+
Ballonizer.prototype.getBallonInitialText = function () {
|
103
|
+
return this.config.ballonInitialText;
|
104
|
+
};
|
105
|
+
|
106
|
+
Ballonizer.prototype.getContext = function () {
|
107
|
+
return this.context;
|
108
|
+
};
|
109
|
+
|
110
|
+
Ballonizer.prototype.getForm = function () {
|
111
|
+
return this.formNode;
|
112
|
+
};
|
113
|
+
|
114
|
+
Ballonizer.prototype.getBallonizedImageContainers = function () {
|
115
|
+
return this.ballonizedImages;
|
116
|
+
};
|
117
|
+
|
118
|
+
Ballonizer.prototype.generateBallonizerFormNode = function () {
|
119
|
+
var form = $("<form class='ballonizer_page_form' method='post' >" +
|
120
|
+
"<input name='ballonizer_data' type='hidden' />" +
|
121
|
+
"<input name='ballonizer_submit' type='submit' " +
|
122
|
+
"value='" + this.config.submitButtonValue +
|
123
|
+
"' /></form>");
|
124
|
+
|
125
|
+
form.attr("action", this.actionFormURL);
|
126
|
+
|
127
|
+
return form;
|
128
|
+
};
|
129
|
+
|
130
|
+
Ballonizer.prototype.notifyBallonChange = function () {
|
131
|
+
var submitButton = $("input[type='submit']", this.formNode);
|
132
|
+
var dataInput = $("input[type='hidden']", this.formNode);
|
133
|
+
|
134
|
+
if (!submitButton.hasClass("ballonizer_ballons_have_changes")) {
|
135
|
+
submitButton.addClass("ballonizer_ballons_have_changes");
|
136
|
+
}
|
137
|
+
|
138
|
+
dataInput.val(jQuery.toJSON(this.serialize()));
|
139
|
+
|
140
|
+
return true;
|
141
|
+
};
|
142
|
+
|
143
|
+
Ballonizer.prototype.serialize = function () {
|
144
|
+
var serialization = {};
|
145
|
+
|
146
|
+
jQuery.each(this.ballonizedImages, function (ix, el) {
|
147
|
+
// makeArray remove non-numerical keys as img_src
|
148
|
+
serialization[ix] = jQuery.makeArray(el.serialize());
|
149
|
+
});
|
150
|
+
|
151
|
+
return serialization;
|
152
|
+
};
|
153
|
+
|
154
|
+
BallonizedImageContainer = (function () {
|
155
|
+
var BallonizedImageContainer = function (ballonizerInstance,
|
156
|
+
containerNode) {
|
157
|
+
|
158
|
+
// Implicit constructor pattern
|
159
|
+
if (!(this instanceof BallonizedImageContainer)) {
|
160
|
+
return new BallonizedImageContainer(ballonizerInstance,
|
161
|
+
containerNode);
|
162
|
+
}
|
163
|
+
|
164
|
+
this.ballonizerInstance = ballonizerInstance;
|
165
|
+
this.containerNode = containerNode;
|
166
|
+
this.ballons = [];
|
167
|
+
|
168
|
+
// Insert the form for the ballons in edit mode
|
169
|
+
$("<form class='ballonizer_image_form' method='#'></form>").insertBefore(
|
170
|
+
this.containerNode.children().first()
|
171
|
+
);
|
172
|
+
|
173
|
+
var ballons = $(".ballonizer_ballon",
|
174
|
+
this.ballonizerInstance.getContext());
|
175
|
+
|
176
|
+
ballons.each($.proxy(function (ix, element) {
|
177
|
+
this.ballons.push(new InterfaceBallon(this, $(element)));
|
178
|
+
}, this));
|
179
|
+
|
180
|
+
var img = $("img", this.containerNode);
|
181
|
+
img.click($.proxy(function (event) {
|
182
|
+
this.click(event);
|
183
|
+
}, this));
|
184
|
+
img.dblclick($.proxy(function (event) {
|
185
|
+
this.dblclick(event);
|
186
|
+
}, this));
|
187
|
+
};
|
188
|
+
|
189
|
+
BallonizedImageContainer.prototype.getContainerNode = function () {
|
190
|
+
return this.containerNode;
|
191
|
+
};
|
192
|
+
|
193
|
+
BallonizedImageContainer.prototype.getBallonizerInstance = function () {
|
194
|
+
return this.ballonizerInstance;
|
195
|
+
};
|
196
|
+
|
197
|
+
BallonizedImageContainer.prototype.notifyBallonChange = function () {
|
198
|
+
return this.ballonizerInstance.notifyBallonChange();
|
199
|
+
};
|
200
|
+
|
201
|
+
BallonizedImageContainer.prototype.removeBallonFromList = function (ballon) {
|
202
|
+
var ix = jQuery.inArray(ballon, this.ballons);
|
203
|
+
this.ballons.splice(ix, 1);
|
204
|
+
|
205
|
+
this.notifyBallonChange();
|
206
|
+
|
207
|
+
return this;
|
208
|
+
};
|
209
|
+
|
210
|
+
BallonizedImageContainer.prototype.getBallons = function () {
|
211
|
+
return this.ballons;
|
212
|
+
};
|
213
|
+
|
214
|
+
BallonizedImageContainer.prototype.click = function (event) {
|
215
|
+
// The container don't have an action to do when it's clicked,
|
216
|
+
// but it have when are double-clicked, and a double-click
|
217
|
+
// trigger a click event too. To avoid problems with webcomic
|
218
|
+
// pages who are links to the next page we disable the default
|
219
|
+
// efects of the click.
|
220
|
+
event.preventDefault();
|
221
|
+
};
|
222
|
+
|
223
|
+
BallonizedImageContainer.prototype.dblclick = function (event) {
|
224
|
+
event.preventDefault();
|
225
|
+
event.stopImmediatePropagation();
|
226
|
+
var offset = this.containerNode.offset();
|
227
|
+
var ballonX = event.pageX - offset.left;
|
228
|
+
var ballonY = event.pageY - offset.top;
|
229
|
+
|
230
|
+
// Width and height are magic number choosen for no good reason
|
231
|
+
var ballonWidth = 129, ballonHeight = 41;
|
232
|
+
var ballon = new InterfaceBallon(this, ballonX, ballonY,
|
233
|
+
ballonWidth, ballonHeight,
|
234
|
+
this.ballonizerInstance.getBallonInitialText());
|
235
|
+
|
236
|
+
this.ballons.push(ballon);
|
237
|
+
|
238
|
+
this.notifyBallonChange();
|
239
|
+
};
|
240
|
+
|
241
|
+
BallonizedImageContainer.prototype.serialize = function () {
|
242
|
+
/* jshint camelcase: false */
|
243
|
+
var serialization = {
|
244
|
+
// The img_src is out of style (not in camel case) because
|
245
|
+
// it will be submitted to the ruby, and i'm giving
|
246
|
+
// preference to the ruby convention when in conflict
|
247
|
+
img_src: $("img", this.containerNode).prop("src"),
|
248
|
+
length: this.ballons.length
|
249
|
+
};
|
250
|
+
|
251
|
+
jQuery.each(this.ballons, function (ix, el) {
|
252
|
+
serialization[ix] = el.serialize();
|
253
|
+
});
|
254
|
+
|
255
|
+
return serialization;
|
256
|
+
};
|
257
|
+
|
258
|
+
return BallonizedImageContainer;
|
259
|
+
})();
|
260
|
+
|
261
|
+
InterfaceBallon = (function () {
|
262
|
+
var InterfaceBallon = function (imgContainer, xOrNode, y, width,
|
263
|
+
height, initialText) {
|
264
|
+
|
265
|
+
// Implicit constructor pattern
|
266
|
+
if (!(this instanceof InterfaceBallon)) {
|
267
|
+
return new InterfaceBallon(imgContainer, xOrNode, y, width,
|
268
|
+
height, initialText);
|
269
|
+
}
|
270
|
+
|
271
|
+
this.imgContainer = imgContainer;
|
272
|
+
this.state = "initial";
|
273
|
+
|
274
|
+
if (2 === arguments.length) {
|
275
|
+
this.node = xOrNode;
|
276
|
+
this.updatePositionAndSize();
|
277
|
+
this.text = this.node.text();
|
278
|
+
} else {
|
279
|
+
this.left = xOrNode;
|
280
|
+
this.top = y;
|
281
|
+
this.width = width;
|
282
|
+
this.height = height;
|
283
|
+
var containerNode = this.imgContainer.getContainerNode();
|
284
|
+
|
285
|
+
// The ifs purpose are avoid the ballon of being created
|
286
|
+
// partially outside of the image (after the creation the
|
287
|
+
// jQueryUI handles this for us).
|
288
|
+
if (this.left + this.width > containerNode.width()) {
|
289
|
+
if (containerNode.width() < this.width) {
|
290
|
+
this.left = 0;
|
291
|
+
this.width = containerNode.width();
|
292
|
+
} else {
|
293
|
+
this.left = containerNode.width() - this.width;
|
294
|
+
}
|
295
|
+
}
|
296
|
+
if (this.top + this.height > containerNode.height()) {
|
297
|
+
if (containerNode.height() < this.height) {
|
298
|
+
this.top = 0;
|
299
|
+
this.height = containerNode.height();
|
300
|
+
} else {
|
301
|
+
this.top = containerNode.height() - this.height;
|
302
|
+
}
|
303
|
+
}
|
304
|
+
|
305
|
+
this.text = initialText;
|
306
|
+
this.node = this.generateBallonNode();
|
307
|
+
this.node.insertBefore(
|
308
|
+
this.imgContainer.getContainerNode().children().first()
|
309
|
+
);
|
310
|
+
}
|
311
|
+
|
312
|
+
this.node.draggable({
|
313
|
+
containment: "parent",
|
314
|
+
opacity: 0.5,
|
315
|
+
distance: 5,
|
316
|
+
stop: $.proxy(function (event, ui) {
|
317
|
+
/* jshint unused: false */
|
318
|
+
this.updatePositionAndSize();
|
319
|
+
}, this)
|
320
|
+
});
|
321
|
+
this.node.resizable({
|
322
|
+
containment: "parent",
|
323
|
+
opacity: 0.5,
|
324
|
+
distance: 5,
|
325
|
+
// Hides the handle when not hovering
|
326
|
+
autoHide: true,
|
327
|
+
stop: $.proxy(function (event, ui) {
|
328
|
+
/* jshint unused: false */
|
329
|
+
this.updatePositionAndSize();
|
330
|
+
}, this)
|
331
|
+
});
|
332
|
+
|
333
|
+
var imageForm = $(".ballonizer_image_form",
|
334
|
+
this.imgContainer.getContainerNode());
|
335
|
+
var editionBallon = $(
|
336
|
+
"<textarea class='ballonizer_edition_ballon'></textarea>"
|
337
|
+
).val(this.text);
|
338
|
+
|
339
|
+
this.editionNode = editionBallon;
|
340
|
+
imageForm.prepend(this.editionNode);
|
341
|
+
|
342
|
+
this.editionNode.blur($.proxy(function (event) {
|
343
|
+
this.blur(event);
|
344
|
+
}, this));
|
345
|
+
this.node.dblclick($.proxy(function (event) {
|
346
|
+
this.dblclick(event);
|
347
|
+
}, this));
|
348
|
+
};
|
349
|
+
|
350
|
+
InterfaceBallon.prototype.getBallonizedImageContainer = function () {
|
351
|
+
return this.imgContainer;
|
352
|
+
};
|
353
|
+
|
354
|
+
InterfaceBallon.prototype.generateBallonNode = function () {
|
355
|
+
var nodeStyle = ["left: ", this.left, "px; ", "top: ",
|
356
|
+
this.top, "px; ", "width: ", this.width, "px; ",
|
357
|
+
"height: ", this.height, "px;"].join("");
|
358
|
+
var node = $("<p class='ballonizer_ballon' ></p>");
|
359
|
+
|
360
|
+
// The use of ".text" will escape '<', '>', and others
|
361
|
+
node.text(this.text).attr("style", nodeStyle);
|
362
|
+
|
363
|
+
return node;
|
364
|
+
};
|
365
|
+
|
366
|
+
InterfaceBallon.prototype.getText = function () {
|
367
|
+
return this.text;
|
368
|
+
};
|
369
|
+
InterfaceBallon.prototype.getPositionAndSize = function () {
|
370
|
+
return {
|
371
|
+
left: this.left,
|
372
|
+
top: this.top,
|
373
|
+
width: this.width,
|
374
|
+
height: this.height
|
375
|
+
};
|
376
|
+
};
|
377
|
+
InterfaceBallon.prototype.updatePositionAndSize = function () {
|
378
|
+
var newX = this.node.position().left;
|
379
|
+
var newY = this.node.position().top;
|
380
|
+
var newWidth = this.node.width();
|
381
|
+
var newHeight = this.node.height();
|
382
|
+
|
383
|
+
if (newX !== this.left || newY !== this.top ||
|
384
|
+
newWidth !== this.width || newHeight !== this.height) {
|
385
|
+
|
386
|
+
this.left = newX;
|
387
|
+
this.top = newY;
|
388
|
+
this.width = newWidth;
|
389
|
+
this.height = newHeight;
|
390
|
+
this.imgContainer.notifyBallonChange();
|
391
|
+
}
|
392
|
+
};
|
393
|
+
InterfaceBallon.prototype.getState = function () {
|
394
|
+
return this.state;
|
395
|
+
};
|
396
|
+
InterfaceBallon.prototype.getNode = function () {
|
397
|
+
if (this.state.match(/edit/)) {
|
398
|
+
return this.editionNode;
|
399
|
+
} else {
|
400
|
+
return this.node;
|
401
|
+
}
|
402
|
+
};
|
403
|
+
InterfaceBallon.prototype.getNormalNode = function () {
|
404
|
+
return this.node;
|
405
|
+
};
|
406
|
+
InterfaceBallon.prototype.getEditionNode = function () {
|
407
|
+
return this.editionNode;
|
408
|
+
};
|
409
|
+
InterfaceBallon.prototype.blur = function () {
|
410
|
+
/* jshint loopfunc: true */
|
411
|
+
if ("edit" !== this.state) {
|
412
|
+
throw new Ballonizer.Error(
|
413
|
+
"losing focus when not in the edit state"
|
414
|
+
);
|
415
|
+
}
|
416
|
+
var oldText = this.text;
|
417
|
+
this.text = this.editionNode.val();
|
418
|
+
if ((/^\s*$/).test(this.text)) {
|
419
|
+
this.node.remove();
|
420
|
+
this.editionNode.remove();
|
421
|
+
// implies notifyBallonChange
|
422
|
+
this.imgContainer.removeBallonFromList(this);
|
423
|
+
// throw a exception if any method is called
|
424
|
+
for (var method in InterfaceBallon.prototype)
|
425
|
+
{
|
426
|
+
if (InterfaceBallon.prototype.hasOwnProperty(method)) {
|
427
|
+
this[method] = function () {
|
428
|
+
throw new Ballonizer.Error(
|
429
|
+
"this ballon already have been destroyed" +
|
430
|
+
", do not call any methods over it ('" +
|
431
|
+
method + "' was called)");
|
432
|
+
};
|
433
|
+
}
|
434
|
+
}
|
435
|
+
} else {
|
436
|
+
this.state = "initial";
|
437
|
+
// only change the text in the node, and not what
|
438
|
+
// the resizable insert inside the element (the handles)
|
439
|
+
this.node.contents().filter(function () {
|
440
|
+
return this.nodeType === 3;
|
441
|
+
}).replaceWith(htmlEscape(this.text));
|
442
|
+
this.node.removeClass("ballonizer_ballon_hidden_for_edition");
|
443
|
+
this.editionNode.removeClass("ballonizer_ballon_in_edition");
|
444
|
+
if (this.text !== oldText) {
|
445
|
+
this.imgContainer.notifyBallonChange();
|
446
|
+
}
|
447
|
+
}
|
448
|
+
};
|
449
|
+
|
450
|
+
InterfaceBallon.prototype.dblclick = function () {
|
451
|
+
this.state = this.state.replace("initial", "edit");
|
452
|
+
this.node.addClass("ballonizer_ballon_hidden_for_edition");
|
453
|
+
this.editionNode.attr("style", this.node.attr("style"));
|
454
|
+
this.editionNode.addClass("ballonizer_ballon_in_edition");
|
455
|
+
// focus after is visible, otherwise will crash in IE
|
456
|
+
// http://api.jquery.com/focus/#focus
|
457
|
+
this.editionNode.focus();
|
458
|
+
};
|
459
|
+
|
460
|
+
InterfaceBallon.prototype.serialize = function () {
|
461
|
+
var containerWidth = this.imgContainer.getContainerNode().width();
|
462
|
+
var containerHeight = this.imgContainer.getContainerNode().height();
|
463
|
+
|
464
|
+
return {
|
465
|
+
left: this.left / containerWidth,
|
466
|
+
top: this.top / containerHeight,
|
467
|
+
width: this.width / containerWidth,
|
468
|
+
height: this.height / containerHeight,
|
469
|
+
text: this.text
|
470
|
+
};
|
471
|
+
};
|
472
|
+
|
473
|
+
return InterfaceBallon;
|
474
|
+
})();
|
475
|
+
|
476
|
+
return Ballonizer;
|
477
|
+
})();
|
478
|
+
|
479
|
+
global.Ballonizer = Ballonizer;
|
480
|
+
|
481
|
+
})(this);
|
482
|
+
|
@@ -0,0 +1,78 @@
|
|
1
|
+
.ballonizer_image_container
|
2
|
+
{
|
3
|
+
position: relative;
|
4
|
+
display: inline-block;
|
5
|
+
padding: 0;
|
6
|
+
margin: 0;
|
7
|
+
border: 0;
|
8
|
+
}
|
9
|
+
|
10
|
+
.ballonizer_ballon
|
11
|
+
{
|
12
|
+
position: absolute;
|
13
|
+
margin: 0;
|
14
|
+
background-color: white;
|
15
|
+
color: black;
|
16
|
+
text-align: center;
|
17
|
+
overflow: hidden;
|
18
|
+
}
|
19
|
+
|
20
|
+
.ballonizer_ballon_hidden_for_edition
|
21
|
+
{
|
22
|
+
display: none;
|
23
|
+
}
|
24
|
+
|
25
|
+
/* The ballonizer image form is hidden but we don't use "display: none".
|
26
|
+
* The "display: none" in a parent element make impossible to diplay the child
|
27
|
+
* elements. So we remove any padding. As the childs are positioned
|
28
|
+
* absolutely they don't ocuppy space. This make height equal zero, but
|
29
|
+
* width is equal to the width of the parent element. Using "display: inline-block"
|
30
|
+
* fix this problem and the height is equal to the sum of the childs who
|
31
|
+
* occupy space (no one). With this the form is "hidden" because don't occupy
|
32
|
+
* any space (the width AND height are equal zero).
|
33
|
+
*/
|
34
|
+
.ballonizer_image_form
|
35
|
+
{
|
36
|
+
margin: 0;
|
37
|
+
padding: 0;
|
38
|
+
border: 0;
|
39
|
+
display: inline-block;
|
40
|
+
}
|
41
|
+
|
42
|
+
.ballonizer_image_form > *
|
43
|
+
{
|
44
|
+
position: absolute;
|
45
|
+
display: none;
|
46
|
+
margin: 0;
|
47
|
+
text-align: center;
|
48
|
+
}
|
49
|
+
|
50
|
+
.ballonizer_image_form > .ballonizer_ballon_in_edition
|
51
|
+
{
|
52
|
+
display: block;
|
53
|
+
}
|
54
|
+
|
55
|
+
/* We use the same trick used in the image form in the page form */
|
56
|
+
.ballonizer_page_form
|
57
|
+
{
|
58
|
+
display: inline-block;
|
59
|
+
padding: 0;
|
60
|
+
margin: 0;
|
61
|
+
border: 0;
|
62
|
+
}
|
63
|
+
|
64
|
+
.ballonizer_page_form > *
|
65
|
+
{
|
66
|
+
display: none;
|
67
|
+
margin: 0;
|
68
|
+
padding: 0;
|
69
|
+
}
|
70
|
+
|
71
|
+
.ballonizer_page_form > .ballonizer_ballons_have_changes
|
72
|
+
{
|
73
|
+
display: block;
|
74
|
+
position: fixed;
|
75
|
+
top: 0;
|
76
|
+
right: 0;
|
77
|
+
}
|
78
|
+
|