├── .gitignore ├── Makefile ├── README.md ├── demo-src ├── constants.js ├── deepExtend.js ├── gateway │ ├── index.js │ ├── rest.js │ └── restFormatters.js ├── index.js ├── preview │ └── model.js ├── ui │ ├── bracketedDevicePixelRatio.js │ ├── index.less │ ├── mediawiki.less │ │ ├── mediawiki.mixins.animation.less │ │ ├── mediawiki.mixins.less │ │ ├── mediawiki.mixins.rotation.less │ │ └── mediawiki.ui │ │ │ ├── mixins.less │ │ │ └── variables.less │ ├── mixins.less │ ├── parseHTML.js │ ├── pointer-mask.svg │ ├── renderer.js │ ├── templates │ │ ├── pagePreview │ │ │ ├── pagePreview.js │ │ │ └── pagePreview.less │ │ ├── popup.less │ │ ├── preview │ │ │ ├── preview.js │ │ │ └── preview.less │ │ └── templateUtil.js │ ├── thumbnail.js │ └── variables.less └── wait.js ├── dist ├── BBC - Earth - Conservation success for otters on the brink.html ├── BBC - Earth - Conservation success for otters on the brink_files │ ├── BrightcoveExperiences.js │ ├── NotificationsMain.js │ ├── a4621041136.html │ ├── adverts.js │ ├── api.min.js │ ├── bbc-blocks-dark.png │ ├── bbcdotcom-async.css │ ├── bbcdotcom.js │ ├── bump-3.js │ ├── cc.js │ ├── chartbeat.js │ ├── comscore.js │ ├── config │ ├── config.json │ ├── core.css │ ├── cultural-calender.css │ ├── edr.min.js │ ├── features_prod.js │ ├── fig.js │ ├── flag │ ├── font.min.js │ ├── gpt.js │ ├── id-cta-v5.css │ ├── id-cta.css │ ├── idcta-1.min.js │ ├── istats-1.js │ ├── jquery-1.7.2.js │ ├── jquery-1.9.1.js │ ├── jquery.js │ ├── lib.js │ ├── logger.js │ ├── main.css │ ├── main.js │ ├── main.min.css │ ├── maps.css │ ├── orb.min.css │ ├── orb.min.js │ ├── p0301msf.jpg │ ├── p03gmwjl.jpg │ ├── p03xp2xp.jpg │ ├── p03zwq47.jpg │ ├── p04384rk.jpg │ ├── p04cpts8.jpg │ ├── p04dxxv0.jpg │ ├── p04vjy8l.jpg │ ├── p04xp7cj.jpg │ ├── p04z0qvq.jpg │ ├── p04z86mz.jpg │ ├── p056c7hw.jpg │ ├── p056c814.jpg │ ├── p056c89n.jpg │ ├── p056c8n2.jpg │ ├── p056c8yq.jpg │ ├── p056c95v.jpg │ ├── p056c9bh.jpg │ ├── p056cj5w.jpg │ ├── p05cq9pg.jpg │ ├── p05lq24t.jpg │ ├── p05zsncb.jpg │ ├── p064xt7t.jpg │ ├── p065zrc4.jpg │ ├── p065zwzj.jpg │ ├── p065zzdd.jpg │ ├── p0663yhl.jpg │ ├── p0665rlj.jpg │ ├── responsive-core.css │ ├── rt=ifr.html │ ├── slideshow-gallery.css │ ├── sol-core.css │ ├── statusbar.js │ ├── super-section.css │ ├── tpc.css │ ├── translations │ ├── var=ccauds │ ├── video-playlist-player.css │ └── weather.css ├── Brian-Eno.jpg ├── The Galaxy Next Door May Be Blowing Giant Double Bubbles.html ├── The Galaxy Next Door May Be Blowing Giant Double Bubbles_files │ ├── 0221(1).js │ ├── 0221.js │ ├── 2102697_300.jpg │ ├── 2246268_300.jpg │ ├── 2651092_300.jpg │ ├── 2755652_300.jpg │ ├── 2801630_300.jpg │ ├── 2911482_300.jpg │ ├── CookieBanner.standalone.js │ ├── HCo_fonts.css │ ├── all-ndg-3138116172.js │ ├── analytics.min.js │ ├── asynctracker.js │ ├── autotrack.min.js │ ├── chartbeat.js │ ├── dest5.html │ ├── extra.min.js │ ├── gpt.js │ ├── init-v2.ngsversion.5afa3b6c.js │ ├── main.ngsversion.5afa3b6c.css │ ├── ng-black-logo.ngsversion.5afa3b6c.png │ ├── ngs-global.ngsversion.5afa3b6c.js │ ├── ngs-vendor.ngsversion.5afa3b6c.js │ ├── polyfill.min.js │ ├── s33834570218441 │ ├── utag.js │ ├── utag.sync.js │ └── zerg.js ├── atlantic-logo.svg ├── bbc-logo.svg ├── bolt-natgeo.svg ├── bolt.svg ├── bundle.js ├── cnn-logo.jpg ├── cnn-logo.svg ├── cnn-news-img.jpg ├── cnn.css ├── cnn.html ├── context-cards-ui-test.js ├── context-cards.js ├── delta5.jpg ├── index.html ├── natgeo-logo.svg ├── pitchfork-logo-bw.png ├── pitchfork-logo.png ├── pitchfork.css ├── pitchfork.html ├── test.html ├── wikifact-branding.svg ├── wikifact-header-branding.svg └── wikifact.css ├── elm.json ├── package-lock.json ├── package.json ├── popup.png └── src ├── Card.elm ├── ContextCards.elm ├── Data.elm ├── UiTests.elm └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | elm-stuff/ 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY=all dist dev watch clean deploy 2 | 3 | default: dist 4 | 5 | ELM_MAKE_FLAGS= 6 | 7 | OUTFOLDER=dist/ 8 | JSFILE=$(OUTFOLDER)/context-cards.js 9 | JSUITESTFILE=$(OUTFOLDER)/context-cards-ui-test.js 10 | 11 | SRC=src/ 12 | ELMMAIN=$(SRC)/ContextCards.elm 13 | JSMAIN=$(SRC)/index.js 14 | ELMUITESTMAIN=$(SRC)/UiTests.elm 15 | 16 | ELM_SOURCES:=$(wildcard src/*.elm) 17 | 18 | $(JSFILE): $(ELMMAIN) $(JSMAIN) $(ELM_SOURCES) 19 | @echo "Building elm" 20 | @elm make $< --output $@ $(ELM_MAKE_FLAGS) 21 | @cat $(JSMAIN) >> $@ 22 | 23 | $(JSUITESTFILE): $(ELMUITESTMAIN) $(ELM_SOURCES) 24 | @echo "Building elm UI tests" 25 | @elm make $< --output $@ 26 | 27 | clean: 28 | rm $(JSFILE) $(JSUITESTFILE) 29 | 30 | # dev: ELM_MAKE_FLAGS += --debug 31 | dev: all 32 | 33 | dist: ELM_MAKE_FLAGS += --optimize 34 | dist: all 35 | @echo "Minifying JS file" 36 | @uglifyjs "$(JSFILE)" --compress 'pure_funcs="F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9",pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle --output="$(JSFILE)" 37 | 38 | all: $(JSFILE) $(JSUITESTFILE) 39 | 40 | watch: 41 | @find src -name '*.elm' -or -name '*.js' | entr $(MAKE) dev 42 | 43 | deploy: clean dist 44 | ./node_modules/.bin/gh-pages -d $(OUTFOLDER) 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wikipedia Context Cards 2 | 3 | Get Wikipedia page previews on any page! 4 | 5 |

6 | English Wikipedia Vorticism preview 7 |

8 | 9 | See some live examples at , or check our 10 | [UI tests](https://joakin.github.io/context-cards/test.html) for examples of 11 | many previews. 12 | 13 | ## How to use 14 | 15 | Include the library in your page: 16 | 17 | ``` 18 | https://unpkg.com/context-cards/dist/context-cards.js 19 | ``` 20 | 21 | Mark some links with `data-wiki-title` and `data-wiki-lang`. 22 | 23 | ```html 24 | 32 | ``` 33 | 34 | Those links should show the previews now on hover and focus! 35 | 36 | ### Right to left languages: previews and content 37 | 38 | If you add a link with a language that is right to left, the preview content 39 | will automatically show up appropriately 40 | ([example](https://chimeces.com/context-cards/test.html#LTR%20CONTENT,%20RTL%20POPUP%20-%20%D7%A4%D7%A8%D7%94%D7%99%D7%A1%D7%98%D7%95%D7%A8%D7%99%D7%94)). 41 | 42 | If the content of your page is right to left, and you have the `dir` attribute 43 | set on any of the parent elements of the link, or the link itself, then things 44 | should work fine by default, by positioning the card anchored to the bottom 45 | right of your link. 46 | 47 | If you are doing something strange with your content and don't have those 48 | attributes, you can manually add `dir="rtl"` to the link and the card will work 49 | as intended. 50 | 51 | ### Dynamic content 52 | 53 | Sometimes you have content that loads later on the page, and is not there on 54 | `DOMContentLoaded`. If that is the case, you will need to tell context cards to 55 | refresh the links so that it can bind its interactions as needed. 56 | 57 | When including the script in the page, context cards binds itself to 58 | `window.ContextCards`. If you need to tell it to refresh the links and search 59 | for new ones, you can call `ContextCards.bindLinks()`. That should appropriately 60 | bind the event handlers for the new links marked with the `data` attributes 61 | mentioned above. 62 | 63 | ### Self hosting the script file 64 | 65 | If you don't want to use a npm CDN like unpkg, you can always get the script 66 | file for the popups from the npm package after installing it, in the 67 | `dist/context-cards.js` file. 68 | 69 | Another alternative is getting it from this git repo. The file you can include 70 | is always in `dist/context-cards.js`, and you can get it from the master branch, 71 | or from the git tags which match the npm versions. 72 | 73 | ## Credits 74 | 75 | Based on the original work on 76 | [Extension:Popups](https://mediawiki.org/wiki/Extension:Popups). 77 | -------------------------------------------------------------------------------- /demo-src/constants.js: -------------------------------------------------------------------------------- 1 | import bracketedDevicePixelRatio from "./ui/bracketedDevicePixelRatio"; 2 | 3 | /** 4 | * @module constants 5 | */ 6 | const pixelRatio = bracketedDevicePixelRatio(); 7 | 8 | export default { 9 | THUMBNAIL_SIZE: 320 * pixelRatio, 10 | EXTRACT_LENGTH: 525, 11 | // See the following for context around this value. 12 | // 13 | // * https://phabricator.wikimedia.org/T161284 14 | // * https://phabricator.wikimedia.org/T70861#3129780 15 | FETCH_START_DELAY: 150, // ms. 16 | 17 | // The delay after which a FETCH_COMPLETE action should be dispatched. 18 | // 19 | // If the API endpoint responds faster than 500 ms (or, say, the API 20 | // response is served from the UA's cache), then we introduce a delay of 21 | // 500 - t to make the preview delay consistent to the user. 22 | FETCH_COMPLETE_TARGET_DELAY: 500, // ms. 23 | 24 | ABANDON_END_DELAY: 300 // ms. 25 | }; 26 | -------------------------------------------------------------------------------- /demo-src/deepExtend.js: -------------------------------------------------------------------------------- 1 | export default function deepExtend(out) { 2 | out = out || {}; 3 | 4 | for (var i = 1; i < arguments.length; i++) { 5 | var obj = arguments[i]; 6 | 7 | if (!obj) continue; 8 | 9 | for (var key in obj) { 10 | if (obj.hasOwnProperty(key)) { 11 | if (typeof obj[key] === "object") 12 | out[key] = deepExtend(out[key], obj[key]); 13 | else out[key] = obj[key]; 14 | } 15 | } 16 | } 17 | 18 | return out; 19 | } 20 | -------------------------------------------------------------------------------- /demo-src/gateway/index.js: -------------------------------------------------------------------------------- 1 | import constants from "../constants"; 2 | import createRESTBaseGateway from "./rest"; 3 | import * as formatters from "./restFormatters"; 4 | 5 | // Note that this interface definition is in the global scope. 6 | /** 7 | * The interface implemented by all preview gateways. 8 | * 9 | * @interface Gateway 10 | */ 11 | 12 | /** 13 | * Fetches a preview for a page. 14 | * 15 | * If the underlying request is successful and contains data about the page, 16 | * then the resulting promise will resolve. If not, then it will reject. 17 | * 18 | * @function 19 | * @name Gateway#getPageSummary 20 | * @param {String} title The title of the page 21 | * @return {jQuery.Promise} 22 | */ 23 | 24 | /** 25 | * Creates a gateway with sensible values for the dependencies. 26 | * 27 | * @param {String} lang 28 | * @return {Gateway} 29 | */ 30 | export default function createGateway(lang) { 31 | const restConfig = Object.assign({}, constants, { 32 | endpoint: `https://${lang}.wikipedia.org/api/rest_v1/page/summary/`, 33 | url: title => `https://${lang}.wikipedia.org/wiki/${title}` 34 | }); 35 | return createRESTBaseGateway(restConfig, formatters.parseHTMLResponse); 36 | } 37 | -------------------------------------------------------------------------------- /demo-src/gateway/rest.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module gateway/rest 3 | */ 4 | 5 | import { createModel, createNullModel } from "../preview/model"; 6 | import deepExtend from "../deepExtend"; 7 | 8 | const RESTBASE_PROFILE = "https://www.mediawiki.org/wiki/Specs/Summary/1.2.0"; 9 | 10 | /** 11 | * @interface RESTBaseGateway 12 | * @extends Gateway 13 | * 14 | * @global 15 | */ 16 | 17 | /** 18 | * Creates an instance of the RESTBase gateway. 19 | * 20 | * This gateway differs from the {@link MediaWikiGateway MediaWiki gateway} in 21 | * that it fetches page data from [the RESTBase page summary endpoint][0]. 22 | * 23 | * [0]: https://en.wikipedia.org/api/rest_v1/#!/Page_content/get_page_summary_title 24 | * 25 | * @param {Object} config Configuration that affects the major behavior of the 26 | * gateway. 27 | * @param {Function} extractParser A function that takes response and returns 28 | * parsed extract 29 | * @return {RESTBaseGateway} 30 | */ 31 | export default function createRESTBaseGateway(config, extractParser) { 32 | /** 33 | * Fetches page data from [the RESTBase page summary endpoint][0]. 34 | * 35 | * [0]: https://en.wikipedia.org/api/rest_v1/#!/Page_content/get_page_summary_title 36 | * 37 | * @function 38 | * @name MediaWikiGateway#fetch 39 | * @param {String} title 40 | * @return {jQuery.Promise} 41 | */ 42 | function fetch(title) { 43 | return new Promise((resolve, reject) => { 44 | const endpoint = config.endpoint; 45 | const url = endpoint + encodeURIComponent(title); 46 | 47 | var request = new XMLHttpRequest(); 48 | request.open("GET", url, true); 49 | request.setRequestHeader( 50 | "Accept", 51 | `application/json; charset=utf-8; profile="${RESTBASE_PROFILE}"` 52 | ); 53 | 54 | request.onload = function() { 55 | if (request.status >= 200 && request.status < 400) { 56 | // Success! 57 | var resp = request.responseText; 58 | try { 59 | resolve(JSON.parse(resp)); 60 | } catch (e) { 61 | reject(new Error("Failed to parse JSON")); 62 | } 63 | } else { 64 | const error = new Error("Failed to parse JSON"); 65 | error.request = request; 66 | reject(error); 67 | } 68 | }; 69 | 70 | request.onerror = function() { 71 | // There was a connection error of some sort 72 | const error = new Error("Request Error"); 73 | error.request = request; 74 | reject(error); 75 | }; 76 | 77 | request.send(); 78 | }); 79 | } 80 | 81 | function getPageSummary(title) { 82 | return new Promise((resolve, reject) => { 83 | fetch(title).then( 84 | page => { 85 | // Endpoint response may be empty or simply missing a title. 86 | if (!page || !page.title) { 87 | page = deepExtend(page || {}, { title }); 88 | } 89 | // And extract may be omitted if empty string 90 | if (page.extract === undefined) { 91 | page.extract = ""; 92 | } 93 | resolve( 94 | convertPageToModel( 95 | config, 96 | page, 97 | config.THUMBNAIL_SIZE, 98 | extractParser 99 | ) 100 | ); 101 | }, 102 | error => { 103 | // Adapt the response to the ideal API. 104 | // TODO: should we just let the client handle this too? 105 | if (error.request.status === 404) { 106 | resolve(createNullModel(title, config.url(title))); 107 | } else { 108 | // The client will choose how to handle these errors which may 109 | // include those due to HTTP 5xx status. The rejection typing 110 | // matches Fetch failures. 111 | reject(error); 112 | } 113 | } 114 | ); 115 | }); 116 | } 117 | 118 | return { 119 | getPageSummary 120 | }; 121 | } 122 | 123 | /** 124 | * Resizes the thumbnail to the requested width, preserving its aspect ratio. 125 | * 126 | * The requested width is limited to that of the original image unless the image 127 | * is an SVG, which can be scaled infinitely. 128 | * 129 | * This function is only intended to mangle the pretty thumbnail URLs used on 130 | * Wikimedia Commons. Once [an official thumb API](https://phabricator.wikimedia.org/T66214) 131 | * is fully specified and implemented, this function can be made more general. 132 | * 133 | * @param {Object} thumbnail The thumbnail image 134 | * @param {Object} original The original image 135 | * @param {Number} thumbSize The requested size 136 | * @return {Object} 137 | */ 138 | function generateThumbnailData(thumbnail, original, thumbSize) { 139 | const parts = thumbnail.source.split("/"), 140 | lastPart = parts[parts.length - 1]; 141 | 142 | // The last part, the thumbnail's full filename, is in the following form: 143 | // ${width}px-${filename}.${extension}. Splitting the thumbnail's filename 144 | // makes this function resilient to the thumbnail not having the same 145 | // extension as the original image, which is definitely the case for SVG's 146 | // where the thumbnail's extension is .svg.png. 147 | const filenamePxIndex = lastPart.indexOf("px-"); 148 | if (filenamePxIndex === -1) { 149 | // The thumbnail size is not customizable. Presumably, RESTBase requested a 150 | // width greater than the original and so MediaWiki returned the original's 151 | // URL instead of a thumbnail compatible URL. An original URL does not have 152 | // a "thumb" path, e.g.: 153 | // 154 | // https://upload.wikimedia.org/wikipedia/commons/a/aa/Red_Giant_Earth_warm.jpg 155 | // 156 | // Instead of: 157 | // 158 | // https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Red_Giant_Earth_warm.jpg/512px-Red_Giant_Earth_warm.jpg 159 | // 160 | // Use the original. 161 | return original; 162 | } 163 | const filename = lastPart.substr(filenamePxIndex + 3); 164 | 165 | // Scale the thumbnail's largest dimension. 166 | let width, height; 167 | if (thumbnail.width > thumbnail.height) { 168 | width = thumbSize; 169 | height = Math.floor(thumbSize / thumbnail.width * thumbnail.height); 170 | } else { 171 | width = Math.floor(thumbSize / thumbnail.height * thumbnail.width); 172 | height = thumbSize; 173 | } 174 | 175 | // If the image isn't an SVG, then it shouldn't be scaled past its original 176 | // dimensions. 177 | if (width >= original.width && filename.indexOf(".svg") === -1) { 178 | return original; 179 | } 180 | 181 | parts[parts.length - 1] = `${width}px-${filename}`; 182 | 183 | return { 184 | source: parts.join("/"), 185 | width, 186 | height 187 | }; 188 | } 189 | 190 | /** 191 | * Converts the API response to a preview model. 192 | * 193 | * @function 194 | * @name RESTBaseGateway#convertPageToModel 195 | * @param {Object} config 196 | * @param {Object} page 197 | * @param {Number} thumbSize 198 | * @param {Function} extractParser 199 | * @return {PreviewModel} 200 | */ 201 | function convertPageToModel(config, page, thumbSize, extractParser) { 202 | return createModel( 203 | page.title, 204 | config.url(page.title), 205 | page.lang, 206 | page.dir, 207 | extractParser(page), 208 | page.type, 209 | page.thumbnail 210 | ? generateThumbnailData(page.thumbnail, page.originalimage, thumbSize) 211 | : undefined, 212 | page.pageid 213 | ); 214 | } 215 | -------------------------------------------------------------------------------- /demo-src/gateway/restFormatters.js: -------------------------------------------------------------------------------- 1 | import parseHTML from "../ui/parseHTML"; 2 | 3 | /** 4 | * Prepare extract 5 | * @param {Object} page Rest response 6 | * @return {Array} An array of DOM Elements 7 | */ 8 | export function parseHTMLResponse(page) { 9 | const extract = page.extract_html; 10 | 11 | return extract.length === 0 ? [] : parseHTML(extract); 12 | } 13 | -------------------------------------------------------------------------------- /demo-src/index.js: -------------------------------------------------------------------------------- 1 | import "./ui/index.less"; 2 | import createGateway from "./gateway"; 3 | import { init, render } from "./ui/renderer"; 4 | import { 5 | FETCH_COMPLETE_TARGET_DELAY, 6 | FETCH_START_DELAY, 7 | ABANDON_END_DELAY 8 | } from "./constants"; 9 | import wait from "./wait"; 10 | 11 | const linkSelector = "a[data-wiki-title]"; 12 | 13 | if (typeof window.Promise === "function") { 14 | document.addEventListener("DOMContentLoaded", () => { 15 | let preview = null; 16 | 17 | // Init the renderer 18 | init(); 19 | 20 | function onAbandon(oldUI) { 21 | if ((preview && preview.ui()) === oldUI) preview = null; 22 | } 23 | 24 | document.querySelectorAll(linkSelector).forEach(link => { 25 | link.addEventListener("mouseenter", event => { 26 | const link = event.target; 27 | const title = link.dataset.wikiTitle; 28 | const lang = link.dataset.wikiLang; 29 | 30 | if (preview && link === preview.link()) { 31 | preview.mouseenter(event); 32 | } else { 33 | preview && preview.die(); 34 | preview = createPreviewState(link, title, lang, onAbandon); 35 | preview.load(event); 36 | } 37 | }); 38 | }); 39 | 40 | document.querySelectorAll(linkSelector).forEach(link => { 41 | link.addEventListener("mouseout", event => { 42 | preview.mouseout(event); 43 | }); 44 | }); 45 | }); 46 | } 47 | 48 | function createPreviewState(link, title, lang, onAbandon) { 49 | const gateway = createGateway(lang); 50 | let preview = null; 51 | let onLimbo = false; 52 | let limboTimeout = null; 53 | let ded = false; 54 | 55 | function die() { 56 | preview.hide(); 57 | ded = true; 58 | onAbandon(preview); 59 | } 60 | 61 | function previewBehavior() { 62 | return { 63 | settingsUrl: () => {}, 64 | showSettings: () => {}, 65 | previewDwell: () => { 66 | onLimbo = false; 67 | clearTimeout(limboTimeout); 68 | }, 69 | previewAbandon: () => { 70 | onLimbo = true; 71 | setTimeout(() => { 72 | if (onLimbo && preview) { 73 | die(); 74 | } 75 | }, ABANDON_END_DELAY); 76 | }, 77 | previewShow: () => {}, 78 | click: () => {} 79 | }; 80 | } 81 | 82 | return { 83 | mouseenter(e) { 84 | previewBehavior().previewDwell(); 85 | }, 86 | mouseout(e) { 87 | previewBehavior().previewAbandon(); 88 | }, 89 | die() { 90 | die(); 91 | }, 92 | link() { 93 | return link; 94 | }, 95 | load(event) { 96 | const request = wait(FETCH_START_DELAY).then( 97 | _ => 98 | !ded 99 | ? gateway.getPageSummary(title).then(response => { 100 | preview = render(response); 101 | }) 102 | : null 103 | ); 104 | 105 | return Promise.all([ 106 | request, 107 | wait(FETCH_COMPLETE_TARGET_DELAY - FETCH_START_DELAY) 108 | ]).then(() => { 109 | if (!ded) preview.show(event, previewBehavior(), "token"); 110 | }); 111 | }, 112 | ui() { 113 | return preview; 114 | } 115 | }; 116 | } 117 | -------------------------------------------------------------------------------- /demo-src/preview/model.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module preview/model 3 | */ 4 | 5 | /** 6 | * Page Preview types as defined in Schema:Popups 7 | * https://meta.wikimedia.org/wiki/Schema:Popups 8 | * 9 | * @constant {Object} 10 | */ 11 | const previewTypes = { 12 | /** empty preview */ 13 | TYPE_GENERIC: "generic", 14 | /** standard preview */ 15 | TYPE_PAGE: "page", 16 | /** disambiguation preview */ 17 | TYPE_DISAMBIGUATION: "disambiguation" 18 | }; 19 | 20 | export { previewTypes }; 21 | 22 | /** 23 | * Preview Model 24 | * 25 | * @typedef {Object} PreviewModel 26 | * @property {String} title 27 | * @property {String} url The canonical URL of the page being previewed 28 | * @property {String} languageCode 29 | * @property {String} languageDirection Either "ltr" or "rtl" 30 | * @property {?Array} extract `undefined` if the extract isn't 31 | * viable, e.g. if it's empty after having ellipsis and parentheticals 32 | * removed; this can be used to present default or error states 33 | * @property {String} type One of TYPE_GENERIC, TYPE_PAGE, TYPE_DISAMBIGUATION 34 | * @property {?Object} thumbnail 35 | * 36 | * @global 37 | */ 38 | 39 | /** 40 | * Creates a preview model. 41 | * 42 | * @param {String} title 43 | * @param {String} url The canonical URL of the page being previewed 44 | * @param {String} languageCode 45 | * @param {String} languageDirection Either "ltr" or "rtl" 46 | * @param {?Array} extract 47 | * @param {String} type 48 | * @param {?Object} thumbnail 49 | * @param {?Number} pageId 50 | * @return {PreviewModel} 51 | */ 52 | export function createModel( 53 | title, 54 | url, 55 | languageCode, 56 | languageDirection, 57 | extract, 58 | type, 59 | thumbnail, 60 | pageId 61 | ) { 62 | const processedExtract = processExtract(extract), 63 | previewType = getPreviewType(type, processedExtract); 64 | 65 | return { 66 | title, 67 | url, 68 | languageCode, 69 | languageDirection, 70 | extract: processedExtract, 71 | type: previewType, 72 | thumbnail, 73 | pageId 74 | }; 75 | } 76 | 77 | /** 78 | * Creates an empty preview model. 79 | * 80 | * @param {!String} title 81 | * @param {!String} url 82 | * @return {!PreviewModel} 83 | */ 84 | export function createNullModel(title, url) { 85 | return createModel(title, url, "", "", [], ""); 86 | } 87 | 88 | /** 89 | * Processes the extract returned by the TextExtracts MediaWiki API query 90 | * module. 91 | * 92 | * If the extract is `undefined`, `null`, or empty, then `undefined` is 93 | * returned. 94 | * 95 | * @param {Array|undefined|null} extract 96 | * @return {Array|undefined} Array when extract is an not empty array, undefined 97 | * otherwise 98 | */ 99 | function processExtract(extract) { 100 | if (extract === undefined || extract === null || extract.length === 0) { 101 | return undefined; 102 | } 103 | return extract; 104 | } 105 | 106 | /** 107 | * Determines the preview type based on whether or not: 108 | * a. Is the preview empty. 109 | * b. The preview type matches one of previewTypes. 110 | * c. Assume standard page preview if both above are false 111 | * 112 | * @param {String} type 113 | * @param {string} [processedExtract] 114 | * @return {String} one of TYPE_GENERIC, TYPE_PAGE, TYPE_DISAMBIGUATION. 115 | */ 116 | 117 | function getPreviewType(type, processedExtract) { 118 | if (processedExtract === undefined) { 119 | return previewTypes.TYPE_GENERIC; 120 | } 121 | 122 | switch (type) { 123 | case previewTypes.TYPE_GENERIC: 124 | case previewTypes.TYPE_DISAMBIGUATION: 125 | case previewTypes.TYPE_PAGE: 126 | return type; 127 | default: 128 | /** 129 | * Assume type="page" if extract exists & not one of previewTypes. 130 | * Note: 131 | * - Restbase response includes "type" prop but other gateways don't. 132 | * - event-logging Schema:Popups requires type="page" but restbase 133 | * provides type="standard". Model must conform to event-logging schema. 134 | */ 135 | return previewTypes.TYPE_PAGE; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /demo-src/ui/bracketedDevicePixelRatio.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Responsive images based on `srcset` and `window.devicePixelRatio` emulation where needed. 3 | * 4 | * Call `.hidpi()` on a document or part of a document to proces image srcsets within that section. 5 | * 6 | * `$.devicePixelRatio()` can be used as a substitute for `window.devicePixelRatio`. 7 | * It provides a familiar interface to retrieve the pixel ratio for browsers that don't 8 | * implement `window.devicePixelRatio` but do have a different way of getting it. 9 | * 10 | * @class jQuery.plugin.hidpi 11 | */ 12 | 13 | /** 14 | * Get reported or approximate device pixel ratio. 15 | * 16 | * - 1.0 means 1 CSS pixel is 1 hardware pixel 17 | * - 2.0 means 1 CSS pixel is 2 hardware pixels 18 | * - etc. 19 | * 20 | * Uses `window.devicePixelRatio` if available, or CSS media queries on IE. 21 | * 22 | * @static 23 | * @inheritable 24 | * @return {number} Device pixel ratio 25 | */ 26 | function devicePixelRatio() { 27 | if (window.devicePixelRatio !== undefined) { 28 | // Most web browsers: 29 | // * WebKit/Blink (Safari, Chrome, Android browser, etc) 30 | // * Opera 31 | // * Firefox 18+ 32 | // * Microsoft Edge (Windows 10) 33 | return window.devicePixelRatio; 34 | } else if (window.msMatchMedia !== undefined) { 35 | // Windows 8 desktops / tablets, probably Windows Phone 8 36 | // 37 | // IE 10/11 doesn't report pixel ratio directly, but we can get the 38 | // screen DPI and divide by 96. We'll bracket to [1, 1.5, 2.0] for 39 | // simplicity, but you may get different values depending on zoom 40 | // factor, size of screen and orientation in Metro IE. 41 | if (window.msMatchMedia("(min-resolution: 192dpi)").matches) { 42 | return 2; 43 | } else if (window.msMatchMedia("(min-resolution: 144dpi)").matches) { 44 | return 1.5; 45 | } else { 46 | return 1; 47 | } 48 | } else { 49 | // Legacy browsers... 50 | // Assume 1 if unknown. 51 | return 1; 52 | } 53 | } 54 | 55 | /** 56 | * Bracket a given device pixel ratio to one of [1, 1.5, 2]. 57 | * 58 | * This is useful for grabbing images on the fly with sizes based on the display 59 | * density, without causing slowdown and extra thumbnail renderings on devices 60 | * that are slightly different from the most common sizes. 61 | * 62 | * The bracketed ratios match the default 'srcset' output on MediaWiki thumbnails, 63 | * so will be consistent with default renderings. 64 | * 65 | * @static 66 | * @inheritable 67 | * @param {number} baseRatio Base ratio 68 | * @return {number} Device pixel ratio 69 | */ 70 | function bracketDevicePixelRatio(baseRatio) { 71 | if (baseRatio > 1.5) { 72 | return 2; 73 | } else if (baseRatio > 1) { 74 | return 1.5; 75 | } else { 76 | return 1; 77 | } 78 | } 79 | 80 | /** 81 | * Get reported or approximate device pixel ratio, bracketed to [1, 1.5, 2]. 82 | * 83 | * This is useful for grabbing images on the fly with sizes based on the display 84 | * density, without causing slowdown and extra thumbnail renderings on devices 85 | * that are slightly different from the most common sizes. 86 | * 87 | * The bracketed ratios match the default 'srcset' output on MediaWiki thumbnails, 88 | * so will be consistent with default renderings. 89 | * 90 | * - 1.0 means 1 CSS pixel is 1 hardware pixel 91 | * - 1.5 means 1 CSS pixel is 1.5 hardware pixels 92 | * - 2.0 means 1 CSS pixel is 2 hardware pixels 93 | * 94 | * @static 95 | * @inheritable 96 | * @return {number} Device pixel ratio 97 | */ 98 | export default function bracketedDevicePixelRatio() { 99 | return bracketDevicePixelRatio(devicePixelRatio()); 100 | } 101 | -------------------------------------------------------------------------------- /demo-src/ui/index.less: -------------------------------------------------------------------------------- 1 | @import "./mixins.less"; 2 | @import "./templates/popup.less"; 3 | @import "./templates/pagePreview/pagePreview.less"; 4 | @import "./templates/preview/preview.less"; 5 | 6 | #mwe-popups-svg { 7 | position: absolute; 8 | top: -1000px; 9 | } 10 | -------------------------------------------------------------------------------- /demo-src/ui/mediawiki.less/mediawiki.mixins.animation.less: -------------------------------------------------------------------------------- 1 | .animation( ... ) { 2 | -webkit-animation: @arguments; // Chrome 4-42, Safari 4-8, Opera 15-29, Android 2.1-4.4.4 3 | -moz-animation: @arguments; // Firefox 5-15 4 | animation: @arguments; // Chrome 43+, Firefox 16+, IE 10+, Edge 12+, Safari 9+, Opera 12.10 & 30+, iOS 9+, Android 47+ 5 | } 6 | 7 | .transform-rotate( @deg ) { 8 | -webkit-transform: rotate( @deg ); 9 | -moz-transform: rotate( @deg ); 10 | transform: rotate( @deg ); 11 | } 12 | -------------------------------------------------------------------------------- /demo-src/ui/mediawiki.less/mediawiki.mixins.less: -------------------------------------------------------------------------------- 1 | // Common Less mixin library for MediaWiki 2 | // 3 | // By default the folder containing this file is included in $wgResourceLoaderLESSImportPaths, 4 | // which makes this file importable by all less files via `@import 'mediawiki.mixins';`. 5 | // 6 | // The mixins included below are considered a public interface for MediaWiki extensions. 7 | // The signatures of parametrized mixins should be kept as stable as possible. 8 | // 9 | // See for more information about how to write mixins. 10 | 11 | .background-image( @url ) { 12 | background-image: e( '/* @embed */' ) url( @url ); 13 | } 14 | 15 | // Deprecated in MW 1.27 16 | .background-size( @width, @height ) { 17 | // Vendor prefix is added to support Android 2 18 | -webkit-background-size: @width @height; 19 | background-size: @width @height; 20 | } 21 | 22 | .vertical-gradient( @startColor: gray, @endColor: white, @startPos: 0, @endPos: 100% ) { 23 | background-color: @endColor; 24 | background-image: -webkit-linear-gradient( top, @startColor @startPos, @endColor @endPos ); // Safari 5.1+, Chrome 10+ 25 | background-image: -moz-linear-gradient( top, @startColor @startPos, @endColor @endPos ); // Firefox 3.6+ 26 | background-image: linear-gradient( @startColor @startPos, @endColor @endPos ); // Standard 27 | } 28 | 29 | // SVG support using a transparent gradient to guarantee cross-browser 30 | // compatibility (browsers able to understand gradient syntax support also SVG). 31 | // http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique 32 | // 33 | // We do not embed the fallback image on the assumption that the gain for old browsers 34 | // is not worth the harm done to modern ones. 35 | .background-image-svg( @svg, @fallback ) { 36 | background-image: url( @fallback ); 37 | background-image: linear-gradient( transparent, transparent ), e( '/* @embed */' ) url( @svg ); 38 | } 39 | 40 | // Shorthand for background-image-svg. Use if your PNG and SVG have the same name 41 | // and only if you cannot use ResourceLoaderImage module for some particular reason. 42 | .background-image-svg-quick( @url ) { 43 | .background-image-svg( ~'@{url}.svg', ~'@{url}.png' ); 44 | } 45 | 46 | .list-style-image( @url ) { 47 | list-style-image: e( '/* @embed */' ) url( @url ); 48 | } 49 | 50 | .list-style-image-svg( @svg, @fallback ) { 51 | list-style-image: e( '/* @embed */' ) url( @svg ); 52 | /* Fallback to PNG bullet for IE 8 and below using CSS hack */ 53 | list-style-image: e( '/* @embed */' ) url( @fallback ) e( '\9' ); 54 | } 55 | 56 | .hyphens( @value: auto ) { 57 | & when ( @value = auto ){ 58 | // Legacy `word-wrap`; IE 6-11, Edge 12+, Firefox 3.5+, Chrome 4+, Safari 3.1+, 59 | // Opera 11.5+, iOS 3.2+, Android 2.1+ 60 | // `overflow-wrap` is W3 standard, but it doesn't seem as if browser vendors 61 | // will abandon `word-wrap` (it has wider support), therefore no duplication. 62 | word-wrap: break-word; 63 | } 64 | & when ( @value = none ) { 65 | word-wrap: normal; 66 | } 67 | 68 | // CSS3 hyphenation 69 | -webkit-hyphens: @value; // Safari 5.1+, iOS 4.3+ 70 | -moz-hyphens: @value; // Firefox 6-42 71 | -ms-hyphens: @value; // IE 10-11, Edge 12+ 72 | hyphens: @value; // Firefox 43+, Chrome 55+, Android 62+, UC Browser 11.8+, Samsung 6.2+ 73 | } 74 | 75 | .transform( @value ) { 76 | -webkit-transform: @value; // Safari 3.1-8.0, iOS 3.2-8.4, Android 2.1-4.4.4 77 | -moz-transform: @value; // Firefox 3.5-15 78 | transform: @value; // Chrome 36+, Firefox 16+, IE 10+, Safari 9+, Opera 23+, iOS 9.2+, Android 5+ 79 | } 80 | 81 | .transition( @value ) { 82 | -webkit-transition: @value; // Safari 3.1-6.0, iOS 3.2-6.1, Android 2.1-4.3 83 | -moz-transition: @value; // Firefox 4-15 84 | transition: @value; // Chrome 26+, Firefox 16+, IE 10+, Safari 6.1+, Opera 12.1+, iOS 7+, Android 4.4+ 85 | } 86 | 87 | // Provide a hardware accelerated transform transition 88 | // We can't use `.transition()` because WebKit requires `-webkit-` prefix before `transform` 89 | // Example usage: `.transition-transform( 1s, opacity 2s );` 90 | // First parameter is additional options for `transform` transition commencing with 91 | // duration property @see https://www.w3.org/TR/css3-transitions/#transition-duration-property 92 | // and remaining parameters are additional transitions." 93 | .transition-transform( ... ) { 94 | -webkit-backface-visibility: hidden; // Older Webkit browsers: Promote element to a composite layer & involve the GPU 95 | 96 | -webkit-transition: -webkit-transform @arguments; // Safari 3.1-8, iOS 3.2-8.4, Android 2.1-4.4.4 97 | -moz-transition: -moz-transform @arguments; // Firefox 4-15 for `-moz-transition` 98 | transition: transform @arguments; // Chrome 36+, Firefox 16+, IE 10+, Safari 9+, Opera 12.1+, iOS 9.2+, Android 36+ 99 | } 100 | 101 | .box-sizing( @value ) { 102 | -webkit-box-sizing: @value; // Safari 3.1-5.0, iOS 3.2-4.3, Android 2.1-3.0 103 | -moz-box-sizing: @value; // Firefox 4-28, 104 | box-sizing: @value; // Chrome 10+, Firefox 29+, IE 8+, Safari 5.1+, Opera 10+, iOS 5+, Android 4+ 105 | } 106 | 107 | .box-shadow( @value ) { 108 | -webkit-box-shadow: @value; // Safari 3.1-5.0, iOS 3.2-4.3, Android 2.1-3.0 109 | box-shadow: @value; // Chrome 10+, Firefox 4+, IE 9+, Safari 5.1+, Opera 11+, iOS 5+, Android 4+ 110 | } 111 | 112 | .column-count( @value ) { 113 | -webkit-column-count: @value; 114 | -moz-column-count: @value; 115 | column-count: @value; 116 | } 117 | 118 | .column-width( @value ) { 119 | -webkit-column-width: @value; // Chrome Any, Safari 3+, Opera 15+ 120 | -moz-column-width: @value; // Firefox 1.5+ 121 | column-width: @value; // IE 10+, Opera 11.1-12.1 122 | } 123 | 124 | .column-break-inside-avoid() { 125 | -webkit-column-break-inside: avoid; // Chrome Any, Safari 3+, Opera 15+ 126 | page-break-inside: avoid; // Firefox 1.5+ 127 | break-inside: avoid-column; // IE 10+, Opera 11.1-12.1 128 | } 129 | 130 | .flex-display( @display: flex ) { 131 | display: ~'-webkit-@{display}'; // iOS 6-, Safari 3.1-6 132 | display: ~'-moz-@{display}'; // Firefox 21- 133 | display: ~'-ms-@{display}box'; // IE 10 134 | display: @display; 135 | } 136 | 137 | .flex-wrap( @wrap: wrap ) { 138 | -webkit-flex-wrap: @wrap; // iOS 6-, Safari 3.1-6 139 | -moz-flex-wrap: @wrap; // Firefox 21- 140 | -ms-flex-wrap: @wrap; // IE 10 141 | flex-wrap: @wrap; 142 | } 143 | 144 | .flex( @grow: 1, @shrink: 1, @width: auto, @order: 1 ) { 145 | // For 2009/2012 spec alignment consistency with current default 146 | -webkit-box-pack: justify; // iOS 6-, Safari 3.1-6 147 | -moz-box-pack: justify; // Firefox 21- 148 | -ms-flex-pack: justify; // IE 10 (2012 spec) 149 | justify-content: space-between; // Current default 150 | 151 | // 2009 spec only supports 'flexible' as opposed to grow (flexPositive) 152 | // and shrink (flexNegative); default to grow value 153 | -webkit-box-flex: @grow; // iOS 6-, Safari 3.1-6 154 | -moz-box-flex: @grow; // Firefox 21- 155 | width: @width; // Fallback for flex-basis 156 | 157 | -ms-flex: @grow @shrink @width; // IE 10 158 | flex: @grow @shrink @width; 159 | 160 | -webkit-box-ordinal-group: @order; // iOS 6-, Safari 3.1-6 161 | -moz-box-ordinal-group: @order; // Firefox 21- 162 | -ms-flex-order: @order; // IE 10 163 | order: @order; 164 | } 165 | 166 | /* stylelint-disable selector-no-vendor-prefix, at-rule-no-unknown */ 167 | .mixin-placeholder( @rules ) { 168 | // WebKit, Blink, Edge 169 | &::-webkit-input-placeholder { 170 | @rules(); 171 | } 172 | // Internet Explorer 10-11 173 | &:-ms-input-placeholder { 174 | @rules(); 175 | } 176 | // Firefox 19- 177 | &::-moz-placeholder { 178 | @rules(); 179 | } 180 | // Firefox 4-18 181 | &:-moz-placeholder { 182 | @rules(); 183 | } 184 | // W3C Standard Selectors Level 4 185 | &::placeholder { 186 | @rules(); 187 | } 188 | } 189 | /* stylelint-enable selector-no-vendor-prefix, at-rule-no-unknown */ 190 | 191 | // Screen Reader Helper Mixin 192 | .mixin-screen-reader-text() { 193 | display: block; 194 | position: absolute !important; /* stylelint-disable-line declaration-no-important */ 195 | clip: rect( 1px, 1px, 1px, 1px ); 196 | width: 1px; 197 | height: 1px; 198 | margin: -1px; 199 | border: 0; 200 | padding: 0; 201 | overflow: hidden; 202 | } 203 | -------------------------------------------------------------------------------- /demo-src/ui/mediawiki.less/mediawiki.mixins.rotation.less: -------------------------------------------------------------------------------- 1 | // This is a separate file because importing the mixin causes 2 | // the keyframes blocks to be included in the output, regardless 3 | // of whether .rotation is used. 4 | @import "./mediawiki.mixins.animation.less"; 5 | 6 | .rotate-frames() { 7 | from { 8 | .transform-rotate(0deg); 9 | } 10 | to { 11 | .transform-rotate(360deg); 12 | } 13 | } 14 | 15 | @-webkit-keyframes rotate { 16 | .rotate-frames; 17 | } 18 | 19 | @-moz-keyframes rotate { 20 | .rotate-frames; 21 | } 22 | 23 | @keyframes rotate { 24 | .rotate-frames; 25 | } 26 | 27 | .rotation( @time ) { 28 | .animation(rotate, @time, infinite, linear); 29 | } 30 | -------------------------------------------------------------------------------- /demo-src/ui/mediawiki.less/mediawiki.ui/mixins.less: -------------------------------------------------------------------------------- 1 | // ---------------------------------------------------------------------------- 2 | // Form styling mixins 3 | // ---------------------------------------------------------------------------- 4 | .agora-label-styling() { 5 | font-size: 0.9em; 6 | color: @colorText; 7 | 8 | * { 9 | font-weight: normal; 10 | } 11 | } 12 | 13 | .agora-inline-label-styling() { 14 | margin-bottom: 0.5em; 15 | cursor: pointer; 16 | vertical-align: bottom; 17 | line-height: normal; 18 | font-weight: normal; 19 | 20 | & > input[ type='checkbox' ], 21 | & > input[ type='radio' ] { 22 | width: auto; 23 | height: auto; 24 | margin: 0 0.1em 0 0; 25 | padding: 0; 26 | border: 1px solid @colorGray7; 27 | cursor: pointer; 28 | } 29 | } 30 | 31 | // ---------------------------------------------------------------------------- 32 | // Button styling 33 | // ---------------------------------------------------------------------------- 34 | 35 | .button-colors( @bgColor, @highlightColor, @activeColor ) { 36 | background-color: @bgColor; 37 | color: @colorButtonText; 38 | border: 1px solid @colorFieldBorder; 39 | 40 | // Make sure that `color` isn't inheriting from user-agent styles 41 | &:visited { 42 | color: @colorButtonText; 43 | } 44 | 45 | &:hover { 46 | background-color: @highlightColor; 47 | color: @colorGray4; 48 | border-color: @colorGray10; 49 | } 50 | 51 | &:focus { 52 | background-color: @highlightColor; 53 | // Make sure that `color` isn't inheriting from user-agent styles 54 | color: @colorButtonText; 55 | border-color: @colorProgressive; 56 | box-shadow: inset 0 0 0 1px @colorProgressive, inset 0 0 0 2px #fff; 57 | } 58 | 59 | &:active, 60 | &.is-on, 61 | &.mw-ui-checked { 62 | background-color: @activeColor; 63 | color: @colorGray1; 64 | border-color: @colorGray7; 65 | box-shadow: none; 66 | } 67 | 68 | &:disabled { 69 | background-color: @colorGray12; 70 | color: #fff; 71 | border-color: @colorGray12; 72 | 73 | // Make sure disabled buttons don't have hover and active states 74 | &:hover, 75 | &:active { 76 | background-color: @colorGray12; 77 | color: #fff; 78 | box-shadow: none; 79 | border-color: @colorGray12; 80 | } 81 | } 82 | } 83 | 84 | .button-colors-primary( @bgColor, @highlightColor, @activeColor ) { 85 | background-color: @bgColor; 86 | color: #fff; 87 | // border of the same color as background so that light background and 88 | // dark background buttons are the same height and width 89 | border: 1px solid @bgColor; 90 | 91 | &:hover { 92 | background-color: @highlightColor; 93 | border-color: @highlightColor; 94 | } 95 | 96 | &:focus { 97 | box-shadow: inset 0 0 0 1px @bgColor, inset 0 0 0 2px #fff; 98 | } 99 | 100 | &:active, 101 | &.is-on, 102 | &.mw-ui-checked { 103 | background-color: @activeColor; 104 | border-color: @activeColor; 105 | box-shadow: none; 106 | } 107 | 108 | &:disabled { 109 | background-color: @colorGray12; 110 | color: #fff; 111 | border-color: @colorGray12; 112 | 113 | // Make sure disabled buttons don't have hover and active states 114 | &:hover, 115 | &:active, 116 | &.mw-ui-checked { 117 | background-color: @colorGray12; 118 | color: #fff; 119 | border-color: @colorGray12; 120 | box-shadow: none; 121 | } 122 | } 123 | } 124 | 125 | .button-colors-quiet( @textColor, @highlightColor, @activeColor ) { 126 | // Quiet buttons all start gray, and reveal 127 | // progressive/destructive color on hover and active. 128 | color: @colorButtonText; 129 | 130 | &:hover { 131 | background-color: transparent; 132 | color: @highlightColor; 133 | } 134 | 135 | &:active, 136 | &.mw-ui-checked { 137 | color: @activeColor; 138 | } 139 | 140 | &:focus { 141 | background-color: transparent; 142 | color: @textColor; 143 | } 144 | 145 | &:disabled { 146 | color: @colorDisabledText; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /demo-src/ui/mediawiki.less/mediawiki.ui/variables.less: -------------------------------------------------------------------------------- 1 | /** 2 | * Minimum available screen width at which a device can be considered a mobile device 3 | * Many older feature phones have screens smaller than this value. 4 | * Number is prone to change with new information. 5 | * @since 1.31 6 | */ 7 | @width-breakpoint-mobile: 320px; 8 | 9 | /** 10 | * Minimum available screen width at which a device can be considered a tablet 11 | * The number is currently based on the device width of a Samsung Galaxy S5 mini and is low 12 | * enough to cover iPad (768px). Number is prone to change with new information. 13 | * @since 1.31 14 | */ 15 | @width-breakpoint-tablet: 720px; 16 | /** 17 | * Minimum available screen width at which a device can be considered a desktop 18 | * Number is prone to change with new information. 19 | * @since 1.31 20 | */ 21 | @width-breakpoint-desktop: 1000px; 22 | 23 | // Colors for use in mediawiki.ui and elsewhere 24 | 25 | // Although this defines many shades, be parsimonious in your own use of grays. 26 | // Prefer semantic color names such as `@colorText` below. 27 | @colorGray1: #000; // darkest 28 | @colorGray2: #222; 29 | @colorGray4: #444; 30 | @colorGray5: #54595d; 31 | @colorGray7: #72777d; 32 | @colorGray10: #a2a9b1; 33 | @colorGray12: #c8ccd1; 34 | @colorGray14: #eaecf0; 35 | @colorGray15: #f8f9fa; // lightest 36 | @colorBaseInverted: #fff; 37 | 38 | // Semantic colors 39 | // Blue; for contextual use of a continuing action 40 | @colorProgressive: #36c; 41 | @colorProgressiveHighlight: #447ff5; 42 | @colorProgressiveActive: #2a4b8d; 43 | // Orange; for contextual use of returning to a past action 44 | @colorRegressive: #ff5d00; 45 | // Red; for contextual use of a negative action of high severity 46 | @colorDestructive: #d33; 47 | @colorDestructiveHighlight: #ff4242; 48 | @colorDestructiveActive: #b32424; 49 | // Orange; for contextual use of a potentially negative action of medium severity 50 | @colorMediumSevere: #ff5d00; 51 | // Yellow; for contextual use of a potentially negative action of low severity 52 | @colorLowSevere: #fc3; 53 | 54 | // Used in mixins to darken contextual colors by the same amount (eg. focus) 55 | @colorDarkenPercentage: 13.5%; 56 | // Used in mixins to lighten contextual colors by the same amount (eg. hover) 57 | @colorLightenPercentage: 13.5%; 58 | 59 | // Text colors 60 | @colorText: @colorGray2; 61 | @colorTextLight: @colorGray5; 62 | @colorButtonText: @colorGray2; 63 | @colorButtonTextHighlight: @colorGray4; 64 | @colorButtonTextActive: @colorGray1; 65 | @colorDisabledText: @colorGray12; 66 | @colorErrorText: #d33; 67 | @colorWarningText: #705000; 68 | 69 | // UI colors 70 | @backgroundColorInputBinaryChecked: @colorProgressive; 71 | @backgroundColorInputBinaryActive: @colorProgressiveActive; 72 | @colorFieldBorder: #a2a9b1; 73 | @colorShadow: @colorGray14; 74 | @colorPlaceholder: @colorGray10; 75 | @colorNeutral: @colorGray7; 76 | 77 | // Border colors 78 | @borderColorInputBinaryChecked: @colorProgressive; 79 | @borderColorInputBinaryActive: @colorProgressiveActive; 80 | 81 | // Checked radio input border-width, equal to OOUI at 14px base font-size 82 | @borderWidthRadioChecked: 0.4285em; 83 | 84 | // Global border radius to be used to buttons and inputs 85 | @borderRadius: 2px; 86 | 87 | // Box shadows 88 | @boxShadowWidget: inset 0 0 0 1px transparent; 89 | @boxShadowWidgetFocus: inset 0 0 0 1px @colorProgressive; 90 | @boxShadowProgressiveFocus: inset 0 0 0 1px @colorProgressive, inset 0 0 0 2px @colorBaseInverted; 91 | @boxShadowInputBinaryActive: inset 0 0 0 1px @colorProgressiveActive; 92 | 93 | // Icon related variables 94 | @iconSize: 1.5em; 95 | @iconGutterWidth: 1em; 96 | 97 | // Form input sizes, equal to OOUI at 14px base font-size 98 | @sizeInputBinary: 1.5625em; 99 | 100 | // Deprecated color variables from when WikimediaUI color palette wasn't around 101 | // See https://wikimedia.github.io/WikimediaUI-Style-Guide/visual-style_colors.html 102 | @colorGray3: #333; 103 | @colorGray6: #666; 104 | @colorGray8: #888; 105 | @colorGray9: #999; 106 | @colorGray11: #bbb; 107 | @colorGray13: #ddd; 108 | -------------------------------------------------------------------------------- /demo-src/ui/mixins.less: -------------------------------------------------------------------------------- 1 | @import "./mediawiki.less/mediawiki.mixins.animation.less"; 2 | @import "./mediawiki.less/mediawiki.mixins.less"; 3 | 4 | .mwe-popups-border-pointer-top( @size, @left, @color, @extra ) { 5 | content: ""; 6 | position: absolute; 7 | border: (@size + @extra) solid transparent; 8 | border-top: 0; 9 | border-bottom: (@size + @extra) solid @color; 10 | top: -@size; 11 | left: @left; 12 | } 13 | 14 | .mwe-popups-border-pointer-bottom( @size, @left, @color, @extra ) { 15 | content: ""; 16 | position: absolute; 17 | border: (@size + @extra) solid transparent; 18 | border-bottom: 0; 19 | border-top: (@size + @extra) solid @color; 20 | bottom: -@size; 21 | left: @left; 22 | } 23 | 24 | .mwe-popups-translate( @x, @y ) { 25 | -webkit-transform: translate(@x, @y); 26 | -moz-transform: translate(@x, @y); 27 | -ms-transform: translate(@x, @y); 28 | transform: translate(@x, @y); 29 | } 30 | 31 | /* FIXME: Use Phuedx's approach to make this cleaner 32 | https://gist.github.com/phuedx/0639a279b6efb1a71474 */ 33 | @-webkit-keyframes mwe-popups-fade-in-up { 34 | .mwe-popups-fade-in-up-frames; 35 | } 36 | 37 | @-moz-keyframes mwe-popups-fade-in-up { 38 | .mwe-popups-fade-in-up-frames; 39 | } 40 | 41 | @keyframes mwe-popups-fade-in-up { 42 | .mwe-popups-fade-in-up-frames; 43 | } 44 | 45 | @-webkit-keyframes mwe-popups-fade-in-down { 46 | .mwe-popups-fade-in-down-frames; 47 | } 48 | 49 | @-moz-keyframes mwe-popups-fade-in-down { 50 | .mwe-popups-fade-in-down-frames; 51 | } 52 | 53 | @keyframes mwe-popups-fade-in-down { 54 | .mwe-popups-fade-in-down-frames; 55 | } 56 | 57 | @-webkit-keyframes mwe-popups-fade-out-down { 58 | .mwe-popups-fade-out-down-frames; 59 | } 60 | 61 | @-moz-keyframes mwe-popups-fade-out-down { 62 | .mwe-popups-fade-out-down-frames; 63 | } 64 | 65 | @keyframes mwe-popups-fade-out-down { 66 | .mwe-popups-fade-out-down-frames; 67 | } 68 | 69 | @-webkit-keyframes mwe-popups-fade-out-up { 70 | .mwe-popups-fade-out-up-frames; 71 | } 72 | 73 | @-moz-keyframes mwe-popups-fade-out-up { 74 | .mwe-popups-fade-out-up-frames; 75 | } 76 | 77 | @keyframes mwe-popups-fade-out-up { 78 | .mwe-popups-fade-out-up-frames; 79 | } 80 | 81 | .mwe-popups-fade-in-up-frames() { 82 | 0% { 83 | opacity: 0; 84 | .mwe-popups-translate(0, 20px); 85 | } 86 | 87 | 100% { 88 | opacity: 1; 89 | .mwe-popups-translate(0, 0); 90 | } 91 | } 92 | 93 | .mwe-popups-fade-in-down-frames() { 94 | 0% { 95 | opacity: 0; 96 | .mwe-popups-translate(0, -20px); 97 | } 98 | 99 | 100% { 100 | opacity: 1; 101 | .mwe-popups-translate(0, 0); 102 | } 103 | } 104 | 105 | .mwe-popups-fade-out-down-frames() { 106 | 0% { 107 | opacity: 1; 108 | .mwe-popups-translate(0, 0); 109 | } 110 | 111 | 100% { 112 | opacity: 0; 113 | .mwe-popups-translate(0, 20px); 114 | } 115 | } 116 | 117 | .mwe-popups-fade-out-up-frames() { 118 | 0% { 119 | opacity: 1; 120 | .mwe-popups-translate(0, 0); 121 | } 122 | 123 | 100% { 124 | opacity: 0; 125 | .mwe-popups-translate(0, -20px); 126 | } 127 | } 128 | 129 | .mwe-popups-fade-in-up { 130 | .animation(mwe-popups-fade-in-up, 0.2s, ease, forwards); 131 | } 132 | 133 | .mwe-popups-fade-in-down { 134 | .animation(mwe-popups-fade-in-down, 0.2s, ease, forwards); 135 | } 136 | 137 | .mwe-popups-fade-out-down { 138 | .animation(mwe-popups-fade-out-down, 0.2s, ease, forwards); 139 | } 140 | 141 | .mwe-popups-fade-out-up { 142 | .animation(mwe-popups-fade-out-up, 0.2s, ease, forwards); 143 | } 144 | -------------------------------------------------------------------------------- /demo-src/ui/parseHTML.js: -------------------------------------------------------------------------------- 1 | export default function parseHTML(str) { 2 | const tmp = document.implementation.createHTMLDocument(); 3 | tmp.body.innerHTML = str; 4 | return tmp.body.children.length === 1 5 | ? tmp.body.children[0] 6 | : tmp.body.children; 7 | } 8 | -------------------------------------------------------------------------------- /demo-src/ui/pointer-mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /demo-src/ui/templates/pagePreview/pagePreview.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module pagePreview 3 | */ 4 | 5 | /** 6 | * @param {ext.popups.PreviewModel} model 7 | * @param {boolean} hasThumbnail 8 | * @return {string} HTML string. 9 | */ 10 | export function renderPagePreview( 11 | { url, languageCode, languageDirection }, 12 | hasThumbnail 13 | ) { 14 | return ` 15 | 28 | `.trim(); 29 | } 30 | -------------------------------------------------------------------------------- /demo-src/ui/templates/pagePreview/pagePreview.less: -------------------------------------------------------------------------------- 1 | @import "../../mediawiki.less/mediawiki.ui/variables.less"; 2 | 3 | .mwe-popups-settings-icon { 4 | // For purpose of active and hover states 5 | border-radius: @borderRadius; 6 | // Icon sizes are relative to font size. Override any parents. 7 | font-size: 16px; 8 | // position icon 9 | /* stylelint-disable value-keyword-case */ 10 | margin-right: -@iconSize / 2; 11 | 12 | &:hover { 13 | background-color: @colorGray14; 14 | } 15 | 16 | &:active { 17 | background-color: @colorGray12; 18 | } 19 | } 20 | 21 | .mwe-popups { 22 | .mwe-popups-container { 23 | footer { 24 | .mwe-popups-settings-icon { 25 | float: right; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /demo-src/ui/templates/popup.less: -------------------------------------------------------------------------------- 1 | @import '../variables.less'; 2 | @import '../mixins.less'; 3 | 4 | // Code adapted from Yair Rand's NavPopupsRestyled.js 5 | // https://en.wikipedia.org/wiki/User:Yair_rand/NavPopupsRestyled.js 6 | // 7 | // "Tall" terminology, although applied to the popup, refers only to the 8 | // thumbnail and not the popup itself: 9 | // Class Thumbnail Popup 10 | // Tall Portrait Landscape 11 | // Not tall Landscape Portrait 12 | // Not tall Missing Landscape 13 | 14 | .mwe-popups { 15 | background: #fff; 16 | position: absolute; 17 | z-index: @zIndexPopup; 18 | .box-shadow( 0 30px 90px -20px rgba( 0, 0, 0, 0.3 ), 0 0 1px @colorGray10; ); 19 | padding: 0; 20 | display: none; 21 | font-size: 14px; 22 | line-height: @lineHeight; 23 | min-width: 300px; 24 | border-radius: @borderRadius; 25 | 26 | .mw-ui-icon { 27 | // mw-ui-icon assumes a font size of 16px so we must declare it here 28 | font-size: 16px; 29 | margin: 21px 0 8px 0; 30 | } 31 | 32 | .mwe-popups-container { 33 | color: @colorText; 34 | margin-top: -9px; 35 | padding-top: 9px; 36 | text-decoration: none; 37 | 38 | footer { 39 | padding: @popupPadding; 40 | margin: 0; 41 | font-size: 10px; 42 | position: absolute; 43 | bottom: 0; 44 | left: 0; 45 | } 46 | } 47 | 48 | .mwe-popups-extract { 49 | // T156800, T139297: "Pad" the extract horizontally using a margin so the 50 | // SVG element is forced not to occlude the truncating pseudo-element and 51 | // the settings cog in IE9-11. 52 | margin: @popupPadding; 53 | 54 | display: block; 55 | color: @colorText; 56 | text-decoration: none; 57 | position: relative; 58 | 59 | &:hover { 60 | text-decoration: none; 61 | } 62 | 63 | &:after { 64 | content: ' '; 65 | position: absolute; 66 | bottom: 0; 67 | width: 25%; 68 | height: @lineHeight; 69 | background-color: transparent; 70 | } 71 | 72 | /* Stylelint rule broken for vendor prefixes: https://github.com/stylelint/stylelint/issues/1939 */ 73 | /* stylelint-disable function-linear-gradient-no-nonstandard-direction */ 74 | &[ dir='ltr' ]:after { 75 | /* @noflip */ 76 | right: 0; 77 | /* @noflip */ 78 | background-image: -webkit-linear-gradient( to right, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% ); 79 | /* @noflip */ 80 | background-image: -moz-linear-gradient( to right, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% ); 81 | /* @noflip */ 82 | background-image: -o-linear-gradient( to right, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% ); 83 | /* @noflip */ 84 | background-image: linear-gradient( to right, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% ); 85 | } 86 | 87 | &[ dir='rtl' ]:after { 88 | /* @noflip */ 89 | left: 0; 90 | /* @noflip */ 91 | background-image: -webkit-linear-gradient( to left, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% ); 92 | /* @noflip */ 93 | background-image: -moz-linear-gradient( to left, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% ); 94 | /* @noflip */ 95 | background-image: -o-linear-gradient( to left, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% ); 96 | /* @noflip */ 97 | background-image: linear-gradient( to left, rgba( 255, 255, 255, 0 ), rgba( 255, 255, 255, 1 ) 50% ); 98 | } 99 | /* stylelint-enable function-linear-gradient-no-nonstandard-direction */ 100 | 101 | // Make the text fit in exactly as many lines as we want. 102 | p { 103 | margin: 0; 104 | } 105 | ul, 106 | ol, 107 | li, 108 | dl, 109 | dd, 110 | dt { 111 | margin-top: 0; 112 | margin-bottom: 0; 113 | } 114 | } 115 | 116 | svg { 117 | overflow: hidden; 118 | } 119 | 120 | &.mwe-popups-is-tall { 121 | width: 450px; 122 | 123 | > div > a > svg { 124 | vertical-align: middle; 125 | } 126 | 127 | .mwe-popups-extract { 128 | width: @popupTallWidth; 129 | height: 9 * @lineHeight; 130 | overflow: hidden; 131 | float: left; 132 | } 133 | 134 | footer { 135 | width: @popupTallWidth; 136 | left: 0; 137 | } 138 | } 139 | 140 | &.mwe-popups-is-not-tall { 141 | width: @popupWidth; 142 | 143 | .mwe-popups-extract { 144 | @minHeight: 2 * @lineHeight; 145 | // On short summaries, we want to avoid an overlap with the gradient. 146 | min-height: @minHeight; 147 | max-height: 7 * @lineHeight; 148 | overflow: hidden; 149 | margin-bottom: @minHeight + 7px; 150 | padding-bottom: 0; 151 | } 152 | 153 | footer { 154 | width: @popupWidth - @cogIconSize; 155 | } 156 | } 157 | 158 | &.mwe-popups-type-generic, 159 | &.mwe-popups-type-disambiguation { 160 | .mwe-popups-extract { 161 | min-height: auto; 162 | padding-top: 4px; 163 | margin-bottom: 60px; 164 | margin-top: 0; 165 | } 166 | 167 | .mwe-popups-read-link { 168 | font-weight: bold; 169 | font-size: 12px; 170 | } 171 | 172 | // When the user dwells on the "There was an issue displaying this preview" 173 | // text, which is a link to the page, then highlight the "Go to this page" 174 | // link too. 175 | .mwe-popups-extract:hover + footer .mwe-popups-read-link { 176 | text-decoration: underline; 177 | } 178 | } 179 | 180 | /* Triangles/Pointers */ 181 | &.mwe-popups-no-image-pointer { 182 | &:before { 183 | .mwe-popups-border-pointer-top( 8px, 10px, @colorGray10, 0px ); 184 | } 185 | 186 | &:after { 187 | .mwe-popups-border-pointer-top( 7px, 7px, #fff, 4px ); 188 | } 189 | } 190 | 191 | &.flipped-x.mwe-popups-no-image-pointer { 192 | &:before { 193 | left: auto; 194 | right: 10px; 195 | } 196 | 197 | &:after { 198 | left: auto; 199 | right: 7px; 200 | } 201 | } 202 | 203 | &.mwe-popups-image-pointer { 204 | &:before { 205 | .mwe-popups-border-pointer-top( 9px, 9px, @colorGray10, 0px ); 206 | z-index: @zIndexBackground; 207 | } 208 | 209 | &:after { 210 | .mwe-popups-border-pointer-top( 8px, 6px, #fff, 4px ); 211 | z-index: @zIndexForeground; 212 | } 213 | 214 | &.flipped-x { 215 | &:before { 216 | .mwe-popups-border-pointer-top( 9px, 273px, @colorGray10, 0px ); 217 | } 218 | 219 | &:after { 220 | .mwe-popups-border-pointer-top( 8px, 269px, #fff, 4px ); 221 | } 222 | } 223 | 224 | .mwe-popups-extract { 225 | padding-top: 32px; 226 | margin-top: 190px; 227 | } 228 | 229 | > div > a > svg { 230 | margin-top: -8px; 231 | position: absolute; 232 | z-index: @zIndexThumbnailMask; 233 | left: 0; 234 | } 235 | } 236 | 237 | &.flipped-x.mwe-popups-is-tall { 238 | min-height: 242px; 239 | 240 | &:before { 241 | .mwe-popups-border-pointer-top( 9px, 420px, @colorGray10, 0px ); 242 | z-index: @zIndexBackground; 243 | } 244 | 245 | > div > a > svg { 246 | margin: 0; 247 | margin-top: -8px; 248 | margin-bottom: -7px; 249 | position: absolute; 250 | z-index: @zIndexThumbnailMask; 251 | right: 0; 252 | } 253 | } 254 | 255 | &.flipped-x-y { 256 | &:before { 257 | .mwe-popups-border-pointer-bottom( 9px, 272px, @colorGray10, 0px ); 258 | z-index: @zIndexBackground; 259 | } 260 | 261 | &:after { 262 | .mwe-popups-border-pointer-bottom( 8px, 269px, #fff, 4px ); 263 | z-index: @zIndexForeground; 264 | } 265 | 266 | &.mwe-popups-is-tall { 267 | min-height: 242px; 268 | 269 | &:before { 270 | .mwe-popups-border-pointer-bottom( 9px, 420px, @colorGray10, 0px ); 271 | } 272 | 273 | &:after { 274 | .mwe-popups-border-pointer-bottom( 8px, 417px, #fff, 4px ); 275 | } 276 | 277 | > div > a > svg { 278 | margin: 0; 279 | margin-bottom: -9px; 280 | position: absolute; 281 | z-index: @zIndexThumbnailMask; 282 | right: 0; 283 | } 284 | } 285 | } 286 | 287 | &.flipped-y { 288 | &:before { 289 | .mwe-popups-border-pointer-bottom( 8px, 10px, @colorGray10, 0px ); 290 | } 291 | 292 | &:after { 293 | .mwe-popups-border-pointer-bottom( 7px, 7px, #fff, 4px ); 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /demo-src/ui/templates/preview/preview.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module preview 3 | */ 4 | 5 | import { escapeHTML } from "../templateUtil"; 6 | 7 | /** 8 | * @param {ext.popups.PreviewModel} model 9 | * @param {boolean} showTitle 10 | * @param {string} extractMsg 11 | * @param {string} linkMsg 12 | * @return {string} HTML string. 13 | */ 14 | export function renderPreview( 15 | { title, url, type }, 16 | showTitle, 17 | extractMsg, 18 | linkMsg 19 | ) { 20 | title = escapeHTML(title); 21 | extractMsg = escapeHTML(extractMsg); 22 | linkMsg = escapeHTML(linkMsg); 23 | return ` 24 | 36 | `.trim(); 37 | } 38 | -------------------------------------------------------------------------------- /demo-src/ui/templates/preview/preview.less: -------------------------------------------------------------------------------- 1 | .mwe-popups { 2 | .mwe-popups-title { 3 | display: block; 4 | font-weight: bold; 5 | margin: 0 16px; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /demo-src/ui/templates/templateUtil.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module templateUtil 3 | */ 4 | 5 | function escapeCallback(s) { 6 | switch (s) { 7 | case "'": 8 | return "'"; 9 | case '"': 10 | return """; 11 | case "<": 12 | return "<"; 13 | case ">": 14 | return ">"; 15 | case "&": 16 | return "&"; 17 | } 18 | } 19 | 20 | /** 21 | * Escape a string for HTML. 22 | * 23 | * Converts special characters to HTML entities. 24 | * 25 | * mw.html.escape( '< > \' & "' ); 26 | * // Returns < > ' & " 27 | * 28 | * @param {string} s The string to escape 29 | * @return {string} HTML 30 | */ 31 | function escape(s) { 32 | return s.replace(/['"<>&]/g, escapeCallback); 33 | } 34 | 35 | /** 36 | * @param {string} str 37 | * @return {string} The string with any HTML entities escaped. 38 | */ 39 | export function escapeHTML(str) { 40 | return escape(str); 41 | } 42 | -------------------------------------------------------------------------------- /demo-src/ui/thumbnail.js: -------------------------------------------------------------------------------- 1 | import bracketedDevicePixelRatio from "./bracketedDevicePixelRatio"; 2 | 3 | /** 4 | * @module thumbnail 5 | */ 6 | 7 | const SIZES = { 8 | portraitImage: { 9 | h: 250, // Exact height 10 | w: 203 // Max width 11 | }, 12 | landscapeImage: { 13 | h: 200, // Max height 14 | w: 320 // Exact Width 15 | } 16 | }; 17 | 18 | export { SIZES }; 19 | 20 | /** 21 | * @typedef {Object} ext.popups.Thumbnail 22 | * @property {Element} el 23 | * @property {Boolean} isTall Whether or not the thumbnail is portrait 24 | */ 25 | 26 | /** 27 | * Creates a thumbnail from the representation of a thumbnail returned by the 28 | * PageImages MediaWiki API query module. 29 | * 30 | * If there's no thumbnail, the thumbnail is too small, or the thumbnail's URL 31 | * contains characters that could be used to perform an 32 | * [XSS attack via CSS](https://www.owasp.org/index.php/Testing_for_CSS_Injection_(OTG-CLIENT-005)), 33 | * then `null` is returned. 34 | * 35 | * Extracted from `mw.popups.renderer.article.createThumbnail`. 36 | * 37 | * @param {Object} rawThumbnail 38 | * @return {ext.popups.Thumbnail|null} 39 | */ 40 | export function createThumbnail(rawThumbnail) { 41 | const devicePixelRatio = bracketedDevicePixelRatio(); 42 | 43 | if (!rawThumbnail) { 44 | return null; 45 | } 46 | 47 | const tall = rawThumbnail.width < rawThumbnail.height; 48 | const thumbWidth = rawThumbnail.width / devicePixelRatio; 49 | const thumbHeight = rawThumbnail.height / devicePixelRatio; 50 | 51 | if ( 52 | // Image too small for landscape display 53 | (!tall && thumbWidth < SIZES.landscapeImage.w) || 54 | // Image too small for portrait display 55 | (tall && thumbHeight < SIZES.portraitImage.h) || 56 | // These characters in URL that could inject CSS and thus JS 57 | (rawThumbnail.source.indexOf("\\") > -1 || 58 | rawThumbnail.source.indexOf("'") > -1 || 59 | rawThumbnail.source.indexOf('"') > -1) 60 | ) { 61 | return null; 62 | } 63 | 64 | let x, y, width, height; 65 | if (tall) { 66 | x = 67 | thumbWidth > SIZES.portraitImage.w 68 | ? (thumbWidth - SIZES.portraitImage.w) / -2 69 | : SIZES.portraitImage.w - thumbWidth; 70 | y = 71 | thumbHeight > SIZES.portraitImage.h 72 | ? (thumbHeight - SIZES.portraitImage.h) / -2 73 | : 0; 74 | width = SIZES.portraitImage.w; 75 | height = SIZES.portraitImage.h; 76 | } else { 77 | x = 0; 78 | y = 79 | thumbHeight > SIZES.landscapeImage.h 80 | ? (thumbHeight - SIZES.landscapeImage.h) / -2 81 | : 0; 82 | width = SIZES.landscapeImage.w; 83 | height = 84 | thumbHeight > SIZES.landscapeImage.h 85 | ? SIZES.landscapeImage.h 86 | : thumbHeight; 87 | } 88 | 89 | return { 90 | el: createThumbnailElement( 91 | tall ? "mwe-popups-is-tall" : "mwe-popups-is-not-tall", 92 | rawThumbnail.source, 93 | x, 94 | y, 95 | thumbWidth, 96 | thumbHeight, 97 | width, 98 | height 99 | ), 100 | isTall: tall, 101 | width: thumbWidth, 102 | height: thumbHeight 103 | }; 104 | } 105 | 106 | /** 107 | * Creates the SVG image element that represents the thumbnail. 108 | * 109 | * This function is distinct from `createThumbnail` as it abstracts away some 110 | * browser issues that are uncovered when manipulating elements across 111 | * namespaces. 112 | * 113 | * @param {String} className 114 | * @param {String} url 115 | * @param {Number} x 116 | * @param {Number} y 117 | * @param {Number} thumbnailWidth 118 | * @param {Number} thumbnailHeight 119 | * @param {Number} width 120 | * @param {Number} height 121 | * @param {String} clipPath 122 | * @return {jQuery} 123 | */ 124 | export function createThumbnailElement( 125 | className, 126 | url, 127 | x, 128 | y, 129 | thumbnailWidth, 130 | thumbnailHeight, 131 | width, 132 | height 133 | ) { 134 | const nsSvg = "http://www.w3.org/2000/svg", 135 | nsXlink = "http://www.w3.org/1999/xlink"; 136 | 137 | const thumbnailSVGImage = document.createElementNS(nsSvg, "image"); 138 | thumbnailSVGImage.setAttributeNS(nsXlink, "href", url); 139 | thumbnailSVGImage.classList.add(className); 140 | thumbnailSVGImage.setAttribute("x", x); 141 | thumbnailSVGImage.setAttribute("y", y); 142 | thumbnailSVGImage.setAttribute("width", thumbnailWidth); 143 | thumbnailSVGImage.setAttribute("height", thumbnailHeight); 144 | 145 | const thumbnail = document.createElementNS(nsSvg, "svg"); 146 | thumbnail.setAttribute("xmlns", nsSvg); 147 | thumbnail.setAttribute("width", width); 148 | thumbnail.setAttribute("height", height); 149 | thumbnail.appendChild(thumbnailSVGImage); 150 | 151 | return thumbnail; 152 | } 153 | -------------------------------------------------------------------------------- /demo-src/ui/variables.less: -------------------------------------------------------------------------------- 1 | @import "./mediawiki.less/mediawiki.ui/variables.less"; 2 | 3 | @popupPadding: 16px; 4 | @popupWidth: 320px; 5 | @popupTallWidth: 215px; 6 | @cogIconSize: 30px; 7 | @lineHeight: 20px; 8 | 9 | @zIndexPopup: 110; 10 | @zIndexBackground: 111; 11 | @zIndexForeground: 112; 12 | @zIndexThumbnailMask: 113; 13 | -------------------------------------------------------------------------------- /demo-src/wait.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module wait 3 | */ 4 | 5 | /** 6 | * Sugar around `window.setTimeout`. 7 | * 8 | * @example 9 | * import wait from './wait'; 10 | * 11 | * wait( 150 ) 12 | * .then( () => { 13 | * // Continue processing... 14 | * } ); 15 | * 16 | * @param {Number} delay The number of milliseconds to wait 17 | * @return {Promise} 18 | */ 19 | export default function wait(delay) { 20 | return new Promise(resolve => { 21 | setTimeout(() => { 22 | resolve(); 23 | }, delay); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/a4621041136.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/adverts.js: -------------------------------------------------------------------------------- 1 | define(["jquery-1"],function(a){if(window.bbcdotcom&&typeof bbcdotcom.objects!=="undefined"&&bbcdotcom.objects("bbcdotcom.av.emp.adverts","create")){bbcdotcom.av.emp.adverts=(function(){function s(x){var u=new RegExp(x+"=(.*?)(?:;|$)");var w=u.exec(document.cookie);var v=(w&&w.length?w[1]:false);return v}function q(){}function j(){var v="",u,w=window.ccauds;if(typeof w!=="undefined"){for(u=0;u0){v+=","}v+=w.Profile.Audiences.Audience[u].abbr}}return v}function k(){var x,w=bbcdotcom.adverts.keyValues.getAll(),v="&cust_params=",u;for(x in w){if(w.hasOwnProperty(x)&&w[x]!==""){v+=x+"%3D"+w[x]+"%26"}}v+="ccaud%3D"+encodeURIComponent(j())+"%26";if(bbcdotcom.objects("bbcdotcom.adverts.adUnit.getPreviewUid")){u="uid="+bbcdotcom.adverts.adUnit.getPreviewUid();v+=encodeURIComponent(u)}if(bbcdotcom.config.getWindowLocation().pathname.match(/\/embed$/g)!==null){v+=encodeURIComponent("client=embedplayer")}return v}function b(u){}function p(v){var u;if(typeof BBC!=="undefined"&&typeof BBC.adverts!=="undefined"&&typeof BBC.adverts.empCompanion==="function"){u=BBC.adverts.empCompanion()}else{if(typeof bbcdotcom!=="undefined"&&typeof bbcdotcom.advert!=="undefined"&&typeof bbcdotcom.advert.getPrerollAdTag==="function"){u=bbcdotcom.advert.getPrerollAdTag(bbcdotcom.adUnit)}else{if(typeof bbcdotcom!=="undefined"&&typeof bbcdotcom.objects!=="undefined"&&bbcdotcom.objects("bbcdotcom.adverts.adUnit.get")&&bbcdotcom.objects("bbcdotcom.adverts.keyValues.getAll")){u="http://pubads.g.doubleclick.net/gampad/ads?sz=512x288&iu="+bbcdotcom.adverts.adUnit.get()+k()+"&env=vp&gdfp_req=1&impl=s&output=xml_vast3&unviewed_position_start=1&ord="+(Math.floor(Math.random()*1000000000))+"&url="+encodeURIComponent(encodeURIComponent(bbcdotcom.config.getWindowLocation().href))+"&ad_rule=1"}}}return u}function n(w){var u=p();var v=new RegExp(/(ad_rule=)\d{1}/g);if(v.test(u)===false){u=u+"&ad_rule="+w||1}return u}function g(u){}function e(u){}function h(u){}function l(u){}function t(u){}function m(u){}function r(v,u){}function i(u,v){}function f(u){u.set("preroll",p())}function c(z,x){var y;var w=p();var v=s("bbcdotcomHtml5AdsDebug");var u={name:"AdsPluginParameters",data:{adTag:w,debug:v}};for(y in x){u.data[y]=x[y]}return u}function d(v,u){var w=u.settings();w.suppressItemKind=v;u.settings(w);return u}function o(y,x,w){var u,v=w||{};if(typeof x==="undefined"){return}if(x.loadPlugin!==undefined&&typeof x.loadPlugin==="function"){u=c(y,v);x.loadPlugin({html:"name:dfpAds.js",swf:"name:dfpAds.swf"},u)}d(["ident"],x)}return{getPrerollAdTag:p,getPrerollAdTagWithAdRule:n,setupCompanionSlots:g,enableCompanions:q,getCompanionSlotId:e,getCompanionSlots:h,encodeCompanionSlots:l,decodeCompanionSlots:t,defineCompanionSlots:m,setCompanionFlashVars:i,playerBeforeEachWrite:f,addSmpPlugin:o}}());return bbcdotcom.av.emp.adverts}}); -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/api.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Barlesque - ORB and all 3 | * Copyright (c) 2015 BBC, all rights reserved. 4 | */ 5 | define("orb/api",function(){"use strict";var n={layout:[]},t=(window.orb.fig(),{}),i={layout:function(t){n.layout.push(t)},trigger:function(t,i){if(n[t])for(var o=0,r=n[t].length;o 2 | 3 | Error 4 | -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/fig.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var o={'uk':0, 3 | 'ck':1, 4 | 'ad':1, 5 | 'ap':0, 6 | 'tb':0, 7 | 'mb':0, 8 | 'eu':1, 9 | 'df':0 // this is not a default fig 10 | }; 11 | 12 | window.orb=window.orb||{}; 13 | window.fig=window.fig||{}; 14 | 15 | if (window.name.match(/ orb_fig_referrer=([^ ]*)/)) { 16 | window.orb.referrer = decodeURIComponent(RegExp.$1); 17 | window.name = window.name.replace(/ orb_fig_referrer=([^ ]*)/, ''); 18 | } 19 | 20 | if (window.name.match(/ orb_fig_last_hostname=([^ ]*)/)) { 21 | window.orb.lastHostname = decodeURIComponent(RegExp.$1); 22 | window.name = window.name.replace(/ orb_fig_last_hostname=([^ ]*)/, ''); 23 | } 24 | 25 | //is fig.js loaded by the manager? 26 | var figManagerLoaded = window.fig && window.fig.manager 27 | 28 | //a default is set by figmanager that we should override. 29 | if(figManagerLoaded && window.orb.fig && window.orb.fig.isDefault()) { 30 | window.fig.manager.setFig(window, o); 31 | } 32 | 33 | //if there is no fig manager then we may not have a fig at all. 34 | if(!figManagerLoaded && !window.orb.fig) { 35 | window.orb.fig = function(k){return (arguments.length)? o[k]:o}; 36 | } 37 | 38 | if (window.fig.async && typeof JSON != 'undefined') { 39 | var jsonFig = JSON.stringify(o); 40 | var date = new Date(); 41 | date.setTime(date.getTime()+(24*60*60*1000)); 42 | document.cookie = 'ckns_orb_cachedfig=' + jsonFig + '; expires=' + date.toGMTString() + '; path=/' 43 | } 44 | 45 | })(); 46 | 47 | orb._clientsideRedirect=function(h,o){var j=false,m;o=o||window;m=(o.document.cookie.match(/ckps_d=(.)/)?RegExp.$1:"");if(orb._redirectionIsEnabled(o)&&orb._dependenciesSatisfied(h,o)){var p=(o.location.hostname||"").toLowerCase(),a=(o.location.href||""),k={isUk:/(^|\.)bbc\.co\.uk$/i.test(p),isInt:/(^|\.)bbc\.com$/i.test(p),isMb:/^m\./i.test(p),isDesk:/^(www|pal)\./i.test(p)},c={isUk:h("uk"),isMb:h("mb")},n,b;if(o.bbcredirection.geo===true){if(k.isInt===true&&c.isUk===1){o.name+=" orb_fig_referrer="+encodeURIComponent(document.referrer);b=a.replace(/^(.+?bbc)\.com/i,"$1.co.uk")}else{if(k.isUk===true&&c.isUk===0){o.name+=" orb_fig_referrer="+encodeURIComponent(document.referrer);b=a.replace(/^(.+?bbc)\.co\.uk/i,"$1.com")}}}n=(b||a);if(o.bbcredirection.device===true){if(k.isDesk===true&&(m==="m"||(!m&&c.isMb===1))){o.name+=" orb_fig_referrer="+encodeURIComponent(document.referrer);n=n.replace(/^(https?:\/\/)(www|pal)\./i,"$1m.")}else{if(k.isMb===true&&(m==="d"||(!m&&c.isMb===0))){o.name+=" orb_fig_referrer="+encodeURIComponent(document.referrer);n=n.replace(/^(https?:\/\/)m\./i,"$1www.")}}}if(n&&a.toLowerCase()!==n.toLowerCase()){var l=o.orb&&o.orb.lastHostname===o.location.hostname;var g=o.orb&&o.orb.lastHostname&&n.indexOf(o.orb.lastHostname)>-1;var d=o.location.pathname==="/";try{if(!l&&(!g||d)){o.name+=" orb_fig_last_hostname="+o.location.hostname;j=true;o.location.replace(n)}}catch(i){j=false;o.require(["istats-1"],function(e){e.log("redirection_fail","",{})})}}}return j};orb._redirectionIsEnabled=function(a){return(a.bbcredirection&&(a.bbcredirection.geo===true||a.bbcredirection.device===true))};orb._dependenciesSatisfied=function(b,a){return(typeof b==="function"&&typeof a.location.replace!=="undefined")};orb.fig.device={};orb.fig.geo={};orb.fig.user={};orb.fig.device.isTablet=function(){return window.orb.fig("no")?undefined:window.orb.fig("tb")};orb.fig.device.isMobile=function(){return window.orb.fig("no")?undefined:window.orb.fig("mb")};orb.fig.geo.isUK=function(){return window.orb.fig("no")?undefined:window.orb.fig("uk")};orb.fig.geo.isEU=function(){return window.orb.fig("no")?undefined:window.orb.fig("eu")};window.orb.fig.isDefault=function(){return window.orb.fig("df")}; orb._clientsideRedirect(window.mockFig || window.orb.fig, window.mockWindow || window); 48 | -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/flag: -------------------------------------------------------------------------------- 1 | bbcdotcom.flag={a:1,s:1} -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/font.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Barlesque - ORB and all 3 | * Copyright (c) 2015 BBC, all rights reserved. 4 | */ 5 | !function(){!function(){"use strict";function t(t){l.push(t),1==l.length&&f()}function e(){for(;l.length;)l[0](),l.shift()}function n(t){this.a=u,this.b=void 0,this.f=[];var e=this;try{t(function(t){r(e,t)},function(t){a(e,t)})}catch(n){a(e,n)}}function o(t){return new n(function(e,n){n(t)})}function i(t){return new n(function(e){e(t)})}function r(t,e){if(t.a==u){if(e==t)throw new TypeError;var n=!1;try{var o=e&&e.then;if(null!=e&&"object"==typeof e&&"function"==typeof o)return void o.call(e,function(e){n||r(t,e),n=!0},function(e){n||a(t,e),n=!0})}catch(i){return void(n||a(t,i))}t.a=0,t.b=e,s(t)}}function a(t,e){if(t.a==u){if(e==t)throw new TypeError;t.a=1,t.b=e,s(t)}}function s(e){t(function(){if(e.a!=u)for(;e.f.length;){var t=e.f.shift(),n=t[0],o=t[1],i=t[2],t=t[3];try{0==e.a?i("function"==typeof n?n.call(void 0,e.b):e.b):1==e.a&&("function"==typeof o?i(o.call(void 0,e.b)):t(e.b))}catch(r){t(r)}}})}function c(t){return new n(function(e,n){function o(n){return function(o){a[n]=o,r+=1,r==t.length&&e(a)}}var r=0,a=[];0==t.length&&e(a);for(var s=0;sparseInt(t[1],10)}else u=!1;return u}function c(){return null===p&&(p=!!document.fonts),p}function d(){if(null===h){var t=document.createElement("div");try{t.style.font="condensed 100px sans-serif"}catch(e){}h=""!==t.style.font}return h}function f(t,e){return[t.style,t.weight,d()?t.stretch:"","100px",e].join(" ")}var l=null,u=null,h=null,p=null;a.prototype.load=function(t,i){var a=this,d=t||"BESbswy",u=0,h=i||3e3,p=(new Date).getTime();return new Promise(function(t,i){if(c()&&!s()){var m=new Promise(function(t,e){function n(){(new Date).getTime()-p>=h?e():document.fonts.load(f(a,'"'+a.family+'"'),d).then(function(e){1<=e.length?t():setTimeout(n,25)},function(){e()})}n()}),w=new Promise(function(t,e){u=setTimeout(e,h)});Promise.race([w,m]).then(function(){clearTimeout(u),t(a)},function(){i(a)})}else e(function(){function e(){var e;(e=-1!=v&&-1!=y||-1!=v&&-1!=b||-1!=y&&-1!=b)&&((e=v!=y&&v!=b&&y!=b)||(null===l&&(e=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent),l=!!e&&(536>parseInt(e[1],10)||536===parseInt(e[1],10)&&11>=parseInt(e[2],10))),e=l&&(v==g&&y==g&&b==g||v==x&&y==x&&b==x||v==E&&y==E&&b==E)),e=!e),e&&(T.parentNode&&T.parentNode.removeChild(T),clearTimeout(u),t(a))}function s(){if((new Date).getTime()-p>=h)T.parentNode&&T.parentNode.removeChild(T),i(a);else{var t=document.hidden;!0!==t&&void 0!==t||(v=c.a.offsetWidth,y=m.a.offsetWidth,b=w.a.offsetWidth,e()),u=setTimeout(s,50)}}var c=new n(d),m=new n(d),w=new n(d),v=-1,y=-1,b=-1,g=-1,x=-1,E=-1,T=document.createElement("div");T.dir="ltr",o(c,f(a,"sans-serif")),o(m,f(a,"serif")),o(w,f(a,"monospace")),T.appendChild(c.a),T.appendChild(m.a),T.appendChild(w.a),document.body.appendChild(T),g=c.a.offsetWidth,x=m.a.offsetWidth,E=w.a.offsetWidth,s(),r(c,function(t){v=t,e()}),o(c,f(a,'"'+a.family+'",sans-serif')),r(m,function(t){y=t,e()}),o(m,f(a,'"'+a.family+'",serif')),r(w,function(t){b=t,e()}),o(w,f(a,'"'+a.family+'",monospace'))})})},"undefined"!=typeof module?module.exports=a:(window.FontFaceObserver=a,window.FontFaceObserver.prototype.load=a.prototype.load)}();var t=navigator.userAgent.toLowerCase(),e=t.indexOf("msie")!==-1&&parseInt(t.split("msie")[1],10)<=10,n=!e;if(n&&document.documentElement.className.indexOf("b-reith-sans-font")!=-1){var o=new FontFaceObserver("ReithSans"),i=new FontFaceObserver("ReithSans",{weight:"bold"});Promise.all([o.load(),i.load()]).then(function(){document.documentElement.className+=" b-reith-sans-loaded",require.defined("orb/nav")&&require(["orb/nav"],function(t){t.refresh()})})["catch"](function(t){console.log("Error loading font: "+t)})}}(); -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/istats-1.js: -------------------------------------------------------------------------------- 1 | define("istats-1",[],function(){function e(e){return"undefined"==typeof e.istats&&(e.istats={}),"undefined"==typeof e.istats.enabled&&(e.istats.enabled=!0),e}function t(t){return t?e(t).istats:window.istats}function n(e){return"function"==typeof e&&(J?e():z.push(e)),J}function r(){if("string"==typeof fe)return Y.type="internal",i(decodeURIComponent(fe))}function o(e){return e=e.replace(/\+/g,"%20"),decodeURIComponent(e)}function i(e){for(var t={},n=e.split("&"),r=0;rn&&(t[r]=e[r]);_(JSON.stringify(t))}function j(){if(!J){var e;if(s()){e=c()||r(),O();var n=w(),o={};for(var i in n)o[i]=n[i].value;m(o),J=!0;var a=v({},H);if(e&&($(e)||(e.intlink_from_url=void 0,e.intlink_ts=void 0,e.link_location=void 0),a=v(a,e),t()._linkTracked=!0),q=E(V),W.log("pageview",W.getCountername(),a),se.length>0)for(var l in se)R(se[l].labels,se[l].callback,se[l].event,!0);if(z&&z.length>0)for(var u=0;u=n.value},debug:function(){this.invoke(n.DEBUG,arguments)},info:function(){this.invoke(n.INFO,arguments)},warn:function(){this.invoke(n.WARN,arguments)},error:function(){this.invoke(n.ERROR,arguments)},invoke:function(e,n){t&&this.enabledFor(e)&&t(n,i({level:e},this.context))}};var u=new f({filterLevel:n.OFF});!function(){var e=n;e.enabledFor=r(u,u.enabledFor),e.debug=r(u,u.debug),e.info=r(u,u.info),e.warn=r(u,u.warn),e.error=r(u,u.error),e.log=e.info}(),n.setHandler=function(e){t=e},n.setLevel=function(e){u.setLevel(e);for(var n in o)o.hasOwnProperty(n)&&o[n].setLevel(e)},n.get=function(e){return o[e]||(o[e]=new f(i({name:e},u.context)))},n.useDefaults=function(t){"console"in e&&(n.setLevel(t||n.DEBUG),n.setHandler(function(t,o){var r=e.console,i=r.log;o.name&&(t[0]="["+o.name+"] "+t[0]),o.level===n.WARN&&r.warn?i=r.warn:o.level===n.ERROR&&r.error?i=r.error:o.level===n.INFO&&r.info&&(i=r.info),"function"==typeof i&&i.apply(r,t)}))},"function"==typeof define&&define.amd?define("util/logger",n):"undefined"!=typeof module&&module.exports?module.exports=n:e.Logger=n}(window); -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p0301msf.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p0301msf.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p03gmwjl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p03gmwjl.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p03xp2xp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p03xp2xp.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p03zwq47.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p03zwq47.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p04384rk.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p04384rk.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p04cpts8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p04cpts8.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p04dxxv0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p04dxxv0.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p04vjy8l.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p04vjy8l.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p04xp7cj.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p04xp7cj.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p04z0qvq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p04z0qvq.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p04z86mz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p04z86mz.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p056c7hw.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p056c7hw.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p056c814.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p056c814.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p056c89n.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p056c89n.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p056c8n2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p056c8n2.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p056c8yq.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p056c8yq.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p056c95v.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p056c95v.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p056c9bh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p056c9bh.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p056cj5w.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p056cj5w.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p05cq9pg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p05cq9pg.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p05lq24t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p05lq24t.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p05zsncb.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p05zsncb.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p064xt7t.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p064xt7t.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p065zrc4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p065zrc4.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p065zwzj.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p065zwzj.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p065zzdd.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p065zzdd.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p0663yhl.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p0663yhl.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/p0665rlj.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/BBC - Earth - Conservation success for otters on the brink_files/p0665rlj.jpg -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/rt=ifr.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/statusbar.js: -------------------------------------------------------------------------------- 1 | define(["idcta/idCookie","idcta/id-config","idcta/apiUtils"],function(d,c,h){var b={};function e(j){try{this.id=null;this.element=null;this.ctaLink=null;this.ctaName=null;if(f(j)){this.id=j.id;this.element=document.getElementById(j.id);if(!j.blq){this.ctaLink=document.getElementById("idcta-link");this.ctaName=this.element.getElementsByTagName("span")[0]}else{this.ctaLink=document.getElementById(j["link-id"])?document.getElementById(j["link-id"]):this.element.getElementsByTagName("a")[0];this.ctaName=j["name-id"]?document.getElementById(j["name-id"]):this.element.getElementsByTagName("span")[1]}var i=this;if(j.publiclyCacheable===true){if(d.getInstance().hasCookie()){if(c.status_url&&i.ctaLink.href!==c.status_url){i.ctaLink.href=c.status_url}a(i,d.getInstance())}else{if(c.signin_url){i.ctaLink.href=c.signin_url}i.ctaName.innerHTML=c.translation_signedout}}}}catch(k){h.logCaughtError(k)}}function a(m,k){try{var j=k.getNameFromCookie()||c.translation_signedin;var i=c.translation_signedin;if(j){i=g(j,14)}m.element.className=m.element.className+" idcta-signedin";m.ctaName.innerHTML=i}catch(l){h.logCaughtError(l)}}function g(j,i){if(j.length>i){return j.substring(0,i-1)+"…"}return j}function f(i){if(!document.getElementById(i.id)){return false}if(!i.blq&&!document.getElementById("idcta-link")){return false}if(i.blq&&!document.getElementById(i["link-id"])){return false}return true}b.Statusbar=e;b.updateForAuthorisedState=a;return b}); -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/tpc.css: -------------------------------------------------------------------------------- 1 | .icon, 2 | .icon-sections { 3 | top: 0.3em; 4 | position: absolute; 5 | left: 50%; 6 | } 7 | .icon-sections { 8 | overflow: hidden; 9 | background-image: url("../../img/shared/shared-sprite-mobile.png"); 10 | background-repeat: no-repeat; 11 | background-position: -118px -112px; 12 | width: 12px; 13 | height: 11px; 14 | margin-left: -6px; 15 | } 16 | @media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and ( min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx) { 17 | .icon-sections { 18 | background-image: url("../../img/shared/shared-sprite-mobile@2x.png"); 19 | background-size: 356px 174px; 20 | } 21 | } 22 | .icon-arrow { 23 | top: 0.3em; 24 | position: absolute; 25 | left: 50%; 26 | margin-left: -7px; 27 | margin-top: -7px; 28 | top: 50%; 29 | } 30 | .icon-arrow-large { 31 | top: 0.3em; 32 | position: absolute; 33 | left: 50%; 34 | margin-left: -7px; 35 | margin-top: -7px; 36 | top: 50%; 37 | margin-left: -13px; 38 | margin-top: -16px; 39 | } 40 | .icon-arrow-right-small { 41 | top: 0.3em; 42 | position: absolute; 43 | left: 50%; 44 | margin-left: -7px; 45 | margin-top: -7px; 46 | top: 50%; 47 | overflow: hidden; 48 | background-image: url("../../img/shared/shared-sprite-mobile.png"); 49 | background-repeat: no-repeat; 50 | background-position: -206px -160px; 51 | width: 14px; 52 | height: 14px; 53 | } 54 | @media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and ( min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx) { 55 | .icon-arrow-right-small { 56 | background-image: url("../../img/shared/shared-sprite-mobile@2x.png"); 57 | background-size: 356px 174px; 58 | } 59 | } 60 | .icon-arrow-left-small { 61 | top: 0.3em; 62 | position: absolute; 63 | left: 50%; 64 | margin-left: -7px; 65 | margin-top: -7px; 66 | top: 50%; 67 | overflow: hidden; 68 | background-image: url("../../img/shared/shared-sprite-mobile.png"); 69 | background-repeat: no-repeat; 70 | background-position: -165px -160px; 71 | width: 14px; 72 | height: 14px; 73 | } 74 | @media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and ( min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx) { 75 | .icon-arrow-left-small { 76 | background-image: url("../../img/shared/shared-sprite-mobile@2x.png"); 77 | background-size: 356px 174px; 78 | } 79 | } 80 | .icon-arrow-right-large { 81 | top: 0.3em; 82 | position: absolute; 83 | left: 50%; 84 | margin-left: -7px; 85 | margin-top: -7px; 86 | top: 50%; 87 | margin-left: -13px; 88 | margin-top: -16px; 89 | overflow: hidden; 90 | background-image: url("../../img/shared/shared-sprite-mobile.png"); 91 | background-repeat: no-repeat; 92 | background-position: -208px -108px; 93 | width: 25px; 94 | height: 32px; 95 | } 96 | @media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and ( min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx) { 97 | .icon-arrow-right-large { 98 | background-image: url("../../img/shared/shared-sprite-mobile@2x.png"); 99 | background-size: 356px 174px; 100 | } 101 | } 102 | .icon-arrow-left-large { 103 | top: 0.3em; 104 | position: absolute; 105 | left: 50%; 106 | margin-left: -7px; 107 | margin-top: -7px; 108 | top: 50%; 109 | margin-left: -13px; 110 | margin-top: -16px; 111 | overflow: hidden; 112 | background-image: url("../../img/shared/shared-sprite-mobile.png"); 113 | background-repeat: no-repeat; 114 | background-position: -151px -109px; 115 | width: 25px; 116 | height: 32px; 117 | } 118 | @media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and ( min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx) { 119 | .icon-arrow-left-large { 120 | background-image: url("../../img/shared/shared-sprite-mobile@2x.png"); 121 | background-size: 356px 174px; 122 | } 123 | } 124 | .icon-arrow-right-large-hover { 125 | top: 0.3em; 126 | position: absolute; 127 | left: 50%; 128 | margin-left: -7px; 129 | margin-top: -7px; 130 | top: 50%; 131 | margin-left: -13px; 132 | margin-top: -16px; 133 | overflow: hidden; 134 | background-image: url("../../img/shared/shared-sprite-mobile.png"); 135 | background-repeat: no-repeat; 136 | background-position: -208px -108px; 137 | width: 25px; 138 | height: 32px; 139 | background-position: -328px -109px; 140 | } 141 | @media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and ( min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx) { 142 | .icon-arrow-right-large-hover { 143 | background-image: url("../../img/shared/shared-sprite-mobile@2x.png"); 144 | background-size: 356px 174px; 145 | } 146 | } 147 | .icon-arrow-left-large-hover { 148 | top: 0.3em; 149 | position: absolute; 150 | left: 50%; 151 | margin-left: -7px; 152 | margin-top: -7px; 153 | top: 50%; 154 | margin-left: -13px; 155 | margin-top: -16px; 156 | overflow: hidden; 157 | background-image: url("../../img/shared/shared-sprite-mobile.png"); 158 | background-repeat: no-repeat; 159 | background-position: -151px -109px; 160 | width: 25px; 161 | height: 32px; 162 | background-position: -270px -109px; 163 | } 164 | @media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and ( min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx) { 165 | .icon-arrow-left-large-hover { 166 | background-image: url("../../img/shared/shared-sprite-mobile@2x.png"); 167 | background-size: 356px 174px; 168 | } 169 | } 170 | .icon-arrow-right-small-hover { 171 | top: 0.3em; 172 | position: absolute; 173 | left: 50%; 174 | margin-left: -7px; 175 | margin-top: -7px; 176 | top: 50%; 177 | overflow: hidden; 178 | background-image: url("../../img/shared/shared-sprite-mobile.png"); 179 | background-repeat: no-repeat; 180 | background-position: -206px -160px; 181 | width: 14px; 182 | height: 14px; 183 | background-position: -325px -160px; 184 | } 185 | @media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and ( min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx) { 186 | .icon-arrow-right-small-hover { 187 | background-image: url("../../img/shared/shared-sprite-mobile@2x.png"); 188 | background-size: 356px 174px; 189 | } 190 | } 191 | .icon-arrow-left-small-hover { 192 | top: 0.3em; 193 | position: absolute; 194 | left: 50%; 195 | margin-left: -7px; 196 | margin-top: -7px; 197 | top: 50%; 198 | overflow: hidden; 199 | background-image: url("../../img/shared/shared-sprite-mobile.png"); 200 | background-repeat: no-repeat; 201 | background-position: -165px -160px; 202 | width: 14px; 203 | height: 14px; 204 | background-position: -285px -160px; 205 | } 206 | @media only screen and (-webkit-min-device-pixel-ratio: 2),only screen and ( min--moz-device-pixel-ratio: 2),only screen and (-o-min-device-pixel-ratio: 2/1),only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi),only screen and (min-resolution: 2dppx) { 207 | .icon-arrow-left-small-hover { 208 | background-image: url("../../img/shared/shared-sprite-mobile@2x.png"); 209 | background-size: 356px 174px; 210 | } 211 | } 212 | /* mixins */ 213 | /* mixins */ 214 | .third-party-content-page-container .extra-footer { 215 | margin-top: 0em; 216 | } 217 | .third-party-content__frame { 218 | width: 100%; 219 | display: block; 220 | } 221 | .filter { 222 | background: rgba(17, 17, 17, 0.2); 223 | border: solid #545454; 224 | border-width: 1px 0; 225 | -webkit-box-sizing: border-box; 226 | -moz-box-sizing: border-box; 227 | box-sizing: border-box; 228 | padding-top: 2.4em; 229 | padding-bottom: 2.4em; 230 | padding-left: 1.5%; 231 | padding-right: 1.5%; 232 | display: table; 233 | width: 100%; 234 | } 235 | .filter li { 236 | float: none; 237 | display: table-cell; 238 | margin: 0; 239 | margin-top: 0.4em; 240 | margin-bottom: 0.4em; 241 | } 242 | .filter .link-box { 243 | background-color: transparent; 244 | display: inline-block; 245 | margin: 0; 246 | position: relative; 247 | text-align: center; 248 | width: 100%; 249 | } 250 | .filter .link-box:before { 251 | font-size: 2.2em; 252 | line-height: 1.22727273em; 253 | margin-top: 1.22727273em; 254 | margin-bottom: 0; 255 | letter-spacing: -0.1px; 256 | color: #00a6b5; 257 | content: '•'; 258 | display: inline-block; 259 | left: -3px; 260 | margin: 0; 261 | position: absolute; 262 | vertical-align: middle; 263 | } 264 | .filter .link-box .link-text { 265 | font-size: 2.2em; 266 | line-height: 1.22727273em; 267 | margin-top: 1.22727273em; 268 | margin-bottom: 0; 269 | letter-spacing: -0.1px; 270 | font-family: 'Merriweather-Regular', 'Lora', Georgia, serif; 271 | font-weight: normal; 272 | -webkit-transition: color 0.2s; 273 | -moz-transition: color 0.2s; 274 | -ms-transition: color 0.2s; 275 | -o-transition: color 0.2s; 276 | float: none; 277 | margin: 0; 278 | padding: 0; 279 | vertical-align: middle; 280 | } 281 | .filter .link-box .link-text { 282 | color: #fff; 283 | } 284 | .filter .link-box:before { 285 | display: inline-block; 286 | } 287 | .filter #all .link-box { 288 | position: relative; 289 | padding-left: 10%; 290 | } 291 | .filter #all .link-box:before { 292 | background: #999; 293 | content: ''; 294 | height: 100%; 295 | left: 0; 296 | position: absolute; 297 | width: 1px; 298 | } 299 | .filter #all .link-box .link-text { 300 | font-family: 'Merriweather-Italic', 'Lora', Georgia, serif; 301 | } 302 | .filter li:first-child .link-box:before { 303 | display: none; 304 | } 305 | -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/translations: -------------------------------------------------------------------------------- 1 | define({"translation_statusbar_signout":"Sign out","translation_statusbar_settings":"Settings","translation_statusbar_signedin":"Your account","translation_statusbar_signedout":"Sign in","translation_outofcapacity":"","translation_signedin_anonymous":"You are signed in","translation_signedin":"You are signed in as %username%","translation_agewithpurpose":"Sorry, it looks like you\u2019re not the right age to %purpose%.","translation_age":"Sorry, it looks like you\u2019re not the right age to use this.","translation_missingagewithpurpose":"We need a bit more info before you can %purpose%. Add info<\/a>.","translation_missingage":"We need a bit more info before you can use this. Add info<\/a>.","translation_missingdisplaynamewithpurpose":"You need a display name to %purpose%. Create a display name<\/a>.","translation_missingdisplayname":"You need a display name to use this. Create a display name<\/a>.","translation_missingdetailswithpurpose":"We need a bit more info before you can %purpose%. Add info<\/a>.","translation_missingdetails":"We need a bit more info before you can use this. Add info<\/a>.","translation_missingemailwithpurpose":"We need a bit more info before you can %purpose%. Add info<\/a>.","translation_missingemail":"We need a bit more info before you can use this. Add info<\/a>.","translation_guardianconsentrequiredwithpurpose":"You need permission from a parent or guardian to %purpose%. Ask for permission<\/a>.","translation_guardianconsentrequired":"You need permission from a parent or guardian to use this. Ask for permission<\/a>.","translation_emailnotvalidatedwithpurpose":"You need to verify your email address before you can %purpose%. Check your inbox for the email we sent when you registered. Can't find it? Resend the email from here<\/a>.","translation_emailnotvalidated":"You need to verify your email address before you can use this. Check your inbox for the email we sent when you registered. Can't find it? Resend the email from here<\/a>.","translation_buttons_with_purpose":"Register<\/a> to %purpose%","translation_buttons":"Register<\/a>","translation_register":"","translation_bbcid":"","translation_signout":"Sign out","translation_signedout":"","locale":"en-GB"}); -------------------------------------------------------------------------------- /dist/BBC - Earth - Conservation success for otters on the brink_files/var=ccauds: -------------------------------------------------------------------------------- 1 | ccauds={"Profile": {"tpid":"","pid":"","Audiences": {"Audience":[]}}}; -------------------------------------------------------------------------------- /dist/Brian-Eno.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/Brian-Eno.jpg -------------------------------------------------------------------------------- /dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2102697_300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2102697_300.jpg -------------------------------------------------------------------------------- /dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2246268_300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2246268_300.jpg -------------------------------------------------------------------------------- /dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2651092_300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2651092_300.jpg -------------------------------------------------------------------------------- /dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2755652_300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2755652_300.jpg -------------------------------------------------------------------------------- /dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2801630_300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2801630_300.jpg -------------------------------------------------------------------------------- /dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2911482_300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/2911482_300.jpg -------------------------------------------------------------------------------- /dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/all-ndg-3138116172.js: -------------------------------------------------------------------------------- 1 | !function(x,d,s,ids,cs,co,cp){var getn="getElementsByTagName",this_script="";var yjs=d[getn](s)[0],Gndg={},getId=function(k){var n=k+"=",q="cookie";var ca=d[q].split(";");for(var i=0;i0)te.push(["override",1]);return te;},hasFeature=function(feature){var script=find_me();if(script===false)return false;var q=parseQuery(script.src);if(typeof q.ndg_flag!="undefined"){var ndg_flags=q.ndg_flag.split(",");if(ndg_flags.indexOf(feature)!==-1){return true;}} 8 | return false;},getExtra=function(){var ext=getOP();var te=get_canonical();ext=ext.concat(te);return ext;},getUrl=function(){var url=x.location.href;var isInIframe=(parent!==window);if(isInIframe){url=d.referrer;} 9 | return url;};var c=cs;x["FallsmGlobalObj"]=c;x[c]=x[c]||function(){(c.q=c.q||[]).push(arguments)};c=x[c];c("url",getUrl());c("uid","6463634279");c("ctz","Europe/London");c("referrer",d.referrer);var ext=getExtra();for(var i=0;ia()/2&&!1===t.scroll_fired&&(t.scroll_fired=!0,"function"==typeof fbq&&fbq("trackCustom","scrollPercent",{scrollPercent:"50"}))},attention_6:function(e){e>=36&&!1===t.attention_fired&&(t.attention_fired=!0,"function"==typeof fbq&&fbq("trackCustom","Attention",{Attention:"0.6"}))}}}}},p=new function(){this.Utils={Tld:function(){var t=location.host,e=t.split(".");return e.length,"www"!=e[0]?t:e.slice(1).join(".")},getDate:function(t){var e=new Date;return e.setMinutes(e.getMinutes()+t),e},repeat:function(t,e){for(var n="",i=0;i=n[i][0]&&e<=n[i][1])return n[i][0]-1}return 0},scroll:function(t){var e=this.get_scroll_height_adjuster(t);if(t-=e,_.event.scroll(t),this.globals.last_scroll_pos=t,t=(t+c())/(a()-e),this.data.max_scroll=0;o--)e.call(t,n[o])}(t,function(t){parseInt(t)t-e/1.5&&i/5*e=20&&f.notifyInactive(),!f.timer.paused()){var t=n.getCollectData();o(t,g.collectStatic()),n.engine.trigger({data:JSON.stringify(t)}),g.clean()}},i.message_rate)},init_mouse_events:function(){s("mousemove",{},t,function(t){f.registerMouseMove(t)}),s("mouseup",{},t,function(t){f.registerClick(t)}),setInterval(function(){f.getScrollPosition()},300)},fix_inifinity_scroll_heights:function(){void 0===t.ndg_tracker_info&&(t.ndg_tracker_info={});var e=function(e,n){0==e?(t.ndg_tracker_info.doc_heights=[],t.ndg_tracker_info.doc_heights.push([0,n]),t.ndg_tracker_info.old_doc_height=n):e=1e3&&e(i,n)},1e3):e(i,n)},init:function(){var t,n,o,r,s,a;this.engine=h.rimg(),t=i.uuid_c_key,n=i.cs_uuid,o=p.Utils.getDate(1051200).toGMTString(),r="/",s=p.Utils.Tld(),e.cookie=t+"="+escape(n)+(o?";expires="+o:"")+(r?";path="+r:"")+(s?";domain="+s:"")+(a&&1==a?"; secure":""),this.fix_inifinity_scroll_heights(),this.init_session_timer(),this.init_mouse_events(),(new u).send("//cdn.ndg.io/shared/extra.min.js"),i.disallowed_lr.includes(i.owner_id)||v.notification_imgs()}});!function(){if("undefined"==typeof ndg)return!1;var t=function(){r(ndg.q,function(t,e){var n,o,s,a,c,u,d="",f="",h="",_=[];if("object"==typeof e[0]?(d=e[0].name||"",f=e[0].content_id||"","string"==typeof e[1]&&_.push(e[1])):"string"==typeof e[0]&&(d=e[0]||"",f=e[1]||""),"function"==typeof e[1]?h=e[1]:"function"==typeof e[2]&&(h=e[2]),e.length>2)for(var p=2;p 3 | -------------------------------------------------------------------------------- /dist/The Galaxy Next Door May Be Blowing Giant Double Bubbles_files/init-v2.ngsversion.5afa3b6c.js: -------------------------------------------------------------------------------- 1 | require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 10 ) { 19 | ZERG['running'] = 0; 20 | } 21 | setTimeout( loadWidget, 200 ); 22 | return false; 23 | } else if ( ZERG['running'] == 0 ) { 24 | ZERG['running'] = 1; 25 | } 26 | JSONP.get( 'https://www.zergnet.com/output.js', {id:widgetId,time:timestamp}, function(data){ 27 | ZERG['running'] = 0; 28 | if (typeof window.opera != 'undefined') { 29 | document.write(data); 30 | } else { 31 | node.innerHTML = data; 32 | } 33 | }); 34 | }; 35 | 36 | if ( node && typeof( node.className ) !== "undefined" && node.className.indexOf("widget-loaded") === -1 ) { 37 | if ( node.className ) { 38 | node.className += " "; 39 | } 40 | node.className += "widget-loaded"; 41 | 42 | loadWidget(); 43 | } else if ( !node && typeof( console ) !== "undefined" && typeof("console.log") !== "undefined" ) { 44 | console.log("ZERG CONTAINER MISSING: "+nodeId); 45 | } else if ( node && typeof( node.className ) !== "undefined" && node.className.indexOf("widget-loaded") !== -1 && typeof( console ) !== "undefined" && typeof("console.log") !== "undefined" ) { 46 | console.log("ZERG CONTAINER ALREADY LOADED: "+nodeId); 47 | } 48 | })(); 49 | 50 | -------------------------------------------------------------------------------- /dist/atlantic-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 24 | 28 | 29 | 30 | 52 | 54 | 55 | 57 | image/svg+xml 58 | 60 | 61 | 62 | 63 | 64 | 69 | 72 | 74 | 77 | 80 | 85 | 86 | 89 | 94 | 95 | 98 | 103 | 104 | 107 | 112 | 113 | 116 | 121 | 122 | 125 | 130 | 131 | 134 | 139 | 140 | 143 | 148 | 149 | 152 | 157 | 158 | 161 | 166 | 167 | 168 | 169 | 172 | 177 | 178 | 179 | 180 | 181 | -------------------------------------------------------------------------------- /dist/bbc-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 12 | 15 | 16 | 19 | 22 | 23 | 26 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /dist/bolt-natgeo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape Copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /dist/bolt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Shape Copy 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /dist/cnn-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/cnn-logo.jpg -------------------------------------------------------------------------------- /dist/cnn-logo.svg: -------------------------------------------------------------------------------- 1 | CNNE -------------------------------------------------------------------------------- /dist/cnn-news-img.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/cnn-news-img.jpg -------------------------------------------------------------------------------- /dist/cnn.css: -------------------------------------------------------------------------------- 1 | body { 2 | 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | .container { 8 | max-width: 600px; 9 | margin: 0 auto; 10 | } 11 | 12 | h1 { 13 | font-weight: normal; 14 | font-size: 48px; 15 | line-height: 51px; 16 | margin-top: 40px; 17 | color: #666; 18 | } 19 | 20 | header { 21 | background: #c90813; 22 | height: 80px; 23 | } 24 | 25 | .pflogo { 26 | background-image: url(cnn-logo.jpg); 27 | background-repeat: no-repeat; 28 | display: inline-block; 29 | width: 120px; 30 | height: 80px; 31 | background-size: 80px; 32 | background-position: left center; 33 | } 34 | 35 | p { 36 | line-height: 140%; 37 | } -------------------------------------------------------------------------------- /dist/cnn.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ninguna elección puede ser normal en Venezuela 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 18 | 19 | 20 |
21 |
22 |

23 | Ninguna elección puede ser normal en Venezuela 24 | 25 |

26 | 27 |
28 |
29 |

Nota del editor: Pedro Brieger es periodista y sociólogo, autor de más de siete libros y colaborador en publicaciones sobre temas internacionales. Actualmente se desempeña como director de NODAL y es columnista de CNN en Español. Las opiniones expresadas en este artículo corresponden exclusivamente a su autor.

30 |

(CNN Español) – Los procesos electorales en Venezuela se han internacionalizado de tal manera que es muy difícil pensarlos únicamente en clave venezolana. Está claro que el significado político que tienen excede la puja entre oficialismo y oposición como podría ser en cualquier otro país de América Latina.

31 |

32 |

Para el gobierno de EE.UU. acabar con el chavismo como movimiento es un eje central de su política exterior. Es lo que permite comprender por qué tantos de sus funcionarios –y exfuncionarios– opinan constantemente sobre Venezuela. Pocos días antes de las elecciones, Roger Noriega, exembajador de EE.UU. en la OEA, la Organización de Estados Americanos, publicó un artículo en el New York Times alentando la destitución del presidente Nicolás Maduro para luego convocar a elecciones que entonces sí, según Noriega, serían “libres”.

33 |

El secretario general de la OEA, Luis Almagro, utiliza casi a diario la herramienta de Twitter para criticar al gobierno de Nicolás Maduro; por eso sus menciones sobre Venezuela exceden largamente a las de cualquier otro país. Y, al margen de los organismos regionales, se creó el Grupo de Lima, compuesto por catorce países de América para aislar al gobierno de Nicolás Maduro, y que también pidió suspender las elecciones presidenciales.

34 |

En este contexto, ninguna elección puede ser normal en Venezuela.

35 |

La diferencia con los tres procesos electorales de 2017 es que cesaron las violentas protestas callejeras impulsadas por la oposición y cuyo fracaso produjo profundas diferencias en sus filas. Un sector de la oposición decidió participar de las elecciones del 20 de mayo y presentar candidatos a la presidencia, mientras que otro las desconoce y las considera fraudulentas.

36 |

Henri Falcón, uno de los principales referentes opositores, decidió participar en estas elecciones porque consideró que la violencia en 2017 había fracasado en su intento de derrocar al gobierno, que la abstención no ofrecía ninguna salida e implicaba quedarse a la espera de una intervención extranjera que sería desastrosa y podría llevar a una guerra fratricida.

37 |

Estas elecciones entrañan una paradoja. El opositor Falcón está convencido de que ganará porque –según él– el 80% de la población está en contra de Maduro, como siempre dice toda la oposición. Sin embargo, varios gobiernos latinoamericanos afirman que desconocerán el resultado de las elecciones. ¿Y si gana Falcón? 38 |

39 | 40 | 41 | 42 |
43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /dist/delta5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/delta5.jpg -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ContextCards 9 | 10 | 11 | 12 | 13 |
14 | 15 |

Context Cards

16 | 27 |

28 | ContextCards are a way to embed content from Wikipedia Projects. As the name suggests, ContextCards are a great way to provide context from Wikipedia on any website. They reduce the cost of exploration and makes up for a better reading experience. Content publishers around the world will be able to use these nifty little cards on their own platform.

29 |

30 | It's also very easy to embed this service on any platform. Just include a script from our CDN and mark up the links you would like to see previews on. Read our technical documentation 31 |

32 | 33 | 34 |

35 |
36 | 37 |

38 |

Blast (magazine)

39 |

40 | Blast was the short-lived literary magazine of the Vorticist movement in Britain. Two editions were published: the first on 2 July 1914 (dated 20 June 1914, but publication was delayed) and published with a bright pink cover, referred to by Ezra Pound as the "great MAGENTA cover'd opusculus"; and the second a year later on 15 July 1915. Both editions were written primarily by Wyndham Lewis. The magazine is emblematic of the modern art movement in England, and recognised as a seminal text of pre-war 20th-century modernism.The magazine originally cost 2/6. 41 |

42 |
43 | 44 |

Publisher Demos

45 | 59 | 60 |

61 | Logos from Wikimedia Commons and Wikipedia
62 | - https://en.wikipedia.org/wiki/File:Pitchfork_logo.svg
63 | - https://commons.wikimedia.org/wiki/File:BBC.svg
64 | - https://commons.wikimedia.org/wiki/File:CNN.svg 65 |

66 | 67 | 68 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /dist/natgeo-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | ng-logo 8 | 9 | 10 | 11 | 13 | 15 | 16 | 17 | 21 | 23 | 25 | 26 | 27 | 28 | 32 | 34 | 38 | 42 | 46 | 48 | 52 | 53 | 54 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /dist/pitchfork-logo-bw.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/pitchfork-logo-bw.png -------------------------------------------------------------------------------- /dist/pitchfork-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/dist/pitchfork-logo.png -------------------------------------------------------------------------------- /dist/pitchfork.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Charis SIL"; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | .container { 8 | max-width: 600px; 9 | margin: 0 auto; 10 | } 11 | 12 | h1 { 13 | font-weight: bolder; 14 | font-size: 32px; 15 | line-height: 35px; 16 | margin-top: 40px; 17 | } 18 | 19 | header { 20 | background: black; 21 | height: 80px; 22 | } 23 | 24 | .pflogo { 25 | background-image: url(pitchfork-logo-bw.png); 26 | background-repeat: no-repeat; 27 | display: inline-block; 28 | width: 120px; 29 | height: 80px; 30 | background-size: 120px; 31 | background-position: left center; 32 | } -------------------------------------------------------------------------------- /dist/pitchfork.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pitchfork - News in Brief: Delta 5, Michael Yonkers, Kings of Leon, Rose Elinor Dougall 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 17 | 18 | 19 |
20 |
21 |

22 | News in Brief: Delta 5, Michael Yonkers, Kings of Leon, Rose Elinor Dougall 23 | 24 |

25 |
26 | 27 | 28 |

29 | -- On November 10, Kill Rock Stars will celebrate the 30th anniversary of Delta 5's taunting, scratchy postpunk classic "Mind Your Own Business". The label is reissuing the song as a limited edition 7" single, with the album version on one side and a remix from Man Ray, otherwise known as former New Order/Joy Division bassist Peter Hook, on the flip. The label is also reissuing the band's career-spanning compilation Singles and Sessions 1979-81, this time including the Man Ray remix, plus remixes by Deerhoof and Monnei Lamar. 30 |

31 |

32 | -- Before becoming an experimental psych cult hero, Michael Yonkers led a garage rock group called Michael and the Mumbles. On November 3, De Stijl will reissue that band's self-titled 1966 album on vinyl. Hear a track from it here. 33 |

34 |

35 | -- Hugely successful butt-rockers Kings of Leon have entered the live DVD stage of their career. On November 10, the band will release Live at the O2 London, England, which documents a show in June 2009. The Blu-ray version is due November 24. 36 |

37 | 38 |
39 |
40 | 41 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /dist/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 |

Context cards UI tests

14 |
15 | 16 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /dist/wikifact.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | body { 4 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Lato, "Oxygen-Sans", Ubuntu, Cantarell, "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | 6 | } 7 | 8 | body.demoPage { 9 | max-width: 980px; 10 | margin: 0 auto; 11 | margin-top: 40px; 12 | background: #F2F4F7; 13 | } 14 | 15 | .demoPage a { 16 | color: #36c; 17 | text-decoration: none; 18 | } 19 | 20 | 21 | a.wikiPreview { 22 | border-bottom: 1px dotted #666; 23 | color: #000 !important; 24 | margin-right: 2px; 25 | text-decoration: none !important; 26 | } 27 | 28 | a.wikiPreview:after { 29 | content: " "; 30 | height: 16px; 31 | width: 16px; 32 | position: relative; 33 | right: -3px; 34 | top: 2px; 35 | 36 | background-image: url('bolt.svg'); 37 | background-repeat: no-repeat; 38 | background-size: 10px 15px; 39 | 40 | display: inline-block; 41 | 42 | } 43 | 44 | 45 | .demoPage .container { 46 | background: #fff; 47 | padding: 40px 60px; 48 | border-radius: 2px; 49 | box-shadow: 0px 2px 2px rgba(0,0,0,0.1); 50 | padding-top: 60px; 51 | padding-bottom: 100px; 52 | } 53 | 54 | .wikiFactBrand { 55 | display: inline-block; 56 | background-image: url(wikifact-header-branding.svg); 57 | background-repeat: no-repeat; 58 | text-indent: -9999px; 59 | width: 300px; 60 | height: 80px; 61 | background-size: 300px; 62 | } 63 | 64 | .demoPage p { 65 | line-height: 150%; 66 | 67 | } 68 | 69 | .demoPage p.lead { 70 | font-size: 120%; 71 | font-weight: 300; 72 | } 73 | 74 | .demoPage h1.wikiTitle { 75 | font-family: "Charter"; 76 | font-size: 28px; 77 | font-weight: bolder; 78 | font-style: italic; 79 | } 80 | 81 | .pubTiles { 82 | margin: 0; 83 | padding: 0; 84 | } 85 | 86 | .pubTiles::after { 87 | content: ""; 88 | clear: both; 89 | display: table; 90 | } 91 | 92 | .pubTiles li { 93 | float: left; 94 | list-style: none; 95 | margin-right: 2%; 96 | width: 23%; 97 | 98 | } 99 | .pubTiles li a { 100 | display: block; 101 | height: 100px; 102 | 103 | text-indent: -9999px; 104 | border: 1px solid #ccc; 105 | border-radius: 2px; 106 | 107 | } 108 | .pubTiles li a:hover { 109 | box-shadow: 0px 10px 20px rgba(0,0,0,0.1); 110 | -webkit-transition: box-shadow 0.3s; /* Safari */ 111 | 112 | } 113 | .pubTiles li.bbc a { 114 | background-image: url(bbc-logo.svg); 115 | background-repeat: no-repeat; 116 | background-position: center center; 117 | background-size: 100px; 118 | } 119 | 120 | .pubTiles li.natgeo a { 121 | background-image: url(natgeo-logo.svg); 122 | background-repeat: no-repeat; 123 | background-position: center center; 124 | background-size: 120px; 125 | } 126 | 127 | .pubTiles li.pitchfork a { 128 | background-image: url(pitchfork-logo.png); 129 | background-repeat: no-repeat; 130 | background-position: center center; 131 | background-size: 120px; 132 | } 133 | .pubTiles li.cnn a { 134 | background-image: url(cnn-logo.jpg); 135 | background-repeat: no-repeat; 136 | background-position: center center; 137 | background-size: 70px; 138 | } 139 | 140 | 141 | .bbcPage .mwe-popups .mwe-popups-extract { 142 | font-size: 8px !important; 143 | 144 | } 145 | 146 | .mwe-popups .mwe-popups-extract { 147 | padding-top: 10px !important; 148 | } 149 | .mwe-popups .mwe-popups-extract:before { 150 | content: " "; 151 | background-image: url(wikifact-branding.svg); 152 | background-size: 80px; 153 | display: inline-block; 154 | height: 20px; 155 | width: 100px; 156 | background-repeat: no-repeat; 157 | background-position: left center; 158 | } 159 | 160 | .demoPage .mwe-popups .mwe-popups-extract { 161 | font-size: 15px; 162 | line-height: 15px; 163 | 164 | } 165 | 166 | .natgeoPage .mwe-popups a.mwe-popups-extract, 167 | .natgeoPage .mwe-popups a.mwe-popups-extract:visited, 168 | .natgeoPage .mwe-popups a.mwe-popups-extract:active { 169 | border-bottom: 0px solid transparent !important; 170 | } 171 | 172 | .footnote { 173 | font-size: 12px; 174 | color: #999; 175 | margin-top: 20px; 176 | 177 | } -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src" 5 | ], 6 | "elm-version": "0.19.1", 7 | "dependencies": { 8 | "direct": { 9 | "elm/browser": "1.0.2", 10 | "elm/core": "1.0.5", 11 | "elm/html": "1.0.0", 12 | "elm/http": "1.0.0", 13 | "elm/json": "1.1.3" 14 | }, 15 | "indirect": { 16 | "elm/time": "1.0.0", 17 | "elm/url": "1.0.0", 18 | "elm/virtual-dom": "1.0.3" 19 | } 20 | }, 21 | "test-dependencies": { 22 | "direct": {}, 23 | "indirect": {} 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "context-cards", 3 | "version": "2.1.3", 4 | "description": "", 5 | "main": "dist/context-cards.js", 6 | "scripts": { 7 | "deploy": "make dist && gh-pages -d dist" 8 | }, 9 | "files": [ 10 | "dist/context-cards.js", 11 | "dist/bundle.js" 12 | ], 13 | "keywords": [], 14 | "author": "Joaquin Oltra (http://chimeces.com)", 15 | "license": "ISC", 16 | "dependencies": {}, 17 | "devDependencies": { 18 | "gh-pages": "^2.0.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /popup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joakin/context-cards/64dffd73398ef6ca0cd008477f6665ad1884773b/popup.png -------------------------------------------------------------------------------- /src/Card.elm: -------------------------------------------------------------------------------- 1 | module Card exposing (ClientRect, Events, Link, styles, view) 2 | 3 | import Browser.Dom exposing (Viewport) 4 | import Data exposing (Dir(..), Summary, Thumbnail, dirToString) 5 | import Html exposing (..) 6 | import Html.Attributes exposing (attribute, class, classList, dir, href, src, style, target) 7 | import Html.Events exposing (onMouseEnter, onMouseLeave) 8 | import Html.Lazy as L 9 | import Json.Decode as D 10 | 11 | 12 | type alias Link = 13 | { lang : String 14 | , title : String 15 | , domElement : D.Value 16 | , rect : ClientRect 17 | , viewport : Viewport 18 | , contentDir : Dir 19 | } 20 | 21 | 22 | type alias ClientRect = 23 | { x : Float 24 | , y : Float 25 | , width : Float 26 | , height : Float 27 | , top : Float 28 | , bottom : Float 29 | , left : Float 30 | , right : Float 31 | } 32 | 33 | 34 | type PreviewKind 35 | = Horizontal 36 | | Vertical 37 | 38 | 39 | type alias Dimensions = 40 | { kind : PreviewKind 41 | , top : Float 42 | , left : Float 43 | , constrainedSize : { styleAttr : String, value : Float } 44 | , extractWidth : Float 45 | , extractMaxHeight : String 46 | , extractOrder : Int 47 | , thumbnailWidth : Float 48 | , thumbnailHeight : Float 49 | } 50 | 51 | 52 | type alias Events msg = 53 | { mouseEnter : Link -> msg, mouseLeave : Link -> msg } 54 | 55 | 56 | view : Events msg -> Link -> Maybe Summary -> Bool -> Html msg 57 | view events link maybeSummary dismissed = 58 | case maybeSummary of 59 | Just summary -> 60 | let 61 | dimensions = 62 | getDimensions link summary 63 | 64 | eventAttrs = 65 | if dismissed then 66 | [] 67 | 68 | else 69 | [ onMouseEnter (events.mouseEnter link) 70 | , onMouseLeave (events.mouseLeave link) 71 | ] 72 | in 73 | div 74 | ([ classList 75 | [ ( "ContextCard", True ) 76 | , ( "ContextCardDismissed", dismissed ) 77 | ] 78 | , dir <| dirToString summary.dir 79 | , style "top" (px dimensions.top) 80 | , style "left" (px dimensions.left) 81 | ] 82 | ++ eventAttrs 83 | ) 84 | [ L.lazy3 viewSummary link dimensions summary ] 85 | 86 | Nothing -> 87 | text "" 88 | 89 | 90 | styles : String 91 | styles = 92 | """ 93 | @keyframes contextCardsFadeIn { 94 | from { 95 | opacity: 0; 96 | transform: translate3d(0, 50%, 0); 97 | } 98 | 99 | to { 100 | opacity: 1; 101 | transform: translate3d(0, 0, 0); 102 | } 103 | } 104 | @keyframes contextCardsFadeOut { 105 | from { 106 | opacity: 1; 107 | transform: translate3d(0, 0, 0); 108 | } 109 | 110 | to { 111 | opacity: 0; 112 | transform: translate3d(0, 50%, 0); 113 | } 114 | } 115 | .ContextCard, .ContextCard * { 116 | box-sizing: border-box; 117 | } 118 | 119 | .ContextCard { 120 | position: absolute; 121 | z-index: 10000; 122 | background-color: white; 123 | box-shadow: 0 30px 90px -20px rgba( 0, 0, 0, 0.3 ), 0 0 1px #a2a9b1; 124 | animation-name: contextCardsFadeIn; 125 | animation-duration: 300ms; 126 | animation-fill-mode: both; 127 | border-radius: 2px; 128 | overflow: hidden; 129 | } 130 | .ContextCard.ContextCardDismissed { 131 | animation-name: contextCardsFadeOut; 132 | pointer-events: none; 133 | } 134 | .ContextCardLogo { 135 | height: 15px; 136 | } 137 | .ContextCardSummary { 138 | display: flex; 139 | } 140 | .ContextCardExtract { 141 | padding: 1em; 142 | overflow: hidden; 143 | position: relative; 144 | font-size: 14px; 145 | line-height: 1.4; 146 | } 147 | .ContextCardExtract p { 148 | margin: 0.4em 0; 149 | } 150 | .ContextCardExtract:before, .ContextCardExtract:after { 151 | content: ''; 152 | display: block; 153 | background: linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 1) 80%), linear-gradient(to bottom right, rgba(255, 255, 255, 0) 80%, rgba(255, 255, 255, 1) 100%); 154 | position: absolute; 155 | bottom: 0px; 156 | left: 1px; 157 | right: 1px; 158 | height: 3em; 159 | } 160 | .ContextCardThumbnail { 161 | flex-shrink: 0; 162 | background-position: center center; 163 | background-size: 110%; 164 | box-shadow: 0 0 1px #a2a9b1; 165 | } 166 | """ 167 | 168 | 169 | innerHtml : String -> Attribute msg 170 | innerHtml html = 171 | attribute "inner-html" html 172 | 173 | 174 | px : Float -> String 175 | px n = 176 | String.fromFloat n ++ "px" 177 | 178 | 179 | getDimensions : Link -> Summary -> Dimensions 180 | getDimensions link { thumbnail } = 181 | let 182 | rect = 183 | link.rect 184 | 185 | viewport = 186 | link.viewport.viewport 187 | 188 | isHorizontalPreview = 189 | thumbnail 190 | |> Maybe.map (\t -> t.height > t.width) 191 | |> Maybe.withDefault True 192 | 193 | kind = 194 | if isHorizontalPreview then 195 | Horizontal 196 | 197 | else 198 | Vertical 199 | 200 | horizontalPreviewHeight = 201 | 250 202 | 203 | verticalPreviewWidth = 204 | 320 205 | 206 | hasThumbnail = 207 | case thumbnail of 208 | Just _ -> 209 | True 210 | 211 | Nothing -> 212 | False 213 | 214 | ( thumbnailMaxSize, thumbnailOtherDimension ) = 215 | case thumbnail of 216 | Just thumb -> 217 | if isHorizontalPreview then 218 | ( horizontalPreviewHeight 219 | , thumb.width * horizontalPreviewHeight / thumb.height 220 | ) 221 | 222 | else 223 | ( verticalPreviewWidth 224 | , thumb.height * verticalPreviewWidth / thumb.width 225 | ) 226 | 227 | Nothing -> 228 | -- Won't be used 229 | ( 0, 0 ) 230 | 231 | ( thumbnailWidth, thumbnailHeight ) = 232 | if isHorizontalPreview then 233 | ( thumbnailOtherDimension, thumbnailMaxSize ) 234 | 235 | else 236 | ( thumbnailMaxSize, thumbnailOtherDimension ) 237 | 238 | constrainedSize = 239 | if isHorizontalPreview then 240 | { styleAttr = "max-height", value = horizontalPreviewHeight } 241 | 242 | else 243 | { styleAttr = "max-width", value = verticalPreviewWidth } 244 | 245 | extractWidth = 246 | if isHorizontalPreview then 247 | if hasThumbnail then 248 | 260 249 | 250 | else 251 | 320 252 | 253 | else 254 | verticalPreviewWidth 255 | 256 | verticalExtractMaxHeight = 257 | 190 258 | 259 | extractMaxHeight = 260 | if isHorizontalPreview then 261 | "100%" 262 | 263 | else 264 | px verticalExtractMaxHeight 265 | 266 | extractOrder = 267 | if isHorizontalPreview then 268 | 0 269 | 270 | else 271 | 1 272 | 273 | ( maxWidth, _ ) = 274 | if isHorizontalPreview then 275 | ( extractWidth + thumbnailWidth, horizontalPreviewHeight ) 276 | 277 | else 278 | ( verticalPreviewWidth, verticalExtractMaxHeight + thumbnailHeight ) 279 | 280 | ( topPosition, leftPosition ) = 281 | ( rect.top + viewport.y + rect.height 282 | , case link.contentDir of 283 | LTR -> 284 | min (rect.left + viewport.x) 285 | (viewport.x + viewport.width - maxWidth) 286 | 287 | RTL -> 288 | max (rect.left + viewport.x - (maxWidth - rect.width)) 289 | viewport.x 290 | ) 291 | in 292 | { kind = kind 293 | , top = topPosition 294 | , left = leftPosition 295 | , constrainedSize = constrainedSize 296 | , extractWidth = extractWidth 297 | , extractMaxHeight = extractMaxHeight 298 | , extractOrder = extractOrder 299 | , thumbnailWidth = thumbnailWidth 300 | , thumbnailHeight = thumbnailHeight 301 | } 302 | 303 | 304 | viewLogo : Html msg 305 | viewLogo = 306 | let 307 | logoUrl = 308 | "https://en.m.wikipedia.org/static/images/mobile/copyright/wikipedia-wordmark-en.svg" 309 | in 310 | img [ class "ContextCardLogo", src logoUrl ] [] 311 | 312 | 313 | viewSummary : Link -> Dimensions -> Summary -> Html msg 314 | viewSummary link dimensions ({ thumbnail } as summary) = 315 | let 316 | { constrainedSize, kind, extractOrder, extractWidth, extractMaxHeight } = 317 | dimensions 318 | 319 | summaryStyles = 320 | [ style "flex-direction" 321 | (case kind of 322 | Horizontal -> 323 | "row" 324 | 325 | Vertical -> 326 | "column" 327 | ) 328 | , style constrainedSize.styleAttr (px constrainedSize.value) 329 | ] 330 | 331 | extractStyles = 332 | [ style "order" (String.fromInt extractOrder) 333 | , style "width" (px extractWidth) 334 | , style "max-height" extractMaxHeight 335 | ] 336 | 337 | url = 338 | wikipediaUrl link 339 | in 340 | div 341 | (class "ContextCardSummary" :: summaryStyles) 342 | [ div 343 | (class "ContextCardExtract" :: extractStyles) 344 | [ a [ href url, target "_blank" ] [ viewLogo ] 345 | , div [ innerHtml summary.contentHtml ] [ text summary.contentText ] 346 | ] 347 | , case thumbnail of 348 | Just thumb -> 349 | viewThumbnail url dimensions thumb 350 | 351 | Nothing -> 352 | text "" 353 | ] 354 | 355 | 356 | viewThumbnail : String -> Dimensions -> Thumbnail -> Html msg 357 | viewThumbnail url dimensions thumbnail = 358 | a 359 | [ href url 360 | , target "_blank" 361 | , class "ContextCardThumbnail" 362 | , style "background-image" ("url(" ++ thumbnail.source ++ ")") 363 | , style "width" (px dimensions.thumbnailWidth) 364 | , style "height" (px dimensions.thumbnailHeight) 365 | ] 366 | [] 367 | 368 | 369 | wikipediaUrl : Link -> String 370 | wikipediaUrl link = 371 | "https://" ++ link.lang ++ ".wikipedia.org/wiki/" ++ link.title 372 | -------------------------------------------------------------------------------- /src/ContextCards.elm: -------------------------------------------------------------------------------- 1 | port module ContextCards exposing (main) 2 | 3 | import Browser 4 | import Browser.Dom exposing (Viewport) 5 | import Card exposing (ClientRect, Link) 6 | import Data exposing (Dir(..), Summary, decodeSummary, dirFromString) 7 | import Html exposing (Html, node, text) 8 | import Html.Attributes exposing (id) 9 | import Html.Keyed as Keyed 10 | import Html.Lazy as L 11 | import Http exposing (Error(..)) 12 | import Json.Decode as D 13 | import Process 14 | import Task 15 | 16 | 17 | main : Program () Model Msg 18 | main = 19 | Browser.element 20 | { init = init 21 | , subscriptions = subscriptions 22 | , update = 23 | \msg model -> 24 | -- let 25 | -- _ = 26 | -- Debug.log "msg" msg 27 | -- in 28 | -- Debug.log "result" <| 29 | update msg model 30 | , view = view 31 | } 32 | 33 | 34 | type Model 35 | = Idle (Maybe ( Link, Summary )) 36 | | Active Link InteractionStatus (Maybe Summary) 37 | 38 | 39 | type InteractionStatus 40 | = OnPreview 41 | | LeavingPreview 42 | 43 | 44 | type Msg 45 | = HoverIn Link 46 | | HoverOutStart Link 47 | | HoverOutEnd Link 48 | | Fetch Link 49 | | SummaryResponse Link (Result Http.Error Summary) 50 | 51 | 52 | init : () -> ( Model, Cmd Msg ) 53 | init () = 54 | ( Idle Nothing 55 | , Cmd.none 56 | ) 57 | 58 | 59 | update : Msg -> Model -> ( Model, Cmd Msg ) 60 | update msg model = 61 | case ( model, msg ) of 62 | ( Active currentLink OnPreview Nothing, Fetch link ) -> 63 | if currentLink.domElement == link.domElement then 64 | ( model 65 | , Http.send (SummaryResponse link) (getSummary link.lang link.title) 66 | ) 67 | 68 | else 69 | ( model, Cmd.none ) 70 | 71 | ( Active currentLink interactionStatus Nothing, SummaryResponse link response ) -> 72 | if currentLink.domElement == link.domElement then 73 | case response of 74 | Ok summary -> 75 | ( Active link interactionStatus (Just summary), renderHTML () ) 76 | 77 | Err err -> 78 | ( Idle Nothing, log ("Request failed\n" ++ requestErrorToString err) ) 79 | 80 | else 81 | ( model, Cmd.none ) 82 | 83 | ( Active currentLink OnPreview summary, HoverOutStart link ) -> 84 | if currentLink.domElement == link.domElement then 85 | ( Active link LeavingPreview summary, abandonTimeout (HoverOutEnd link) ) 86 | 87 | else 88 | ( model, Cmd.none ) 89 | 90 | ( Active currentLink LeavingPreview maybeSummary, HoverOutEnd link ) -> 91 | if currentLink.domElement == link.domElement then 92 | ( maybeSummary 93 | |> Maybe.map (\summary -> Idle (Just ( link, summary ))) 94 | |> Maybe.withDefault (Idle Nothing) 95 | , Cmd.none 96 | ) 97 | 98 | else 99 | ( model, Cmd.none ) 100 | 101 | ( Active currentLink _ summary, HoverIn link ) -> 102 | if currentLink.domElement == link.domElement then 103 | ( Active currentLink OnPreview summary, Cmd.none ) 104 | 105 | else 106 | ( Active link OnPreview Nothing, fetchTimeout link ) 107 | 108 | ( Idle (Just ( oldLink, summary )), HoverIn link ) -> 109 | if oldLink.domElement == link.domElement then 110 | ( Active link OnPreview (Just summary), renderHTML () ) 111 | 112 | else 113 | ( Active link OnPreview Nothing, fetchTimeout link ) 114 | 115 | ( Idle Nothing, HoverIn link ) -> 116 | ( Active link OnPreview Nothing, fetchTimeout link ) 117 | 118 | ( _, _ ) -> 119 | ( model, Cmd.none ) 120 | 121 | 122 | abandonTimeout : Msg -> Cmd Msg 123 | abandonTimeout msg = 124 | Process.sleep 300 125 | |> Task.perform (\() -> msg) 126 | 127 | 128 | fetchTimeout : Link -> Cmd Msg 129 | fetchTimeout link = 130 | Process.sleep 150 |> Task.perform (\() -> Fetch link) 131 | 132 | 133 | cardEvents : Card.Events Msg 134 | cardEvents = 135 | { mouseEnter = HoverIn 136 | , mouseLeave = HoverOutStart 137 | } 138 | 139 | 140 | view : Model -> Html Msg 141 | view model = 142 | let 143 | viewLink link summary dismissed = 144 | [ ( link.lang ++ " " ++ link.title 145 | , L.lazy4 Card.view cardEvents link summary dismissed 146 | ) 147 | ] 148 | in 149 | Keyed.node "div" 150 | [ id "ContextCardsContainer" ] 151 | <| 152 | ( "styles", node "style" [] [ text Card.styles ] ) 153 | :: (case model of 154 | Idle Nothing -> 155 | [] 156 | 157 | Idle (Just ( lastLink, lastSummary )) -> 158 | viewLink lastLink (Just lastSummary) True 159 | 160 | Active link _ summary -> 161 | viewLink link summary False 162 | ) 163 | 164 | 165 | subscriptions : Model -> Sub Msg 166 | subscriptions _ = 167 | mouseEvent mouseEventJsonToMouseEvent 168 | 169 | 170 | type alias MouseEventJson = 171 | { kind : String 172 | , lang : String 173 | , title : String 174 | , link : D.Value 175 | , rect : ClientRect 176 | , viewport : Viewport 177 | , contentDir : String 178 | } 179 | 180 | 181 | mouseEventJsonToMouseEvent : MouseEventJson -> Msg 182 | mouseEventJsonToMouseEvent json = 183 | let 184 | link = 185 | { lang = json.lang 186 | , title = json.title 187 | , domElement = json.link 188 | , rect = json.rect 189 | , viewport = json.viewport 190 | , contentDir = dirFromString json.contentDir |> Maybe.withDefault LTR 191 | } 192 | in 193 | if json.kind == "enter" then 194 | HoverIn link 195 | 196 | else 197 | HoverOutStart link 198 | 199 | 200 | port mouseEvent : (MouseEventJson -> msg) -> Sub msg 201 | 202 | 203 | port renderHTML : () -> Cmd msg 204 | 205 | 206 | port log : String -> Cmd msg 207 | 208 | 209 | 210 | -- Data Fetching 211 | 212 | 213 | url : String -> String -> String 214 | url lang title = 215 | "https://" ++ lang ++ ".wikipedia.org/api/rest_v1/page/summary/" ++ title 216 | 217 | 218 | getSummary : String -> String -> Http.Request Summary 219 | getSummary lang title = 220 | Http.get (url lang title) decodeSummary 221 | 222 | 223 | requestErrorToString : Http.Error -> String 224 | requestErrorToString err = 225 | case err of 226 | BadUrl str -> 227 | "Bad URL: " ++ str 228 | 229 | Timeout -> 230 | "Request timed out" 231 | 232 | NetworkError -> 233 | "Network error" 234 | 235 | BadStatus res -> 236 | "Status error: " ++ String.fromInt res.status.code ++ " - " ++ res.status.message 237 | 238 | BadPayload errMsg _ -> 239 | "Payload error:\n" ++ errMsg 240 | -------------------------------------------------------------------------------- /src/Data.elm: -------------------------------------------------------------------------------- 1 | module Data exposing 2 | ( Dir(..) 3 | , Summary 4 | , Thumbnail 5 | , decodeDir 6 | , decodeSummary 7 | , decodeThumbnail 8 | , dirFromString 9 | , dirToString 10 | ) 11 | 12 | import Json.Decode as D exposing (Decoder) 13 | 14 | 15 | type alias Summary = 16 | { title : String 17 | , displayTitle : String 18 | , contentHtml : String 19 | , contentText : String 20 | , thumbnail : Maybe Thumbnail 21 | , dir : Dir 22 | } 23 | 24 | 25 | type alias Thumbnail = 26 | { source : String 27 | , width : Float 28 | , height : Float 29 | } 30 | 31 | 32 | type Dir 33 | = LTR 34 | | RTL 35 | 36 | 37 | decodeSummary : Decoder Summary 38 | decodeSummary = 39 | D.map6 Summary 40 | (D.field "title" D.string) 41 | (D.field "displaytitle" D.string) 42 | (D.field "extract_html" D.string) 43 | (D.field "extract" D.string) 44 | (D.maybe <| D.field "thumbnail" decodeThumbnail) 45 | (D.field "dir" decodeDir) 46 | 47 | 48 | decodeThumbnail : Decoder Thumbnail 49 | decodeThumbnail = 50 | D.map3 Thumbnail 51 | (D.field "source" D.string) 52 | (D.field "width" D.float) 53 | (D.field "height" D.float) 54 | 55 | 56 | decodeDir : Decoder Dir 57 | decodeDir = 58 | D.string 59 | |> D.andThen 60 | (\str -> 61 | dirFromString str 62 | |> Maybe.map D.succeed 63 | |> Maybe.withDefault (D.fail ("Unknown language direction: " ++ str)) 64 | ) 65 | 66 | 67 | dirFromString : String -> Maybe Dir 68 | dirFromString dir = 69 | case dir of 70 | "ltr" -> 71 | Just LTR 72 | 73 | "rtl" -> 74 | Just RTL 75 | 76 | _ -> 77 | Nothing 78 | 79 | 80 | dirToString : Dir -> String 81 | dirToString dir = 82 | case dir of 83 | LTR -> 84 | "ltr" 85 | 86 | RTL -> 87 | "rtl" 88 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Elm output will be prepended to this file. 2 | // ------------------------------------------ 3 | window.ContextCards = (function() { 4 | var LINK_SELECTOR = "a[data-wiki-title]"; 5 | var contextCardsApp = null; 6 | 7 | document.addEventListener("DOMContentLoaded", function() { 8 | var div = document.createElement("div"); 9 | document.body.appendChild(div); 10 | contextCardsApp = Elm.ContextCards.init({ node: div }); 11 | contextCardsApp.ports.renderHTML.subscribe(renderHTML); 12 | contextCardsApp.ports.log.subscribe(console.log); 13 | bindLinks(); 14 | }); 15 | 16 | function bindLinks() { 17 | var links = document.querySelectorAll(LINK_SELECTOR); 18 | 19 | for (var i = 0; i < links.length; i++) { 20 | var link = links[i]; 21 | 22 | if (link.dataset.wikiPreviewEventsBound !== "events-set") { 23 | link.dataset.wikiPreviewEventsBound = "events-set"; 24 | 25 | bindLink(link); 26 | } 27 | } 28 | } 29 | 30 | function bindLink(link) { 31 | link.addEventListener("mouseenter", function(event) { 32 | sendMouseEvent("enter", event); 33 | }); 34 | link.addEventListener("mouseout", function(event) { 35 | sendMouseEvent("leave", event); 36 | }); 37 | link.addEventListener("focus", function(event) { 38 | sendMouseEvent("enter", event); 39 | }); 40 | link.addEventListener("blur", function(event) { 41 | sendMouseEvent("leave", event); 42 | }); 43 | } 44 | 45 | function sendMouseEvent(kind, event) { 46 | var link = event.target; 47 | var closestDir = link.closest("[dir]"); 48 | var data = { 49 | kind: kind, 50 | link: link, 51 | title: link.dataset.wikiTitle, 52 | lang: link.dataset.wikiLang, 53 | rect: link.getBoundingClientRect(), 54 | viewport: getViewport(), 55 | contentDir: link.dir 56 | ? link.dir 57 | : closestDir 58 | ? closestDir.dir.toLowerCase() 59 | : "ltr" 60 | }; 61 | contextCardsApp.ports.mouseEvent.send(data); 62 | } 63 | 64 | function getViewport() { 65 | return { 66 | scene: getScene(), 67 | viewport: { 68 | x: window.pageXOffset, 69 | y: window.pageYOffset, 70 | width: document.documentElement.clientWidth, 71 | height: document.documentElement.clientHeight 72 | } 73 | }; 74 | } 75 | 76 | function getScene() { 77 | var body = document.body; 78 | var elem = document.documentElement; 79 | return { 80 | width: Math.max( 81 | body.scrollWidth, 82 | body.offsetWidth, 83 | elem.scrollWidth, 84 | elem.offsetWidth, 85 | elem.clientWidth 86 | ), 87 | height: Math.max( 88 | body.scrollHeight, 89 | body.offsetHeight, 90 | elem.scrollHeight, 91 | elem.offsetHeight, 92 | elem.clientHeight 93 | ) 94 | }; 95 | } 96 | 97 | function renderHTML() { 98 | raf(function() { 99 | var nodes = document.querySelectorAll(".ContextCard [inner-html]"); 100 | for (var i = 0; i < nodes.length; i++) { 101 | var node = nodes[i]; 102 | node.innerHTML = node.getAttribute("inner-html"); 103 | node.removeAttribute("inner-html"); 104 | } 105 | }); 106 | } 107 | 108 | function raf(fn) { 109 | (window.requestAnimationFrame || 110 | function(f) { 111 | setTimeout(f, 16); 112 | })(fn); 113 | } 114 | 115 | return { 116 | bindLinks: bindLinks 117 | }; 118 | })(); 119 | --------------------------------------------------------------------------------