ballonizer 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|