forked from joseph/Monocle
-
Notifications
You must be signed in to change notification settings - Fork 1
/
stencil.js
352 lines (298 loc) · 9.22 KB
/
stencil.js
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
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
Monocle.Controls.Stencil = function (reader) {
if (Monocle.Controls == this) { return new this.Stencil(reader); }
var API = { constructor: Monocle.Controls.Stencil }
var k = API.constants = API.constructor;
var p = API.properties = {
reader: reader,
activeComponent: null,
components: {},
cutouts: []
}
// Create the stencil container and listen for draw/update events.
//
function createControlElements(holder) {
p.container = holder.dom.make('div', k.CLS.container);
p.reader.listen('monocle:turn', draw);
p.reader.listen('monocle:stylesheetchange', update);
p.reader.listen('monocle:resize', update);
p.reader.listen('monocle:componentchange', function (evt) {
Monocle.defer(update);
});
p.reader.listen('monocle:interactive:on', disable);
p.reader.listen('monocle:interactive:off', enable);
p.baseURL = getBaseURL();
return p.container;
}
// Resets any pre-calculated rectangles for the active component,
// recalculates them, and forces cutouts to be "drawn" (moved into the new
// rectangular locations).
//
function update() {
var pageDiv = p.reader.visiblePages()[0];
var cmptId = pageComponentId(pageDiv);
p.components[cmptId] = null;
calculateRectangles(pageDiv);
draw();
}
// Aligns the stencil container to the shape of the page, then moves the
// cutout links to sit above any currently visible rectangles.
//
function draw() {
var pageDiv = p.reader.visiblePages()[0];
var cmptId = pageComponentId(pageDiv);
if (!p.components[cmptId]) {
return;
}
// Position the container.
alignToComponent(pageDiv);
// Layout the cutouts.
var placed = 0;
if (!p.disabled) {
var rects = p.components[cmptId];
if (rects && rects.length) {
placed = layoutRectangles(pageDiv, rects);
}
}
// Hide remaining rects.
while (placed < p.cutouts.length) {
hideCutout(placed);
placed += 1;
}
}
// Iterate over all the <a> elements in the active component, and
// create an array of rectangular points corresponding to their positions.
//
function calculateRectangles(pageDiv) {
var cmptId = pageComponentId(pageDiv);
p.activeComponent = cmptId;
var doc = pageDiv.m.activeFrame.contentDocument;
var offset = getOffset(pageDiv);
// BROWSERHACK: Gecko doesn't subtract translations from GBCR values.
if (Monocle.Browser.is.Gecko) {
offset.l = 0;
}
var calcRects = false;
if (!p.components[cmptId]) {
p.components[cmptId] = []
calcRects = true;
}
var iElems = doc.getElementsByTagName('a');
for (var i = 0; i < iElems.length; ++i) {
if (iElems[i].href) {
var href = deconstructHref(iElems[i].href);
fixLink(iElems[i], href, clickHandler);
if (calcRects && iElems[i].getClientRects) {
var r = iElems[i].getClientRects();
for (var j = 0; j < r.length; j++) {
p.components[cmptId].push({
link: iElems[i],
href: href,
left: Math.ceil(r[j].left + offset.l),
top: Math.ceil(r[j].top),
width: Math.floor(r[j].width),
height: Math.floor(r[j].height)
});
}
}
}
}
return p.components[cmptId];
}
// Find the offset position in pixels from the left of the current page.
//
function getOffset(pageDiv) {
return {
l: pageDiv.m.offset || 0,
w: pageDiv.m.dimensions.properties.measurements.width
};
}
// Update location of visible rectangles - creating as required.
//
function layoutRectangles(pageDiv, rects) {
var offset = getOffset(pageDiv);
var visRects = [];
for (var i = 0; i < rects.length; ++i) {
if (rectVisible(rects[i], offset.l, offset.l + offset.w)) {
visRects.push(rects[i]);
}
}
for (i = 0; i < visRects.length; ++i) {
if (!p.cutouts[i]) {
p.cutouts[i] = createCutout();
}
var link = p.cutouts[i];
link.dom.setStyles({
display: 'block',
left: (visRects[i].left - offset.l)+"px",
top: visRects[i].top+"px",
width: visRects[i].width+"px",
height: visRects[i].height+"px"
});
link.relatedLink = visRects[i].link;
fixLink(link, visRects[i].href, cutoutClick);
}
return i;
}
// Set the link (either the original <a> tag or a cutout) to listen for
// clicks and go to the corresponding component (or open the external URL
// in a new window).
//
// NB: if the original link already has a click handler on it (eg, if the
// content is scripted), that click handler can:
//
// * stopPropagation if it is defined first
// * run Monocle.Events.deafen(link, 'click', link.stencilClickHandler)
//
// in order to prevent the default stencil click behaviour when in
// interactive mode.
//
function fixLink(link, hrefObject, handler) {
link.setAttribute('target', '_blank');
link.deconstructedHref = hrefObject;
if (link.stencilClickHandler) { return; }
link.stencilClickHandler = handler;
Monocle.Events.listen(link, 'click', link.stencilClickHandler);
}
function createCutout() {
var cutout = p.container.dom.append('a', k.CLS.cutout);
return cutout;
}
// Returns the active component id for the given page, or the current
// page if no argument passed in.
//
function pageComponentId(pageDiv) {
pageDiv = pageDiv || p.reader.visiblePages()[0];
return pageDiv.m.activeFrame.m.component.properties.id;
}
// Positions the stencil container over the active frame.
//
function alignToComponent(pageDiv) {
cmpt = pageDiv.m.activeFrame.parentNode;
p.container.dom.setStyles({
top: cmpt.offsetTop + "px",
left: cmpt.offsetLeft + "px"
});
}
function hideCutout(index) {
p.cutouts[index].dom.setStyles({ display: 'none' });
}
function rectVisible(rect, l, r) {
return rect.left >= l && rect.left < r;
}
// Make the active cutouts visible (by giving them a class -- override style
// in monocle.css).
//
function toggleHighlights() {
var cls = k.CLS.highlights
if (p.container.dom.hasClass(cls)) {
p.container.dom.removeClass(cls);
} else {
p.container.dom.addClass(cls);
}
}
// Returns an object with either:
//
// - an 'external' property -- an absolute URL with a protocol,
// host & etc, which should be treated as an external resource (eg,
// open in new window)
//
// OR
//
// - a 'componentId' property -- a relative URL with no forward slash,
// which must be treated as a componentId; and
// - a 'hash' property -- which may be an anchor in the form "#foo", or
// may be blank.
//
// Expects an absolute URL to be passed in. A weird but useful property
// of <a> tags is that while link.getAttribute('href') will return the
// actual string value of the attribute (eg, 'foo.html'), link.href will
// return the absolute URL (eg, 'http://example.com/monocles/foo.html').
//
function deconstructHref(url) {
var result = {};
var re = new RegExp("^"+p.baseURL+"([^#]*)(#.*)?$");
var match = url.match(re);
if (match) {
result.componentId = match[1] || pageComponentId();
result.hash = match[2] || '';
} else {
result.external = url;
}
return result;
}
// Returns the base URL for the reader's host page, which can be used
// to deconstruct the hrefs of individual links within components.
//
function getBaseURL() {
var a = document.createElement('a');
a.setAttribute('href', 'x');
return a.href.replace(/x$/,'')
}
// Invoked when a cutout is clicked -- opens external URL in new window,
// or moves to an internal component.
//
function cutoutClick(evt) {
var link = evt.currentTarget;
olink = link.relatedLink;
Monocle.Events.listen(olink, 'click', clickHandler);
var mimicEvt = document.createEvent('MouseEvents');
mimicEvt.initMouseEvent(
'click',
true,
true,
document.defaultView,
evt.detail,
evt.screenX,
evt.screenY,
evt.screenX,
evt.screenY,
evt.ctrlKey,
evt.altKey,
evt.shiftKey,
evt.metaKey,
evt.which,
null
);
try {
olink.dispatchEvent(mimicEvt);
} finally {
Monocle.Events.deafen(olink, 'click', clickHandler);
}
}
function clickHandler(evt) {
if (evt.defaultPrevented) { // NB: unfortunately not supported in Gecko.
return;
}
var link = evt.currentTarget;
var href = link.deconstructedHref;
if (!href) {
return;
}
if (href.external) {
link.href = href.external;
return;
}
var cmptId = href.componentId + href.hash;
p.reader.skipToChapter(cmptId);
evt.preventDefault();
}
function disable() {
p.disabled = true;
draw();
}
function enable() {
p.disabled = false;
draw();
}
API.createControlElements = createControlElements;
API.draw = draw;
API.update = update;
API.toggleHighlights = toggleHighlights;
return API;
}
Monocle.Controls.Stencil.CLS = {
container: 'controls_stencil_container',
cutout: 'controls_stencil_cutout',
highlights: 'controls_stencil_highlighted'
}
Monocle.pieceLoaded('controls/stencil');