From 56ee7f8df47a3826e10d9b0876f313f5ced4c98e Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Tue, 24 Dec 2019 21:45:57 -0500 Subject: [PATCH] Update display content generation to use HTML templates --- ext/bg/search.html | 90 +++++++ ext/fg/float.html | 92 ++++++- ext/mixed/css/display-dark.css | 61 +++-- ext/mixed/css/display-default.css | 51 ++-- ext/mixed/css/display.css | 433 ++++++++++++++++++++++-------- ext/mixed/js/display-generator.js | 332 +++++++++++++++++++++++ ext/mixed/js/display.js | 87 ++++-- 7 files changed, 963 insertions(+), 183 deletions(-) create mode 100644 ext/mixed/js/display-generator.js diff --git a/ext/bg/search.html b/ext/bg/search.html index 409243dd..5d9babea 100644 --- a/ext/bg/search.html +++ b/ext/bg/search.html @@ -54,9 +54,98 @@
+ +
+ + + + + + + + + + + + + + + + + + + @@ -72,6 +161,7 @@ + diff --git a/ext/fg/float.html b/ext/fg/float.html index 886e5e8b..f5f56853 100644 --- a/ext/fg/float.html +++ b/ext/fg/float.html @@ -19,10 +19,21 @@ + +
+ +
-
+

Yomichan Updated!

The Yomichan extension has been updated to a new version! In order to continue @@ -31,6 +42,84 @@

+ + + + + + + + + + + + + + + + + @@ -40,6 +129,7 @@ + diff --git a/ext/mixed/css/display-dark.css b/ext/mixed/css/display-dark.css index e26c72aa..088fc741 100644 --- a/ext/mixed/css/display-dark.css +++ b/ext/mixed/css/display-dark.css @@ -19,36 +19,53 @@ body { background-color: #1e1e1e; color: #d4d4d4; } -hr { border-top-color: #2f2f2f; } - -.tag-default { background-color: #69696e; } -.tag-name { background-color: #489148; } -.tag-expression { background-color: #b07f39; } -.tag-popular { background-color: #025caa; } -.tag-frequent { background-color: #4490a7; } -.tag-archaism { background-color: #b04340; } -.tag-dictionary { background-color: #9057ad; } -.tag-frequency { background-color: #489148; } -.tag-partOfSpeech { background-color: #565656; } - -.reasons { color: #888888; } -.glossary li { color: #888888; } -.glossary-item { color: #d4d4d4; } -.label { color: #e1e1e1; } - -.expression .kanji-link { - border-bottom-color: #888888; - color: #CCCCCC; +.navigation-header { + background-color: #1e1e1e; + border-bottom-color: #2f2f2f; } -.expression-popular, .expression-popular .kanji-link { +.entry+.entry { border-top-color: #2f2f2f; } + +.kanji-glyph-data>tbody>tr>* { border-top-color: #3f3f3f; } + +.tag { color: #e1e1e1; } +.tag[data-category=default] { background-color: #69696e; } +.tag[data-category=name] { background-color: #489148; } +.tag[data-category=expression] { background-color: #b07f39; } +.tag[data-category=popular] { background-color: #025caa; } +.tag[data-category=frequent] { background-color: #4490a7; } +.tag[data-category=archaism] { background-color: #b04340; } +.tag[data-category=dictionary] { background-color: #9057ad; } +.tag[data-category=frequency] { background-color: #489148; } +.tag[data-category=partOfSpeech] { background-color: #565656; } + +.term-reasons { color: #888888; } + +.term-expression>.term-expression-text .kanji-link { + border-bottom-color: #888888; + color: #cccccc; +} + +.term-expression[data-frequency=popular]>.term-expression-text, +.term-expression[data-frequency=popular]>.term-expression-text .kanji-link { color: #0275d8; } -.expression-rare, .expression-rare .kanji-link { +.term-expression[data-frequency=rare]>.term-expression-text, +.term-expression[data-frequency=rare]>.term-expression-text .kanji-link { color: #666666; } +.term-definition-container, +.kanji-glossary-container { + color: #888888; +} + +.term-glossary, +.kanji-glossary { + color: #d4d4d4; +} + .icon-checkbox:checked + label { /* invert colors */ background-color: #d4d4d4; diff --git a/ext/mixed/css/display-default.css b/ext/mixed/css/display-default.css index ac237e79..69141c9d 100644 --- a/ext/mixed/css/display-default.css +++ b/ext/mixed/css/display-default.css @@ -19,36 +19,53 @@ body { background-color: #ffffff; color: #333333; } -hr { border-top-color: #eeeeee; } +.navigation-header { + background-color: #ffffff; + border-bottom-color: #eeeeee; +} -.tag-default { background-color: #8a8a91; } -.tag-name { background-color: #5cb85c; } -.tag-expression { background-color: #f0ad4e; } -.tag-popular { background-color: #0275d8; } -.tag-frequent { background-color: #5bc0de; } -.tag-archaism { background-color: #d9534f; } -.tag-dictionary { background-color: #aa66cc; } -.tag-frequency { background-color: #5cb85c; } -.tag-partOfSpeech { background-color: #565656; } +.entry+.entry { border-top-color: #eeeeee; } -.reasons { color: #777777; } -.glossary li { color: #777777; } -.glossary-item { color: #000000; } -.label { color: #ffffff; } +.kanji-glyph-data>tbody>tr>* { border-top-color: #dddddd; } -.expression .kanji-link { +.tag { color: #ffffff; } +.tag[data-category=default] { background-color: #8a8a91; } +.tag[data-category=name] { background-color: #5cb85c; } +.tag[data-category=expression] { background-color: #f0ad4e; } +.tag[data-category=popular] { background-color: #0275d8; } +.tag[data-category=frequent] { background-color: #5bc0de; } +.tag[data-category=archaism] { background-color: #d9534f; } +.tag[data-category=dictionary] { background-color: #aa66cc; } +.tag[data-category=frequency] { background-color: #5cb85c; } +.tag[data-category=partOfSpeech] { background-color: #565656; } + +.term-reasons { color: #777777; } + +.term-expression>.term-expression-text .kanji-link { border-bottom-color: #777777; color: #333333; } -.expression-popular, .expression-popular .kanji-link { +.term-expression[data-frequency=popular]>.term-expression-text, +.term-expression[data-frequency=popular]>.term-expression-text .kanji-link { color: #0275d8; } -.expression-rare, .expression-rare .kanji-link { +.term-expression[data-frequency=rare]>.term-expression-text, +.term-expression[data-frequency=rare]>.term-expression-text .kanji-link { color: #999999; } +.term-definition-container, +.kanji-glossary-container { + color: #777777; +} + +.term-glossary, +.kanji-glossary { + color: #000000; +} + .icon-checkbox:checked + label { /* invert colors */ background-color: #333333; diff --git a/ext/mixed/css/display.css b/ext/mixed/css/display.css index 7a00bccb..ff42771a 100644 --- a/ext/mixed/css/display.css +++ b/ext/mixed/css/display.css @@ -38,30 +38,28 @@ html:root[data-yomichan-page=float]:not([data-yomichan-theme]) body { body { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px; - line-height: 1.42857143; + line-height: 1.42857143; /* 14px => 20px */ margin: 0; border: 0; padding: 0; } -hr { - padding: 0px; - margin: 0px; - border: 0; - border-top-width: 1px; - border-top-style: solid; -} - ol, ul { margin-top: 0; - margin-bottom: 10px; + margin-bottom: 0.72em; } #spinner { - bottom: 5px; display: none; position: fixed; - right: 5px; + z-index: 1; + right: 0.36em; + bottom: 0.36em; +} + +#spinner>img { + width: 2.28571428em; /* 14px => 32px */ + height: 2.28571428em; /* 14px => 32px */ } #error-orphaned { @@ -76,10 +74,38 @@ ol, ul { * Navigation */ -.term-navigation { +.navigation-header { + top: 0; + left: 0; + width: 100%; + height: 2.1em; + box-sizing: border-box; + padding: 0.25em 0.5em; + border-bottom-width: 0.07142857em; /* 14px => 1px */ + border-bottom-style: solid; +} + +html:root[data-yomichan-page=float] .navigation-header { position: fixed; - top: 0px; - right: 0px; +} + +.navigation-header:not([hidden])~.navigation-header-spacer { + height: 2.1em; +} + +.navigation-header-actions { + display: flex; +} + +.navigation-header:not([data-has-previous=true]) .navigation-header-actions .action-previous>img, +.navigation-header:not([data-has-next=true]) .navigation-header-actions .action-next>img { + opacity: 0.25; + -webkit-filter: grayscale(100%); + filter: grayscale(100%); +} + +.action-next>img { + transform: scaleX(-1); } @@ -91,20 +117,20 @@ ol, ul { display: none; } -.icon-checkbox + label { +.icon-checkbox+label { cursor: pointer; - font-size: 22px; - padding: 2px; + font-size: 1.6em; + padding: 0.1em; user-select: none; } #query-parser { - margin-top: 10px; - font-size: 24px; + margin-top: 0.5em; + font-size: 2em; } .query-parser-term { - margin-right: 5px; + margin-right: 0.2em; } html:root[data-yomichan-page=search] body { @@ -116,15 +142,14 @@ html:root[data-yomichan-page=search] body { * Entries */ -.entry, .note { - padding-top: 20px; - padding-bottom: 10px; +.entry { + padding-top: 0.72em; + padding-bottom: 0.72em; } -html:root[data-yomichan-page=float] .entry, -html:root[data-yomichan-page=float] .note { - padding-left: 10px; - padding-right: 10px; +html:root[data-yomichan-page=float] .entry { + padding-left: 0.72em; + padding-right: 0.72em; } .actions .disabled { @@ -143,8 +168,9 @@ html:root[data-yomichan-page=float] .note { } .actions { - display: block; + display: flex; float: right; + margin: -0.25em; } .actions:after { @@ -153,104 +179,35 @@ html:root[data-yomichan-page=float] .note { display: block; } -.expression { +.action-button { display: inline-block; - font-size: 24px; + border: 0; + margin: 0; + padding: 0.25em; + background: transparent; } -.expression .kanji-link { - border-bottom-width: 1px; +button.action-button { + cursor: pointer; +} + +.icon-image { + width: 1.14285714em; /* 14px => 16px */ + height: 1.14285714em; /* 14px => 16px */ + display: block; +} + +.term-expression .kanji-link { + border-bottom-width: 0.03571428em; /* 28px => 1px */ border-bottom-style: dashed; text-decoration: none; } -.expression .peek-wrapper { - font-size: 14px; - white-space: nowrap; - display: inline-block; - position: relative; - width: 0px; - height: 0px; - visibility: hidden; -} - -.expression .peek-wrapper .action-play-audio { - position: absolute; - left: 0px; - bottom: 10px; -} - -.expression .peek-wrapper .tags { - position: absolute; - left: 0px; - bottom: -10px; -} - -.expression .peek-wrapper .frequencies { - position: absolute; - left: 0px; - bottom: -30px; -} - -.expression:hover .peek-wrapper { - visibility: visible; -} - -.reasons { - display: inline-block; -} - -.compact-info { - display: inline-block; -} - -.glossary ol, .glossary ul { - padding-left: 1.4em; -} - -.glossary ul.compact-glossary { - display: inline; - list-style: none; - padding-left: 0px; -} - -.glossary .compact-glossary li { - display: inline; -} - -.glossary .compact-glossary li:not(:first-child):before { - content: " | "; -} - -div.glossary-item.compact-glossary { - display: inline; -} - -.glyph { - font-family: kanji-stroke-orders; - font-size: 120px; - line-height: 120px; - padding: 0.01em; - vertical-align: top; -} - -.glyph-data { - margin-top: 10px; -} - -.info-output { - width: 100%; -} - -.info-output td { - text-align: right; -} - .entry:not(.entry-current) .current { display: none; } -.label { +.tag { display: inline; padding: 0.2em 0.6em 0.3em; font-size: 75%; @@ -261,3 +218,247 @@ div.glossary-item.compact-glossary { vertical-align: baseline; border-radius: 0.25em; } + +.tag-list>.tag+.tag { + margin-left: 0.375em; +} + +.entry-header2, +.entry-header3 { + display: inline; +} + +.term-frequency-separator::before { + content: ":"; +} + +.entry+.entry { + border-top-width: 0.07142857em; /* 14px => 1px */ + border-top-style: solid; +} + +.entry[data-type=term][data-multi-expression=true] .actions>.action-play-audio { + display: none; +} + +.term-reasons { + display: inline-block; +} + +.term-reasons>.term-reason+.term-reason:before { + content: " \00AB "; /* The two spaces is not a typo */ + display: inline; +} + +.term-expression-list { + display: inline-block; +} + +.term-expression { + display: inline-block; +} + +.term-expression-text { + display: inline-block; + font-size: 2em; +} + +.term-expression-details { + display: inline; +} + +.term-expression-details>.action-play-audio { + display: none; +} + +.term-expression-details>.tags { + display: inline; +} + +.term-expression-details>.frequencies { + display: none; +} + +.term-expression-list>.term-expression:not(:last-of-type):after { + font-size: 2em; + content: "\3001"; +} + +.term-expression-list[data-multi=true] .term-expression-details { + visibility: hidden; +} + +.term-expression-list[data-multi=true] .term-expression-details { + display: inline-block; + position: relative; + width: 0; + height: 0; + visibility: hidden; +} + +.term-expression-list[data-multi=true] .term-expression:hover .term-expression-details { + visibility: visible; +} + +.term-expression-list[data-multi=true] .term-expression-details>.action-play-audio { + position: absolute; + left: 0; + bottom: 0.5em; +} + +.term-expression-list[data-multi=true] .term-expression-details>.action-play-audio { + display: block; +} + +.term-expression-list[data-multi=true] .term-expression-details>.tags { + display: block; + position: absolute; + left: 0; + bottom: -0.5em; + white-space: nowrap; +} + +.term-expression-list[data-multi=true] .term-expression-details>.frequencies { + display: block; + position: absolute; + left: 0; + bottom: -1.9em; + white-space: nowrap; +} + +.term-definition-list { + margin: 0; + padding: 0; + list-style-type: none; +} + +.term-definition-list:not([data-count="0"]):not([data-count="1"]) { + padding-left: 1.4em; + list-style-type: decimal; +} + +.term-glossary-list { + margin: 0; + padding: 0; + list-style-type: none; +} + +.term-glossary-list:not([data-count="0"]):not([data-count="1"]) { + padding-left: 1.4em; + list-style-type: circle; +} + +.term-definition-only-list[data-count="0"] { + display: none; +} + +.term-definition-only-list:before { + content: "("; +} + +.term-definition-only-list:after { + content: " only)"; +} + +.term-definition-only+.term-definition-only:before { + content: ", "; +} + +.debug-info { + display: none; +} + +:root[data-debug=true] .debug-info { + display: block; +} + +:root[data-anki-enabled=false] .action-view-note, +:root[data-anki-enabled=false] .action-add-note { + display: none; +} + +:root[data-audio-enabled=false] .action-play-audio { + display: none; +} + +:root[data-compact-glossaries=true] .term-definition-tag-list, +:root[data-compact-glossaries=true] .term-definition-only-list:not([data-count="0"]) { + display: inline-block; +} + +:root[data-compact-glossaries=true] .term-glossary-list { + display: inline; + list-style: none; + padding-left: 0; +} + +:root[data-compact-glossaries=true] .term-glossary-list>li { + display: inline; +} + +:root[data-compact-glossaries=true] .term-glossary-list>li:not(:first-child):before { + content: " | "; +} + +/* + * Kanji + */ + +.kanji-glyph { + font-family: kanji-stroke-orders; + font-size: 8.5em; + line-height: 1; + padding: 0.01em; + vertical-align: top; +} + +.kanji-glyph-data { + margin-top: 0.75em; + border-spacing: 0; + border-collapse: collapse; +} + +.kanji-glyph-data>tbody>tr>* { + border-top-width: 0.07142857em; /* 14px => 1px */ + border-top-style: solid; + text-align: left; + vertical-align: top; + padding: 0.36em; + margin: 0; +} + +.kanji-info-table { + width: 100%; +} + +.kanji-info-table>tbody>tr>th, +.kanji-info-table>tbody>tr>td { + text-align: left; + vertical-align: top; + padding: 0; + margin: 0; +} + +.kanji-info-table>tbody>tr>td { + text-align: right; +} + +.kanji-glyph-data dl { + margin-top: 0; + margin-bottom: 1.4em; +} + +.kanji-glyph-data dd { + margin-left: 0; +} + +.kanji-glossary-list { + margin: 0; + padding: 0; + list-style-type: none; +} + +.kanji-glossary-list:not([data-count="0"]):not([data-count="1"]) { + padding-left: 1.4em; + list-style-type: decimal; +} diff --git a/ext/mixed/js/display-generator.js b/ext/mixed/js/display-generator.js new file mode 100644 index 00000000..37be5041 --- /dev/null +++ b/ext/mixed/js/display-generator.js @@ -0,0 +1,332 @@ +/* + * Copyright (C) 2019-2020 Alex Yatskov + * Author: Alex Yatskov + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + +class DisplayGenerator { + constructor() { + this._termEntryTemplate = document.querySelector('#term-entry-template'); + this._termExpressionTemplate = document.querySelector('#term-expression-template'); + this._termDefinitionItemTemplate = document.querySelector('#term-definition-item-template'); + this._termDefinitionOnlyTemplate = document.querySelector('#term-definition-only-template'); + this._termGlossaryItemTemplate = document.querySelector('#term-glossary-item-template'); + this._termReasonTemplate = document.querySelector('#term-reason-template'); + + this._kanjiEntryTemplate = document.querySelector('#kanji-entry-template'); + this._kanjiInfoTableTemplate = document.querySelector('#kanji-info-table-template'); + this._kanjiInfoTableItemTemplate = document.querySelector('#kanji-info-table-item-template'); + this._kanjiInfoTableEmptyTemplate = document.querySelector('#kanji-info-table-empty-template'); + this._kanjiGlossaryItemTemplate = document.querySelector('#kanji-glossary-item-template'); + this._kanjiReadingTemplate = document.querySelector('#kanji-reading-template'); + + this._tagTemplate = document.querySelector('#tag-template'); + this._tagFrequencyTemplate = document.querySelector('#tag-frequency-template'); + } + + createTermEntry(details) { + const node = DisplayGenerator._instantiateTemplate(this._termEntryTemplate); + + const expressionsContainer = node.querySelector('.term-expression-list'); + const reasonsContainer = node.querySelector('.reasons'); + const frequenciesContainer = node.querySelector('.frequencies'); + const definitionsContainer = node.querySelector('.term-definition-list'); + const debugInfoContainer = node.querySelector('.debug-info'); + + const multiExpression = Array.isArray(details.expressions); + const multiDefinition = Array.isArray(details.definitions); + + node.dataset.multiExpression = `${multiExpression}`; + node.dataset.multiDefinition = `${multiDefinition}`; + + DisplayGenerator._appendMultiple(expressionsContainer, this.createTermExpression.bind(this), details.expressions, [details]); + DisplayGenerator._appendMultiple(reasonsContainer, this.createTermReason.bind(this), details.reasons); + DisplayGenerator._appendMultiple(frequenciesContainer, this.createFrequencyTag.bind(this), details.frequencies); + DisplayGenerator._appendMultiple(definitionsContainer, this.createTermDefinitionItem.bind(this), details.definitions, [details]); + + if (debugInfoContainer !== null) { + debugInfoContainer.textContent = JSON.stringify(details, null, 4); + } + + return node; + } + + createTermExpression(details) { + const node = DisplayGenerator._instantiateTemplate(this._termExpressionTemplate); + + const expressionContainer = node.querySelector('.term-expression-text'); + const tagContainer = node.querySelector('.tags'); + const frequencyContainer = node.querySelector('.frequencies'); + + if (details.termFrequency) { + node.dataset.frequency = details.termFrequency; + } + + if (expressionContainer !== null) { + const segments = [{text: details.expression, furigana: details.reading}]; // TODO : Use proper furigana segmentation + DisplayGenerator._appendFurigana(expressionContainer, segments, this._appendKanjiLinks.bind(this)); + } + + DisplayGenerator._appendMultiple(tagContainer, this.createTag.bind(this), details.termTags); + DisplayGenerator._appendMultiple(frequencyContainer, this.createFrequencyTag.bind(this), details.frequencies); + + return node; + } + + createTermReason(reason) { + const node = DisplayGenerator._instantiateTemplate(this._termReasonTemplate); + node.textContent = reason; + node.dataset.reason = reason; + return node; + } + + createTermDefinitionItem(details) { + const node = DisplayGenerator._instantiateTemplate(this._termDefinitionItemTemplate); + + const tagListContainer = node.querySelector('.term-definition-tag-list'); + const onlyListContainer = node.querySelector('.term-definition-only-list'); + const glossaryContainer = node.querySelector('.term-glossary-list'); + + node.dataset.dictionary = details.dictionary; + + DisplayGenerator._appendMultiple(tagListContainer, this.createTag.bind(this), details.definitionTags); + DisplayGenerator._appendMultiple(onlyListContainer, this.createTermOnly.bind(this), details.only); + DisplayGenerator._appendMultiple(glossaryContainer, this.createTermGlossaryItem.bind(this), details.glossary); + + return node; + } + + createTermGlossaryItem(glossary) { + const node = DisplayGenerator._instantiateTemplate(this._termGlossaryItemTemplate); + const container = node.querySelector('.term-glossary'); + if (container !== null) { + DisplayGenerator._appendMultilineText(container, glossary); + } + return node; + } + + createTermOnly(only) { + const node = DisplayGenerator._instantiateTemplate(this._termDefinitionOnlyTemplate); + node.dataset.only = only; + node.textContent = only; + return node; + } + + createKanjiLink(character) { + const node = document.createElement('a'); + node.href = '#'; + node.className = 'kanji-link'; + node.textContent = character; + return node; + } + + createKanjiEntry(details) { + const node = DisplayGenerator._instantiateTemplate(this._kanjiEntryTemplate); + + const glyphContainer = node.querySelector('.kanji-glyph'); + const frequenciesContainer = node.querySelector('.frequencies'); + const tagContainer = node.querySelector('.tags'); + const glossaryContainer = node.querySelector('.kanji-glossary-list'); + const chineseReadingsContainer = node.querySelector('.kanji-readings-chinese'); + const japaneseReadingsContainer = node.querySelector('.kanji-readings-japanese'); + const statisticsContainer = node.querySelector('.kanji-statistics'); + const classificationsContainer = node.querySelector('.kanji-classifications'); + const codepointsContainer = node.querySelector('.kanji-codepoints'); + const dictionaryIndicesContainer = node.querySelector('.kanji-dictionary-indices'); + const debugInfoContainer = node.querySelector('.debug-info'); + + if (glyphContainer !== null) { + glyphContainer.textContent = details.character; + } + + DisplayGenerator._appendMultiple(frequenciesContainer, this.createFrequencyTag.bind(this), details.frequencies); + DisplayGenerator._appendMultiple(tagContainer, this.createTag.bind(this), details.tags); + DisplayGenerator._appendMultiple(glossaryContainer, this.createKanjiGlossaryItem.bind(this), details.glossary); + DisplayGenerator._appendMultiple(chineseReadingsContainer, this.createKanjiReading.bind(this), details.onyomi); + DisplayGenerator._appendMultiple(japaneseReadingsContainer, this.createKanjiReading.bind(this), details.kunyomi); + + if (statisticsContainer !== null) { + statisticsContainer.appendChild(this.createKanjiInfoTable(details.stats.misc)); + } + if (classificationsContainer !== null) { + classificationsContainer.appendChild(this.createKanjiInfoTable(details.stats.class)); + } + if (codepointsContainer !== null) { + codepointsContainer.appendChild(this.createKanjiInfoTable(details.stats.code)); + } + if (dictionaryIndicesContainer !== null) { + dictionaryIndicesContainer.appendChild(this.createKanjiInfoTable(details.stats.index)); + } + + if (debugInfoContainer !== null) { + debugInfoContainer.textContent = JSON.stringify(details, null, 4); + } + + return node; + } + + createKanjiGlossaryItem(glossary) { + const node = DisplayGenerator._instantiateTemplate(this._kanjiGlossaryItemTemplate); + const container = node.querySelector('.kanji-glossary'); + if (container !== null) { + DisplayGenerator._appendMultilineText(container, glossary); + } + return node; + } + + createKanjiReading(reading) { + const node = DisplayGenerator._instantiateTemplate(this._kanjiReadingTemplate); + node.textContent = reading; + return node; + } + + createKanjiInfoTable(details) { + const node = DisplayGenerator._instantiateTemplate(this._kanjiInfoTableTemplate); + + const container = node.querySelector('.kanji-info-table-body'); + + if (container !== null) { + const count = DisplayGenerator._appendMultiple(container, this.createKanjiInfoTableItem.bind(this), details); + if (count === 0) { + const n = this.createKanjiInfoTableItemEmpty(); + container.appendChild(n); + } + } + + return node; + } + + createKanjiInfoTableItem(details) { + const node = DisplayGenerator._instantiateTemplate(this._kanjiInfoTableItemTemplate); + const nameNode = node.querySelector('.kanji-info-table-item-header'); + const valueNode = node.querySelector('.kanji-info-table-item-value'); + if (nameNode !== null) { + nameNode.textContent = details.notes || details.name; + } + if (valueNode !== null) { + valueNode.textContent = details.value; + } + return node; + } + + createKanjiInfoTableItemEmpty() { + return DisplayGenerator._instantiateTemplate(this._kanjiInfoTableEmptyTemplate); + } + + createTag(details) { + const node = DisplayGenerator._instantiateTemplate(this._tagTemplate); + + node.title = details.notes; + node.textContent = details.name; + node.dataset.category = details.category; + + return node; + } + + createFrequencyTag(details) { + const node = DisplayGenerator._instantiateTemplate(this._tagFrequencyTemplate); + + let n = node.querySelector('.term-frequency-dictionary-name'); + if (n !== null) { + n.textContent = details.dictionary; + } + + n = node.querySelector('.term-frequency-value'); + if (n !== null) { + n.textContent = `${details.frequency}`; + } + + node.dataset.dictionary = details.dictionary; + node.dataset.frequency = details.frequency; + + return node; + } + + _appendKanjiLinks(container, text) { + let part = ''; + for (const c of text) { + if (DisplayGenerator._isCharacterKanji(c)) { + if (part.length > 0) { + container.appendChild(document.createTextNode(part)); + part = ''; + } + + const link = this.createKanjiLink(c); + container.appendChild(link); + } else { + part += c; + } + } + if (part.length > 0) { + container.appendChild(document.createTextNode(part)); + } + } + + static _isCharacterKanji(c) { + const code = c.charCodeAt(0); + return ( + code >= 0x4e00 && code < 0x9fb0 || + code >= 0x3400 && code < 0x4dc0 + ); + } + + static _appendMultiple(container, createItem, detailsArray, fallback=[]) { + if (container === null) { return 0; } + + const isArray = Array.isArray(detailsArray); + if (!isArray) { detailsArray = fallback; } + + container.dataset.multi = `${isArray}`; + container.dataset.count = `${detailsArray.length}`; + + for (const details of detailsArray) { + const item = createItem(details); + if (item === null) { continue; } + container.appendChild(item); + } + + return detailsArray.length; + } + + static _appendFurigana(container, segments, addText) { + for (const {text, furigana} of segments) { + if (furigana) { + const ruby = document.createElement('ruby'); + const rt = document.createElement('rt'); + addText(ruby, text); + ruby.appendChild(rt); + rt.appendChild(document.createTextNode(furigana)); + container.appendChild(ruby); + } else { + addText(container, text); + } + } + } + + static _appendMultilineText(container, text) { + const parts = text.split('\n'); + container.appendChild(document.createTextNode(parts[0])); + for (let i = 1, ii = parts.length; i < ii; ++i) { + container.appendChild(document.createElement('br')); + container.appendChild(document.createTextNode(parts[i])); + } + } + + static _instantiateTemplate(template) { + const content = document.importNode(template.content, true); + return content.firstChild; + } +} diff --git a/ext/mixed/js/display.js b/ext/mixed/js/display.js index e756f948..f61e76b5 100644 --- a/ext/mixed/js/display.js +++ b/ext/mixed/js/display.js @@ -37,6 +37,7 @@ class Display { this.eventListenersActive = false; this.clickScanPrevent = false; + this.displayGenerator = new DisplayGenerator(); this.windowScroll = new WindowScroll(); this.setInteractive(true); @@ -240,11 +241,20 @@ class Display { async updateOptions(options) { this.options = options ? options : await apiOptionsGet(this.getOptionsContext()); + this.updateDocumentOptions(this.options); this.updateTheme(this.options.general.popupTheme); this.setCustomCss(this.options.general.customPopupCss); audioPrepareTextToSpeech(this.options); } + updateDocumentOptions(options) { + const data = document.documentElement.dataset; + data.ankiEnabled = `${options.anki.enable}`; + data.audioEnabled = `${options.audio.enable}`; + data.compactGlossaries = `${options.general.compactGlossaries}`; + data.debug = `${options.general.debugInfo}`; + } + updateTheme(themeName) { document.documentElement.dataset.yomichanTheme = themeName; @@ -277,6 +287,9 @@ class Display { if (interactive) { Display.addEventListener(this.persistentEventListeners, document, 'keydown', this.onKeyDown.bind(this), false); Display.addEventListener(this.persistentEventListeners, document, 'wheel', this.onWheel.bind(this), {passive: false}); + Display.addEventListener(this.persistentEventListeners, document.querySelector('.action-previous'), 'click', this.onSourceTermView.bind(this)); + Display.addEventListener(this.persistentEventListeners, document.querySelector('.action-next'), 'click', this.onNextTermView.bind(this)); + Display.addEventListener(this.persistentEventListeners, document.querySelector('.navigation-header'), 'wheel', this.onHistoryWheel.bind(this), {passive: false}); } else { Display.clearEventListeners(this.persistentEventListeners); } @@ -293,9 +306,6 @@ class Display { this.addEventListeners('.action-view-note', 'click', this.onNoteView.bind(this)); this.addEventListeners('.action-play-audio', 'click', this.onAudioPlay.bind(this)); this.addEventListeners('.kanji-link', 'click', this.onKanjiLookup.bind(this)); - this.addEventListeners('.source-term', 'click', this.onSourceTermView.bind(this)); - this.addEventListeners('.next-term', 'click', this.onNextTermView.bind(this)); - this.addEventListeners('.term-navigation', 'wheel', this.onHistoryWheel.bind(this), {passive: false}); if (this.options.scanning.enablePopupSearch) { this.addEventListeners('.glossary-item', 'mouseup', this.onGlossaryMouseUp.bind(this)); this.addEventListeners('.glossary-item', 'mousedown', this.onGlossaryMouseDown.bind(this)); @@ -347,25 +357,25 @@ class Display { } const sequence = ++this.sequence; - const params = { - definitions, - source: !!this.context.previous, - next: !!this.context.next, - addable: options.anki.enable, - grouped: options.general.resultOutputMode === 'group', - merged: options.general.resultOutputMode === 'merge', - playback: options.audio.enabled, - compactGlossaries: options.general.compactGlossaries, - debug: options.general.debugInfo - }; for (const definition of definitions) { definition.cloze = Display.clozeBuild(context.sentence, definition.source); definition.url = context.url; } - const content = await apiTemplateRender('terms.html', params); - this.container.innerHTML = content; + this.updateNavigation(this.context.previous, this.context.next); + this.setNoContentVisible(definitions.length === 0); + + const fragment = document.createDocumentFragment(); + for (const definition of definitions) { + fragment.appendChild(this.displayGenerator.createTermEntry(definition)); + } + + await Promise.resolve(); // Delay to help avoid forced reflow warnings in Chrome + + this.container.textContent = ''; + this.container.appendChild(fragment); + const {index, scroll, disableScroll} = context; if (!disableScroll) { this.entryScrollIntoView(index || 0, scroll); @@ -391,8 +401,6 @@ class Display { if (!this.isInitialized()) { return; } try { - const options = this.options; - this.setEventListenersActive(false); if (context.focus !== false) { @@ -408,21 +416,25 @@ class Display { } const sequence = ++this.sequence; - const params = { - definitions, - source: !!this.context.previous, - next: !!this.context.next, - addable: options.anki.enable, - debug: options.general.debugInfo - }; for (const definition of definitions) { definition.cloze = Display.clozeBuild(context.sentence, definition.character); definition.url = context.url; } - const content = await apiTemplateRender('kanji.html', params); - this.container.innerHTML = content; + this.updateNavigation(this.context.previous, this.context.next); + this.setNoContentVisible(definitions.length === 0); + + const fragment = document.createDocumentFragment(); + for (const definition of definitions) { + fragment.appendChild(this.displayGenerator.createKanjiEntry(definition)); + } + + await Promise.resolve(); // Delay to help avoid forced reflow warnings in Chrome + + this.container.textContent = ''; + this.container.appendChild(fragment); + const {index, scroll} = context; this.entryScrollIntoView(index || 0, scroll); @@ -445,6 +457,26 @@ class Display { if (errorOrphaned !== null) { errorOrphaned.style.setProperty('display', 'block', 'important'); } + + this.updateNavigation(null, null); + this.setNoContentVisible(false); + } + + setNoContentVisible(visible) { + const noResults = document.querySelector('#no-results'); + + if (noResults !== null) { + noResults.hidden = !visible; + } + } + + updateNavigation(previous, next) { + const navigation = document.querySelector('#navigation-header'); + if (navigation !== null) { + navigation.hidden = !(previous || next); + navigation.dataset.hasPrevious = `${!!previous}`; + navigation.dataset.hasNext = `${!!next}`; + } } autoPlayAudio() { @@ -733,6 +765,7 @@ class Display { } static addEventListener(eventListeners, object, type, listener, options) { + if (object === null) { return; } object.addEventListener(type, listener, options); eventListeners.push([object, type, listener, options]); }