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
This commit is contained in:
parent
8aa060337c
commit
7a2ab86609
@ -90,8 +90,9 @@ function generateRules(cssFile, overridesCssFile) {
|
||||
const stylesheet1 = css.parse(content1, {}).stylesheet;
|
||||
const stylesheet2 = css.parse(content2, {}).stylesheet;
|
||||
|
||||
const removePropertyPattern = /^remove-property\s+([a-zA-Z0-9-]+)$/;
|
||||
const removePropertyPattern = /^remove-property\s+([\w\W]+)$/;
|
||||
const removeRulePattern = /^remove-rule$/;
|
||||
const propertySeparator = /\s+/;
|
||||
|
||||
const rules = [];
|
||||
|
||||
@ -139,10 +140,11 @@ function generateRules(cssFile, overridesCssFile) {
|
||||
const comment = declaration.comment.trim();
|
||||
let m;
|
||||
if ((m = removePropertyPattern.exec(comment)) !== null) {
|
||||
const property = m[1];
|
||||
const removeCount = removeProperty(rules[index].styles, property, removedProperties);
|
||||
if (removeCount === 0) { throw new Error(`Property removal is unnecessary; ${property} does not exist`); }
|
||||
} else if ((m = removeRulePattern.exec(comment)) !== null) {
|
||||
for (const property of m[1].split(propertySeparator)) {
|
||||
const removeCount = removeProperty(rules[index].styles, property, removedProperties);
|
||||
if (removeCount === 0) { throw new Error(`Property removal is unnecessary; ${property} does not exist`); }
|
||||
}
|
||||
} else if (removeRulePattern.test(comment)) {
|
||||
rules.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
@ -54,3 +54,10 @@
|
||||
border-width: 1px;
|
||||
border-color: currentColor;
|
||||
}
|
||||
.gloss-link-text {
|
||||
/* remove-rule */
|
||||
}
|
||||
.gloss-link-external-icon {
|
||||
display: none;
|
||||
/* remove-property background-color vertical-align width height margin-left background-color position */
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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]*"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -293,6 +293,12 @@
|
||||
["display", "inline"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"selectors": [".gloss-link-external-icon"],
|
||||
"styles": [
|
||||
["display", "none"]
|
||||
]
|
||||
},
|
||||
{
|
||||
"selectors": [".gloss-sc-table-container"],
|
||||
"styles": [
|
||||
|
1
ext/images/external-link.svg
Normal file
1
ext/images/external-link.svg
Normal file
@ -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>
|
After Width: | Height: | Size: 194 B |
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -25,11 +25,11 @@
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="32"
|
||||
inkscape:cx="4.203125"
|
||||
inkscape:cy="5.734375"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="-1.03125"
|
||||
inkscape:cy="3.84375"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer53"
|
||||
inkscape:current-layer="layer54"
|
||||
showgrid="true"
|
||||
units="px"
|
||||
inkscape:snap-center="true"
|
||||
@ -566,7 +566,6 @@
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
@ -1650,8 +1649,9 @@
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer53"
|
||||
inkscape:label="Connection">
|
||||
id="g268"
|
||||
inkscape:label="Connection"
|
||||
style="display:none">
|
||||
<g
|
||||
id="g1369"
|
||||
transform="matrix(0,-1,-1,0,16,16)">
|
||||
@ -1663,4 +1663,14 @@
|
||||
sodipodi:nodetypes="ssccccsssscssssccccsssscssssssssssssssssss" />
|
||||
</g>
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer54"
|
||||
inkscape:label="External Link"
|
||||
style="display:inline">
|
||||
<path
|
||||
id="rect1109"
|
||||
style="fill:#000000;stroke-linecap:round;stroke-opacity:0.387097;fill-opacity:1"
|
||||
d="M 8.25 2 L 10.5 4 L 6.5 8 L 8 9.5 L 12 5.5 L 14 7.75 L 14 2 L 8.25 2 z M 2 4 L 2 14 L 12 14 L 12 7 L 10 9 L 10 12 L 4 12 L 4 6 L 7 6 L 9 4 L 2 4 z " />
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 104 KiB After Width: | Height: | Size: 105 KiB |
@ -179,6 +179,31 @@
|
||||
]
|
||||
},
|
||||
" text 3"
|
||||
]},
|
||||
{"type": "structured-content", "content": [
|
||||
{
|
||||
"tag": "a",
|
||||
"href": "?",
|
||||
"content": [
|
||||
"internal link 1"
|
||||
]
|
||||
},
|
||||
" ",
|
||||
{
|
||||
"tag": "a",
|
||||
"href": "?query=よみ&wildcards=off",
|
||||
"content": [
|
||||
"internal link 2"
|
||||
]
|
||||
},
|
||||
" ",
|
||||
{
|
||||
"tag": "a",
|
||||
"href": "https://foosoft.net/projects/yomichan/",
|
||||
"content": [
|
||||
"external link"
|
||||
]
|
||||
}
|
||||
]}
|
||||
],
|
||||
100, "P E1"
|
||||
|
Loading…
Reference in New Issue
Block a user