aboutsummaryrefslogtreecommitdiff
path: root/ext
diff options
context:
space:
mode:
Diffstat (limited to 'ext')
-rw-r--r--ext/css/material.css1
-rw-r--r--ext/css/structured-content.css15
-rw-r--r--ext/data/schemas/dictionary-term-bank-v3-schema.json23
-rw-r--r--ext/data/structured-content-style.json6
-rw-r--r--ext/images/external-link.svg1
-rw-r--r--ext/js/display/display-content-manager.js46
-rw-r--r--ext/js/display/display.js9
-rw-r--r--ext/js/display/sandbox/structured-content-generator.js28
-rw-r--r--ext/js/templates/sandbox/anki-template-renderer-content-manager.js10
9 files changed, 136 insertions, 3 deletions
diff --git a/ext/css/material.css b/ext/css/material.css
index 55ed13e6..4d8eda51 100644
--- a/ext/css/material.css
+++ b/ext/css/material.css
@@ -275,6 +275,7 @@ body {
.icon[data-icon=tag] { --icon-image: url(/images/tag.svg); }
.icon[data-icon=accessibility] { --icon-image: url(/images/accessibility.svg); }
.icon[data-icon=connection] { --icon-image: url(/images/connection.svg); }
+.icon[data-icon=external-link] { --icon-image: url(/images/external-link.svg); }
.icon[data-icon=material-down-arrow] {
--icon-image: url(/images/material-down-arrow.svg);
--icon-size: var(--material-arrow-dimension2) var(--material-arrow-dimension1);
diff --git a/ext/css/structured-content.css b/ext/css/structured-content.css
index 092d9e7c..485527e5 100644
--- a/ext/css/structured-content.css
+++ b/ext/css/structured-content.css
@@ -198,6 +198,21 @@
}
+/* Links */
+.gloss-link-text {
+ vertical-align: middle;
+}
+.gloss-link-external-icon {
+ display: inline-block;
+ vertical-align: middle;
+ width: calc(16em / var(--font-size-no-units));
+ height: calc(16em / var(--font-size-no-units));
+ margin-left: 0.25em;
+ background-color: var(--link-color);
+ position: relative;
+}
+
+
/* Structured content glossary styles */
.gloss-sc-table-container {
display: block;
diff --git a/ext/data/schemas/dictionary-term-bank-v3-schema.json b/ext/data/schemas/dictionary-term-bank-v3-schema.json
index 9898a15e..268a2c11 100644
--- a/ext/data/schemas/dictionary-term-bank-v3-schema.json
+++ b/ext/data/schemas/dictionary-term-bank-v3-schema.json
@@ -185,6 +185,29 @@
"enum": ["px", "em"]
}
}
+ },
+ {
+ "type": "object",
+ "description": "Link tag.",
+ "required": [
+ "tag",
+ "href"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "tag": {
+ "type": "string",
+ "const": "a"
+ },
+ "content": {
+ "$ref": "#/definitions/structuredContent"
+ },
+ "href": {
+ "type": "string",
+ "description": "The URL for the link. URLs starting with a ? are treated as internal links to other dictionary content.",
+ "pattern": "^(?:https?:|\\?)[\\w\\W]*"
+ }
+ }
}
]
}
diff --git a/ext/data/structured-content-style.json b/ext/data/structured-content-style.json
index ae216abd..fe222486 100644
--- a/ext/data/structured-content-style.json
+++ b/ext/data/structured-content-style.json
@@ -294,6 +294,12 @@
]
},
{
+ "selectors": [".gloss-link-external-icon"],
+ "styles": [
+ ["display", "none"]
+ ]
+ },
+ {
"selectors": [".gloss-sc-table-container"],
"styles": [
["display", "block"]
diff --git a/ext/images/external-link.svg b/ext/images/external-link.svg
new file mode 100644
index 00000000..f8bbba58
--- /dev/null
+++ b/ext/images/external-link.svg
@@ -0,0 +1 @@
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M8.25 2l2.25 2-4 4L8 9.5l4-4 2 2.25V2H8.25zM2 4v10h10V7l-2 2v3H4V6h3l2-2H2z"/></svg> \ No newline at end of file
diff --git a/ext/js/display/display-content-manager.js b/ext/js/display/display-content-manager.js
index 0818f915..0b91b40c 100644
--- a/ext/js/display/display-content-manager.js
+++ b/ext/js/display/display-content-manager.js
@@ -37,11 +37,14 @@
class DisplayContentManager {
/**
* Creates a new instance of the class.
+ * @param {Display} display The display instance that owns this object.
*/
- constructor() {
+ constructor(display) {
+ this._display = display;
this._token = {};
this._mediaCache = new Map();
this._loadMediaData = [];
+ this._eventListeners = new EventListenerCollection();
}
/**
@@ -77,6 +80,23 @@ class DisplayContentManager {
this._mediaCache.clear();
this._token = {};
+
+ this._eventListeners.removeAllEventListeners();
+ }
+
+ /**
+ * Sets up attributes and events for a link element.
+ * @param {Element} element The link element.
+ * @param {string} href The URL.
+ * @param {boolean} internal Whether or not the URL is an internal or external link.
+ */
+ prepareLink(element, href, internal) {
+ element.href = href;
+ if (!internal) {
+ element.target = '_blank';
+ element.rel = 'noreferrer noopener';
+ }
+ this._eventListeners.addEventListener(element, 'click', this._onLinkClick.bind(this));
}
async _loadMedia(path, dictionary, onLoad, onUnload) {
@@ -127,4 +147,28 @@ class DisplayContentManager {
}
return cachedData;
}
+
+ _onLinkClick(e) {
+ const {href} = e.currentTarget;
+ if (typeof href !== 'string') { return; }
+
+ const baseUrl = new URL(location.href);
+ const url = new URL(href, baseUrl);
+ const internal = (url.protocol === baseUrl.protocol && url.host === baseUrl.host);
+ if (!internal) { return; }
+
+ e.preventDefault();
+
+ const params = {};
+ for (const [key, value] of url.searchParams.entries()) {
+ params[key] = value;
+ }
+ this._display.setContent({
+ historyMode: 'new',
+ focus: false,
+ params,
+ state: null,
+ content: null
+ });
+ }
}
diff --git a/ext/js/display/display.js b/ext/js/display/display.js
index a89008b4..02d8513f 100644
--- a/ext/js/display/display.js
+++ b/ext/js/display/display.js
@@ -52,7 +52,7 @@ class Display extends EventDispatcher {
this._styleNode = null;
this._eventListeners = new EventListenerCollection();
this._setContentToken = null;
- this._contentManager = new DisplayContentManager();
+ this._contentManager = new DisplayContentManager(this);
this._hotkeyHelpController = new HotkeyHelpController();
this._displayGenerator = new DisplayGenerator({
japaneseUtil,
@@ -938,7 +938,7 @@ class Display extends EventDispatcher {
this._dictionaryEntries = dictionaryEntries;
- this._updateNavigation(this._history.hasPrevious(), this._history.hasNext());
+ this._updateNavigationAuto();
this._setNoContentVisible(dictionaryEntries.length === 0 && lookup);
const container = this._container;
@@ -1002,6 +1002,7 @@ class Display extends EventDispatcher {
_clearContent() {
this._container.textContent = '';
+ this._updateNavigationAuto();
this._setQuery('', '', 0);
this._triggerContentUpdateStart();
@@ -1058,6 +1059,10 @@ class Display extends EventDispatcher {
document.title = title;
}
+ _updateNavigationAuto() {
+ this._updateNavigation(this._history.hasPrevious(), this._history.hasNext());
+ }
+
_updateNavigation(previous, next) {
const {documentElement} = document;
if (documentElement !== null) {
diff --git a/ext/js/display/sandbox/structured-content-generator.js b/ext/js/display/sandbox/structured-content-generator.js
index 799da586..eb847d07 100644
--- a/ext/js/display/sandbox/structured-content-generator.js
+++ b/ext/js/display/sandbox/structured-content-generator.js
@@ -59,6 +59,8 @@ class StructuredContentGenerator {
return this._createStructuredContentElement(tag, content, dictionary, 'simple', true, true);
case 'img':
return this.createDefinitionImage(content, dictionary);
+ case 'a':
+ return this._createLinkElement(content, dictionary);
}
return null;
}
@@ -253,4 +255,30 @@ class StructuredContentGenerator {
if (typeof marginRight === 'number') { style.marginRight = `${marginRight}em`; }
if (typeof marginBottom === 'number') { style.marginBottom = `${marginBottom}em`; }
}
+
+ _createLinkElement(content, dictionary) {
+ let {href} = content;
+ const internal = href.startsWith('?');
+ if (internal) {
+ href = `${location.protocol}//${location.host}/search.html${href.length > 1 ? href : ''}`;
+ }
+
+ const node = this._createElement('a', 'gloss-link');
+ node.dataset.external = `${!internal}`;
+
+ const text = this._createElement('span', 'gloss-link-text');
+ node.appendChild(text);
+
+ const child = this.createStructuredContent(content.content, dictionary);
+ if (child !== null) { text.appendChild(child); }
+
+ if (!internal) {
+ const icon = this._createElement('span', 'gloss-link-external-icon icon');
+ icon.dataset.icon = 'external-link';
+ node.appendChild(icon);
+ }
+
+ this._contentManager.prepareLink(node, href, internal);
+ return node;
+ }
}
diff --git a/ext/js/templates/sandbox/anki-template-renderer-content-manager.js b/ext/js/templates/sandbox/anki-template-renderer-content-manager.js
index a2573e09..0dabd86e 100644
--- a/ext/js/templates/sandbox/anki-template-renderer-content-manager.js
+++ b/ext/js/templates/sandbox/anki-template-renderer-content-manager.js
@@ -69,4 +69,14 @@ class AnkiTemplateRendererContentManager {
}
this._onUnloadCallbacks = [];
}
+
+ /**
+ * Sets up attributes and events for a link element.
+ * @param {Element} element The link element.
+ * @param {string} href The URL.
+ * @param {boolean} internal Whether or not the URL is an internal or external link.
+ */
+ prepareLink(element, href, internal) {
+ element.href = internal ? '#' : href;
+ }
}