Skip to content
This repository
Browse code

Billboard alignment, refactoring stencil masking with 'behaviors'.

You can add your own behaviors to the stencil control by providing a
simple object that responds to findElements and fitMask. See
the Monocle.Controls.Stencil.Links object for details, in controls/stencil.js.
  • Loading branch information...
commit ec6101861fe4e4cf1aea5f186f6dc53c1215e613 1 parent 3cb6b03
Joseph Pearson authored
204 src/controls/stencil.js
... ... @@ -1,9 +1,10 @@
1   -Monocle.Controls.Stencil = function (reader) {
  1 +Monocle.Controls.Stencil = function (reader, behaviors) {
2 2
3 3 var API = { constructor: Monocle.Controls.Stencil }
4 4 var k = API.constants = API.constructor;
5 5 var p = API.properties = {
6 6 reader: reader,
  7 + behaviors: behaviors || [ new Monocle.Controls.Stencil.Links(API) ],
7 8 components: {},
8 9 masks: []
9 10 }
@@ -21,6 +22,16 @@ Monocle.Controls.Stencil = function (reader) {
21 22 }
22 23
23 24
  25 + function addBehavior(bhvr) {
  26 + if (typeof bhvr.findElements != 'function') {
  27 + console.warn('Missing "findElements" property for behavior: %o', bhvr);
  28 + return;
  29 + }
  30 + p.behaviors.push(bhvr);
  31 + update();
  32 + }
  33 +
  34 +
24 35 // Resets any pre-calculated rectangles for the active component,
25 36 // recalculates them, and forces masks to be "drawn" (moved into the new
26 37 // rectangular locations).
@@ -78,19 +89,23 @@ Monocle.Controls.Stencil = function (reader) {
78 89 // BROWSERHACK: Gecko doesn't subtract translations from GBCR values.
79 90 if (Monocle.Browser.is.Gecko) { offset.l = 0; }
80 91
81   - var elems = doc.querySelectorAll('a, img');
82   - for (var i = 0; i < elems.length; ++i) {
83   - var elem = elems[i];
84   - if (filterElement(elem) && elem.getClientRects) {
85   - var r = elem.getClientRects();
86   - for (var j = 0; j < r.length; j++) {
87   - p.components[cmptId].push({
88   - element: elem,
89   - left: Math.ceil(r[j].left + offset.l),
90   - top: Math.ceil(r[j].top),
91   - width: Math.floor(r[j].width),
92   - height: Math.floor(r[j].height)
93   - });
  92 + for (var b = 0, bb = p.behaviors.length; b < bb; ++b) {
  93 + var bhvr = p.behaviors[b];
  94 + var elems = bhvr.findElements(doc);
  95 + for (var i = 0; i < elems.length; ++i) {
  96 + var elem = elems[i];
  97 + if (elem.getClientRects) {
  98 + var r = elem.getClientRects();
  99 + for (var j = 0; j < r.length; j++) {
  100 + p.components[cmptId].push({
  101 + element: elem,
  102 + behavior: bhvr,
  103 + left: Math.ceil(r[j].left + offset.l),
  104 + top: Math.ceil(r[j].top),
  105 + width: Math.floor(r[j].width),
  106 + height: Math.floor(r[j].height)
  107 + });
  108 + }
94 109 }
95 110 }
96 111 }
@@ -112,13 +127,13 @@ Monocle.Controls.Stencil = function (reader) {
112 127
113 128 for (i = 0; i < visRects.length; ++i) {
114 129 var r = visRects[i];
115   - var mask = createMask();
116 130 var cr = {
117 131 left: r.left - offset.l,
118 132 top: r.top,
119 133 width: r.width,
120 134 height: r.height
121 135 };
  136 + var mask = createMask(r.element, r.behavior);
122 137 mask.dom.setStyles({
123 138 display: 'block',
124 139 left: cr.left+"px",
@@ -126,12 +141,7 @@ Monocle.Controls.Stencil = function (reader) {
126 141 width: cr.width+"px",
127 142 height: cr.height+"px"
128 143 });
129   - mask.originElement = r.element;
130 144 mask.stencilRect = cr;
131   - var maskIsListening = maskAssigned(r.element, mask);
132   - if (!maskIsListening) {
133   - Monocle.Events.listen(mask, 'click', maskClick);
134   - }
135 145 }
136 146 }
137 147
@@ -173,50 +183,19 @@ Monocle.Controls.Stencil = function (reader) {
173 183 }
174 184
175 185
176   - function createMask() {
177   - var mask = p.container.dom.append('div', k.CLS.mask);
  186 + function createMask(element, bhvr) {
  187 + var mask = p.container.dom.append(bhvr.maskTagName || 'div', k.CLS.mask);
178 188 Monocle.Events.listenForContact(mask, {
179   - start: function () {
180   - p.reader.dispatchEvent('monocle:magic:stop');
181   - },
182   - end: function () {
183   - p.reader.dispatchEvent('monocle:magic:init');
184   - }
  189 + start: function () { p.reader.dispatchEvent('monocle:magic:stop'); },
  190 + end: function () { p.reader.dispatchEvent('monocle:magic:init'); }
185 191 });
  192 + bhvr.fitMask(element, mask);
186 193 return mask;
187 194 }
188 195
189 196
190   - // Invoked when a mask is clicked -- opens external URL in new window,
191   - // or moves to an internal component.
192   - //
193   - function maskClick(evt) {
194   - var mask = evt.currentTarget;
195   - var originElement = mask.originElement;
196   - var mimicEvt = document.createEvent('MouseEvents');
197   - mimicEvt.initMouseEvent(
198   - 'click',
199   - true,
200   - true,
201   - document.defaultView,
202   - evt.detail,
203   - evt.screenX,
204   - evt.screenY,
205   - evt.screenX,
206   - evt.screenY,
207   - evt.ctrlKey,
208   - evt.altKey,
209   - evt.shiftKey,
210   - evt.metaKey,
211   - evt.which,
212   - null
213   - );
214   - originElement.dispatchEvent(mimicEvt);
215   - }
216   -
217   -
218 197 // Make the active masks visible (by giving them a class -- override style
219   - // in monocle.css).
  198 + // in monoctrl.css).
220 199 //
221 200 function toggleHighlights() {
222 201 var cls = k.CLS.highlights;
@@ -240,48 +219,72 @@ Monocle.Controls.Stencil = function (reader) {
240 219 }
241 220
242 221
243   - function filterElement(elem) {
244   - if (elem.tagName.toLowerCase() == 'img') {
245   - return elem;
  222 + function filterElement(elem, behavior) {
  223 + if (typeof behavior.filterElement == 'function') {
  224 + return behavior.filterElement(elem);
246 225 }
  226 + return elem;
  227 + }
  228 +
247 229
248   - if (!elem.href) {
249   - return false;
  230 + function maskAssigned(elem, mask, behavior) {
  231 + if (typeof behavior.maskAssigned == 'function') {
  232 + return behavior.maskAssigned(elem, mask);
250 233 }
  234 + return false;
  235 + }
251 236
252   - var hrefObject = deconstructHref(elem);
253   - elem.setAttribute('target', '_blank');
254   - Monocle.Events.listen(elem, 'click', function (evt) {
255   - if (evt.defaultPrevented) { // NB: unfortunately not supported in Gecko.
256   - return;
257   - }
258   - if (!hrefObject || hrefObject.external) { return; }
259   - p.reader.skipToChapter(hrefObject.internal);
260   - evt.preventDefault();
261   - });
262   - return elem;
  237 +
  238 + API.createControlElements = createControlElements;
  239 + API.addBehavior = addBehavior;
  240 + API.draw = draw;
  241 + API.update = update;
  242 + API.toggleHighlights = toggleHighlights;
  243 +
  244 + return API;
  245 +}
  246 +
  247 +
  248 +Monocle.Controls.Stencil.CLS = {
  249 + container: 'controls_stencil_container',
  250 + mask: 'controls_stencil_mask',
  251 + highlights: 'controls_stencil_highlighted'
  252 +}
  253 +
  254 +
  255 +Monocle.Controls.Stencil.Links = function (stencil) {
  256 + var API = { constructor: Monocle.Controls.Stencil.Links }
  257 +
  258 + // Optionally specify the HTML tagname of the mask.
  259 + API.maskTagName = 'a';
  260 +
  261 + // Returns an array of all the elements in the given doc that should
  262 + // be covered with a stencil mask for interactivity.
  263 + //
  264 + // (Hint: doc.querySelectorAll() is your friend.)
  265 + //
  266 + API.findElements = function (doc) {
  267 + return doc.querySelectorAll('a[href]');
263 268 }
264 269
265 270
266   - function maskAssigned(elem, mask) {
267   - if (elem.tagName.toLowerCase() == 'img') {
268   - Monocle.Events.listenForTap(mask, function (evt) {
269   - evt.stopPropagation();
  271 + // Return an element. It should usually be a child of the container element,
  272 + // with a className of the given maskClass. You set up the interactivity of
  273 + // the mask element here.
  274 + //
  275 + API.fitMask = function (link, mask) {
  276 + var hrefObject = deconstructHref(link);
  277 + if (hrefObject.internal) {
  278 + mask.setAttribute('href', 'javascript:"Skip to chapter"');
  279 + Monocle.Events.listen(mask, 'click', function (evt) {
  280 + stencil.properties.reader.skipToChapter(hrefObject.internal);
270 281 evt.preventDefault();
271   - var options = {
272   - scrollTo: 'center',
273   - from: [
274   - mask.stencilRect.left+p.container.offsetLeft,
275   - mask.stencilRect.top+p.container.offsetTop
276   - ]
277   - };
278   - var img = document.createElement('img');
279   - img.src = elem.src;
280   - window.monReader.billboard.show(img, options);
281 282 });
282   - return true;
  283 + } else {
  284 + mask.setAttribute('href', hrefObject.external);
  285 + mask.setAttribute('target', '_blank');
  286 + link.setAttribute('target', '_blank'); // For good measure.
283 287 }
284   - return false;
285 288 }
286 289
287 290
@@ -296,10 +299,10 @@ Monocle.Controls.Stencil = function (reader) {
296 299 // - an 'internal' property -- a relative URL (with optional hash anchor),
297 300 // that is treated as a link to component in the book
298 301 //
299   - // A weird but useful property
300   - // of <a> tags is that while link.getAttribute('href') will return the
301   - // actual string value of the attribute (eg, 'foo.html'), link.href will
302   - // return the absolute URL (eg, 'http://example.com/monocles/foo.html').
  302 + // A weird but useful property of <a> tags is that while
  303 + // link.getAttribute('href') will return the actual string value of the
  304 + // attribute (eg, 'foo.html'), link.href will return the absolute URL (eg,
  305 + // 'http://example.com/monocles/foo.html').
303 306 //
304 307 function deconstructHref(elem) {
305 308 var url = elem.href;
@@ -307,7 +310,7 @@ Monocle.Controls.Stencil = function (reader) {
307 310 var m = url.match(/([^#]*)(#.*)?$/);
308 311 var path = m[1];
309 312 var anchor = m[2] || '';
310   - var cmpts = p.reader.getBook().properties.componentIds;
  313 + var cmpts = stencil.properties.reader.getBook().properties.componentIds;
311 314 for (var i = 0, ii = cmpts.length; i < ii; ++i) {
312 315 if (path.substr(0 - cmpts[i].length) == cmpts[i]) {
313 316 return { internal: cmpts[i] + anchor };
@@ -317,18 +320,5 @@ Monocle.Controls.Stencil = function (reader) {
317 320 return { external: url };
318 321 }
319 322
320   -
321   - API.createControlElements = createControlElements;
322   - API.draw = draw;
323   - API.update = update;
324   - API.toggleHighlights = toggleHighlights;
325   -
326 323 return API;
327 324 }
328   -
329   -
330   -Monocle.Controls.Stencil.CLS = {
331   - container: 'controls_stencil_container',
332   - mask: 'controls_stencil_mask',
333   - highlights: 'controls_stencil_highlighted'
334   -}
60 src/core/billboard.js
@@ -13,32 +13,23 @@ Monocle.Billboard = function (reader) {
13 13
14 14 var options = options || {};
15 15 var elem = urlOrElement;
16   - var inner = null;
17 16 p.cntr = reader.dom.append('div', k.CLS.cntr);
18 17 if (typeof urlOrElement == 'string') {
19 18 var url = urlOrElement;
20   - inner = elem = p.cntr.dom.append('iframe', k.CLS.inner);
  19 + p.inner = elem = p.cntr.dom.append('iframe', k.CLS.inner);
21 20 elem.setAttribute('src', url);
22 21 } else {
23   - inner = p.cntr.dom.append('div', k.CLS.inner);
24   - inner.appendChild(elem);
  22 + p.inner = p.cntr.dom.append('div', k.CLS.inner);
  23 + p.inner.appendChild(elem);
25 24 }
  25 + p.dims = [elem.offsetWidth, elem.offsetHeight];
26 26 if (options.closeButton != false) {
27 27 var cBtn = p.cntr.dom.append('div', k.CLS.closeButton);
28 28 Monocle.Events.listenForTap(cBtn, hide);
29 29 }
30   - if (options.scrollTo == 'center') {
31   - inner.scrollLeft = (inner.scrollWidth - inner.offsetWidth) / 2;
32   - inner.scrollTop = (inner.scrollHeight - inner.offsetHeight) / 2;
33   - if (inner.offsetHeight > elem.offsetHeight) {
34   - inner.style.paddingTop = (inner.offsetHeight-elem.offsetHeight)/2+'px';
35   - }
36   - if (inner.offsetWidth > elem.offsetWidth) {
37   - inner.style.paddingLeft = (inner.offsetWidth-elem.offsetWidth)/2+'px';
38   - }
39   - } else {
40   - inner.scrollLeft = 1;
41   - }
  30 + align(options.align || 'left top');
  31 + p.reader.listen('monocle:resize', align);
  32 +
42 33 shrink(options.from);
43 34 Monocle.defer(grow);
44 35 }
@@ -46,7 +37,6 @@ Monocle.Billboard = function (reader) {
46 37
47 38 function hide(evt) {
48 39 shrink();
49   - p.reader.dispatchEvent('monocle:magic:init');
50 40 Monocle.Events.afterTransition(p.cntr, remove);
51 41 }
52 42
@@ -71,19 +61,51 @@ Monocle.Billboard = function (reader) {
71 61
72 62 function remove () {
73 63 p.cntr.parentNode.removeChild(p.cntr);
74   - p.cntr = null;
  64 + p.cntr = p.inner = null;
  65 + p.reader.deafen('monocle:resize', align);
75 66 p.reader.dispatchEvent('monocle:magic:init');
76 67 }
77 68
78 69
  70 + function align(loc) {
  71 + p.alignment = (typeof loc == 'string') ? loc : p.alignment;
  72 + if (!p.alignment) { return; }
  73 + if (p.dims[0] > p.inner.offsetWidth || p.dims[1] > p.inner.offsetHeight) {
  74 + p.cntr.dom.addClass(k.CLS.oversized);
  75 + } else {
  76 + p.cntr.dom.removeClass(k.CLS.oversized);
  77 + }
  78 +
  79 + var s = p.alignment.split(' ');
  80 + var l = 0, t = 0;
  81 + var w = (p.inner.scrollWidth - p.inner.offsetWidth);
  82 + var h = (p.inner.scrollHeight - p.inner.offsetHeight);
  83 + if (s[0] == 'center') {
  84 + l = w / 2;
  85 + } else if (s[0] == 'right') {
  86 + l = w;
  87 + }
  88 + if (!s[1] || s[1] == 'center') {
  89 + t = h / 2;
  90 + } else if (s[1] == 'bottom') {
  91 + t = h;
  92 + }
  93 + p.inner.scrollLeft = l;
  94 + p.inner.scrollTop = t;
  95 + }
  96 +
  97 +
79 98 API.show = show;
80 99 API.hide = hide;
  100 + API.align= align;
81 101
82 102 return API;
83 103 }
84 104
  105 +
85 106 Monocle.Billboard.CLS = {
86 107 cntr: 'billboard_container',
87 108 inner: 'billboard_inner',
88   - closeButton: 'billboard_close'
  109 + closeButton: 'billboard_close',
  110 + oversized: 'billboard_oversized'
89 111 }
8 styles/monocore.css
@@ -69,9 +69,17 @@ div.monelem_billboard_container {
69 69 }
70 70
71 71 div.monelem_billboard_inner {
  72 + min-width: 100%;
  73 + min-height: 100%;
72 74 background: #000;
  75 + text-align: center;
  76 + vertical-align: middle;
  77 + display: -webkit-box;
  78 + -webkit-box-pack: center;
  79 + -webkit-box-align: center;
73 80 }
74 81
  82 +
75 83 div.monelem_billboard_close {
76 84 position: absolute;
77 85 top: 0;
4 styles/monoctrl.css
@@ -142,12 +142,12 @@ div.monelem_controls_stencil_container {
142 142 height: 0;
143 143 }
144 144
145   -div.monelem_controls_stencil_mask {
  145 +.monelem_controls_stencil_mask {
146 146 display: block;
147 147 position: absolute;
148 148 }
149 149
150   -div.monelem_controls_stencil_highlighted div {
  150 +div.monelem_controls_stencil_highlighted .monelem_controls_stencil_mask {
151 151 background: rgba(0,0,255,0.15);
152 152 }
153 153
2  test/stencil/content/0.html
@@ -8,7 +8,7 @@
8 8 <p><a href="1/1.html">A good link</a> to component 1</p>
9 9 <p><a href="2/2.html">A good link</a> to component 2</p>
10 10 <p><a href="3/3.html">A bad link</a> to component 3 (which does not exist)</p>
11   - <p><a href="content/1/1.html">A bad link</a> to component 1 from the wrong relative root, which matches the component id</p>
  11 + <p><a href="content/1/1.html">A good link</a> to component 1, which matches the component id</p>
12 12 <p><a href="/test/stencil/content/1/1.html">A good link</a> to component 1 from the absolute root</p>
13 13 <p><a href="http://monocle.inventivelabs.com.au">A good link</a> to an external address</p>
14 14 </body>

0 comments on commit ec61018

Please sign in to comment.
Something went wrong with that request. Please try again.