├── .babelrc ├── demo ├── context_files │ ├── 7cf07a7ced │ ├── share_gplus_icon-abe9b8552dcfd4bba6e00defc212b02c.png │ ├── share_mail_icon-38578711bfb1c368b6b1d98ba54832ea.png │ ├── share_facebook_icon-eadfd16e764c1a62d7457c5e21317afc.png │ ├── share_twitter_icon-81aaa8b03b4dc324fef98cda701ab7ca.png │ ├── child-reading-1-small-6a1078b2e35dfe551934a991da95d9a7.jpg │ ├── child-reading-2-small-62e716d1fd7193378aede70aa684251d.jpg │ ├── child-reading-3-small-d7251f13cd2a1850594158c45dbae87b.jpg │ ├── child-reading-4-small-f74ff4eb69847b4e51d47a6f8cd01e5f.jpg │ ├── child-reading-5-small-4b840e6bb3e6a7cc74bbff0978552310.jpg │ ├── child-reading-6-small-d02229fcb7ebf3f2f2d7d6a366b4c3ee.jpg │ ├── child-reading-7-small-1e45e7bb06b4e95e38a78a83d17bbe58.jpg │ ├── child-reading-8-small-ac1a82e71f9a5f3a84f88e1f7c30aabd.jpg │ ├── share_pinterest_icon-a1d103f1c4af9daa0118b462ad1120f2.png │ ├── child-reading-10-small-453ed4fe1670ffd06769281ce333c323.jpg │ ├── child-reading-11-small-b932b48beb75801bedd0686bdcc35a79.jpg │ ├── child-reading-12-small-e484eff0f7169f82e9b6e206f678dbca.jpg │ ├── child-reading-13-small-d73de078b3fcadcd89457190d0ce347d.jpg │ ├── child-reading-14-small-ad8ca95699d6cccdbf71be2a9bacbbe5.jpg │ ├── child-reading-15-small-42b19af3e0071fa35bbdf4c7fc2745ed.jpg │ ├── iframe_api │ ├── arrow-down-ccw-long-107x91-395794252ef42cab546eee9367537b1f.svg │ ├── logo-white-148x35-2c2c183a239c3899f64d6830c7804c27.svg │ ├── reviewer1-60x60-170cdd32068bb7d438e75cdf65ffb253.svg │ ├── logo-148x35-b01f90c07210321b26ec9a5873d318cc.svg │ ├── reviewer3-60x60-a1fdb6fec3124963ec75c637651e8951.svg │ ├── reviewer5-60x60-a98e7bb98960eed512c973e3bd1fe868.svg │ ├── picturefill-604f49d58f8ccbee24a50d6824ab3ed9.js │ ├── reviewer2-60x60-94b4f7c02575eab6ebb04ee2812782e9.svg │ ├── reviewer6-60x60-35c5fb0448ac74fa78aae224901e448f.svg │ ├── reviewer4-60x60-bcef0cef3fddd3d7c6688b46c2377597.svg │ ├── shared-head-1776f49ac825f6a064ce575a069c6b5f.js │ ├── age-80x80-be860c1290f072f516be79350bd3875c.svg │ └── shipping-80x80-e3250595ab9e529e70b8cb2b052b7566.svg ├── index.html ├── iframe.html └── actuallytwh.json ├── src ├── js │ ├── helpers │ │ ├── blankLetterCheck.js │ │ ├── isMobile.js │ │ ├── getCentralizedCharCount.js │ │ ├── handleReplace.js │ │ └── preload.js │ ├── steps │ │ ├── generateHtml.js │ │ ├── calculateMonkey.js │ │ ├── generateBaseWrapperElement.js │ │ ├── slider │ │ │ ├── init.js │ │ │ └── generateHtml.js │ │ ├── spread │ │ │ ├── insertSpread.js │ │ │ └── getData.js │ │ ├── insertHtml.js │ │ ├── generatePlatformUrls.js │ │ ├── generateBaseElement.js │ │ ├── checkLanguageChange.js │ │ ├── initMonkey.js │ │ ├── generateUrls.js │ │ ├── getData.js │ │ └── letters │ │ │ ├── generateHtml.js │ │ │ ├── generateOverlay.js │ │ │ └── generateCharPicker.js │ ├── monkey.js │ └── monkeys │ │ ├── mobile.js │ │ └── desktop.js ├── imgs │ ├── loading-32x32.gif │ ├── tooltip-arrow-85x47.png │ └── arrow-3-50x35.svg ├── scss │ ├── styles.scss │ └── monkey │ │ ├── _ie8.scss │ │ ├── _book.scss │ │ ├── _char-picker.scss │ │ ├── _mobile.scss │ │ └── _letters.scss ├── partials │ └── partial.erb.html └── en.yml ├── .eslintrc ├── .gitignore ├── .travis.yml ├── .editorconfig ├── README.md ├── test ├── tests.spec.js ├── index.html ├── _helpers.js ├── helpers.spec.js ├── desktop.spec.js ├── mobile.spec.js └── loading.spec.js ├── package.json └── gulpfile.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015-lmn"] 3 | } 4 | -------------------------------------------------------------------------------- /demo/context_files/7cf07a7ced: -------------------------------------------------------------------------------- 1 | NREUM.setToken({'stn':1}) -------------------------------------------------------------------------------- /src/js/helpers/blankLetterCheck.js: -------------------------------------------------------------------------------- 1 | module.exports = /^[\s\-\'\’]$/; 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/lmn-gulp-tasks/.eslintrc" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | demo/build 4 | demo/partials 5 | *.log 6 | .sass-cache 7 | .DS_Store -------------------------------------------------------------------------------- /src/imgs/loading-32x32.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/src/imgs/loading-32x32.gif -------------------------------------------------------------------------------- /src/imgs/tooltip-arrow-85x47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/src/imgs/tooltip-arrow-85x47.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '4.3.2' 5 | cache: 6 | directories: 7 | - node_modules 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 2 -------------------------------------------------------------------------------- /demo/context_files/share_gplus_icon-abe9b8552dcfd4bba6e00defc212b02c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/share_gplus_icon-abe9b8552dcfd4bba6e00defc212b02c.png -------------------------------------------------------------------------------- /demo/context_files/share_mail_icon-38578711bfb1c368b6b1d98ba54832ea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/share_mail_icon-38578711bfb1c368b6b1d98ba54832ea.png -------------------------------------------------------------------------------- /demo/context_files/share_facebook_icon-eadfd16e764c1a62d7457c5e21317afc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/share_facebook_icon-eadfd16e764c1a62d7457c5e21317afc.png -------------------------------------------------------------------------------- /demo/context_files/share_twitter_icon-81aaa8b03b4dc324fef98cda701ab7ca.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/share_twitter_icon-81aaa8b03b4dc324fef98cda701ab7ca.png -------------------------------------------------------------------------------- /demo/context_files/child-reading-1-small-6a1078b2e35dfe551934a991da95d9a7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-1-small-6a1078b2e35dfe551934a991da95d9a7.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-2-small-62e716d1fd7193378aede70aa684251d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-2-small-62e716d1fd7193378aede70aa684251d.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-3-small-d7251f13cd2a1850594158c45dbae87b.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-3-small-d7251f13cd2a1850594158c45dbae87b.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-4-small-f74ff4eb69847b4e51d47a6f8cd01e5f.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-4-small-f74ff4eb69847b4e51d47a6f8cd01e5f.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-5-small-4b840e6bb3e6a7cc74bbff0978552310.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-5-small-4b840e6bb3e6a7cc74bbff0978552310.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-6-small-d02229fcb7ebf3f2f2d7d6a366b4c3ee.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-6-small-d02229fcb7ebf3f2f2d7d6a366b4c3ee.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-7-small-1e45e7bb06b4e95e38a78a83d17bbe58.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-7-small-1e45e7bb06b4e95e38a78a83d17bbe58.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-8-small-ac1a82e71f9a5f3a84f88e1f7c30aabd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-8-small-ac1a82e71f9a5f3a84f88e1f7c30aabd.jpg -------------------------------------------------------------------------------- /demo/context_files/share_pinterest_icon-a1d103f1c4af9daa0118b462ad1120f2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/share_pinterest_icon-a1d103f1c4af9daa0118b462ad1120f2.png -------------------------------------------------------------------------------- /demo/context_files/child-reading-10-small-453ed4fe1670ffd06769281ce333c323.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-10-small-453ed4fe1670ffd06769281ce333c323.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-11-small-b932b48beb75801bedd0686bdcc35a79.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-11-small-b932b48beb75801bedd0686bdcc35a79.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-12-small-e484eff0f7169f82e9b6e206f678dbca.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-12-small-e484eff0f7169f82e9b6e206f678dbca.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-13-small-d73de078b3fcadcd89457190d0ce347d.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-13-small-d73de078b3fcadcd89457190d0ce347d.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-14-small-ad8ca95699d6cccdbf71be2a9bacbbe5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-14-small-ad8ca95699d6cccdbf71be2a9bacbbe5.jpg -------------------------------------------------------------------------------- /demo/context_files/child-reading-15-small-42b19af3e0071fa35bbdf4c7fc2745ed.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Lostmyname/monkey/HEAD/demo/context_files/child-reading-15-small-42b19af3e0071fa35bbdf4c7fc2745ed.jpg -------------------------------------------------------------------------------- /src/scss/styles.scss: -------------------------------------------------------------------------------- 1 | @import "chameleon-sass/assets/stylesheets/chameleon"; 2 | @import "heidelberg/sass/heidelberg/heidelberg-config"; 3 | @import "heidelberg/sass/heidelberg/heidelberg-plugin"; 4 | 5 | @import "monkey/book"; 6 | @import "monkey/letters"; 7 | @import "monkey/char-picker"; 8 | @import "monkey/mobile"; 9 | @import "monkey/ie8"; 10 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Monkey: new widget 6 | 7 | 8 |

See monkey in context at partial.html, or the iframe API on iframe.html

9 |

Run tests here, or without slow tests.

10 | 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monkey [![Build Status](https://travis-ci.org/Lostmyname/monkey.svg?branch=master)](https://travis-ci.org/Lostmyname/monkey) 2 | 3 | ![They want books, too](https://i.imgur.com/GqlC4ko.gif) 4 | 5 | Monkey is the new version of the widget which won't use an iframe. 6 | 7 | Use context.html for development and tests.html for testing. index.html doesn't 8 | do much. 9 | 10 | ## Installation 11 | 12 | Run `npm install` and then `gulp` to generate the CSS and start browser-sync. 13 | -------------------------------------------------------------------------------- /src/js/helpers/isMobile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Returns whether browser is a mobile or not. Tests for touch support and 5 | * screen width. 6 | * 7 | * @returns {boolean} True if mobile. 8 | */ 9 | module.exports = function () { 10 | 11 | // If doesn't support touch 12 | if (!('ontouchstart' in window) && !navigator.msMaxTouchPoints) { 13 | return false; 14 | } 15 | 16 | if (!window.matchMedia) { 17 | return false; 18 | } 19 | 20 | return window.matchMedia('(max-width: 777px)').matches; 21 | }; 22 | -------------------------------------------------------------------------------- /test/tests.spec.js: -------------------------------------------------------------------------------- 1 | /* global $, Monkey */ 2 | 3 | 'use strict'; 4 | 5 | describe('Tests', function () { 6 | it('should function', function () { 7 | (10).should.be.above(2); 8 | }); 9 | 10 | it('should have everything they need', function () { 11 | Monkey.should.be.type('function'); 12 | $.should.be.type('function'); 13 | }); 14 | 15 | it('should have jqPromise test', function () { 16 | var promise = $.Deferred().promise(); 17 | promise.should.be.a.jqPromise; 18 | $.should.not.be.a.jqPromise; 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/scss/monkey/_ie8.scss: -------------------------------------------------------------------------------- 1 | .ie8 { 2 | #letters-container { 3 | margin-bottom: 1.5em; 4 | } 5 | .overlay__buttons { 6 | .button { 7 | padding:8px 16px; 8 | } 9 | 10 | .button + .button { 11 | margin-left: 1em; 12 | } 13 | } 14 | .character-picker .char-container .img-container { 15 | &:before { 16 | display: none; 17 | } 18 | &:active:before { 19 | display: block; 20 | } 21 | } 22 | .change-character { 23 | visibility: hidden; 24 | } 25 | .letter-active .change-character { 26 | visibility: visible; 27 | } 28 | } -------------------------------------------------------------------------------- /src/js/steps/generateHtml.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Generate HTML for pages from list of URLs. 5 | * 6 | * Varies depending on the browser. 7 | * 8 | * @todo: Templating? 9 | */ 10 | module.exports = function (lang) { 11 | return function (data) { 12 | // Runs the generateHtml method, using data.monkeyType to determine whether 13 | // it should be desktop or mobile initialisation. This adds the Monkey 14 | // instance to the page, within the predefined container element. 15 | data.html = this.monkeys[data.monkeyType].generateHtml(data, lang); 16 | 17 | return data; 18 | }.bind(this); 19 | }; 20 | -------------------------------------------------------------------------------- /src/js/steps/calculateMonkey.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Decide whether future stuff should be desktop or mobile. 5 | * 6 | * @param {string} monkeyType Either "auto", "mobile" or "desktop". Defaults 7 | * to auto and will calculate whether mobile or desktop is better. 8 | */ 9 | module.exports = function (monkeyType) { 10 | var isMobile = this.helpers.isMobile; 11 | 12 | return function (data) { 13 | if (typeof monkeyType === 'undefined' || monkeyType === 'auto') { 14 | data.monkeyType = isMobile() ? 'mobile' : 'desktop'; 15 | } else { 16 | data.monkeyType = monkeyType; 17 | } 18 | return data; 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /demo/context_files/iframe_api: -------------------------------------------------------------------------------- 1 | 2 | if (!window['YT']) {var YT = {loading: 0,loaded: 0};}if (!window['YTConfig']) {var YTConfig = {'host': 'http://www.youtube.com'};}if (!YT.loading) {YT.loading = 1;(function(){var l = [];YT.ready = function(f) {if (YT.loaded) {f();} else {l.push(f);}};window.onYTReady = function() {YT.loaded = 1;for (var i = 0; i < l.length; i++) {try {l[i]();} catch (e) {}}};YT.setConfig = function(c) {for (var k in c) {if (c.hasOwnProperty(k)) {YTConfig[k] = c[k];}}};var a = document.createElement('script');a.id = 'www-widgetapi-script';a.src = 'https:' + '//s.ytimg.com/yts/jsbin/www-widgetapi-vflHCEcaj/www-widgetapi.js';a.async = true;var b = document.getElementsByTagName('script')[0];b.parentNode.insertBefore(a, b);})();} -------------------------------------------------------------------------------- /src/js/steps/generateBaseWrapperElement.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var $ = require('jquery'); 3 | 4 | /** 5 | * Generate base HTML element for all of monkey. 6 | * 7 | * Varies depending on the browser. 8 | * 9 | */ 10 | module.exports = function ($monkeyContainer) { 11 | return function (data) { 12 | var singleSpreadClass = data.spreads === 'single' ? 'single-spreads' : ''; 13 | 14 | var classes = data.monkeyType === 'mobile' ? 15 | 'monkey-wrapper mobile ' + singleSpreadClass : 16 | 'positioned-relative pos-relative monkey-wrapper desktop'; 17 | 18 | data.$monkeyWrapper = $('
').addClass(classes); 19 | data.$monkeyWrapper.appendTo($monkeyContainer); 20 | 21 | return data; 22 | }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/js/helpers/getCentralizedCharCount.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Returns the number of characters that can be centered on the screen on 5 | * mobile devices, respective to the mobile window width. 6 | * 7 | * @returns {integer} Number of characters, defaults to 5.. 8 | */ 9 | module.exports = function () { 10 | var windowWidth = window.innerWidth || 11 | document.documentElement.clientWidth || 12 | document.body.clientWidth; 13 | // We start with a 5 character minimum, then for every 50px after 420px window 14 | // width, we add a character. 15 | var numOfChars = 5; 16 | if (window.innerWidth >= 420) { 17 | numOfChars = Math.round(parseFloat(((windowWidth - 420) / 50) + 5)); 18 | } 19 | return numOfChars; 20 | }; 21 | -------------------------------------------------------------------------------- /src/js/steps/slider/init.js: -------------------------------------------------------------------------------- 1 | export default function ($events) { 2 | return function (data) { 3 | if (!data.sliderElement) { 4 | return data; 5 | } 6 | 7 | var $slider = data.sliderElement.find('input'); 8 | var letters = data.letters.length; 9 | var sliderActive = false; 10 | 11 | $events.on('letterChange', function (e, page) { 12 | if (sliderActive) { 13 | return; 14 | } 15 | $slider.val(200 * page / letters); 16 | }); 17 | 18 | $slider.on('mousedown', () => sliderActive = true); 19 | 20 | $slider.on('mouseup', function () { 21 | setTimeout(() => sliderActive = false, 2000); 22 | }); 23 | 24 | $slider.on('input', function () { 25 | data.turnToPage(Math.round(letters * $slider.val() / 200)); 26 | }); 27 | 28 | return data; 29 | }; 30 | } 31 | -------------------------------------------------------------------------------- /src/js/steps/spread/insertSpread.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Inserts a new spread into the Heidelberg instance. 4 | * 5 | * @return {function} 6 | */ 7 | module.exports = function () { 8 | /** 9 | * Takes the data object and the URL of the missing spread, and combines the 10 | * query string to compress/optimize the image, before adding the spread to 11 | * the Heidelberg. 12 | * @param {object} monkeyData The data object passed through the Promise chain 13 | * @param {string} spreadUrl The URL of the new spread image 14 | * @return {null} 15 | */ 16 | return function (monkeyData, spreadUrl) { 17 | spreadUrl += monkeyData.queryString; 18 | 19 | monkeyData.html.find('.page-spreadMissing') 20 | .removeClass('page-spreadMissing') 21 | .addClass('page-spread') 22 | .find('img') 23 | .attr('src', spreadUrl); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /src/js/helpers/handleReplace.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var lastReplacements = null; 4 | 5 | /** 6 | * Templating sort of? Takes a string and an object and makes replacements. 7 | * 8 | * If replacements is {foo: 'bar'}, `{{ foo }}` will be replaced with `bar`. 9 | * 10 | * @param {string} string String to do replacements on. 11 | * @param {object} [replacements] Object of replacements. If ommitted, will use 12 | * object from last time. 13 | * @returns {string} The string with replacements made. 14 | */ 15 | module.exports = function (string, replacements) { 16 | if (typeof replacements !== 'object') { 17 | replacements = lastReplacements; 18 | } else { 19 | lastReplacements = replacements; 20 | } 21 | 22 | return string.replace(/\{\{\s*([a-z]+)\s*\}\}/g, function (full, item) { 23 | return replacements.hasOwnProperty(item) ? replacements[item] : full; 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lmn-monkey", 3 | "version": "11.0.3", 4 | "description": "", 5 | "main": "src/js/monkey.js", 6 | "dependencies": { 7 | "babel-preset-es2015": "^6.5.0", 8 | "babel-preset-es2015-lmn": "^1.0.0", 9 | "babelify": "^7.2.0", 10 | "chameleon-sass": "^0.5.9", 11 | "diacritics": "^1.2.1", 12 | "heidelberg": "^1.1.0", 13 | "jquery": "^1.11.3", 14 | "lang": "^0.1.1", 15 | "nums": "^1.0.0" 16 | }, 17 | "devDependencies": { 18 | "browser-sync": "^2.7.12", 19 | "gulp": "^3.8.8", 20 | "lmn-gulp-tasks": "^7.0.0", 21 | "lmn.jester.theme.default": "^1.0.0", 22 | "mocha": "^2.1.0", 23 | "phantomjs": "^1.9.16", 24 | "serve": "^1.4.0", 25 | "should": "^7.1.0" 26 | }, 27 | "author": "Callum Macrae", 28 | "browserify": { 29 | "transform": [ 30 | "babelify" 31 | ] 32 | }, 33 | "scripts": { 34 | "test": "gulp js-quality --fail" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/js/steps/slider/generateHtml.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | /** 4 | * Generate HTML for slider. 5 | * 6 | * @param {object} options Options passed to monkey. 7 | */ 8 | export default function (options) { 9 | return function (data) { 10 | var $sliderContainer = $('
', { 11 | id: 'slider-container', 12 | class: 'row aligned-center md-mar-b', 13 | 'data-key': 'monkey-slider' 14 | }); 15 | 16 | $('

').appendTo($sliderContainer) 17 | .addClass('no-mar') 18 | .text(options.lang.bookFor); 19 | 20 | $('

').appendTo($sliderContainer) 21 | .addClass('h5') 22 | .text(options.lang.bookTo); 23 | 24 | var $sliderInnerContainer = $('
').appendTo($sliderContainer) 25 | .addClass('col col-lg-6 col-lg-offset-3 col-sm-12 col-sm-offset-0'); 26 | 27 | $('', { type: 'range' }) 28 | .appendTo($sliderInnerContainer); 29 | 30 | var $book = data.monkeyContainer; 31 | 32 | data.sliderElement = $sliderContainer.appendTo($book); 33 | 34 | return data; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/js/steps/insertHtml.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | 5 | /** 6 | * Inserts HTML into specified container. More specifically, it appends the 7 | * Heidelberg instance into the container element (usually .lmn-book). 8 | * 9 | * @param {string|HTMLElement|jQuery} monkeyContainer The container. 10 | */ 11 | module.exports = function (monkeyContainer) { 12 | var $container = $(monkeyContainer); 13 | 14 | return function (data) { 15 | /** 16 | * We want to remove the loading gif here, and then show the 'Tap to 17 | * Preview' label. We do this because it's nonsensical to have the label 18 | * appear when there's nothing to tap. 19 | */ 20 | $container.find('.loader-img').remove(); 21 | $container.next('.lmn-book__label').addClass('js--show-label'); 22 | $container.next('.lmn-book__label').removeAttr('style'); 23 | 24 | // Append the Heidelberg to the container, and attach the container to the 25 | // data attribute that's passed through the Promise chain. 26 | $container.append(data.html); 27 | 28 | data.container = $container; 29 | 30 | return data; 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Monkey: tests 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/partials/partial.erb.html: -------------------------------------------------------------------------------- 1 |
2 |
9 |
10 | <%= image_tag('loading-32x32.gif', { width: 32, class: 'padded-bottom' }) %> 11 |
<%= t("loading") %>
12 |
13 |
14 | 15 | 16 | <%= t("preview") %> 17 | <%= image_tag('arrow-3-50x35.svg', { class: 'arrow-image' }) %> 18 | 19 |
20 | -------------------------------------------------------------------------------- /src/js/steps/generatePlatformUrls.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | /** 5 | * Generates the image URLs from Imgix, which are prepped and optimized for the 6 | * users's screen size and device resolution. 7 | * 8 | * @param {int} preload Number of images to preload on initialisation. 9 | * @return {data} The data object passed through the Promise chain. 10 | */ 11 | module.exports = function (options) { 12 | var helpers = this.helpers; 13 | 14 | /** 15 | * Takes data and turns letters into URLs. 16 | */ 17 | return function (data) { 18 | var width = Math.round((data.$monkeyWrapper.width() / 2) * (window.devicePixelRatio > 1 ? 2 : 1)); 19 | // data.urls is a list of the raw image URLs, not resized or compressed. 20 | data.urls = $.map(data.letters, function (letterData) { 21 | var url = (letterData.type === 'static') 22 | ? `${letterData.url}?w=${width}` 23 | : `${options.server}${letterData.url}&width=${width}`; 24 | 25 | return url; 26 | }); 27 | 28 | return helpers.preload(data.urls.slice(0, options.preload)) 29 | .then(function () { 30 | return data; 31 | }); 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/js/helpers/preload.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | 5 | /** 6 | * Preload images and fire a callback / promise when they're loaded. 7 | * 8 | * @param {string|string[]} images String(s) containing URL(s) to preload. 9 | * @param {function} [callback] Optional callback. Promises are better! 10 | * @returns {promise} Promise which will be resolved when all the images 11 | * have loaded. 12 | */ 13 | module.exports = function preload(images, callback) { 14 | var defer = $.Deferred(); 15 | 16 | if (!$.isArray(images)) { 17 | images = [images]; 18 | } 19 | 20 | var toLoad = images.length; 21 | 22 | var $images = $.map(images, function (image) { 23 | var $image = $('').attr('src', image); 24 | 25 | $image.on('load', function () { 26 | if (--toLoad === 0) { 27 | defer.resolve($images); 28 | } 29 | }); 30 | 31 | return $image; 32 | }); 33 | 34 | // Convert array of jQuery objects into jQuery object 35 | $images = $($images).map(function () { 36 | return this.toArray(); 37 | }); 38 | 39 | if (typeof callback === 'function') { 40 | defer.then(callback); 41 | } 42 | 43 | return defer.promise(); 44 | }; 45 | -------------------------------------------------------------------------------- /src/js/steps/generateBaseElement.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Generate base HTML element for all of monkey. 5 | * 6 | * Varies depending on the browser. 7 | * 8 | */ 9 | module.exports = function (monkeyContainer, options) { 10 | return function (data) { 11 | // Attach a cloned version of the loading gif to the data object, so we can 12 | // use it later. 13 | data.loading = monkeyContainer.find('.loader-img').clone(); 14 | // Remove all DOM elements within the container (needed for 15 | // re-initialisation when changing name in the edit screen) 16 | monkeyContainer.empty(); 17 | 18 | // If replaceMonkey is true (which means a user has changed the name/gender/ 19 | // language of a book on the product page) then append the loading gif to 20 | // the container – so the user has visible feedback that something is 21 | // loading in. 22 | if (options.replaceMonkey) { 23 | data.loading = data.loading.appendTo(monkeyContainer); 24 | } 25 | // Add either a desktop/mobile CSS class to the container, and attach the 26 | // container element to the data object. 27 | monkeyContainer.addClass(data.monkeyType); 28 | data.monkeyContainer = monkeyContainer; 29 | 30 | return data; 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /src/js/steps/checkLanguageChange.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Check to see whether we need to show the language overlay 5 | * 6 | * @param {string|HTMLElement|jQuery} monkeyContainer The container. 7 | */ 8 | module.exports = function ($monkeyContainer, options) { 9 | 10 | // We need to check whether we have to show the language overlay, and we do 11 | // this by seeing whether the current language is different to the book's 12 | // language when it was initialized, whether we're switching between locales 13 | // which have the character picker and therefore 14 | // don't need the language overlay, and finally, if the book has characters 15 | // that have been changed before the language change. Phew. 16 | if (options.book.locale !== $monkeyContainer.data('locale')) { 17 | delete options.book.characterSelection; 18 | $monkeyContainer.data('character-selection', null); 19 | options.clearSelection = true; 20 | if (!options.showCharPicker && $monkeyContainer.data('changedChars')) { 21 | options.showLanguageOverlay = true; 22 | } 23 | $monkeyContainer.data('locale', options.book.locale); 24 | } 25 | if (options.book.locale !== $monkeyContainer.data('first-book-locale')) { 26 | delete options.book.comparisonBooks; 27 | } 28 | return options; 29 | }; 30 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var browserSync = require('browser-sync'); 4 | var gulp = require('gulp'); 5 | var getLmnTask = require('lmn-gulp-tasks'); 6 | 7 | var jsOpts = { 8 | src: './src/js/monkey.js', 9 | dest: './demo/build/bundle.js' 10 | }; 11 | var jsOptsWatch = { src: jsOpts.src, dest: jsOpts.dest, watch: true }; 12 | 13 | gulp.task('js', getLmnTask('browserify', jsOpts)); 14 | gulp.task('js-watch', getLmnTask('browserify', jsOptsWatch)); 15 | 16 | gulp.task('js-quality', getLmnTask('js-quality', { 17 | src: './src/js/**/*.js' 18 | })); 19 | 20 | gulp.task('scss', getLmnTask('scss', { 21 | src: './src/scss/*.{sass,scss}', 22 | dest: './demo/build', 23 | minify: false 24 | })); 25 | 26 | gulp.task('build', ['js', 'scss']); 27 | gulp.task('default', ['build', 'js-watch'], function () { 28 | var config = { 29 | server: { 30 | baseDir: '.' 31 | }, 32 | startPath: '/demo/index.html' 33 | }; 34 | 35 | browserSync.init([ 36 | 'demo/build/**/*.css', 37 | 'demo/build/**/*.js', 38 | 'demo/**/*.html', 39 | 'src/imgs/**/*', 40 | 'test/**/*.js' 41 | ], config); 42 | 43 | gulp.watch('./src/scss/**/*.{sass,scss}', ['scss']); 44 | gulp.watch('./src/partials/partial.erb.html', ['html']); 45 | gulp.watch('./demo/base.erb.html', ['html']); 46 | }); 47 | -------------------------------------------------------------------------------- /test/_helpers.js: -------------------------------------------------------------------------------- 1 | /* global $, Should */ 2 | /* jshint unused: false */ 3 | 4 | 'use strict'; 5 | 6 | Should.Assertion.add('jqPromise', function () { 7 | this.params = { operator: 'to be jQuery Deferred / Promise' }; 8 | 9 | this.obj.should.be.type('object'); 10 | this.obj.then.should.be.type('function'); 11 | $.Deferred().then.toString().should.equal(this.obj.then.toString()); 12 | }, true); 13 | 14 | Should.Assertion.add('jQuery', function () { 15 | this.params = { operator: 'to be jQuery object' }; 16 | 17 | this.jquery.should.be.type('string'); 18 | this.jquery.should.equal($.fn.jquery); 19 | }); 20 | 21 | function changeMonkeyType(type) { 22 | return function (data) { 23 | data.monkeyType = type; 24 | return data; 25 | }; 26 | } 27 | 28 | function getRandomImage() { 29 | var img = '//lmn-assets.imgix.net/widget/en-GB/v2/girl/R_Robot_s_Page_2.jpg?h=1000&dpr=1&q=60'; 30 | return img.replace('1000', 200 + Math.floor(Math.random() * 100)); 31 | } 32 | 33 | var options = { 34 | book: { 35 | name: 'Heidelberg', 36 | gender: 'girl', 37 | locale: 'en-GB' 38 | }, 39 | clearSelection: true, 40 | lang: { 41 | bookFor: 'A personalised book made for', 42 | noAltText: 'No alternative text for this page, sorry.' 43 | }, 44 | server: 'https://chameleon.lostmy.name/preview.json?callback=?', 45 | icons: true 46 | }; 47 | -------------------------------------------------------------------------------- /src/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | component.monkey: 3 | loading: We're just putting your personalised book together 4 | preview: Tap to preview your book 5 | bookFor: 'A personalised book made for' 6 | noAltText: 'No alternative text for this page, sorry.' 7 | char_picker: 8 | title: 9 | remaining: 'Choose another character for' 10 | # not_remaining: 'No %{letter} characters are available' 11 | not_remaining: 12 | part_1: 'No other' 13 | part_2: 'characters are available.' 14 | change: 'Change' 15 | buttons: 16 | close: 'Close' 17 | select: 'Select' 18 | in_use: 'In Use' 19 | overlay: 20 | nounTypes: 21 | singular: 22 | title: 'Look, new character!' 23 | intro: 'had some of the same characters in their books. Would you like to add these new ones instead?' 24 | plural: 25 | title: 'Look, new characters!' 26 | intro: 'had some of the same characters in their books. Would you like to add these new ones instead?' 27 | buttons: 28 | yes_please: 'Yes Please!' 29 | no_thanks: 'No Thanks!' 30 | language: 31 | title: "You've changed language!" 32 | copy: 'Unfortunately, the character picker is not yet available for this language' 33 | buttons: 34 | okay: "Okay, thanks!" 35 | 36 | 37 | -------------------------------------------------------------------------------- /demo/iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lost My Name: Monkey iframe API 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 |
We're just putting your personalised book together
16 |
17 |
18 |
19 | 20 | 24 | 25 | 26 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /demo/context_files/arrow-down-ccw-long-107x91-395794252ef42cab546eee9367537b1f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/steps/spread/getData.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | 5 | // @todo this needs documenting. 6 | 7 | module.exports = function (monkeyData, options) { 8 | var $monkey = monkeyData.html.find('Heidelberg-Book'); 9 | var defer = $.Deferred(); 10 | var resolved = false; 11 | 12 | var consts = { 13 | INITIAL_DELAY: 3000, 14 | INTERVAL: 5000 15 | }; 16 | 17 | setTimeout(function () { 18 | makeRequest(); 19 | 20 | var interval = setInterval(function () { 21 | if (resolved) { 22 | clearInterval(interval); 23 | return; 24 | } 25 | 26 | makeRequest(); 27 | }, consts.INTERVAL); 28 | }, consts.INITIAL_DELAY); 29 | 30 | var halfDone = false; 31 | 32 | $monkey.on('scroll.monkeyPoll', function () { 33 | if (resolved) { 34 | $monkey.off('scroll.monkeyPoll'); 35 | return; 36 | } 37 | 38 | if (!halfDone && $monkey.scrollLeft() > $monkey.children('div').width() / 2) { 39 | halfDone = true; 40 | makeRequest(); 41 | } 42 | 43 | var A_BIT = 1500; // pixels 44 | if ($monkey.scrollLeft() > $monkey.children('div').width() - A_BIT) { 45 | $monkey.off('scroll.monkeyPoll'); 46 | makeRequest(); 47 | } 48 | }); 49 | 50 | return defer.promise(); 51 | 52 | function makeRequest() { 53 | $.getJSON(options.server, { book: options.book }) 54 | .then(function (data) { 55 | $.each(data.book.letters, function (i, letter) { 56 | if (letter.type === 'spread' && letter.ready) { 57 | resolved = true; 58 | defer.resolve(monkeyData, letter.url); 59 | } 60 | }); 61 | }); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /src/js/steps/initMonkey.js: -------------------------------------------------------------------------------- 1 | /* global analytics */ 2 | 3 | 'use strict'; 4 | 5 | /** 6 | * Initializes Monkey by adding a turnToPage method, which differs depending 7 | * whether you're on a mobile device or desktop. This function is defined in 8 | * /src/js/monkeys/mobile.js and /src/js/monkeys/desktop.js respectively. 9 | * 10 | * @param {object} $events The global Monkey events object, used to pass custom 11 | * events to it. 12 | * @param {object} options The options that Monkey was initialized with. 13 | * @return {data} The data object passed through the Promise chain. 14 | */ 15 | module.exports = function ($events, options) { 16 | return function (data) { 17 | // Adds the turnToPage method, using data.monkeyType to determine whether 18 | // it should be desktop or mobile initialisation. 19 | if (options.clearSelection) { 20 | $events.trigger('clearCharSelection'); 21 | } 22 | data.turnToPage = this.monkeys[data.monkeyType].init(data, $events, options); 23 | 24 | data.monkeyContainer.addClass('ready'); 25 | 26 | // Track errors on image load 27 | var $images = data.monkeyContainer.find('[class*="page"] img'); 28 | $images.on('error', function () { 29 | if (window.analytics) { 30 | analytics.track('Broken image in monkey', { 31 | src: this.src, 32 | pageID: this.getAttribute('data-id') 33 | }); 34 | } 35 | }); 36 | 37 | var remaining = $images.filter(function () { 38 | return !this.complete; 39 | }).length; 40 | 41 | $images.on('load', function () { 42 | remaining--; 43 | 44 | if (!remaining && window.analytics && options.platformAPI) { 45 | analytics.track('Monkey images all loaded'); 46 | } 47 | }); 48 | 49 | return data; 50 | }.bind(this); 51 | }; 52 | -------------------------------------------------------------------------------- /src/js/steps/generateUrls.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | /** 5 | * Generates the image URLs from Imgix, which are prepped and optimized for the 6 | * users's screen size and device resolution. 7 | * 8 | * @param {int} preload Number of images to preload on initialisation. 9 | * @return {data} The data object passed through the Promise chain. 10 | */ 11 | module.exports = function (options) { 12 | var monkeys = this.monkeys; 13 | var helpers = this.helpers; 14 | 15 | var quality = { 16 | desktop: 60, 17 | mobile: 60 18 | }; 19 | 20 | /** 21 | * Takes data and turns letters into URLs. 22 | */ 23 | return function (data) { 24 | var width; 25 | var size = monkeys[data.monkeyType].calculateSize(data); 26 | var dpr = window.devicePixelRatio > 1 ? 2 : 1; 27 | 28 | if (data.spreads === 'single') { 29 | size /= 2; 30 | } 31 | 32 | if (options.dprNotSupported) { 33 | width = 'w=' + Math.round(size * dpr); 34 | } else { 35 | width = 'w=' + size + '&dpr=' + dpr; 36 | } 37 | 38 | var queryString = width + '&q=' + quality[data.monkeyType]; 39 | 40 | // @todo: Is this used? 41 | data.queryString = '?' + queryString; 42 | 43 | // data.urls is a list of the raw image URLs, not resized or compressed. 44 | data.urls = $.map(data.letters, function (letterData) { 45 | // Change the type of letter if we've not got the spread for it, so we 46 | // can try and add it at a later stage. 47 | if (letterData.type === 'spread' && !letterData.ready) { 48 | letterData.type = 'spreadMissing'; 49 | data.needsSpread = true; 50 | } 51 | 52 | var separator = letterData.url.indexOf('?') === -1 ? '?' : '&'; 53 | return letterData.url + separator + queryString; 54 | }); 55 | 56 | return helpers.preload(data.urls.slice(0, options.preload)) 57 | .then(function () { 58 | return data; 59 | }); 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /src/js/steps/getData.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | 5 | /** 6 | * Gets the data from the server. 7 | * 8 | * @returns {object} (via promise) Object with a number of properties such as 9 | * name and gender, and then a letters property containing 10 | * information on the pages. Seriously, just use a debugger. 11 | */ 12 | module.exports = function (options) { 13 | var returnPromise; 14 | var url = options.serverPath 15 | ? trimTrailingSlash(options.server) + '/' + trimLeadingSlash(options.serverPath) 16 | : options.server; 17 | 18 | if (!options.platformAPI) { 19 | returnPromise = $.getJSON(options.server, { book: options.book }) 20 | .then(function (data) { 21 | if (!data) { 22 | data = {}; 23 | } 24 | if (!data.book) { 25 | data.book = {}; 26 | } 27 | if (!data.book.spreads) { 28 | data.book.spreads = 'double'; 29 | } 30 | 31 | return data.book; 32 | }); 33 | } else { 34 | returnPromise = $.ajax({ 35 | dataType: 'JSON', 36 | url, 37 | data: options.book 38 | }) 39 | .then(function (data) { 40 | var transformedData = { 41 | book: { 42 | spreads: 'single', 43 | letters: generatePlatformURLs(data, options) 44 | } 45 | }; 46 | return transformedData.book; 47 | }); 48 | } 49 | return returnPromise; 50 | }; 51 | 52 | function generatePlatformURLs(data, options) { 53 | var dynamicPages = data.images.map(function (image) { 54 | return { 55 | url: image.url, 56 | id: image.id, 57 | type: 'story' 58 | }; 59 | }); 60 | 61 | return [].concat(options.frontPages || [], dynamicPages, options.backPages || []); 62 | } 63 | 64 | function trimTrailingSlash(str) { 65 | return str.replace(/\/+$/, ''); 66 | } 67 | 68 | function trimLeadingSlash(str) { 69 | return str.replace(/^\//, ''); 70 | } 71 | -------------------------------------------------------------------------------- /src/scss/monkey/_book.scss: -------------------------------------------------------------------------------- 1 | .lmn-book { 2 | margin:0 auto; 3 | max-width:1280px; 4 | } 5 | 6 | .Heidelberg-Page.is-active { 7 | .js--active-overlay & { 8 | transform: none; 9 | } 10 | } 11 | 12 | $aspect-ratio: percentage(387 / 1054); 13 | 14 | .monkey-wrapper { 15 | height:auto; 16 | padding-top: $aspect-ratio; 17 | } 18 | 19 | .monkey-overlay { 20 | @include container(700px, 20px); 21 | margin-top: -$aspect-ratio; 22 | padding-top: $aspect-ratio/4; 23 | 24 | & ~ .Heidelberg-Book { 25 | display: none; 26 | } 27 | 28 | .desktop & { 29 | margin-top: -$aspect-ratio; 30 | } 31 | 32 | .mobile & { 33 | margin-top: 0; 34 | } 35 | } 36 | 37 | .Heidelberg-Book { 38 | position: absolute; 39 | left: 0; 40 | transition: left 0.7s; 41 | top:0; 42 | width:100%; 43 | 44 | &.at-front-cover { 45 | left: -25%; 46 | .Heidelberg-Page.is-active:nth-child(odd):hover { 47 | transform: rotateY(-15deg); 48 | } 49 | } 50 | 51 | &.at-rear-cover { 52 | left: 25%; 53 | .Heidelberg-Page.is-active:nth-child(even):hover { 54 | transform: rotateY(15deg); 55 | } 56 | .Heidelberg-Page.is-active:nth-child(odd) { 57 | opacity: 0; 58 | } 59 | } 60 | } 61 | 62 | .Heidelberg-Page { 63 | .Heidelberg-HiddenCover + & { 64 | background: none; 65 | } 66 | .Heidelberg-HiddenCover + & .Heidelberg-Spread { 67 | background-image: none !important; 68 | > img { 69 | visibility: hidden !important; 70 | } 71 | } 72 | 73 | img { 74 | height: auto; 75 | width: 100%; 76 | display: block; 77 | } 78 | } 79 | 80 | .last-page { 81 | position: absolute; 82 | z-index: 2; 83 | width: 100%; 84 | > img { 85 | width: 50%; 86 | margin-left: 50%; 87 | } 88 | } 89 | 90 | 91 | .lmn-book + .italic { 92 | display: none; 93 | } 94 | 95 | @media (min-width:993px) { 96 | .lmn-book + .italic.js--show-label { 97 | display: block; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /test/helpers.spec.js: -------------------------------------------------------------------------------- 1 | /* global $, getRandomImage, Monkey */ 2 | 3 | 'use strict'; 4 | 5 | describe('Monkey helpers', function () { 6 | var helpers = Monkey.helpers; 7 | 8 | it('should support string replacements', function () { 9 | var handleReplace = helpers.handleReplace; 10 | var replacements = { foo: 'bar', hello: 'world', empty: '' }; 11 | 12 | var replaces = { 13 | '{{ foo }}': 'bar', 14 | ' {{hello}}': ' world', 15 | '{{ nope }}': '{{ nope }}', 16 | '{{ empty}}': '', 17 | '{ {test} }': '{ {test} }' 18 | }; 19 | 20 | $.each(replaces, function (input, output) { 21 | handleReplace(input, replacements).should.equal(output); 22 | }); 23 | }); 24 | 25 | it('should remember previous replacements object', function () { 26 | var handleReplace = helpers.handleReplace; 27 | handleReplace('{{ foo }}').should.equal('bar'); 28 | }); 29 | 30 | it('should correctly detect mobile', function () { 31 | helpers.isMobile().should.be.Boolean; 32 | }); 33 | 34 | it('should preload multiple images (slow)', function () { 35 | var randomImages = [getRandomImage(), getRandomImage()]; 36 | return helpers.preload(randomImages) 37 | .then(function ($images) { 38 | $images.should.be.a.jqObject; 39 | $images.length.should.equal(2); 40 | $images.eq(0).attr('src').should.equal(randomImages[0]); 41 | $images.eq(1).attr('src').should.equal(randomImages[1]); 42 | }); 43 | }); 44 | 45 | var randomImage = getRandomImage(); 46 | it('should preload single image (slow)', function () { 47 | return helpers.preload(randomImage) 48 | .then(function ($images) { 49 | $images.should.be.a.jqObject; 50 | $images.length.should.equal(1); 51 | $images.attr('src').should.equal(randomImage); 52 | }); 53 | }); 54 | 55 | // This is only actually slow if above tests are ommitted 56 | it('should accept callbacks (slow)', function (cb) { 57 | helpers.preload(randomImage, function ($images) { 58 | $images.should.be.a.jqObject; 59 | $images.length.should.equal(1); 60 | $images.attr('src').should.equal(randomImage); 61 | cb(); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/imgs/arrow-3-50x35.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/context_files/logo-white-148x35-2c2c183a239c3899f64d6830c7804c27.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/context_files/reviewer1-60x60-170cdd32068bb7d438e75cdf65ffb253.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/context_files/logo-148x35-b01f90c07210321b26ec9a5873d318cc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/desktop.spec.js: -------------------------------------------------------------------------------- 1 | /* global $, Monkey, options */ 2 | 3 | 'use strict'; 4 | 5 | describe('Using monkey on desktop', function () { 6 | var data, monkey; 7 | var $container = $('
').attr('data-key', 'lmn-book'); 8 | 9 | before(function () { 10 | this.timeout(4000); 11 | 12 | monkey = new Monkey($container, { 13 | monkeyType: 'desktop', 14 | book: options.book 15 | }); 16 | 17 | return monkey.promise.then(function (dataa) { 18 | $container.appendTo('body'); 19 | data = dataa; 20 | }); 21 | }); 22 | 23 | it('should be initiated', function () { 24 | $container.children().length.should.equal(2); 25 | }); 26 | 27 | it('should change page when clicked', function () { 28 | var $active = $container.find('.is-active').eq(1); 29 | 30 | $active.click(); 31 | 32 | $container.find('.is-active').get(1).should.not.equal($active.get(0)); 33 | }); 34 | 35 | // @todo: fix in IE 36 | it('should change letters when page is changed (noIE)', function () { 37 | var letterActive = $container.find('.letter-active')[0]; 38 | 39 | for (var i = 0; i < 7; i++) { 40 | $container.find('.is-active').eq(1).click(); 41 | } 42 | 43 | var $newActive = $container.find('.letter-active'); 44 | 45 | $newActive.eq(0).index().should.equal(3); 46 | $newActive[0].should.not.equal(letterActive); 47 | }); 48 | 49 | it('should fire event when clicked', function (cb) { 50 | monkey.$events.on('pageTurn', function () { cb(); }); 51 | $container.find('.is-active').eq(1).click(); 52 | }); 53 | 54 | after(function () { 55 | $container.remove(); 56 | }); 57 | 58 | describe('Special Characters for separating names', function () { 59 | var data, monkey; 60 | var $container = $('
').attr('data-key', 'lmn-book'); 61 | 62 | before(function () { 63 | options.book.name = 'Mary Mary-Jane'; 64 | this.timeout(4000); 65 | 66 | monkey = new Monkey($container, { 67 | monkeyType: 'desktop', 68 | book: options.book, 69 | server: 'https://chameleon.lostmy.name/preview.json?callback=?', 70 | icons: true 71 | }); 72 | 73 | return monkey.promise.then(function (dataa) { 74 | $container.appendTo('body'); 75 | data = dataa; 76 | }); 77 | }); 78 | 79 | it('should not make hyphens in names clickable', function () { 80 | $container.find('.nonclickable.special-char .char:contains(-)').length.should.equal(1); 81 | }); 82 | 83 | it('should not make spaces in names clickable', function () { 84 | $container.find('.nonclickable.special-char .char:contains( )').length.should.equal(1); 85 | }); 86 | 87 | after(function () { 88 | $container.remove(); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/scss/monkey/_char-picker.scss: -------------------------------------------------------------------------------- 1 | .picker-container { 2 | margin: 0 auto; 3 | } 4 | .character-picker { 5 | display: block; 6 | margin-top:28px; 7 | padding: 15px 15px 0 15px; 8 | background-color: #FFF; 9 | border: 2px solid #e6e6e6; 10 | border-radius: 5px; 11 | box-shadow:0px 1px 2px 1px rgba(0,0,0,0.2); 12 | font-weight: normal; 13 | z-index: 12000000000; 14 | color: #707070; 15 | min-width: 310px; 16 | opacity: 0; 17 | top:100%; 18 | left:50%; 19 | transform:scale(1.05); 20 | transition: visibility 0s ease .4s, opacity .4s ease, transform .4s ease; 21 | visibility: hidden; 22 | 23 | .tooltip-arrow { 24 | left:50%; 25 | margin-top: -32px; 26 | height: auto; 27 | width: 30px; 28 | position: absolute; 29 | margin-left: -15px; 30 | } 31 | .char-container { 32 | border-top: 1px solid #d8d4c7; 33 | .img-container { 34 | background: none; 35 | border: none; 36 | display: block; 37 | padding: 15px 0; 38 | position: relative; 39 | text-align: left; 40 | height: 70px; 41 | width:100%; 42 | -webkit-tap-highlight-color: rgba(0,0,0,0); 43 | -webkit-tap-highlight-color: transparent; 44 | 45 | &:focus, 46 | &:active { 47 | outline: none; 48 | } 49 | 50 | &:before { 51 | background: #f1f1f1; 52 | border-radius:4px; 53 | bottom:8px; 54 | content:""; 55 | left:-8px; 56 | opacity:0; 57 | position: absolute; 58 | right:-8px; 59 | top:8px; 60 | transition: .25s ease; 61 | } 62 | 63 | &:active, 64 | &:focus { 65 | &:before { 66 | opacity:1; 67 | } 68 | } 69 | } 70 | .img-container + .img-container { 71 | border-top:1px solid #d8d4c7; 72 | } 73 | img { 74 | height: 40px; 75 | border-radius: 5px; 76 | margin-right: 10px; 77 | position: absolute; 78 | border: 1px solid #d8d4c7; 79 | margin-top: -20px; 80 | left:0; 81 | top:50%; 82 | width:40px; 83 | } 84 | img:last-child { 85 | margin-right: 0; 86 | } 87 | .character-name { 88 | font-size: 1.1rem; 89 | padding: 0 100px 0 50px; 90 | position: relative; 91 | text-transform: uppercase; 92 | } 93 | .button { 94 | font-size: 0.8rem; 95 | margin:-15px 0 0; 96 | padding:8px; 97 | padding: .5rem 0.5rem; 98 | position: absolute; 99 | right:0; 100 | top:50%; 101 | } 102 | } 103 | .title { 104 | color: #f06362; 105 | font-family: "freight-text-pro", serif; 106 | font-style: italic; 107 | font-size: 18px; 108 | padding:4px 0 16px; 109 | } 110 | hr { 111 | width: 100%; 112 | height: 1px; 113 | } 114 | } 115 | .character-picker.active { 116 | display: inline; 117 | } 118 | 119 | .character-picker--active { 120 | opacity: 1; 121 | transform: none; 122 | transition-delay:0s, 0s, 0s; 123 | visibility: visible; 124 | .tooltip-arrow { 125 | display: block; 126 | } 127 | } -------------------------------------------------------------------------------- /test/mobile.spec.js: -------------------------------------------------------------------------------- 1 | /* global $, Monkey, options */ 2 | 3 | 'use strict'; 4 | 5 | describe('Using monkey on mobile', function () { 6 | var $monkey, monkey; 7 | var $container = $('
').attr('data-key', 'lmn-book'); 8 | 9 | before(function () { 10 | monkey = new Monkey($container, { 11 | monkeyType: 'mobile', 12 | book: { 13 | name: 'Tal', 14 | gender: 'boy', 15 | locale: 'en-GB' 16 | } 17 | }); 18 | 19 | return monkey.promise.then(function (data) { 20 | data.html.trigger('touchstart'); 21 | 22 | $monkey = $container.find('.monkey-wrapper'); 23 | $container.appendTo('body'); 24 | }); 25 | }); 26 | 27 | it('should be initiated', function () { 28 | $container.children().length.should.equal(2); 29 | }); 30 | 31 | it('should scroll', function () { 32 | $monkey.scrollLeft(100); 33 | $monkey.scrollLeft().should.equal(100); 34 | }); 35 | 36 | it('should change letters when page is changed', function (done) { 37 | $monkey 38 | .scrollLeft($monkey.find('.landscape-images-inner').width() / 2) 39 | .trigger('scroll'); 40 | 41 | setTimeout(function () { 42 | $container.find('.letter-active').index().should.be.within(2, 4); 43 | done(); 44 | }, 300); 45 | }); 46 | 47 | it('should fire event when scrolled', function (cb) { 48 | this.timeout(500); // If it isn't fired in this time, it won't be 49 | monkey.$events.on('halfway', function () { 50 | cb(); 51 | }); 52 | $monkey.scrollLeft($monkey.find('.landscape-images-inner').width() / 1.5).trigger('scroll'); 53 | }); 54 | 55 | after(function () { 56 | $container.remove(); 57 | }); 58 | }); 59 | 60 | describe('Using TJH monkey on mobile', function () { 61 | var $monkey, monkey, monkeyData; 62 | var $container = $('
') 63 | .attr('id', 'monkey') 64 | .attr('data-key', 'tjh-book'); 65 | 66 | before(function () { 67 | monkey = new Monkey($container, { 68 | monkeyType: 'mobile', 69 | letters: false, 70 | platformAPI: true, 71 | server: 'https://prod1-platform.lostmy.name', 72 | serverPath: 'product-builder/tjh/images', 73 | book: { 74 | name: 'Tal', 75 | gender: 'boy', 76 | locale: 'en-gb', 77 | inscription: 'test', 78 | country_code: 'gb', // eslint-disable-line camelcase 79 | address: '', 80 | door_number: '10', // eslint-disable-line camelcase 81 | lat: '51.171223', 82 | lng: '-1.789289', 83 | phototype: 'type-ii' 84 | } 85 | }); 86 | 87 | return monkey.promise.then(function (data) { 88 | data.animateToPage = true; 89 | data.html.trigger('touchstart'); 90 | monkeyData = data; 91 | $monkey = $container.find('.monkey-wrapper'); 92 | $container.appendTo('body'); 93 | }); 94 | }); 95 | 96 | it('should be initiated', function () { 97 | $container.children().length.should.equal(1); 98 | }); 99 | 100 | it('should have the animateToPage value set to true', function () { 101 | monkeyData.animateToPage.should.equal(true); 102 | }); 103 | 104 | it('should change the page with the turnToPage function', function (done) { 105 | monkey.turnToPage(5); 106 | setTimeout(function () { 107 | if ($monkey.scrollLeft() > 0) { 108 | done(); 109 | } 110 | }, 500); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /src/scss/monkey/_mobile.scss: -------------------------------------------------------------------------------- 1 | .tooltip-arrow { 2 | display: none; 3 | } 4 | 5 | .monkey.mobile { 6 | overflow: visible; 7 | } 8 | 9 | .monkey-wrapper.single-spreads .page:nth-child(odd) { 10 | margin-right: 10px; 11 | } 12 | 13 | .monkey-wrapper.mobile { 14 | -webkit-overflow-scrolling: touch; 15 | overflow: scroll; 16 | padding:0; 17 | &::-webkit-scrollbar { 18 | display:none; 19 | } 20 | &.portrait { 21 | margin-bottom: -5px; 22 | } 23 | } 24 | .mobile.monkey.monkey-icons { 25 | #letters { 26 | margin-left: -250px; 27 | width: 700px; 28 | } 29 | .letter-spans { 30 | height: 94px; 31 | overflow-x: scroll; 32 | } 33 | } 34 | 35 | @keyframes name-agitator { 36 | 0% { 37 | transform: translateX(-8px); 38 | } 39 | 6% { 40 | transform: translateX(0); 41 | } 42 | 12% { 43 | transform: translateX(-8px); 44 | } 45 | 18% { 46 | transform: translateX(0); 47 | } 48 | 94% { 49 | transform: translateX(0); 50 | } 51 | 100% { 52 | transform: translateX(-8px); 53 | } 54 | } 55 | .letter-spans--agitated { 56 | animation: name-agitator 3s .2s infinite ease; 57 | } 58 | 59 | .landscape .lmn-book { 60 | position: fixed; 61 | top: 0; 62 | left: 0; 63 | right: 0; 64 | bottom: 0; 65 | z-index: 40; 66 | padding-top: 0; 67 | margin-top: 0; 68 | background-color: #f6f5f1; 69 | } 70 | 71 | .landscape-images { 72 | .landscape-images-inner { 73 | vertical-align: top; 74 | clear: both; 75 | } 76 | } 77 | 78 | .page { 79 | position: relative; 80 | float: left; 81 | min-width: 490px; 82 | padding-right: 10px; 83 | overflow: hidden; 84 | 85 | .landscape & { 86 | background: #333333; 87 | } 88 | } 89 | 90 | .page-first { 91 | padding-right: 0; 92 | margin-right: 10px; 93 | 94 | img { 95 | position: relative; 96 | left: -100%; 97 | } 98 | } 99 | 100 | .page-halfwidth { 101 | min-width: 245px; 102 | } 103 | 104 | #letters-container { 105 | margin-left:0; 106 | margin-right: 0; 107 | } 108 | 109 | // Mobile overlay 110 | 111 | .js--active-overlay { 112 | &.monkey.mobile { 113 | .landscape-images { 114 | display: none; 115 | } 116 | .overlay { 117 | height:auto; 118 | position: relative; 119 | } 120 | .overlay__inner { 121 | position: relative; 122 | } 123 | } 124 | } 125 | 126 | // Mobile Char Picker 127 | 128 | .mobile .picker-container { 129 | position: relative; 130 | } 131 | .mobile { 132 | .character-picker { 133 | left:2.5%; 134 | margin-top: -4px; 135 | min-width:0; 136 | top:0; 137 | width:95%; 138 | } 139 | .character-picker--no-arrow { 140 | margin-top:-24px; 141 | 142 | .tooltip-arrow { 143 | display: none; 144 | } 145 | } 146 | } 147 | 148 | .picker-container__bg { 149 | background:black; 150 | height:100%; 151 | left:0; 152 | opacity:0; 153 | position: fixed; 154 | top:0; 155 | transition: visibility 0s ease .2s, opacity .2s ease; 156 | visibility: hidden; 157 | width:100%; 158 | z-index:3; 159 | } 160 | 161 | .picker-container__bg--active { 162 | opacity: .6; 163 | transition-delay:0s, 0s; 164 | visibility: visible; 165 | } 166 | 167 | .letter-spans { 168 | //overflow: hidden !important; 169 | } 170 | 171 | .character-picker__close { 172 | left:0; 173 | margin-top: 16px; 174 | position: absolute; 175 | top:100%; 176 | width:100%; 177 | } 178 | -------------------------------------------------------------------------------- /demo/context_files/reviewer3-60x60-a1fdb6fec3124963ec75c637651e8951.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/context_files/reviewer5-60x60-a98e7bb98960eed512c973e3bd1fe868.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/context_files/picturefill-604f49d58f8ccbee24a50d6824ab3ed9.js: -------------------------------------------------------------------------------- 1 | window.matchMedia||(window.matchMedia=function(){"use strict";var e=window.styleMedia||window.media;if(!e){var t=document.createElement("style"),r=document.getElementsByTagName("script")[0],n=null;t.type="text/css",t.id="matchmediajs-test",r.parentNode.insertBefore(t,r),n="getComputedStyle"in window&&window.getComputedStyle(t,null)||t.currentStyle,e={matchMedium:function(e){var r="@media "+e+"{ #matchmediajs-test { width: 1px; } }";return t.styleSheet?t.styleSheet.cssText=r:t.textContent=r,"1px"===n.width}}}return function(t){return{matches:e.matchMedium(t||"all"),media:t||"all"}}}()),function(e,t){"use strict";function r(e){var t,r,n,s,o,a=e||{};t=a.elements||i.getAllElements();for(var c=0,l=t.length;l>c;c++)if(r=t[c],n=r.parentNode,s=void 0,o=void 0,r[i.ns]||(r[i.ns]={}),a.reevaluate||!r[i.ns].evaluated){if("PICTURE"===n.nodeName.toUpperCase()){if(i.removeVideoShim(n),s=i.getMatch(r,n),s===!1)continue}else s=void 0;("PICTURE"===n.nodeName.toUpperCase()||r.srcset&&!i.srcsetSupported||!i.sizesSupported&&r.srcset&&r.srcset.indexOf("w")>-1)&&i.dodgeSrcset(r),s?(o=i.processSourceSet(s),i.applyBestCandidate(o,r)):(o=i.processSourceSet(r),(void 0===r.srcset||r[i.ns].srcset)&&i.applyBestCandidate(o,r)),r[i.ns].evaluated=!0}}function n(){r();var n=setInterval(function(){return r(),/^loaded|^i|^c/.test(t.readyState)?void clearInterval(n):void 0},250);if(e.addEventListener){var i;e.addEventListener("resize",function(){e._picturefillWorking||(e._picturefillWorking=!0,e.clearTimeout(i),i=e.setTimeout(function(){r({reevaluate:!0}),e._picturefillWorking=!1},60))},!1)}}if(e.HTMLPictureElement)return void(e.picturefill=function(){});t.createElement("picture");var i={};i.ns="picturefill",i.srcsetSupported="srcset"in t.createElement("img"),i.sizesSupported=e.HTMLImageElement.sizes,i.trim=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")},i.endsWith=function(e,t){return e.endsWith?e.endsWith(t):-1!==e.indexOf(t,e.length-t.length)},i.matchesMedia=function(t){return e.matchMedia&&e.matchMedia(t).matches},i.getDpr=function(){return e.devicePixelRatio||1},i.getWidthFromLength=function(e){return e=e&&(parseFloat(e)>0||e.indexOf("calc(")>-1)?e:"100vw",e=e.replace("vw","%"),i.lengthEl||(i.lengthEl=t.createElement("div"),t.documentElement.insertBefore(i.lengthEl,t.documentElement.firstChild)),i.lengthEl.style.cssText="position: absolute; left: 0; width: "+e+";",i.lengthEl.offsetWidth<=0&&(i.lengthEl.style.cssText="width: 100%;"),i.lengthEl.offsetWidth},i.types={},i.types["image/jpeg"]=!0,i.types["image/gif"]=!0,i.types["image/png"]=!0,i.types["image/svg+xml"]=t.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#Image","1.1"),i.types["image/webp"]=function(){var t=new e.Image,n="image/webp";t.onerror=function(){i.types[n]=!1,r()},t.onload=function(){i.types[n]=1===t.width,r()},t.src="data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA="},i.verifyTypeSupport=function(e){var t=e.getAttribute("type");return null===t||""===t?!0:"function"==typeof i.types[t]?(i.types[t](),"pending"):i.types[t]},i.parseSize=function(e){var t=/(\([^)]+\))?\s*(.+)/g.exec(e);return{media:t&&t[1],length:t&&t[2]}},i.findWidthFromSourceSize=function(e){for(var t,r=i.trim(e).split(/\s*,\s*/),n=0,s=r.length;s>n;n++){var o=r[n],a=i.parseSize(o),c=a.length,l=a.media;if(c&&(!l||i.matchesMedia(l))){t=c;break}}return i.getWidthFromLength(t)},i.parseSrcset=function(e){for(var t=[];""!==e;){e=e.replace(/^\s+/g,"");var r,n=e.search(/\s/g),i=null;if(-1!==n){r=e.slice(0,n);var s=r[r.length-1];if((","===s||""===r)&&(r=r.replace(/,+$/,""),i=""),e=e.slice(n+1),null===i){var o=e.indexOf(",");-1!==o?(i=e.slice(0,o),e=e.slice(o+1)):(i=e,e="")}}else r=e,e="";(r||i)&&t.push({url:r,descriptor:i})}return t},i.parseDescriptor=function(e,t){var r,n=t||"100vw",s=e&&e.replace(/(^\s+|\s+$)/g,""),o=i.findWidthFromSourceSize(n);if(s)for(var a=s.split(" "),c=a.length+1;c>=0;c--)if(void 0!==a[c]){var l=a[c],u=l&&l.slice(l.length-1);if("h"!==u&&"w"!==u||i.sizesSupported){if("x"===u){var d=l&&parseFloat(l,10);r=d&&!isNaN(d)?d:1}}else r=parseFloat(parseInt(l,10)/o)}return r||1},i.getCandidatesFromSourceSet=function(e,t){for(var r=i.parseSrcset(e),n=[],s=0,o=r.length;o>s;s++){var a=r[s];n.push({url:a.url,resolution:i.parseDescriptor(a.descriptor,t)})}return n},i.dodgeSrcset=function(e){e.srcset&&(e[i.ns].srcset=e.srcset,e.removeAttribute("srcset"))},i.processSourceSet=function(e){var t=e.getAttribute("srcset"),r=e.getAttribute("sizes"),n=[];return"IMG"===e.nodeName.toUpperCase()&&e[i.ns]&&e[i.ns].srcset&&(t=e[i.ns].srcset),t&&(n=i.getCandidatesFromSourceSet(t,r)),n},i.applyBestCandidate=function(e,t){var r,n,s;e.sort(i.ascendingSort),n=e.length,s=e[n-1];for(var o=0;n>o;o++)if(r=e[o],r.resolution>=i.getDpr()){s=r;break}s&&!i.endsWith(t.src,s.url)&&(t.src=s.url,t.currentSrc=t.src)},i.ascendingSort=function(e,t){return e.resolution-t.resolution},i.removeVideoShim=function(e){var t=e.getElementsByTagName("video");if(t.length){for(var r=t[0],n=r.getElementsByTagName("source");n.length;)e.insertBefore(n[0],r);r.parentNode.removeChild(r)}},i.getAllElements=function(){for(var e=[],r=t.getElementsByTagName("img"),n=0,s=r.length;s>n;n++){var o=r[n];("PICTURE"===o.parentNode.nodeName.toUpperCase()||null!==o.getAttribute("srcset")||o[i.ns]&&null!==o[i.ns].srcset)&&e.push(o)}return e},i.getMatch=function(e,t){for(var r,n=t.childNodes,s=0,o=n.length;o>s;s++){var a=n[s];if(1===a.nodeType){if(a===e)return r;if("SOURCE"===a.nodeName.toUpperCase()){null!==a.getAttribute("src")&&void 0!==typeof console&&console.warn("The `src` attribute is invalid on `picture` `source` element; instead, use `srcset`.");var c=a.getAttribute("media");if(a.getAttribute("srcset")&&(!c||i.matchesMedia(c))){var l=i.verifyTypeSupport(a);if(l===!0){r=a;break}if("pending"===l)return!1}}}}return r},n(),r._=i,"object"==typeof module&&"object"==typeof module.exports?module.exports=r:"function"==typeof define&&define.amd?define(function(){return r}):"object"==typeof e&&(e.picturefill=r)}(this,this.document); -------------------------------------------------------------------------------- /demo/actuallytwh.json: -------------------------------------------------------------------------------- 1 | { 2 | "book":{ 3 | "spreads": "single", 4 | "letters":[ 5 | { 6 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=3", 7 | "type":"story" 8 | }, 9 | { 10 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=1", 11 | "type":"story" 12 | }, 13 | { 14 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=2", 15 | "type":"story" 16 | }, 17 | { 18 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=3", 19 | "type":"story" 20 | }, 21 | { 22 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=4", 23 | "type":"story" 24 | }, 25 | { 26 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=5", 27 | "type":"story" 28 | }, 29 | { 30 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=6", 31 | "type":"story" 32 | }, 33 | { 34 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=7", 35 | "type":"story" 36 | }, 37 | { 38 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=8", 39 | "type":"story" 40 | }, 41 | { 42 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=9", 43 | "type":"story" 44 | }, 45 | { 46 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=10", 47 | "type":"story" 48 | }, 49 | { 50 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=11", 51 | "type":"story" 52 | }, 53 | { 54 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=12", 55 | "type":"story" 56 | }, 57 | { 58 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=13", 59 | "type":"story" 60 | }, 61 | { 62 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=14", 63 | "type":"story" 64 | }, 65 | { 66 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=15", 67 | "type":"story" 68 | }, 69 | { 70 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=16", 71 | "type":"story" 72 | }, 73 | { 74 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=17", 75 | "type":"story" 76 | }, 77 | { 78 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=18", 79 | "type":"story" 80 | }, 81 | { 82 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=19", 83 | "type":"story" 84 | }, 85 | { 86 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=20", 87 | "type":"story" 88 | }, 89 | { 90 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=21", 91 | "type":"story" 92 | }, 93 | { 94 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=22", 95 | "type":"story" 96 | }, 97 | { 98 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=23", 99 | "type":"story" 100 | }, 101 | { 102 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=24", 103 | "type":"story" 104 | }, 105 | { 106 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=25", 107 | "type":"story" 108 | }, 109 | { 110 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=26", 111 | "type":"story" 112 | }, 113 | { 114 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=27", 115 | "type":"story" 116 | }, 117 | { 118 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=28", 119 | "type":"story" 120 | }, 121 | { 122 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=29", 123 | "type":"story" 124 | }, 125 | { 126 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=30", 127 | "type":"story" 128 | }, 129 | { 130 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=31", 131 | "type":"story" 132 | }, 133 | { 134 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=32", 135 | "type":"story" 136 | }, 137 | { 138 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=33", 139 | "type":"story" 140 | }, 141 | { 142 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=34", 143 | "type":"story" 144 | }, 145 | { 146 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=35", 147 | "type":"story" 148 | }, 149 | { 150 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=36", 151 | "type":"story" 152 | }, 153 | { 154 | "url":"//twh-playground-staging.imgix.net/e9c0b8f1-d528-4186-ab88-548c5dbe8947.pdf?page=37", 155 | "type":"story" 156 | } 157 | ] 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /demo/context_files/reviewer2-60x60-94b4f7c02575eab6ebb04ee2812782e9.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /demo/context_files/reviewer6-60x60-35c5fb0448ac74fa78aae224901e448f.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/monkey.js: -------------------------------------------------------------------------------- 1 | (function (Monkey) { 2 | if (typeof module !== 'undefined' && module.exports) { 3 | module.exports = Monkey; 4 | } 5 | 6 | if (typeof window !== 'undefined') { 7 | window.Monkey = Monkey; 8 | } 9 | })((function () { 10 | var $ = require('jquery'); 11 | var lang = require('lang'); 12 | 13 | /** 14 | * Initiate monkey; generate it, and then insert it into the page. 15 | * 16 | * @param {string|HTMLElement|jQuery} monkeyContainer Container for monkey. 17 | * @param {object} options Options! Read the code. Sorry. 18 | */ 19 | function Monkey(monkeyContainer, options) { 20 | var $monkeyContainer = $(monkeyContainer); 21 | 22 | this.options = options = $.extend({ 23 | preload: 3, // Number of pages to preload 24 | letters: true, // Display letters? true, false, or selector 25 | slider: false, // Display slider? true, false, or selector 26 | icons: $monkeyContainer.data('icons'), // Display character icons under letters? true, false 27 | monkeyType: 'auto', // auto, desktop, mobile 28 | animateName: true, 29 | perPage: 4, 30 | replaceMonkey: false, 31 | canClose: false, 32 | showCharPicker: $monkeyContainer.data('show-picker'), 33 | showOverlay: $monkeyContainer.data('show-overlay'), 34 | server: 'https://chameleon.lostmy.name/preview.json?callback=?', 35 | dprNotSupported: false, 36 | 37 | book: { 38 | name: $monkeyContainer.data('name'), 39 | gender: $monkeyContainer.data('gender'), 40 | locale: $monkeyContainer.data('locale'), 41 | characterSelection: $monkeyContainer.data('character-selection') 42 | }, 43 | 44 | lang: { 45 | bookFor: lang('monkey.bookFor'), 46 | noAltText: lang('monkey.noAltText') 47 | } 48 | }, options); 49 | 50 | if ($monkeyContainer.data('first-book-name')) { 51 | options.book.comparisonBooks = [ 52 | { 53 | name: $monkeyContainer.data('first-book-name'), 54 | gender: $monkeyContainer.data('first-book-gender'), 55 | locale: $monkeyContainer.data('first-book-locale'), 56 | characterSelection: $monkeyContainer.data('first-book-character-selection') 57 | } 58 | ]; 59 | } 60 | this.$events = $({}); 61 | 62 | var promise = Monkey._getData(options) 63 | .then(Monkey._calculateMonkey(options.monkeyType)) 64 | .then(Monkey._checkLanguageChange($monkeyContainer, options)) 65 | .then((data) => { 66 | data.monkeyContainer = $monkeyContainer; 67 | return data; 68 | }); 69 | 70 | if (options.letters || this.options.platformAPI) { 71 | promise = promise 72 | .then(Monkey._generateBaseElement($monkeyContainer, options)); 73 | } 74 | 75 | promise = promise 76 | .then(Monkey._generateBaseWrapperElement($monkeyContainer, options)); 77 | 78 | var generateUrls = (this.options.platformAPI) 79 | ? Monkey._generatePlatformUrls(options) 80 | : Monkey._generateUrls(options); 81 | 82 | promise = promise 83 | .then(generateUrls) 84 | .then(Monkey._generateHtml(options.lang)); 85 | 86 | if (options.letters) { 87 | promise = promise 88 | .then(Monkey.letters._generateHtml(options)); 89 | 90 | if (options.showCharPicker) { 91 | promise = promise 92 | .then(Monkey.letters._generateCharPicker( 93 | options, 94 | $monkeyContainer 95 | ) 96 | ); 97 | } 98 | promise = promise.then(Monkey.letters._init(this.$events, options, $monkeyContainer)); 99 | } 100 | 101 | if (options.slider) { 102 | promise = promise 103 | .then(Monkey.slider._generateHtml(options)) 104 | .then(Monkey.slider._init(this.$events)); 105 | } 106 | 107 | promise = promise 108 | .then(Monkey._insertHtml($monkeyContainer)) 109 | .then(Monkey._initMonkey(this.$events, options)); 110 | 111 | if (options.showOverlay) { 112 | promise = promise 113 | .then(Monkey.letters._generateOverlay(options, this.$events)); 114 | } 115 | 116 | promise = promise 117 | .then((data) => { 118 | // exposed turnToPage method 119 | this.turnToPage = data.turnToPage; 120 | 121 | if (data.needsSpread) { 122 | Monkey.spread._getData(data, options) 123 | .then(Monkey.spread._insertSpread()); 124 | } 125 | 126 | return data; 127 | }); 128 | 129 | 130 | this.promise = promise; 131 | } 132 | 133 | Monkey._getData = require('./steps/getData'); 134 | Monkey._generateBaseElement = require('./steps/generateBaseElement'); 135 | Monkey._generateBaseWrapperElement = require('./steps/generateBaseWrapperElement'); 136 | Monkey._calculateMonkey = require('./steps/calculateMonkey'); 137 | Monkey._generateUrls = require('./steps/generateUrls'); 138 | Monkey._generatePlatformUrls = require('./steps/generatePlatformUrls'); 139 | Monkey._generateHtml = require('./steps/generateHtml'); 140 | Monkey._insertHtml = require('./steps/insertHtml'); 141 | Monkey._initMonkey = require('./steps/initMonkey'); 142 | Monkey._checkLanguageChange = require('./steps/checkLanguageChange'); 143 | 144 | Monkey.spread = {}; 145 | Monkey.spread.Monkey = Monkey; 146 | Monkey.spread._getData = require('./steps/spread/getData'); 147 | Monkey.spread._insertSpread = require('./steps/spread/insertSpread'); 148 | 149 | Monkey.letters = {}; 150 | Monkey.letters.Monkey = Monkey; 151 | Monkey.letters._generateHtml = require('./steps/letters/generateHtml'); 152 | Monkey.letters._generateCharPicker = require('./steps/letters/generateCharPicker'); 153 | Monkey.letters._init = require('./steps/letters/init'); 154 | Monkey.letters._generateOverlay = require('./steps/letters/generateOverlay'); 155 | 156 | Monkey.slider = {}; 157 | Monkey.slider.Monkey = Monkey; 158 | Monkey.slider._generateHtml = require('./steps/slider/generateHtml'); 159 | Monkey.slider._init = require('./steps/slider/init'); 160 | 161 | Monkey.helpers = {}; 162 | Monkey.helpers.Monkey = Monkey; 163 | Monkey.helpers.handleReplace = require('./helpers/handleReplace'); 164 | Monkey.helpers.isMobile = require('./helpers/isMobile'); 165 | Monkey.helpers.preload = require('./helpers/preload'); 166 | 167 | Monkey.monkeys = {}; 168 | Monkey.monkeys.mobile = require('./monkeys/mobile'); 169 | Monkey.monkeys.mobile.Monkey = Monkey; 170 | Monkey.monkeys.desktop = require('./monkeys/desktop'); 171 | Monkey.monkeys.desktop.Monkey = Monkey; 172 | 173 | return Monkey; 174 | })()); 175 | -------------------------------------------------------------------------------- /src/js/monkeys/mobile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | 5 | var mobile = module.exports = {}; 6 | var $window = $(window); 7 | var numberOfMonkeys = 0; 8 | var widthModifier = 1.5; 9 | 10 | mobile.calculateSize = function () { 11 | var width = $window.width() * widthModifier; 12 | return Math.round(width); 13 | }; 14 | 15 | /** 16 | * Adds the HTML to the page 17 | * @param {object} data The data object 18 | * @param {object} lang Default language object 19 | * @return {HTMLElement} The monkey wrapper. 20 | */ 21 | mobile.generateHtml = function (data, lang) { 22 | var $images = $('
') 23 | .appendTo(data.$monkeyWrapper) 24 | .addClass('landscape-images'); 25 | var $inner = $('
') 26 | .appendTo($images) 27 | .addClass('landscape-images-inner'); 28 | 29 | // For each image URL we get passed, create the image and add it to the page. 30 | $.each(data.urls, function (i, url) { 31 | var $page = $('
') 32 | .appendTo($inner) 33 | .addClass('page page-' + data.letters[i].type + ' Page-' + i); 34 | 35 | if (data.spreads === 'single') { 36 | $page.addClass('page-halfwidth'); 37 | } else if (i === 0) { 38 | $page.addClass('page-first page-halfwidth'); 39 | } else if (i === data.urls.length - 1) { 40 | $page.addClass('page-halfwidth'); 41 | } 42 | 43 | $('', { 44 | src: url, 45 | alt: data.letters[i].text || lang.noAltText 46 | }).appendTo($page); 47 | }); 48 | 49 | return data.$monkeyWrapper; 50 | }; 51 | 52 | // @todo document this 53 | mobile.init = function (data, $events) { 54 | var windowLeft = 0; 55 | var maxProgress = 0; 56 | var currentIndex = 0; 57 | ++numberOfMonkeys; 58 | 59 | var $monkey = data.html; 60 | var $monkeyParent = $monkey.parents('#monkey'); 61 | $monkeyParent.addClass('mobile'); 62 | var hasSingleSpreads = data.spreads === 'single'; 63 | var RATIO = this.Monkey.IMAGE_RATIO; 64 | data.animateToPage = false; 65 | 66 | $window.on('orientationchange resize', setWidths); 67 | var $page = $('.page'); 68 | var numOfPages = $page.length; 69 | 70 | setTimeout(setWidths); 71 | 72 | function setWidths() { 73 | var width = $window.width() * widthModifier; 74 | var height = Math.ceil(width / RATIO); 75 | $('.landscape-images-inner').width(width * (numOfPages + 1)); 76 | 77 | $page.children('img').css({ 78 | height: height, 79 | width: Math.ceil(width) 80 | }); 81 | 82 | $('.page-halfwidth') 83 | .css('width', Math.ceil(width / 2)) 84 | .find('img').css('height', height); 85 | 86 | if (data.spreads === 'single') { 87 | $('.page-halfwidth img').css('width', Math.ceil(width / 2)); 88 | } 89 | 90 | $monkey.scrollLeft($monkey.find('img').width() * windowLeft); 91 | } 92 | 93 | $monkey.on('scroll', function () { 94 | var width = $window.width() * widthModifier; 95 | if (hasSingleSpreads) { 96 | width = (width / 2) + 6; 97 | } 98 | var scrollLeft = $monkey.scrollLeft(); 99 | var progress = scrollLeft / $monkey.find('.landscape-images-inner').width(); 100 | var index = (Math.floor(scrollLeft / width) * width) / (width * numOfPages) * numOfPages; 101 | 102 | if (progress > maxProgress) { 103 | maxProgress = progress; 104 | $events.trigger('bookprogress', { 105 | progress: progress 106 | }); 107 | } 108 | 109 | if (currentIndex !== index) { 110 | $events.trigger('pageTurn', { 111 | monkeyType: $monkeyParent.parent().attr('data-book'), 112 | index: index 113 | }); 114 | currentIndex = index; 115 | } 116 | 117 | windowLeft = scrollLeft / $monkey.find('img').width(); 118 | 119 | if (scrollLeft / $monkey.find('.landscape-images-inner').width() > 0.5) { 120 | $events.trigger('halfway'); 121 | } 122 | 123 | if ($monkey.find('.page:last').position().left < $(window).width()) { 124 | $events.trigger('finished'); 125 | } 126 | }); 127 | 128 | data.swapPage = function (index, character) { 129 | var pageNumModifier = 3; 130 | var page = Number(index) * 2 + pageNumModifier; 131 | var page1El = $('.Page-' + page).find('img'); 132 | var page2El = $('.Page-' + (page + 1)).find('img'); 133 | page1El.attr({ src: character.url1 + data.queryString }); 134 | page2El.attr({ src: character.url2 + data.queryString }); 135 | }; 136 | 137 | // If we're animating the pageTurn (TJH), we don't want the harassment to execute 138 | // on any rendering of the book after the first. 139 | if ((data.animateToPage && numberOfMonkeys < 2) || !data.animateToPage) { 140 | this.harass(data); 141 | } 142 | 143 | return this.letterHandler(data, $events); 144 | }; 145 | 146 | // @todo document this 147 | mobile.harass = function (data) { 148 | // Taken from jQuery Easing: http://gsgd.co.uk/sandbox/jquery/easing/ 149 | var easing = 'easeInOutQuad'; 150 | $.easing[easing] = function (x, t, b, c, d) { 151 | if ((t /= d / 2) < 1) { 152 | return c / 2 * t * t + b; 153 | } 154 | return -c / 2 * ((--t) * (t - 2) - 1) + b; 155 | }; 156 | 157 | data.container.one('mousedown touchstart pointerdown', function () { 158 | data.html.stop(); 159 | }); 160 | 161 | var animSpeed = 800; 162 | 163 | function scroll() { 164 | data.html.animate({ scrollLeft: 10 }, animSpeed, easing, function () { 165 | data.html.animate({ scrollLeft: 0 }, animSpeed, easing, scroll); 166 | }); 167 | } 168 | 169 | scroll(); 170 | }; 171 | 172 | // @todo document this. 173 | mobile.letterHandler = function (data, $events) { 174 | var $monkey = data.html; 175 | var $pages = $monkey.find('.page'); 176 | 177 | var page = -1; 178 | 179 | $monkey.on('scroll', function () { 180 | var currentPage = 0; 181 | 182 | $pages.each(function (i) { 183 | if ($(this).offset().left >= -$window.width()) { 184 | currentPage = i; 185 | 186 | return false; 187 | } 188 | }); 189 | 190 | if (currentPage !== page && data.canSetUpMobileScrollListener === true) { 191 | page = currentPage; 192 | $events.trigger('letterChange', page); 193 | } 194 | }); 195 | 196 | // Do it on the next free cycle to ensure the event has been added 197 | setTimeout(function () { 198 | $monkey.triggerHandler('scroll'); 199 | }); 200 | 201 | // index is the letter index 202 | return function turnToPage(index) { 203 | if (index !== 0) { 204 | data.html.stop(); 205 | } 206 | var $pages = data.html.find('.page'); 207 | var factor = data.spreads === 'single' ? index : (index * 2 + 1); 208 | var $page = $pages.eq(factor); 209 | var offset = $page.offset().left - $page.parent().offset().left; 210 | var INITIAL_SPEED = 500; 211 | if (data.animateToPage) { 212 | $monkey.animate({ 213 | scrollLeft: offset 214 | }, INITIAL_SPEED * Math.sqrt(factor), 'easeInOutQuad'); 215 | } else { 216 | $monkey.scrollLeft(offset); 217 | } 218 | }; 219 | }; 220 | -------------------------------------------------------------------------------- /src/scss/monkey/_letters.scss: -------------------------------------------------------------------------------- 1 | .letter-spans { 2 | height: 40px; 3 | margin: auto; 4 | } 5 | 6 | #letters { 7 | font-size: 1.6em; 8 | line-height: 1.2; 9 | margin: auto; 10 | position: relative; 11 | display: table; 12 | .letter { 13 | cursor: pointer; 14 | margin: 0px 3px; 15 | border-bottom: none; 16 | display: inline; 17 | float: left; 18 | vertical-align: top; 19 | } 20 | .letter-active { 21 | color: #00b69d; 22 | border-bottom: 2px solid; 23 | } 24 | #letters-container { 25 | height: 25px; 26 | } 27 | } 28 | 29 | #letters-container { 30 | position: relative; 31 | transform: translate(0,0); 32 | z-index:3; 33 | } 34 | 35 | .monkey-icons #letters { 36 | font-size: 1.2em; 37 | 38 | .letter { 39 | border-radius:2px; 40 | padding: 2px 4px 0px 4px; 41 | display: block; 42 | margin: 0 1px 12px 0; 43 | position: relative; 44 | transition: background .25s ease; 45 | -webkit-tap-highlight-color: rgba(0,0,0,0); 46 | -webkit-tap-highlight-color: transparent; 47 | 48 | .char { 49 | border-radius: 50%; 50 | font-weight: bold; 51 | width: 28px; 52 | height: 28px; 53 | line-height: 28px; 54 | text-align: center; 55 | margin: 0 auto 6px auto; 56 | transition: .25s ease; 57 | } 58 | } 59 | .letter.changed { 60 | background-color: #ffc600; 61 | border-radius: 5px; 62 | } 63 | .letter:hover, 64 | .letter:focus{ 65 | .char { 66 | background-color: #f1f1f1; 67 | color: #333; 68 | 69 | } 70 | .character-card { 71 | box-shadow: 0px 1px 2px 0px rgba(0,0,0,0.2); 72 | } 73 | } 74 | .letter:active { 75 | background:#f1f1f1; 76 | } 77 | .change-character { 78 | display: block; 79 | font-size: 10px; 80 | font-weight: bold; 81 | left:-1px; 82 | opacity: 0; 83 | padding:0 4px; 84 | position: absolute; 85 | text-transform: uppercase; 86 | transition: opacity .25s ease; 87 | 88 | top:100%; 89 | 90 | .letter:hover &, 91 | .letter:focus & { 92 | text-decoration: underline; 93 | } 94 | } 95 | .letter-active, .letter-active:hover { 96 | border-bottom: none; 97 | .char { 98 | color: #FFF; 99 | background-color: #f06263; 100 | } 101 | .character-card { 102 | box-shadow: 0px 1px 2px 0px rgba(0,0,0,0.2); 103 | } 104 | } 105 | .letter-active { 106 | .change-character { 107 | opacity: 1; 108 | transition-delay:.5s; 109 | } 110 | } 111 | .letter[data-type="bridge"] { 112 | display: none; 113 | } 114 | } 115 | .monkey-icons #letters { 116 | .letter:first-child, 117 | .letter:last-child, 118 | .letter--cover { 119 | color: black; 120 | display: inline-block; 121 | height:auto; 122 | width: 25px; 123 | font-size: 1.5rem; 124 | line-height: 16px; 125 | padding-left:0; 126 | padding-right: 0; 127 | 128 | .char { 129 | height:16px; 130 | line-height:16px; 131 | margin:7px 5px 5px 4px; 132 | width:16px; 133 | } 134 | } 135 | .letter--cover { 136 | .char { 137 | overflow: hidden; 138 | position: relative; 139 | text-indent:200%; 140 | white-space:nowrap; 141 | 142 | &:before { 143 | background:#000; 144 | border-radius:50%; 145 | content:""; 146 | height:8px; 147 | left:50%; 148 | margin:-4px 0 0 -4px; 149 | position: absolute; 150 | top:50%; 151 | transition: background .25s ease; 152 | width:8px; 153 | } 154 | } 155 | 156 | &.letter-active .char:before { 157 | background:#fff; 158 | height:6px; 159 | margin:-3px 0 0 -3px; 160 | width:6px; 161 | } 162 | } 163 | } 164 | .character-card { 165 | display: inline-block; 166 | height: 40px; 167 | background-color: white; 168 | border-radius: 4px; 169 | border: 1px #e0e0e0 solid; 170 | overflow: hidden; 171 | position: relative; 172 | img { 173 | background:#fff; 174 | display: block; 175 | margin-top: 0; 176 | width: 38px; 177 | // transition: .25s ease-in; 178 | height: 38px; 179 | } 180 | } 181 | 182 | @keyframes old-thumb { 183 | 0% { 184 | opacity:1; 185 | transform: scale(1); 186 | } 187 | 100% { 188 | opacity:0; 189 | transform: scale(0.5); 190 | } 191 | } 192 | @keyframes new-thumb { 193 | 0% { 194 | opacity:0; 195 | transform: scale(1.5); 196 | } 197 | 100% { 198 | opacity:1; 199 | transform: none; 200 | } 201 | } 202 | .character-card__image--old { 203 | animation: old-thumb .3s 1 ease both; 204 | } 205 | 206 | .character-card__image--new { 207 | animation: new-thumb .3s .1s 1 ease both; 208 | left:0; 209 | position: absolute; 210 | top:0; 211 | } 212 | .monkey-icons { 213 | .letter-spans { 214 | margin-top: 10px; 215 | height: 82px; 216 | } 217 | } 218 | .letter--cloned { 219 | 220 | border-radius:2px; 221 | padding: 2px 4px 0px 4px; 222 | display: block; 223 | margin: 0 1px 12px 0; 224 | position: relative; 225 | transition: background .25s ease; 226 | 227 | .char { 228 | color: #FFF; 229 | background-color: #f06263; 230 | border-radius: 50%; 231 | font-weight: bold; 232 | width: 28px; 233 | height: 28px; 234 | line-height: 28px; 235 | text-align: center; 236 | margin: 0 auto 6px auto; 237 | transition: .25s ease; 238 | } 239 | .change-character { 240 | display: none; 241 | } 242 | } 243 | 244 | .overlay { 245 | background: #fff; 246 | bottom:0; 247 | height:100%; 248 | left:0; 249 | position: absolute; 250 | transform:translate(0,0); 251 | width:100%; 252 | z-index:5; 253 | } 254 | 255 | @media (min-width: 1025px) { 256 | .overlay { 257 | left:2.5rem; 258 | right:2.5rem; 259 | width: auto; 260 | } 261 | } 262 | .overlay__inner { 263 | height:100%; 264 | left:0; 265 | top:0; 266 | position: absolute; 267 | width:100%; 268 | z-index:1; 269 | 270 | .row { 271 | margin:0; 272 | width:100%; 273 | } 274 | 275 | .col { 276 | max-width:38em; 277 | } 278 | 279 | .row > .col { 280 | margin: 0 auto; 281 | } 282 | } 283 | 284 | // Browser-proof version (IE8 + 9) 285 | 286 | .overlay__inner { 287 | &:before { 288 | content:""; 289 | display: inline-block; 290 | 291 | height:100%; 292 | margin-left: -1px; 293 | vertical-align: middle; 294 | 295 | width:1px; 296 | } 297 | 298 | .row { 299 | display: inline-block; 300 | max-width:99%; 301 | vertical-align: middle; 302 | } 303 | } 304 | 305 | .overlay__buttons { 306 | padding-top: 1em; 307 | 308 | .desktop & { 309 | padding-bottom: 1.5em; 310 | } 311 | } 312 | 313 | .landscape #letters-container { 314 | position: absolute; 315 | color: #f6f5f1; 316 | #letters { 317 | position: fixed; 318 | z-index: 100; 319 | left: 50%; 320 | padding: 5px 10px; 321 | background-color: rgba(#333333, 0.6); 322 | border-radius: 5px; 323 | color: white; 324 | transform: translate(-50%, 0); 325 | .letter-active { 326 | color: #ff5227; 327 | } 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/js/steps/letters/generateHtml.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | var blankLetterCheck = require('../../helpers/blankLetterCheck'); 5 | // var isMobile = require('../../helpers/isMobile')(); 6 | 7 | // The minimum length for a name is 4 not including a space or - 8 | var shortNameMinimumLength = 4; 9 | 10 | /** 11 | * Generate HTML for letters. 12 | * 13 | * @param {object} options Options object from monkey. 14 | */ 15 | module.exports = function (options) { 16 | var defer = $.Deferred(); 17 | return function (data) { 18 | if (!data.name) { 19 | return data; 20 | } 21 | 22 | var $lettersContainer = $('
'); 23 | $lettersContainer.attr({ 24 | id: 'letters-container', 25 | class: 'aligned-center row leaded md-mar-b', 26 | 'data-key': 'monkey-letters' 27 | }); 28 | 29 | var $hiddenName = $('', { 30 | class: 'for-screen-reader' 31 | }).text(' ' + data.name.toLowerCase()); 32 | 33 | $('

').appendTo($lettersContainer) 34 | .addClass('no-mar spaced-none') 35 | .text(options.lang.bookFor) 36 | .append($hiddenName); 37 | 38 | var $letterSpanContainer = $('

') 39 | .appendTo($lettersContainer) 40 | .addClass('letter-spans') 41 | .attr('aria-hidden', 'true'); 42 | 43 | var $letters = $('
') 44 | .appendTo($letterSpanContainer) 45 | .addClass('strong') 46 | .attr('id', 'letters'); 47 | 48 | // This checks for Eszett character and replaces it with double S's 49 | // Removes apostrophes 50 | var letters = data.name 51 | .replace(/ß/g, 'SS') 52 | .replace(/\'/g, '') 53 | .split(''); 54 | 55 | // Get the total letters, by finding just the first part of each letter 56 | var dataLetters = $(data.letters).filter(function (i, letter) { 57 | return letter.part === 1; 58 | }); 59 | 60 | // If name short, add blank letter for extra story 61 | var paddedLetters = $.map(letters, function (el) { 62 | if (!blankLetterCheck.test(el)) { 63 | return el; 64 | } 65 | }); 66 | if (paddedLetters.length <= shortNameMinimumLength) { 67 | letters.splice(-1, 0, ''); 68 | } 69 | var combinedLetters = data.combinedLetters = combineLetters(letters, dataLetters); 70 | 71 | // Whether we should show the duplicate letters modal if more than one 72 | // book has been created. We return an integer if true because we need to 73 | // change the wording on the overlay if it's a single letter, or multiple. 74 | var determineIfDuplicate = function () { 75 | var outcome = false; 76 | $(combinedLetters).each(function (i, letter) { 77 | if (letter.changed) { 78 | outcome++; 79 | } 80 | }); 81 | return outcome && options.book.comparisonBooks !== undefined; 82 | }; 83 | 84 | var hasLetterChanged = function (letter) { 85 | return letter.selected !== letter.default_character 86 | && options.showOverlay && determineIfDuplicate(); 87 | }; 88 | 89 | data.shouldShowDuplicateModal = determineIfDuplicate(); 90 | 91 | var loadLetterCards = function () { 92 | $(combinedLetters).each(function (i, letter) { 93 | var $letterDiv = $('
'); 94 | if (blankLetterCheck.test(letter.letter)) { 95 | $letterDiv.addClass('special-char nonclickable'); 96 | } 97 | 98 | // jscs:disable requireCamelCaseOrUpperCaseIdentifiers 99 | // Create the letter HTML, and add a whole bunch of data attributes 100 | // that we can access for targeting the appropriate letter when using 101 | // the character picker. 102 | $letterDiv.appendTo($letters) 103 | .addClass('letter') 104 | .attr('data-letter', letter.letter) 105 | .attr('data-character', letter.default_character) 106 | .attr('data-selected-character', letter.selected) 107 | .attr('data-type', letter.type) 108 | .after(' '); 109 | if (letter.thumbnail && letter.thumbnail.indexOf('helper') !== -1) { 110 | $letterDiv.attr('data-helper-character', true); 111 | } 112 | // If we're showing the duplicate letter overlay, and this is one of the 113 | // duplicate letters, we add a class to show visually that this is a 114 | // changed letter. 115 | if (hasLetterChanged(letter)) { 116 | $letterDiv.addClass('changed'); 117 | } 118 | if (letter.selected !== letter.default_character) { 119 | data.monkeyContainer.data('changedChars', true); 120 | } 121 | // jscs:enable requireCamelCaseOrUpperCaseIdentifiers 122 | 123 | var $letterSpan = $('
') 124 | .toggleClass('char', letter.letter !== '') 125 | .text(letter.letter || ''); 126 | $letterSpan.appendTo($letterDiv); 127 | 128 | if (options.icons && letter.thumbnail) { 129 | var $characterCard = $('
'); 130 | $characterCard.appendTo($letterDiv) 131 | .addClass('character-card'); 132 | 133 | var $icon = $('') 134 | .attr('src', letter.thumbnail) 135 | .attr('height', 38) 136 | .attr('width', 38); 137 | $icon.appendTo($characterCard); 138 | } 139 | defer.resolve(); 140 | }); 141 | return defer.promise(); 142 | }; 143 | 144 | return loadLetterCards() 145 | .then(function () { 146 | // After we've loaded the name, we add the two circles either side of 147 | // the name for the front and back covers. 148 | $('
').html('
') 149 | .prependTo($letters) 150 | .addClass('letter letter--cover') 151 | .after(' ') 152 | .clone().appendTo($letters); 153 | 154 | var $book = false; 155 | // if (typeof options.letters !== 'boolean') { 156 | // $book = $(options.letters); 157 | // } 158 | if (!$book || !$book.length) { 159 | $book = data.monkeyContainer; 160 | } 161 | // We replicate some functionality from js/generateBaseElement.js here, 162 | // removing all current content within the container, adding the 163 | // letters element to the DOM (and saving it to the data object), and 164 | // adding the loading gif in whilst the book loads. 165 | // $book.empty(); 166 | data.lettersElement = $lettersContainer.prependTo($book); 167 | // data.loading = data.loading.appendTo($book); 168 | 169 | if (options.icons) { 170 | $lettersContainer.parents('#monkey').addClass('monkey-icons'); 171 | } 172 | return data; 173 | }); 174 | }; 175 | }; 176 | /** 177 | * We combine letters from the name passed through to Monkey as an option, 178 | * with the letters array brought back from the server. 179 | * @param {array} splitLetters The letters in the options 180 | * @param {array} dataLetters The letters from the server 181 | * @return {array} A combined array of both letters, mapped. 182 | */ 183 | function combineLetters(splitLetters, dataLetters) { 184 | var offset = 0; 185 | return $.map(splitLetters, function (val, i) { 186 | var idx = i; 187 | if (splitLetters.length > 5) { 188 | idx = i - offset; 189 | } 190 | dataLetters[idx].changed = 191 | dataLetters[idx].selected !== dataLetters[idx].default_character; 192 | 193 | if (blankLetterCheck.test(val)) { 194 | offset++; 195 | return { letter: val }; 196 | } 197 | 198 | dataLetters[idx].letter = val; 199 | return dataLetters[idx]; 200 | }); 201 | } 202 | -------------------------------------------------------------------------------- /demo/context_files/reviewer4-60x60-bcef0cef3fddd3d7c6688b46c2377597.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/js/steps/letters/generateOverlay.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var $ = require('jquery'); 4 | var isMobile = require('../../helpers/isMobile')(); 5 | var lang = require('lang'); 6 | 7 | 8 | /** 9 | * Generate HTML for the overlay. This contains the code for both the duplicate 10 | * letter overlay, and the language overlay. 11 | * 12 | * @param {string|boolean} [selector] Selector or element to insert letters into. 13 | * @param {object} lang Object containing language stuff. 14 | */ 15 | module.exports = function (options, $events) { 16 | var defer = $.Deferred(); 17 | return function (data) { 18 | function loadOverlay() { 19 | var $overlay = $('
'); 20 | var $monkeyContainer = data.monkeyContainer; 21 | var classes = { 22 | overlayActive: 'js--active-overlay' 23 | }; 24 | 25 | var singularOrPlural = data.shouldShowDuplicateModal === 1 ? 26 | 'singular' : 27 | 'plural'; 28 | 29 | // Trigger an event which we can use in Eagle to hide the book form. 30 | $events.trigger('overlayActive', $monkeyContainer); 31 | 32 | $monkeyContainer.addClass(classes.overlayActive); 33 | 34 | $overlay.prependTo($monkeyContainer.find('.monkey-wrapper')) 35 | .addClass('monkey-overlay'); 36 | 37 | // We want to hide the 'Tap to Preview' label when the overlay is visible 38 | // so the user only focuses on the content within the overlay, and because 39 | // it's nonsensical to have a label to 'Tap to Preview' when you, at that 40 | // stage, can't. 41 | $monkeyContainer 42 | .next('.italic') 43 | .hide(); 44 | 45 | var $overlayContent = $('
') 46 | .addClass('row'); 47 | $overlayContent.appendTo($overlay); 48 | 49 | var overlayTitle, overlayText; 50 | var comparisonName = ''; 51 | if (typeof options.book.comparisonBooks !== 'undefined') { 52 | comparisonName = options.book.comparisonBooks[0].name; 53 | } 54 | 55 | // Here's where we change the content of the overlay dependant on which 56 | // overlay is showing. If we need more overlays for whatever reason, we 57 | // should probably change how this is done as it's not entirely scalable. 58 | if (options.showLanguageOverlay === true && $monkeyContainer.data('changedChars')) { 59 | overlayTitle = lang('monkey.language.title'); 60 | overlayText = lang('monkey.language.copy'); 61 | } else if (comparisonName !== '') { 62 | overlayTitle = lang('monkey.overlay.nounTypes.' + singularOrPlural + '.title'); 63 | overlayText = comparisonName.toUpperCase() + ' & ' + 64 | data.name.toUpperCase() + ' ' + 65 | lang('monkey.overlay.nounTypes.' + singularOrPlural + '.intro'); 66 | } 67 | 68 | var $titleBox = $('

') 69 | .text(overlayTitle); 70 | $titleBox.appendTo($overlayContent); 71 | 72 | 73 | var $messageBox = $('
') 74 | .text(overlayText) 75 | .addClass('overlay__copy col'); 76 | $messageBox.appendTo($overlayContent); 77 | 78 | var $buttonContainer = $('
') 79 | .addClass('col overlay__buttons'); 80 | 81 | var $yesButton = $('