forked from code-dot-org/code-dot-org
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcallouts.js
More file actions
267 lines (241 loc) · 8.33 KB
/
callouts.js
File metadata and controls
267 lines (241 loc) · 8.33 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
import $ from 'jquery';
var clientState = require('./clientState');
/**
* @fileoverview handles creation and updating of dashboard tooltips, aka callouts.
*
* Assumes existence of jQuery qtip2 plugin.
*/
/**
* A callout definition object, typically defined in the levelbuilder, stored
* per-level.
* @typedef {Object} CalloutDefinition
* @property {string} localized_text - text contents for callout
* @property {string} element_id - jQuery selector of element to attach
* callout to (shows relative to element position, callout hides on
* element click)
* @property {object} qtip_config - qtip configuration for callout
* @see {@link http://qtip2.com/options} for full list of options
* @property {?string} on - optional ID of window event which should trigger
* callout show. Callout starts hidden.
*/
/**
* Given a set of callout definitions, installs them on the page
* @param {CalloutDefinition[]} callouts
*/
module.exports = function createCallouts(callouts) {
if (!callouts) {
return;
}
if (!callouts || callouts.length === 0) {
return;
}
// Hide callouts when the function editor is closed (otherwise they jump to
// the top left corner)
$(window).on('function_editor_closed', function () {
$('.cdo-qtips').qtip('hide');
});
// Update callout positions when a blockly editor is scrolled.
$(window).on('block_space_metrics_set', function () {
snapCalloutsToTargets();
showHideWorkspaceCallouts();
});
$(window).on('droplet_change', function (e, dropletEvent) {
switch (dropletEvent) {
case 'scrollace':
// Destroy all ace gutter tooltips on scroll. Ace dynamically reuses
// gutter elements with a scroll of even a singe line, moving one line
// number to a different DOM element, so the only ways to track with the
// gutter movement would be to manually adjust position or to destroy
// the qtips and manually recreate new ones with each scroll.
$('.cdo-qtips').each(function () {
var api = $(this).qtip('api');
var target = $(api.elements.target);
if ($('.ace_gutter').has(target)) {
api.destroy();
}
});
return;
case 'scrollpalette':
case 'scrolleditor':
snapCalloutsToTargets();
break;
}
showHidePaletteCallouts();
showHideDropletGutterCallouts();
});
var showCalloutsMode = document.URL.indexOf('show_callouts=1') !== -1;
$.fn.qtip.zindex = 500;
callouts.forEach(function (callout) {
var selector = callout.element_id; // jquery selector.
if ($(selector).length === 0 && !callout.on) {
return;
}
var defaultConfig = {
codeStudio: {
},
content: {
text: callout.localized_text,
title: {
button: $('<div class="tooltip-x-close"/>')
}
},
style: {
classes: "",
tip: {
width: 20,
height: 20
}
},
position: {
my: "bottom left",
at: "top right"
},
hide: {
event: 'click mousedown touchstart'
},
show: false // don't show on mouseover
};
var customConfig = typeof callout.qtip_config === 'string' ?
$.parseJSON(callout.qtip_config) : callout.qtip_config;
var config = $.extend(true, {}, defaultConfig, customConfig);
config.style.classes = config.style.classes.concat(" cdo-qtips");
callout.seen = clientState.hasSeenCallout(callout.id);
if (showCalloutsMode) {
callout.seen = false;
} else {
if (callout.seen && !config.codeStudio.canReappear) {
return;
}
if (!callout.seen) {
clientState.recordCalloutSeen(callout.id);
}
}
if (callout.hide_target_selector) {
config.hide.target = $(callout.hide_target_selector);
}
// Reverse callouts in RTL mode
if ($('html[dir=rtl]').length) {
config.position.my = reverseCallout(config.position.my);
config.position.at = reverseCallout(config.position.at);
if (config.position.adjust) {
config.position.adjust.x *= -1;
}
}
// Flip the close button if it would overlap the qtip
if (config.position.my === 'top right' || config.position.my === 'right top') {
config.style.classes += ' flip-x-close';
}
if (callout.on) {
$(window).on(callout.on, function (e, action) {
if (!config.codeStudio.selector) {
config.codeStudio.selector = selector;
}
var lastSelector = config.codeStudio.selector;
$(window).trigger('prepareforcallout', [config.codeStudio]);
if (lastSelector !== config.codeStudio.selector && $(lastSelector).length > 0) {
$(lastSelector).qtip(config).qtip('destroy');
}
// 'show' after async delay so that DOM changes that may have taken
// place inside the 'prepareforcallout' event can complete first
setTimeout(function () {
if ($(config.codeStudio.selector).length > 0) {
if (action === 'hashchange' || action === 'hashinit' || !callout.seen) {
$(config.codeStudio.selector).qtip(config).qtip('show');
}
callout.seen = true;
}
}, 0);
});
} else if (!callout.seen) {
$(selector).qtip(config).qtip('show');
}
});
// Insert a hashchange handler to detect triggercallout= hashes and fire
// appropriate events to open the callout
function detectTriggerCalloutOnHash(event) {
var loc = window.location;
var splitHash = loc.hash.split('#triggercallout=');
if (splitHash.length > 1) {
var eventName = splitHash[1];
var eventType = (event && event.type) || 'hashinit';
$(window).trigger(eventName, [eventType]);
// NOTE: normally we go back to avoid populating history, but not during init
if (window.history.go && eventType === 'hashchange') {
history.go(-1);
} else {
loc.hash = '';
}
}
}
// Call once during init to detect the hash from the initial page load
detectTriggerCalloutOnHash();
// Call again when the hash changes:
$(window).on('hashchange', detectTriggerCalloutOnHash);
};
/**
* Snap all callouts to their target positions. Keeps them in
* position when blockspace is scrolled.
*/
function snapCalloutsToTargets() {
var triggerEvent = null;
var animate = false;
$('.cdo-qtips').qtip('reposition', triggerEvent, animate);
}
var showHideWorkspaceCallouts = showOrHideCalloutsByTargetVisibility('#codeWorkspace');
var showHidePaletteCallouts =
showOrHideCalloutsByTargetVisibility('.droplet-palette-scroller');
var showHideDropletGutterCallouts = showOrHideCalloutsByTargetVisibility('.droplet-gutter');
/**
* For callouts with targets in the containerSelector (blockly, flyout elements,
* function editor elements, etc) hides callouts with targets that are
* scrolled out of view, and shows them again when they are scrolled back in
* to view.
* @function
*/
function showOrHideCalloutsByTargetVisibility(containerSelector) {
// Close around this object, which we use to remember which callouts
// were hidden by scrolling and should be shown again when they scroll
// back in.
/**
* Remember callouts hidden due to overlap, keyed by qtip id
* @type {Object.<string, boolean>}
*/
var calloutsHiddenByScrolling = {};
return function () {
var container = $(containerSelector);
$('.cdo-qtips').each(function () {
var api = $(this).qtip('api');
var target = $(api.elements.target);
if ($(document).has(target).length === 0) {
api.destroy(true);
return;
}
var isTargetInContainer = container.has(target).length > 0;
if (!isTargetInContainer) {
return;
}
if (target && target.overlaps(container).length > 0) {
if (calloutsHiddenByScrolling[api.id]) {
api.show();
delete calloutsHiddenByScrolling[api.id];
}
} else {
if ($(this).is(':visible')) {
api.hide();
calloutsHiddenByScrolling[api.id] = true;
}
}
});
};
}
function reverseCallout(position) {
position = position.split(/\s+/);
return reverseDirection(position[0]) + ' ' + reverseDirection(position[1]);
}
function reverseDirection(token) {
switch (token) {
case 'left': return 'right';
case 'right': return 'left';
default: return token;
}
}