|
|
@@ -0,0 +1,985 @@ |
|
|
|
Monocle.Controls.Contents = function (reader) { |
|
|
|
|
|
|
|
var API = { constructor: Monocle.Controls.Contents } |
|
|
|
var k = API.constants = API.constructor; |
|
|
|
var p = API.properties = { |
|
|
|
reader: reader |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function createControlElements() { |
|
|
|
var div = reader.dom.make('div', 'controls_contents_container'); |
|
|
|
contentsForBook(div, reader.getBook()); |
|
|
|
return div; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function contentsForBook(div, book) { |
|
|
|
while (div.hasChildNodes()) { |
|
|
|
div.removeChild(div.firstChild); |
|
|
|
} |
|
|
|
var list = div.dom.append('ol', 'controls_contents_list'); |
|
|
|
|
|
|
|
var contents = book.properties.contents; |
|
|
|
for (var i = 0; i < contents.length; ++i) { |
|
|
|
chapterBuilder(list, contents[i], 0); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function chapterBuilder(list, chp, padLvl) { |
|
|
|
var index = list.childNodes.length; |
|
|
|
var li = list.dom.append('li', 'controls_contents_chapter', index); |
|
|
|
var span = li.dom.append( |
|
|
|
'span', |
|
|
|
'controls_contents_chapterTitle', |
|
|
|
index, |
|
|
|
{ html: chp.title } |
|
|
|
); |
|
|
|
span.style.paddingLeft = padLvl + "em"; |
|
|
|
|
|
|
|
var invoked = function () { |
|
|
|
p.reader.skipToChapter(chp.src); |
|
|
|
p.reader.hideControl(API); |
|
|
|
} |
|
|
|
|
|
|
|
Monocle.Events.listenForTap(li, invoked, 'controls_contents_chapter_active'); |
|
|
|
|
|
|
|
if (chp.children) { |
|
|
|
for (var i = 0; i < chp.children.length; ++i) { |
|
|
|
chapterBuilder(list, chp.children[i], padLvl + 1); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
API.createControlElements = createControlElements; |
|
|
|
|
|
|
|
return API; |
|
|
|
} |
|
|
|
; |
|
|
|
Monocle.Controls.Magnifier = function (reader) { |
|
|
|
|
|
|
|
var API = { constructor: Monocle.Controls.Magnifier } |
|
|
|
var k = API.constants = API.constructor; |
|
|
|
var p = API.properties = { |
|
|
|
buttons: [], |
|
|
|
magnified: false |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function initialize() { |
|
|
|
p.reader = reader; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function createControlElements(holder) { |
|
|
|
var btn = holder.dom.make('div', 'controls_magnifier_button'); |
|
|
|
btn.smallA = btn.dom.append('span', 'controls_magnifier_a', { text: 'A' }); |
|
|
|
btn.largeA = btn.dom.append('span', 'controls_magnifier_A', { text: 'A' }); |
|
|
|
p.buttons.push(btn); |
|
|
|
Monocle.Events.listenForTap(btn, toggleMagnification); |
|
|
|
return btn; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function toggleMagnification(evt) { |
|
|
|
var opacities; |
|
|
|
p.magnified = !p.magnified; |
|
|
|
if (p.magnified) { |
|
|
|
opacities = [0.3, 1]; |
|
|
|
p.reader.formatting.setFontScale(k.MAGNIFICATION, true); |
|
|
|
} else { |
|
|
|
opacities = [1, 0.3]; |
|
|
|
p.reader.formatting.setFontScale(null, true); |
|
|
|
} |
|
|
|
|
|
|
|
for (var i = 0; i < p.buttons.length; i++) { |
|
|
|
p.buttons[i].smallA.style.opacity = opacities[0]; |
|
|
|
p.buttons[i].largeA.style.opacity = opacities[1]; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
API.createControlElements = createControlElements; |
|
|
|
|
|
|
|
initialize(); |
|
|
|
|
|
|
|
return API; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Monocle.Controls.Magnifier.MAGNIFICATION = 1.2; |
|
|
|
// A panel is an invisible column of interactivity. When contact occurs |
|
|
|
// (mousedown, touchstart), the panel expands to the full width of its |
|
|
|
// container, to catch all interaction events and prevent them from hitting |
|
|
|
// other things. |
|
|
|
// |
|
|
|
// Panels are used primarily to provide hit zones for page flipping |
|
|
|
// interactions, but you can do whatever you like with them. |
|
|
|
// |
|
|
|
// After instantiating a panel and adding it to the reader as a control, |
|
|
|
// you can call listenTo() with a hash of methods for any of 'start', 'move' |
|
|
|
// 'end' and 'cancel'. |
|
|
|
// |
|
|
|
Monocle.Controls.Panel = function () { |
|
|
|
|
|
|
|
var API = { constructor: Monocle.Controls.Panel } |
|
|
|
var k = API.constants = API.constructor; |
|
|
|
var p = API.properties = { |
|
|
|
evtCallbacks: {} |
|
|
|
} |
|
|
|
|
|
|
|
function createControlElements(cntr) { |
|
|
|
p.div = cntr.dom.make('div', k.CLS.panel); |
|
|
|
p.div.dom.setStyles(k.DEFAULT_STYLES); |
|
|
|
Monocle.Events.listenForContact( |
|
|
|
p.div, |
|
|
|
{ |
|
|
|
'start': start, |
|
|
|
'move': move, |
|
|
|
'end': end, |
|
|
|
'cancel': cancel |
|
|
|
}, |
|
|
|
{ useCapture: false } |
|
|
|
); |
|
|
|
return p.div; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function setDirection(dir) { |
|
|
|
p.direction = dir; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function listenTo(evtCallbacks) { |
|
|
|
p.evtCallbacks = evtCallbacks; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function deafen() { |
|
|
|
p.evtCallbacks = {} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function start(evt) { |
|
|
|
p.contact = true; |
|
|
|
evt.m.offsetX += p.div.offsetLeft; |
|
|
|
evt.m.offsetY += p.div.offsetTop; |
|
|
|
expand(); |
|
|
|
invoke('start', evt); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function move(evt) { |
|
|
|
if (!p.contact) { |
|
|
|
return; |
|
|
|
} |
|
|
|
invoke('move', evt); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function end(evt) { |
|
|
|
if (!p.contact) { |
|
|
|
return; |
|
|
|
} |
|
|
|
Monocle.Events.deafenForContact(p.div, p.listeners); |
|
|
|
contract(); |
|
|
|
p.contact = false; |
|
|
|
invoke('end', evt); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function cancel(evt) { |
|
|
|
if (!p.contact) { |
|
|
|
return; |
|
|
|
} |
|
|
|
Monocle.Events.deafenForContact(p.div, p.listeners); |
|
|
|
contract(); |
|
|
|
p.contact = false; |
|
|
|
invoke('cancel', evt); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function invoke(evtType, evt) { |
|
|
|
if (p.evtCallbacks[evtType]) { |
|
|
|
p.evtCallbacks[evtType](p.direction, evt.m.offsetX, evt.m.offsetY, API); |
|
|
|
} |
|
|
|
evt.preventDefault(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function expand() { |
|
|
|
if (p.expanded) { |
|
|
|
return; |
|
|
|
} |
|
|
|
p.div.dom.addClass(k.CLS.expanded); |
|
|
|
p.expanded = true; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function contract(evt) { |
|
|
|
if (!p.expanded) { |
|
|
|
return; |
|
|
|
} |
|
|
|
p.div.dom.removeClass(k.CLS.expanded); |
|
|
|
p.expanded = false; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
API.createControlElements = createControlElements; |
|
|
|
API.listenTo = listenTo; |
|
|
|
API.deafen = deafen; |
|
|
|
API.expand = expand; |
|
|
|
API.contract = contract; |
|
|
|
API.setDirection = setDirection; |
|
|
|
|
|
|
|
return API; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Monocle.Controls.Panel.CLS = { |
|
|
|
panel: 'panel', |
|
|
|
expanded: 'controls_panel_expanded' |
|
|
|
} |
|
|
|
Monocle.Controls.Panel.DEFAULT_STYLES = { |
|
|
|
position: 'absolute', |
|
|
|
height: '100%' |
|
|
|
} |
|
|
|
; |
|
|
|
Monocle.Controls.PlaceSaver = function (bookId) { |
|
|
|
|
|
|
|
var API = { constructor: Monocle.Controls.PlaceSaver } |
|
|
|
var k = API.constants = API.constructor; |
|
|
|
var p = API.properties = {} |
|
|
|
|
|
|
|
|
|
|
|
function initialize() { |
|
|
|
applyToBook(bookId); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function assignToReader(reader) { |
|
|
|
p.reader = reader; |
|
|
|
p.reader.listen('monocle:turn', savePlaceToCookie); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function applyToBook(bookId) { |
|
|
|
p.bkTitle = bookId.toLowerCase().replace(/[^a-z0-9]/g, ''); |
|
|
|
p.prefix = k.COOKIE_NAMESPACE + p.bkTitle + "."; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function setCookie(key, value, days) { |
|
|
|
var expires = ""; |
|
|
|
if (days) { |
|
|
|
var d = new Date(); |
|
|
|
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); |
|
|
|
expires = "; expires="+d.toGMTString(); |
|
|
|
} |
|
|
|
var path = "; path=/"; |
|
|
|
document.cookie = p.prefix + key + "=" + value + expires + path; |
|
|
|
return value; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function getCookie(key) { |
|
|
|
if (!document.cookie) { |
|
|
|
return null; |
|
|
|
} |
|
|
|
var regex = new RegExp(p.prefix + key + "=(.+?)(;|$)"); |
|
|
|
var matches = document.cookie.match(regex); |
|
|
|
if (matches) { |
|
|
|
return matches[1]; |
|
|
|
} else { |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function savePlaceToCookie() { |
|
|
|
var place = p.reader.getPlace(); |
|
|
|
setCookie( |
|
|
|
"component", |
|
|
|
encodeURIComponent(place.componentId()), |
|
|
|
k.COOKIE_EXPIRES_IN_DAYS |
|
|
|
); |
|
|
|
setCookie( |
|
|
|
"percent", |
|
|
|
place.percentageThrough(), |
|
|
|
k.COOKIE_EXPIRES_IN_DAYS |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function savedPlace() { |
|
|
|
var locus = { |
|
|
|
componentId: getCookie('component'), |
|
|
|
percent: getCookie('percent') |
|
|
|
} |
|
|
|
if (locus.componentId && locus.percent) { |
|
|
|
locus.componentId = decodeURIComponent(locus.componentId); |
|
|
|
locus.percent = parseFloat(locus.percent); |
|
|
|
return locus; |
|
|
|
} else { |
|
|
|
return null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function restorePlace() { |
|
|
|
var locus = savedPlace(); |
|
|
|
if (locus) { |
|
|
|
p.reader.moveTo(locus); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
API.assignToReader = assignToReader; |
|
|
|
API.savedPlace = savedPlace; |
|
|
|
API.restorePlace = restorePlace; |
|
|
|
|
|
|
|
initialize(); |
|
|
|
|
|
|
|
return API; |
|
|
|
} |
|
|
|
|
|
|
|
Monocle.Controls.PlaceSaver.COOKIE_NAMESPACE = "monocle.controls.placesaver."; |
|
|
|
Monocle.Controls.PlaceSaver.COOKIE_EXPIRES_IN_DAYS = 7; // Set to 0 for session-based expiry. |
|
|
|
; |
|
|
|
Monocle.Controls.Scrubber = function (reader) { |
|
|
|
|
|
|
|
var API = { constructor: Monocle.Controls.Scrubber } |
|
|
|
var k = API.constants = API.constructor; |
|
|
|
var p = API.properties = {} |
|
|
|
|
|
|
|
|
|
|
|
function initialize() { |
|
|
|
p.reader = reader; |
|
|
|
p.reader.listen('monocle:turn', updateNeedles); |
|
|
|
updateNeedles(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function pixelToPlace(x, cntr) { |
|
|
|
if (!p.componentIds) { |
|
|
|
p.componentIds = p.reader.getBook().properties.componentIds; |
|
|
|
p.componentWidth = 100 / p.componentIds.length; |
|
|
|
} |
|
|
|
var pc = (x / cntr.offsetWidth) * 100; |
|
|
|
var cmpt = p.componentIds[Math.floor(pc / p.componentWidth)]; |
|
|
|
var cmptPc = ((pc % p.componentWidth) / p.componentWidth); |
|
|
|
return { componentId: cmpt, percentageThrough: cmptPc }; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function placeToPixel(place, cntr) { |
|
|
|
if (!p.componentIds) { |
|
|
|
p.componentIds = p.reader.getBook().properties.componentIds; |
|
|
|
p.componentWidth = 100 / p.componentIds.length; |
|
|
|
} |
|
|
|
var componentIndex = p.componentIds.indexOf(place.componentId()); |
|
|
|
var pc = p.componentWidth * componentIndex; |
|
|
|
pc += place.percentageThrough() * p.componentWidth; |
|
|
|
return Math.round((pc / 100) * cntr.offsetWidth); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function updateNeedles() { |
|
|
|
if (p.hidden || !p.reader.dom.find(k.CLS.container)) { |
|
|
|
return; |
|
|
|
} |
|
|
|
var place = p.reader.getPlace(); |
|
|
|
var x = placeToPixel(place, p.reader.dom.find(k.CLS.container)); |
|
|
|
var needle, i = 0; |
|
|
|
for (var i = 0, needle; needle = p.reader.dom.find(k.CLS.needle, i); ++i) { |
|
|
|
setX(needle, x - needle.offsetWidth / 2); |
|
|
|
p.reader.dom.find(k.CLS.trail, i).style.width = x + "px"; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function setX(node, x) { |
|
|
|
var cntr = p.reader.dom.find(k.CLS.container); |
|
|
|
x = Math.min(cntr.offsetWidth - node.offsetWidth, x); |
|
|
|
x = Math.max(x, 0); |
|
|
|
Monocle.Styles.setX(node, x); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function createControlElements(holder) { |
|
|
|
var cntr = holder.dom.make('div', k.CLS.container); |
|
|
|
var track = cntr.dom.append('div', k.CLS.track); |
|
|
|
var needleTrail = cntr.dom.append('div', k.CLS.trail); |
|
|
|
var needle = cntr.dom.append('div', k.CLS.needle); |
|
|
|
var bubble = cntr.dom.append('div', k.CLS.bubble); |
|
|
|
|
|
|
|
var cntrListeners, bodyListeners; |
|
|
|
|
|
|
|
var moveEvt = function (evt, x) { |
|
|
|
evt.preventDefault(); |
|
|
|
x = (typeof x == "number") ? x : evt.m.registrantX; |
|
|
|
var place = pixelToPlace(x, cntr); |
|
|
|
setX(needle, x - needle.offsetWidth / 2); |
|
|
|
var book = p.reader.getBook(); |
|
|
|
var chps = book.chaptersForComponent(place.componentId); |
|
|
|
var cmptIndex = p.componentIds.indexOf(place.componentId); |
|
|
|
var chp = chps[Math.floor(chps.length * place.percentageThrough)]; |
|
|
|
if (cmptIndex > -1 && book.properties.components[cmptIndex]) { |
|
|
|
var actualPlace = Monocle.Place.FromPercentageThrough( |
|
|
|
book.properties.components[cmptIndex], |
|
|
|
place.percentageThrough |
|
|
|
); |
|
|
|
chp = actualPlace.chapterInfo() || chp; |
|
|
|
} |
|
|
|
|
|
|
|
if (chp) { |
|
|
|
bubble.innerHTML = chp.title; |
|
|
|
} |
|
|
|
setX(bubble, x - bubble.offsetWidth / 2); |
|
|
|
|
|
|
|
p.lastX = x; |
|
|
|
return place; |
|
|
|
} |
|
|
|
|
|
|
|
var endEvt = function (evt) { |
|
|
|
var place = moveEvt(evt, p.lastX); |
|
|
|
p.reader.moveTo({ |
|
|
|
percent: place.percentageThrough, |
|
|
|
componentId: place.componentId |
|
|
|
}); |
|
|
|
Monocle.Events.deafenForContact(cntr, cntrListeners); |
|
|
|
Monocle.Events.deafenForContact(document.body, bodyListeners); |
|
|
|
bubble.style.display = "none"; |
|
|
|
} |
|
|
|
|
|
|
|
var startFn = function (evt) { |
|
|
|
bubble.style.display = "block"; |
|
|
|
moveEvt(evt); |
|
|
|
cntrListeners = Monocle.Events.listenForContact( |
|
|
|
cntr, |
|
|
|
{ move: moveEvt } |
|
|
|
); |
|
|
|
bodyListeners = Monocle.Events.listenForContact( |
|
|
|
document.body, |
|
|
|
{ end: endEvt } |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
Monocle.Events.listenForContact(cntr, { start: startFn }); |
|
|
|
|
|
|
|
return cntr; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
API.createControlElements = createControlElements; |
|
|
|
API.updateNeedles = updateNeedles; |
|
|
|
|
|
|
|
initialize(); |
|
|
|
|
|
|
|
return API; |
|
|
|
} |
|
|
|
|
|
|
|
Monocle.Controls.Scrubber.CLS = { |
|
|
|
container: 'controls_scrubber_container', |
|
|
|
track: 'controls_scrubber_track', |
|
|
|
needle: 'controls_scrubber_needle', |
|
|
|
trail: 'controls_scrubber_trail', |
|
|
|
bubble: 'controls_scrubber_bubble' |
|
|
|
} |
|
|
|
; |
|
|
|
Monocle.Controls.Spinner = function (reader) { |
|
|
|
|
|
|
|
var API = { constructor: Monocle.Controls.Spinner } |
|
|
|
var k = API.constants = API.constructor; |
|
|
|
var p = API.properties = { |
|
|
|
reader: reader, |
|
|
|
divs: [], |
|
|
|
repeaters: {}, |
|
|
|
showForPages: [] |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function createControlElements(cntr) { |
|
|
|
var anim = cntr.dom.make('div', 'controls_spinner_anim'); |
|
|
|
anim.dom.append('div', 'controls_spinner_inner'); |
|
|
|
p.divs.push(anim); |
|
|
|
return anim; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function registerSpinEvent(startEvtType, stopEvtType) { |
|
|
|
var label = startEvtType; |
|
|
|
p.reader.listen(startEvtType, function (evt) { spin(label, evt) }); |
|
|
|
p.reader.listen(stopEvtType, function (evt) { spun(label, evt) }); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Registers spin/spun event handlers for certain time-consuming events. |
|
|
|
// |
|
|
|
function listenForUsualDelays() { |
|
|
|
registerSpinEvent('monocle:componentloading', 'monocle:componentloaded'); |
|
|
|
registerSpinEvent('monocle:componentchanging', 'monocle:componentchange'); |
|
|
|
registerSpinEvent('monocle:resizing', 'monocle:resize'); |
|
|
|
registerSpinEvent('monocle:jumping', 'monocle:jump'); |
|
|
|
registerSpinEvent('monocle:recalculating', 'monocle:recalculated'); |
|
|
|
p.reader.listen('monocle:notfound', forceSpun); |
|
|
|
p.reader.listen('monocle:componentfailed', forceSpun); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Displays the spinner. Both arguments are optional. |
|
|
|
// |
|
|
|
function spin(label, evt) { |
|
|
|
label = label || k.GENERIC_LABEL; |
|
|
|
p.repeaters[label] = true; |
|
|
|
p.reader.showControl(API); |
|
|
|
|
|
|
|
// If the delay is on a page other than the page we've been assigned to, |
|
|
|
// don't show the animation. p.global ensures that if an event affects |
|
|
|
// all pages, the animation is always shown, even if other events in this |
|
|
|
// spin cycle are page-specific. |
|
|
|
var page = (evt && evt.m && evt.m.page) ? evt.m.page : null; |
|
|
|
if (page && p.divs.length > 1) { |
|
|
|
p.showForPages[page.m.pageIndex] = true; |
|
|
|
} else { |
|
|
|
p.global = true; |
|
|
|
p.reader.dispatchEvent('monocle:modal:on'); |
|
|
|
} |
|
|
|
for (var i = 0; i < p.divs.length; ++i) { |
|
|
|
var show = (p.global || p.showForPages[i]) ? true : false; |
|
|
|
p.divs[i].dom[show ? 'removeClass' : 'addClass']('dormant'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Stops displaying the spinner. Both arguments are optional. |
|
|
|
// |
|
|
|
function spun(label, evt) { |
|
|
|
label = label || k.GENERIC_LABEL; |
|
|
|
p.repeaters[label] = false; |
|
|
|
for (var l in p.repeaters) { |
|
|
|
if (p.repeaters[l]) { return; } |
|
|
|
} |
|
|
|
forceSpun(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function forceSpun() { |
|
|
|
if (p.global) { p.reader.dispatchEvent('monocle:modal:off'); } |
|
|
|
p.global = false; |
|
|
|
p.repeaters = {}; |
|
|
|
p.showForPages = []; |
|
|
|
for (var i = 0; i < p.divs.length; ++i) { |
|
|
|
p.divs[i].dom.addClass('dormant'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
API.createControlElements = createControlElements; |
|
|
|
API.registerSpinEvent = registerSpinEvent; |
|
|
|
API.listenForUsualDelays = listenForUsualDelays; |
|
|
|
API.spin = spin; |
|
|
|
API.spun = spun; |
|
|
|
API.forceSpun = forceSpun; |
|
|
|
|
|
|
|
return API; |
|
|
|
} |
|
|
|
|
|
|
|
Monocle.Controls.Spinner.GENERIC_LABEL = "generic"; |
|
|
|
Monocle.Controls.Stencil = function (reader, behaviorClasses) { |
|
|
|
|
|
|
|
var API = { constructor: Monocle.Controls.Stencil } |
|
|
|
var k = API.constants = API.constructor; |
|
|
|
var p = API.properties = { |
|
|
|
reader: reader, |
|
|
|
behaviors: [], |
|
|
|
components: {}, |
|
|
|
masks: [] |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Create the stencil container and listen for draw/update events. |
|
|
|
// |
|
|
|
function createControlElements(holder) { |
|
|
|
behaviorClasses = behaviorClasses || k.DEFAULT_BEHAVIORS; |
|
|
|
for (var i = 0, ii = behaviorClasses.length; i < ii; ++i) { |
|
|
|
addBehavior(behaviorClasses[i]); |
|
|
|
} |
|
|
|
p.container = holder.dom.make('div', k.CLS.container); |
|
|
|
p.reader.listen('monocle:turning', hide); |
|
|
|
p.reader.listen('monocle:turn:cancel', show); |
|
|
|
p.reader.listen('monocle:turn', update); |
|
|
|
p.reader.listen('monocle:stylesheetchange', update); |
|
|
|
p.reader.listen('monocle:resize', update); |
|
|
|
update(); |
|
|
|
return p.container; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Pass this method an object that responds to 'findElements(doc)' with |
|
|
|
// an array of DOM elements for that document, and to 'fitMask(elem, mask)'. |
|
|
|
// |
|
|
|
// After you have added all your behaviors this way, you would typically |
|
|
|
// call update() to make them take effect immediately. |
|
|
|
// |
|
|
|
function addBehavior(bhvrClass) { |
|
|
|
var bhvr = new bhvrClass(API); |
|
|
|
if (typeof bhvr.findElements != 'function') { |
|
|
|
console.warn('Missing "findElements" method for behavior: %o', bhvr); |
|
|
|
} |
|
|
|
if (typeof bhvr.fitMask != 'function') { |
|
|
|
console.warn('Missing "fitMask" method for behavior: %o', bhvr); |
|
|
|
} |
|
|
|
p.behaviors.push(bhvr); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Resets any pre-calculated rectangles for the active component, |
|
|
|
// recalculates them, and forces masks to be "drawn" (moved into the new |
|
|
|
// rectangular locations). |
|
|
|
// |
|
|
|
function update() { |
|
|
|
var visPages = p.reader.visiblePages(); |
|
|
|
if (!visPages || !visPages.length) { return; } |
|
|
|
var pageDiv = visPages[0]; |
|
|
|
var cmptId = pageComponentId(pageDiv); |
|
|
|
if (!cmptId) { return; } |
|
|
|
p.components[cmptId] = null; |
|
|
|
calculateRectangles(pageDiv); |
|
|
|
draw(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function hide() { |
|
|
|
p.container.style.display = 'none'; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function show() { |
|
|
|
p.container.style.display = 'block'; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Removes any existing masks. |
|
|
|
function clear() { |
|
|
|
while (p.container.childNodes.length) { |
|
|
|
p.container.removeChild(p.container.lastChild); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Aligns the stencil container to the shape of the page, then moves the |
|
|
|
// masks 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); |
|
|
|
|
|
|
|
// Clear old masks. |
|
|
|
clear(); |
|
|
|
|
|
|
|
// Layout the masks. |
|
|
|
if (!p.disabled) { |
|
|
|
show(); |
|
|
|
var rects = p.components[cmptId]; |
|
|
|
if (rects && rects.length) { |
|
|
|
layoutRectangles(pageDiv, rects); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 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); |
|
|
|
if (!p.components[cmptId]) { |
|
|
|
p.components[cmptId] = []; |
|
|
|
} else { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
var doc = pageDiv.m.activeFrame.contentDocument; |
|
|
|
var offset = getOffset(pageDiv); |
|
|
|
|
|
|
|
for (var b = 0, bb = p.behaviors.length; b < bb; ++b) { |
|
|
|
var bhvr = p.behaviors[b]; |
|
|
|
var elems = bhvr.findElements(doc); |
|
|
|
for (var i = 0; i < elems.length; ++i) { |
|
|
|
var elem = elems[i]; |
|
|
|
if (elem.getClientRects) { |
|
|
|
var r = elem.getClientRects(); |
|
|
|
for (var j = 0; j < r.length; j++) { |
|
|
|
p.components[cmptId].push({ |
|
|
|
element: elem, |
|
|
|
behavior: bhvr, |
|
|
|
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]; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 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) { |
|
|
|
var r = visRects[i]; |
|
|
|
var cr = { |
|
|
|
left: r.left - offset.l, |
|
|
|
top: r.top, |
|
|
|
width: r.width, |
|
|
|
height: r.height |
|
|
|
}; |
|
|
|
var mask = createMask(r.element, r.behavior); |
|
|
|
mask.dom.setStyles({ |
|
|
|
display: 'block', |
|
|
|
left: cr.left+"px", |
|
|
|
top: cr.top+"px", |
|
|
|
width: cr.width+"px", |
|
|
|
height: cr.height+"px", |
|
|
|
position: 'absolute' |
|
|
|
}); |
|
|
|
mask.stencilRect = cr; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 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.width |
|
|
|
}; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Is this area presently on the screen? |
|
|
|
// |
|
|
|
function rectVisible(rect, l, r) { |
|
|
|
return rect.left >= l && rect.left < r; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 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]; |
|
|
|
if (!pageDiv.m.activeFrame.m.component) { return; } |
|
|
|
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({ |
|
|
|
left: cmpt.offsetLeft+"px", |
|
|
|
top: cmpt.offsetTop+"px" |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function createMask(element, bhvr) { |
|
|
|
var mask = p.container.dom.append(bhvr.maskTagName || 'div', k.CLS.mask); |
|
|
|
Monocle.Events.listenForContact(mask, { |
|
|
|
start: function () { p.reader.dispatchEvent('monocle:magic:halt'); }, |
|
|
|
move: function (evt) { evt.preventDefault(); }, |
|
|
|
end: function () { p.reader.dispatchEvent('monocle:magic:init'); } |
|
|
|
}); |
|
|
|
bhvr.fitMask(element, mask); |
|
|
|
return mask; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Make the active masks visible (by giving them a class -- override style |
|
|
|
// in monoctrl.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); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function disable() { |
|
|
|
p.disabled = true; |
|
|
|
draw(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function enable() { |
|
|
|
p.disabled = false; |
|
|
|
draw(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function filterElement(elem, behavior) { |
|
|
|
if (typeof behavior.filterElement == 'function') { |
|
|
|
return behavior.filterElement(elem); |
|
|
|
} |
|
|
|
return elem; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function maskAssigned(elem, mask, behavior) { |
|
|
|
if (typeof behavior.maskAssigned == 'function') { |
|
|
|
return behavior.maskAssigned(elem, mask); |
|
|
|
} |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
API.createControlElements = createControlElements; |
|
|
|
API.addBehavior = addBehavior; |
|
|
|
API.draw = draw; |
|
|
|
API.update = update; |
|
|
|
API.toggleHighlights = toggleHighlights; |
|
|
|
|
|
|
|
return API; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Monocle.Controls.Stencil.CLS = { |
|
|
|
container: 'controls_stencil_container', |
|
|
|
mask: 'controls_stencil_mask', |
|
|
|
highlights: 'controls_stencil_highlighted' |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Monocle.Controls.Stencil.Links = function (stencil) { |
|
|
|
var API = { constructor: Monocle.Controls.Stencil.Links } |
|
|
|
|
|
|
|
// Optionally specify the HTML tagname of the mask. |
|
|
|
API.maskTagName = 'a'; |
|
|
|
|
|
|
|
// Returns an array of all the elements in the given doc that should |
|
|
|
// be covered with a stencil mask for interactivity. |
|
|
|
// |
|
|
|
// (Hint: doc.querySelectorAll() is your friend.) |
|
|
|
// |
|
|
|
API.findElements = function (doc) { |
|
|
|
return doc.querySelectorAll('a[href]'); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Return an element. It should usually be a child of the container element, |
|
|
|
// with a className of the given maskClass. You set up the interactivity of |
|
|
|
// the mask element here. |
|
|
|
// |
|
|
|
API.fitMask = function (link, mask) { |
|
|
|
var hrefObject = deconstructHref(link); |
|
|
|
|
|
|
|
if (hrefObject.internal) { |
|
|
|
mask.setAttribute('href', 'javascript:"Skip to chapter"'); |
|
|
|
mask.onclick = function (evt) { |
|
|
|
stencil.properties.reader.skipToChapter(hrefObject.internal); |
|
|
|
evt.preventDefault(); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} else { |
|
|
|
mask.setAttribute('href', hrefObject.external); |
|
|
|
mask.setAttribute('target', '_blank'); |
|
|
|
mask.onclick = function (evt) { return true; } |
|
|
|
} |
|
|
|
|
|
|
|
link.onclick = function (evt) { |
|
|
|
evt.preventDefault(); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 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 |
|
|
|
// |
|
|
|
// - an 'internal' property -- a relative URL (with optional hash anchor), |
|
|
|
// that is treated as a link to component in the book |
|
|
|
// |
|
|
|
// 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(elem) { |
|
|
|
var loc = document.location; |
|
|
|
var origin = loc.protocol+'//'+loc.host; |
|
|
|
var href = elem.href; |
|
|
|
var path = href.substring(origin.length); |
|
|
|
var ext = { external: href }; |
|
|
|
|
|
|
|
// Anchor tags with 'target' attributes are always external URLs. |
|
|
|
if (elem.getAttribute('target')) { |
|
|
|
return ext; |
|
|
|
} |
|
|
|
// URLs with a different protocol or domain are always external. |
|
|
|
//console.log("Domain test: %s <=> %s", origin, href); |
|
|
|
if (href.indexOf(origin) != 0) { |
|
|
|
return ext; |
|
|
|
} |
|
|
|
|
|
|
|
// If it is in a sub-path of the current path, it's internal. |
|
|
|
var topPath = loc.pathname.replace(/[^\/]*\.[^\/]+$/,''); |
|
|
|
if (topPath[topPath.length - 1] != '/') { |
|
|
|
topPath += '/'; |
|
|
|
} |
|
|
|
//console.log("Sub-path test: %s <=> %s", topPath, path); |
|
|
|
if (path.indexOf(topPath) == 0) { |
|
|
|
return { internal: path.substring(topPath.length) } |
|
|
|
} |
|
|
|
|
|
|
|
// If it's a root-relative URL and it's in our list of component ids, |
|
|
|
// it's internal. |
|
|
|
var cmptIds = stencil.properties.reader.getBook().properties.componentIds; |
|
|
|
for (var i = 0, ii = cmptIds.length; i < ii; ++i) { |
|
|
|
//console.log("Component test: %s <=> %s", cmptIds[i], path); |
|
|
|
if (path.indexOf(cmptIds[i]) == 0) { |
|
|
|
return { internal: path } |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// Otherwise it's external. |
|
|
|
return ext; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
return API; |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Monocle.Controls.Stencil.DEFAULT_BEHAVIORS = [Monocle.Controls.Stencil.Links]; |