├── .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 |
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 |
7 |
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 |