diff options
Diffstat (limited to 'ext')
-rw-r--r-- | ext/css/material.css | 1 | ||||
-rw-r--r-- | ext/css/structured-content.css | 15 | ||||
-rw-r--r-- | ext/data/schemas/dictionary-term-bank-v3-schema.json | 23 | ||||
-rw-r--r-- | ext/data/structured-content-style.json | 6 | ||||
-rw-r--r-- | ext/images/external-link.svg | 1 | ||||
-rw-r--r-- | ext/js/display/display-content-manager.js | 46 | ||||
-rw-r--r-- | ext/js/display/display.js | 9 | ||||
-rw-r--r-- | ext/js/display/sandbox/structured-content-generator.js | 28 | ||||
-rw-r--r-- | ext/js/templates/sandbox/anki-template-renderer-content-manager.js | 10 |
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; + } } |