From 7a2ab866099edffaba471ad808593f67ee796b21 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Thu, 17 Mar 2022 19:01:59 -0400 Subject: Structured content links (#2089) * Update CSS to JSON converter to generalize the remove-property comment * Fix navigation not being updated when _clearContent is run * Add structured content schema for link tags * Add test links * Add external-link icon * Pass Display instance to DisplayContentManager * Update structured content generation * Update link styles --- ext/js/display/display-content-manager.js | 46 +++++++++++++++++++++- ext/js/display/display.js | 9 ++++- .../sandbox/structured-content-generator.js | 28 +++++++++++++ .../anki-template-renderer-content-manager.js | 10 +++++ 4 files changed, 90 insertions(+), 3 deletions(-) (limited to 'ext/js') 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; + } } -- cgit v1.2.3