Compare commits
5 Commits
5cfb2d8e6d
..
alar2
| Author | SHA1 | Date | |
|---|---|---|---|
| b13a23bbc4 | |||
| c07b8331c3 | |||
| 0532643cd8 | |||
| 846d9329cd | |||
| ee156dd11b |
@@ -1,110 +1,104 @@
|
||||
{{- define "header" -}}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
|
||||
{{- block "meta" . -}}
|
||||
{% block meta %}
|
||||
<title>
|
||||
{{- if eq .Data.PageType "/" }} {{- .L.T "global.siteName" -}}
|
||||
{{- else if eq .Data.PageType "glossary" }}{{- .L.T "public.glossaryTitle" -}}
|
||||
{{- else if eq .Data.PageType "search" }}{{- .L.Ts "public.searchTitle" "query" .Data.Query.Query -}}
|
||||
{{- else if ne .Data.Title "" }}{{ .Data.Title }}
|
||||
{{- end -}}
|
||||
{%- if page_type == "/" %} {{ t(key="global.siteTitle") }}
|
||||
{%- elif page_type == "glossary" %}{{ ts(key="public.glossaryTitle", lang=from_lang_name) }} - {{ t(key="global.siteName") }}
|
||||
{%- elif page_type == "search" %}{{ ts(key="public.searchTitle", query=query, to_lang=to_lang_name | lower) }} - {{ t(key="global.siteName") }}
|
||||
{%- elif title %}{{ title }} - {{ t(key="global.siteName") }}
|
||||
{%- endif -%}
|
||||
</title>
|
||||
<meta name="description" value="
|
||||
{{- if eq .Data.PageType "/" }}Dictionary website
|
||||
{{- else if eq .Data.PageType "glossary" }}Glossary of words.
|
||||
{{- else if eq .Data.PageType "search" }}{{ .Data.Query.Query }} meaning.
|
||||
{{- else if ne .Data.Description "" }}{{ .Data.Description }}
|
||||
{{- else }}{{ block "description" . }}{{end}}
|
||||
{{- end -}}" />
|
||||
{{- end -}}
|
||||
|
||||
{%- if page_type == "/" %}{{ t(key="public.mainTitle") }}
|
||||
{%- elif page_type == "glossary" %}{{ ts(key="public.glossaryTitle", lang=from_lang_name) }}
|
||||
{%- elif page_type == "search" %}{{ ts(key="public.searchDescription", from_lang=from_lang_name, query=query, to_lang=to_lang_name) }}
|
||||
{%- elif description %}{{ description }}
|
||||
{%- else %}{% block description %}{% endblock %}
|
||||
{%- endif -%}" />
|
||||
{% endblock %}
|
||||
<meta property="og:image" content="{{ consts.root_url | safe }}/static/thumb.png">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
|
||||
<script>
|
||||
window._ROOT_URL = "";
|
||||
window._MAX_CONTENT_ITEMS = {{ .Consts.SiteMaxEntryContentItems }};
|
||||
<script>
|
||||
window._ROOT_URL = "{{ consts.root_url | safe }}";
|
||||
window._MAX_CONTENT_ITEMS = {{ consts.site_max_content_items }};
|
||||
</script>
|
||||
<link rel="shortcut icon" href="/static/favicon.png?v={{ .AssetVer }}" type="image/x-icon" />
|
||||
<link href="/static/flexit.css?v={{ .AssetVer }}" rel="stylesheet" type="text/css" />
|
||||
<link href="/static/style.css?v={{ .AssetVer }}" rel="stylesheet" type="text/css" />
|
||||
<link rel="shortcut icon" href="/static/favicon.png?v={{ asset_ver }}" type="image/x-icon" />
|
||||
<link href="/static/_bundle.css?f=oat.min.css&f=style.css&v={{ asset_ver }}" rel="stylesheet" type="text/css" />
|
||||
<script defer src="/static/oat.min.js?v={{ asset_ver }}"></script>
|
||||
</head>
|
||||
<body class="{{ if eq .Data.PageType "/"}}home{{ end }}">
|
||||
<body class="{% if page_type == "/" %}home{% endif %}">
|
||||
<div class="container">
|
||||
<section class="main">
|
||||
<main class="main">
|
||||
<header class="header">
|
||||
<div class="row">
|
||||
<div class="logo four columns">
|
||||
<a href="/"><img src="/static/logo.svg?v={{ .AssetVer }}" alt="Dictionary logo" /></a>
|
||||
<h3 class="intro">
|
||||
<div class="logo col-4">
|
||||
<a class="unstyled" href="/"><img src="/static/logo.svg?v={{ asset_ver }}" alt="{{ t(key="global.siteName") }}" /></a>
|
||||
<div class="intro">
|
||||
<p>ಶ್ರೀ. ವಿ. ಕೃಷ್ಣ ಅವರ ಕನ್ನಡ - ಇಂಗ್ಲಿಷ್ ನಿಘಂಟು</p>
|
||||
“Alar” V. Krishna's Kannada → English dictionary
|
||||
</h3>
|
||||
"Alar" V. Krishna's Kannada → English dictionary
|
||||
</div>
|
||||
</div><!-- logo col -->
|
||||
|
||||
<div class="search eight columns">
|
||||
<div class="search col-8">
|
||||
<form class="search-form" method="get" action="">
|
||||
<div>
|
||||
<select name="lang" aria-label="Select dictionary">
|
||||
{{ range $d := .Dicts }}
|
||||
{{ $from := index $d 0}}
|
||||
{{ $to := index $d 1}}
|
||||
<option value="{{ $from.ID }}/{{ $to.ID }}">
|
||||
{{ $from.Name }} - {{ $to.Name }}
|
||||
</option>
|
||||
{{ end }}
|
||||
<fieldset class="group">
|
||||
<select name="lang" aria-label="{{ t(key="public.selectDict") }}">
|
||||
{% for d in dicts -%}
|
||||
{%- set from = d.0 -%}
|
||||
{%- set to = d.1 -%}
|
||||
<option value="{{ from.id }}/{{ to.id }}">{{ from.name }} - {{ to.name }}</option>
|
||||
{% endfor -%}
|
||||
</select>
|
||||
<div class="input-group">
|
||||
<input autofocus autocomplete="off" required placeholder="" aria-label="Search word"
|
||||
type="text" id="q" name="q" value="{{ if .Data.Query }}{{ .Data.Query.Query }}{{ end }}" data-autocomp-autoselect="false" />
|
||||
<button type="submit" aria-label="Search"><img src="/static/search.svg?v={{ .AssetVer }}" alt="{{- .L.T "global.btnSearch" -}}" /></button>
|
||||
</div>
|
||||
</div>
|
||||
<input autofocus autocomplete="off" required placeholder="" aria-label="{{ t(key="public.searchWord") }}"
|
||||
type="text" id="q" name="q" value="{{ query | default(value="") }}" />
|
||||
<button type="submit" aria-label="{{ t(key="global.btnSearch") }}"><img src="/static/search.svg?v={{ asset_ver }}" alt="{{ t(key="global.btnSearch") }}" /></button>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{{ end}}
|
||||
|
||||
{{ define "footer" }}
|
||||
<nav class="nav">
|
||||
{{ if .Consts.EnableSubmissions }}
|
||||
<a href="{{ $.Consts.RootURL }}/submit">{{- .L.T "public.submitEntry" -}}</a>
|
||||
{{ end }}
|
||||
{{ if .Consts.EnableGlossary }}
|
||||
{{ range $d := .Dicts }}
|
||||
{{ $from := index $d 0}}
|
||||
{{ $to := index $d 1}}
|
||||
<a href="{{ $.Consts.RootURL }}/glossary/{{ $from.ID }}/{{ $to.ID }}/*" class="tab">
|
||||
{{ title ($.L.Ts "public.glossary" "lang" "") }}
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<a href="/p/about">{{ .L.T "public.about" }}</a>
|
||||
{%- block content %}{% endblock -%}
|
||||
|
||||
<nav class="nav justify-center" aria-label="Site">
|
||||
{% if consts.enable_submissions %}
|
||||
<a href="{{ consts.root_url | safe }}/submit">{{ t(key="public.submitEntry") }}</a>
|
||||
{% endif %}
|
||||
{% if consts.enable_glossary %}
|
||||
{%- for d in dicts %}
|
||||
{%- set from = d.0 -%}
|
||||
{%- set to = d.1 -%}
|
||||
<a href="{{ consts.root_url | safe }}/glossary/{{ from.id }}/{{ to.id }}/*" class="tab">
|
||||
{{ ts(key="public.glossary", lang=from.name) }}
|
||||
</a>
|
||||
{% endfor -%}
|
||||
{% endif %}
|
||||
<a href="{{ consts.root_url | safe }}/p/about">{{ t(key="public.about") }}</a>
|
||||
</nav>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<footer class="footer">
|
||||
<a href="https://indicarchive.org">IDAF</a>
|
||||
<a href="https://zerodha.tech/blog/alar-the-making-of-an-open-source-dictionary/">Support by <img src="/static/zerodha-logo.svg" alt="Zerodha"></a>
|
||||
<footer class="footer align-center text-light">
|
||||
<span>Maintained by <a href="https://indicarchive.org">IDAF</a></span>
|
||||
<span>Supported by <a href="https://zerodha.tech/blog/alar-the-making-of-an-open-source-dictionary/"><img src="/static/zerodha-logo.svg" alt="Zerodha"></a></span>
|
||||
</footer>
|
||||
</div><!-- container -->
|
||||
|
||||
<form class="box form-comments" data-success="{{ .L.T "public.suggestSubmitted" }}">
|
||||
<div>
|
||||
<h4>{{ .L.T "public.submitTitle" }}</h4>
|
||||
<textarea name="comments" autofocus maxlength="500"></textarea>
|
||||
<p>
|
||||
<button type="submit">{{ .L.T "public.suggestSubmitBtn" }}</button>
|
||||
<button class="button-outline close">{{ .L.T "global.btnClose" }}</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
<template id="tpl-form-comments">
|
||||
<article class="card form-comments" popover data-success="{{ t(key="public.suggestSubmitted") }}">
|
||||
<h4>{{ t(key="public.submitTitle") }}</h4>
|
||||
<textarea name="comments" aria-label="{{ t(key="public.submitTitle") }}" maxlength="500"></textarea>
|
||||
|
||||
<script type="module" src="/static/_bundle.js?f=autocomp.js&f=share.js&f=main.js&f=alar.js&v={{ .AssetVer }}"></script>
|
||||
<footer class="hstack mt-2">
|
||||
<button class="submit-comment">{{ t(key="public.suggestSubmitBtn") }}</button>
|
||||
<button class="close" data-variant="secondary">{{ t(key="global.btnClose") }}</button>
|
||||
</footer>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script type="module" src="/static/_bundle.js?f=autocomp.js&f=share.js&f=main.js&f=alar.js&v={{ asset_ver }}"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
{{ end }}
|
||||
|
||||
+9
-12
@@ -1,13 +1,10 @@
|
||||
{{ define "glossary-words" }}
|
||||
{{ $g := .Data.Glossary }}
|
||||
{{ if not $g.Words }}
|
||||
<h2>{{ $.L.T "public.noResultsTitle" }}</h2>
|
||||
<p>{{ $.L.T "public.noResults" }}</p>
|
||||
{{ else }}
|
||||
<ul class="noul words">
|
||||
{{ range $i, $w := $g.Words }}
|
||||
<li><a href="{{ $.Consts.RootURL }}/dictionary/{{ UnicodeURL $g.FromLang }}/{{ UnicodeURL $g.ToLang }}/{{ UnicodeURL (index $w.Content 0) }}">{{ $w.Content | join ", " }}</a></li>
|
||||
{{ end }}
|
||||
{% if not glossary.words or glossary.words | length == 0 %}
|
||||
<h2>{{ t(key="public.noResultsTitle") }}</h2>
|
||||
<p>{{ t(key="public.noResults") }}</p>
|
||||
{% else %}
|
||||
<ul class="unstyled words">
|
||||
{% for w in glossary.words %}
|
||||
<li><a href="{{ consts.root_url }}/dictionary/{{ glossary.from_lang | urlencode }}/{{ glossary.to_lang | urlencode }}/{{ w.content.0 | urlencode }}">{{ w.content | join(sep=", ") }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{% endif %}
|
||||
|
||||
+18
-18
@@ -1,22 +1,22 @@
|
||||
{{ define "glossary" }}
|
||||
{{ template "header" . }}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content glossary">
|
||||
{{ if not .Data.Initials }}
|
||||
<h2>{{ .L.T "public.glossaryTitle" }}</h2>
|
||||
<p>{{ .L.T "public.noResults" }}</p>
|
||||
{{ else }}
|
||||
<nav class="index">
|
||||
{{ range $k, $a := .Data.Initials }}
|
||||
<a href="{{ UnicodeURL $a }}"{{ if eq $a $.Data.Initial }} class="sel"{{ end }}>{{ $a }}</a>
|
||||
{{ end }}
|
||||
</nav>
|
||||
{% if not initials or initials | length == 0 %}
|
||||
<h2>{{ t(key="public.glossaryTitle") }}</h2>
|
||||
<p>{{ t(key="public.noResults") }}</p>
|
||||
{% else %}
|
||||
<menu class="index buttons" aria-label="Alphabetical index">
|
||||
{% for a in initials %}
|
||||
<li><a href="{{ consts.root_url }}/glossary/{{ glossary.from_lang }}/{{ glossary.to_lang }}/{{ a | urlencode }}" class="button {% if a == initial %}small{% else %}ghost small{% endif %}">{{ a }}</a></li>
|
||||
{% endfor %}
|
||||
</menu>
|
||||
|
||||
<nav class="pagination top">{{ .Data.PgBar }}</nav>
|
||||
{{ template "glossary-words" . }}
|
||||
<nav class="pagination bottom">{{ .Data.PgBar }}</nav>
|
||||
{{ end}}
|
||||
<nav class="pagination top align-center mb-6" aria-label="Top pagination">{% include "pagination.html" %}</nav>
|
||||
|
||||
{% include "glossary-words.html" %}
|
||||
|
||||
<nav class="pagination bottom align-center mt-6" aria-label="Bottom pagination">{% include "pagination.html" %}</nav>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
||||
{% endblock %}
|
||||
|
||||
+13
-4
@@ -1,4 +1,13 @@
|
||||
{{ define "index" }}
|
||||
{{ template "header" . }}
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="stats align-center" aria-label="Statistics">
|
||||
<dl class="stats-list hstack justify-center">
|
||||
{%- for lang_code, count in consts.stats.languages -%}
|
||||
{%- set lang_name = langs[lang_code].name -%}
|
||||
<div><dt>{{ lang_name }}</dt> <dd><strong>{{ count }}</strong> {{ t(key="public.statsEntries") }}</dd></div>
|
||||
{%- endfor -%}
|
||||
<div><dt>{{ t(key="public.statsDefs") }}</dt> <dd><strong>{{ consts.stats.relations }}</strong></dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
@@ -4,33 +4,42 @@
|
||||
"global.btnClose": "Close",
|
||||
"global.btnDelete": "Delete",
|
||||
"global.btnSearch": "Search",
|
||||
"global.siteName": "Dictionary",
|
||||
"global.siteName": "Alar (ಅಲರ್) - Kannada - English dictionary. ಕನ್ನಡ-ಇಂಗ್ಲಿಷ್ ನಿಘಂಟು.",
|
||||
"global.siteTitle": "Alar (ಅಲರ್) - V. Krishna's Kannada-English Dictionary",
|
||||
"pagination.next": "Next",
|
||||
"pagination.previous": "Previous",
|
||||
"public.errorMessage": "An error occurred. Please try later.",
|
||||
"public.errorTitle": "Error",
|
||||
"public.about": "About",
|
||||
"public.glossary": "{lang} glossary",
|
||||
"public.glossaryTitle": "Glossary of words",
|
||||
"public.glossaryTitle": "Glossary of {lang} words",
|
||||
"public.mainTitle": "Dictionary website",
|
||||
"public.noResults": "No results where found for the query.",
|
||||
"public.noResultsTitle": "No results",
|
||||
"public.searchTitle": "{query} English meaning - Alar",
|
||||
"public.searchDescription": "{from_lang} word {query} meaning and definitions in {to_lang}",
|
||||
"public.searchTitle": "{query} {to_lang} meaning - Alar",
|
||||
"public.searchWord": "Search word",
|
||||
"public.selectDict": "Select dictionary",
|
||||
"public.shareSnapshot": "Share snapshot",
|
||||
"public.similarTitle": "Similar words",
|
||||
"public.playAudio": "Play audio",
|
||||
"public.subTitle": "Kannada-English dictionary",
|
||||
"public.sourceTag": "Source of the entry",
|
||||
"public.statsDefs": "definitions",
|
||||
"public.statsEntries": "entries",
|
||||
"public.submitEntry": "Suggest new entry",
|
||||
"public.submitEntryTitle": "Suggest new entry",
|
||||
"public.submitTitle": "Comments and suggestions",
|
||||
"public.about": "About",
|
||||
"public.subTitle": "Kannada-English dictionary",
|
||||
"public.suggestAddDefBtn": "Add another definition",
|
||||
"public.suggestContent": "Content",
|
||||
"public.suggestDefLang": "Definition language",
|
||||
"public.suggestDefsTitle": "Definitions",
|
||||
"public.suggestEdit": "Suggest edit for \"{word}\"",
|
||||
"public.suggestEdit": "Suggest an edit",
|
||||
"public.suggestEntryLang": "Entry language",
|
||||
"public.suggestPhones": "Phonetic notations (pronunciation)",
|
||||
"public.suggestPhonesPlaceholder": "eg: pɛts, pɛt (commma separated)",
|
||||
"public.suggestSubmitBtn": "Submit for review",
|
||||
"public.suggestSubmitted": "Submitted for review successfully",
|
||||
"public.suggestTitle": "Suggest a new entry",
|
||||
"public.viewMore": "View {num} more",
|
||||
"public.sourceTag": "Source of the entry"
|
||||
"public.playAudio": "Play audio",
|
||||
"public.viewMore": "View {num} more"
|
||||
}
|
||||
+5
-7
@@ -1,10 +1,8 @@
|
||||
{{ define "message" }}
|
||||
{{ template "header" . }}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<h1>{{ .Data.Title }}</h1>
|
||||
<p>{{ .Data.Description }}</p>
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ description }}</p>
|
||||
</section>
|
||||
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
||||
{% endblock %}
|
||||
|
||||
+10
-11
@@ -1,12 +1,12 @@
|
||||
{{ define "meta" }}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block meta %}
|
||||
<title>About V. Krishna's Alar (Kannada-English dictionary)</title>
|
||||
<meta name="description" content="About V. Krishna's Alar (Kannada-English dictionary)" />
|
||||
{{ end }}
|
||||
{% endblock %}
|
||||
|
||||
{{ define "page-about" }}
|
||||
|
||||
{{ template "header" . }}
|
||||
<section class="page box">
|
||||
{% block content %}
|
||||
<section class="content about page">
|
||||
<h1>About</h1>
|
||||
<p>
|
||||
<em>Alar</em> is an authoritative Kannada-English dictionary corpus
|
||||
@@ -50,7 +50,7 @@
|
||||
<p>
|
||||
This website is published using <a href="https://github.com/knadh/dictpress">dictpress</a>,
|
||||
and uses <a href="https://github.com/knadh/knphone">knphone</a>, a Kannada phonetic
|
||||
indexing algorithm, for search. Search suggestions and transliterations are powered by <a href="https://github.com/varnamproject">Varnam</a>.
|
||||
indexing algorithm, for search. Search suggestions and transliterations are powered by <a href="https://github.com/varnamproject">Varnam</a>.
|
||||
The website's source is <a href="https://github.com/alar-dict/alar.ink">available here</a>.
|
||||
</p>
|
||||
<p>
|
||||
@@ -61,8 +61,8 @@
|
||||
|
||||
<p><a href="https://zerodha.com"><img height="18px" src="/static/zerodha-logo.svg"
|
||||
alt="Zerodha" /></a></p>
|
||||
<p>In 2019, <a href="https://zerodha.tech/blog/alar-the-making-of-an-open-source-dictionary">Zerodha collaborated</a> with
|
||||
V. Krishna to open source and publish his dictionary online and
|
||||
<p>In 2019, <a href="https://zerodha.tech/blog/alar-the-making-of-an-open-source-dictionary">Zerodha collaborated</a> with
|
||||
V. Krishna to open source and publish his dictionary online and
|
||||
awarded him a grant to support his work.
|
||||
</p>
|
||||
|
||||
@@ -72,5 +72,4 @@
|
||||
alt="Indic Archive" /></a></p>
|
||||
<p>Since 2023, Alar is a project under Indic Digital Archive Foundation.</p>
|
||||
</section>
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{% if total_pages > 1 %}
|
||||
<menu class="buttons">
|
||||
<li><a {% if page > 1 %}href="{{ pg_url }}?page={{ page - 1 }}&per_page={{ per_page }}"{% endif %} class="button outline small"{% if page <= 1 %} aria-disabled="true"{% endif %}>← {{ t(key="pagination.previous") }}</a></li>
|
||||
|
||||
{% set window = 10 %}
|
||||
{% set half = 5 %}
|
||||
{% set_global start = page - half %}
|
||||
{% set_global end = start + window - 1 %}
|
||||
{% if start < 1 %}
|
||||
{% set_global start = 1 %}
|
||||
{% set_global end = window %}
|
||||
{% endif %}
|
||||
{% if end > total_pages %}
|
||||
{% set_global end = total_pages %}
|
||||
{% set_global start = total_pages - window + 1 %}
|
||||
{% if start < 1 %}{% set_global start = 1 %}{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if start > 1 %}
|
||||
<li><a href="{{ pg_url }}?page=1&per_page={{ per_page }}" class="button outline small">1</a></li>
|
||||
{% if start > 2 %}<li><span class="ellipsis">...</span></li>{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% for p in range(start=start, end=end + 1) %}
|
||||
{% if p == page %}
|
||||
<li><a class="button small" aria-current="page">{{ p }}</a></li>
|
||||
{% else %}
|
||||
<li><a href="{{ pg_url }}?page={{ p }}&per_page={{ per_page }}" class="button outline small">{{ p }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if end < total_pages %}
|
||||
{% if end < total_pages - 1 %}<li><span class="ellipsis">...</span></li>{% endif %}
|
||||
<li><a href="{{ pg_url }}?page={{ total_pages }}&per_page={{ per_page }}" class="button outline small">{{ total_pages }}</a></li>
|
||||
{% endif %}
|
||||
|
||||
<li><a {% if page < total_pages %}href="{{ pg_url }}?page={{ page + 1 }}&per_page={{ per_page }}"{% endif %} class="button outline small"{% if page >= total_pages %} aria-disabled="true"{% endif %}>{{ t(key="pagination.next") }} →</a></li>
|
||||
</menu>
|
||||
{% endif %}
|
||||
+79
-76
@@ -1,97 +1,100 @@
|
||||
{{ define "results" }}
|
||||
{{ $maxContentItems := .Consts.SiteMaxEntryContentItems }}
|
||||
{{ $numResults := (min (len .Data.Results.Entries) 10) }}
|
||||
{% set max_content_items = consts.site_max_content_items %}
|
||||
{% set num_results = results.entries | length %}
|
||||
{% if num_results > 10 %}{% set num_results = 10 %}{% endif %}
|
||||
|
||||
<div class="row">
|
||||
<div class="eight columns">
|
||||
<ol class="entries">
|
||||
{{- range $k, $r := (mustSlice .Data.Results.Entries 0 $numResults) -}}
|
||||
<li class="entry" data-guid="{{ $r.GUID }}" data-head="{{ $r.Content | join ", " }}">
|
||||
<header class="head">
|
||||
<div class="col-8">
|
||||
<ol class="entries unstyled">
|
||||
{% for r in results.entries | slice(end=num_results) %}
|
||||
{% set shareGuid = "s" ~ r.guid | split(pat="-") | first %}
|
||||
<li class="entry card" id="{{ shareGuid }}" data-head="{{ r.content | join(sep=", ") }}" data-lang="{{ r.lang }}">
|
||||
<header class="head hstack justify-between">
|
||||
<div>
|
||||
<div class="title-wrap">
|
||||
<h3 class="title">{{ $r.Content | join ", " }}</h3>
|
||||
|
||||
{{- if $r.Meta.audio -}}
|
||||
<a href="{{ $r.Meta.audio }}" class="audio" data-audio><img src="/static/audio.svg?v={{ $.AssetVer }}" alt="{{ $.L.T "public.playAudio" }}"></a>
|
||||
{{- end -}}
|
||||
|
||||
{{- if $.Consts.EnableSubmissions -}}
|
||||
<a href="#" data-from="{{ $r.GUID }}" class="edit" title="{{ $.L.Ts "public.suggestEdit" "word" (index $r.Content 0) }}">✏️</a>
|
||||
{{- end -}}
|
||||
<div class="title-wrap hstack items-center gap-2">
|
||||
<h3 class="title">{{ r.content | join(sep=", ") }}</h3>
|
||||
{% if r.meta.audio %}
|
||||
<button type="button" class="audio" data-audio data-src="{{ r.meta.audio }}">
|
||||
<img src="/static/audio.svg?v={{ asset_ver }}" alt="{{ t(key="public.playAudio") }}">
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{{- if $r.Phones -}}
|
||||
<span class="pronun">♪ {{ $r.Phones | join "," }}</span>
|
||||
{{- end -}}
|
||||
|
||||
{% if r.phones and r.phones | length > 0 %}
|
||||
<span class="pronun text-light">♪ {{ r.phones | join(sep=",") }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="meta">
|
||||
{{- if $r.Tags -}}
|
||||
<div class="meta hstack items-center">
|
||||
{% if r.tags and r.tags | length > 0 %}
|
||||
<span class="tags">
|
||||
{{- range $tag := $r.Tags -}}
|
||||
<span class="tag" {{- if hasPrefix "src:" $tag }} title="{{ $.L.T "public.sourceTag" }}"{{- end }}>{{ $tag }}</span>
|
||||
{{- end -}}
|
||||
{% for tag in r.tags %}
|
||||
<span class="badge outline" {% if tag is starting_with("src:") %} title="{{ t(key="public.sourceTag") }}"{% endif %}>{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</span>
|
||||
{{- end -}}
|
||||
<a href="#" title="Share screenshot" class="export" data-guid="{{ $r.GUID }}"><img src="/static/export.svg?v={{ $.AssetVer }}" alt="Share screenshot"></a>
|
||||
{% endif %}
|
||||
|
||||
<div class="toolbar">
|
||||
{% if consts.enable_submissions %}
|
||||
<button class="ghost edit item" data-edit-from="{{ r.guid }}" title="{{ t(key="public.suggestEdit") }}"><img src="/static/edit.svg?v={{ asset_ver }}" alt="{{ t(key="public.suggestEdit") }}"></button>
|
||||
{% endif %}
|
||||
<button class="ghost share item" data-share-entry="{{ shareGuid }}" title="{{ t(key="public.shareSnapshot") }}"><img src="/static/export.svg?v={{ asset_ver }}" alt="{{ t(key="public.shareSnapshot") }}"></button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{{- if $r.Relations -}}
|
||||
{{- $lastType := "" -}}
|
||||
{{- range $k, $d := $r.Relations -}}
|
||||
{{- $types := ($d.RelationTypes | join ", ") -}}
|
||||
{% if r.relations and r.relations | length > 0 %}
|
||||
{%- set_global last_type = "" -%}
|
||||
{%- for d in r.relations -%}
|
||||
{%- set types = d.relation.types | default(value=[]) | join(sep=", ") -%}
|
||||
|
||||
{{- if ne $lastType $types -}}
|
||||
{{- if $lastType -}}</ol>{{- end }}
|
||||
<ol class="defs">
|
||||
{{ if $d.RelationTypes }}
|
||||
<li class="types">
|
||||
{{- range $t := $d.RelationTypes -}}
|
||||
<span>
|
||||
{{- $dType := index (index $.Langs $r.Lang).Types $t }}
|
||||
{{- if $dType }}{{ $dType }} {{ end -}}
|
||||
{{- $rType := index (index $.Langs $d.Lang).Types $t }}
|
||||
{{- if $rType }}({{ $rType }}){{ end -}}
|
||||
</span>
|
||||
{{- end -}}
|
||||
</li>
|
||||
{{ end }}
|
||||
{{- end -}}
|
||||
{%- if last_type != types or loop.first -%}
|
||||
{% if not loop.first %}</ol>{% endif %}
|
||||
<ol class="defs">
|
||||
<li class="types">
|
||||
{%- for t in d.relation.types | default(value=[]) -%}
|
||||
{%- if t != "" -%}
|
||||
<span>
|
||||
{%- set type_label = langs[d.lang].types | get(key=t, default="") -%}
|
||||
{%- if type_label %}{{ type_label }}
|
||||
{%- else %}({{ t }}){% endif -%}
|
||||
</span>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</li>
|
||||
{%- endif %}
|
||||
|
||||
<li><div data-guid="{{ $d.GUID }}" class="def">
|
||||
{{ $d.Content | join ", " }}
|
||||
{{ if or (gt $d.ContentLength $maxContentItems) $d.Meta.synonyms -}}
|
||||
<a href="#" class="more-toggle" data-id="{{ $r.GUID }}-{{ $d.GUID }}" title="{{ $.L.Ts "public.viewMore" "num" $d.ContentLength }}"
|
||||
data-entry-guid="{{ $d.GUID }}" data-from-lang="{{ $r.Lang }}" data-to-lang="{{ $d.Lang }}"
|
||||
role="button" aria-expanded="false" aria-controls="{{ $r.GUID }}-{{ $d.GUID }}">
|
||||
</a>
|
||||
{{- end -}}
|
||||
{{- if $.Consts.EnableSubmissions }}
|
||||
<a href="#" data-from="{{ $r.GUID }}" data-to="{{ $d.GUID }}" class="edit" title="{{ $.L.Ts "public.suggestEdit" "word" (index $d.Content 0) }}">✏️</a>
|
||||
{{ end -}}
|
||||
<li><div data-guid="{{ d.guid }}" class="def" data-lang="{{ d.lang }}">
|
||||
{{ d.content | join(sep=", ") }}
|
||||
{% if d.content_length > max_content_items or d.meta.synonyms %}
|
||||
<a href="#" class="more-toggle" data-id="{{ r.guid }}-{{ d.guid }}" title="{{ t(key="public.viewMore") }}"
|
||||
data-entry-guid="{{ d.guid }}" data-from-lang="{{ r.lang }}" data-to-lang="{{ d.lang }}"
|
||||
role="button" aria-expanded="false" aria-controls="{{ r.guid }}-{{ d.guid }}">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if consts.enable_submissions %}
|
||||
<button type="button" data-edit-from="{{ r.guid }}" data-edit-to="{{ d.guid }}" class="edit" title="{{ t(key="public.suggestEdit") }}">
|
||||
<img src="/static/edit.svg?v={{ asset_ver }}" alt="{{ t(key="public.suggestEdit") }}">
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<!-- This is dynamically populated by JS when the "more" link is clicked -->
|
||||
<div class="more" id="{{ $r.GUID }}-{{ $d.GUID }}" role="region" aria-hidden="true"></div>
|
||||
<div class="more" id="{{ r.guid }}-{{ d.guid }}" role="region" aria-hidden="true"></div>
|
||||
|
||||
</div></li>
|
||||
{{ $lastType = $types }}
|
||||
{{- end -}}
|
||||
</ol>
|
||||
{{ end }}
|
||||
</div></li>
|
||||
{%- set_global last_type = types -%}
|
||||
{%- endfor -%}
|
||||
</ol>
|
||||
{% endif %}
|
||||
|
||||
</li>
|
||||
{{- end -}}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
<nav class="three columns col-offset-1 related">
|
||||
{{ if gt (len .Data.Results.Entries) $numResults }}
|
||||
<h3>{{ .L.T "public.similarTitle" }}</h3>
|
||||
{{ range $k, $r := (mustSlice .Data.Results.Entries $numResults) }}
|
||||
<a href="{{ UnicodeURL (index $r.Content 0) }}" class="word">{{ index $r.Content 0 }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<nav class="col-3 offset-1 related" aria-label="{{ t(key='public.similarTitle') }}">
|
||||
{% if results.entries | length > num_results %}
|
||||
<h3>{{ t(key="public.similarTitle") }}</h3>
|
||||
{% for r in results.entries | slice(start=num_results) %}
|
||||
<a href="{{ consts.root_url }}/dictionary/{{ from_lang }}/{{ to_lang }}/{{ r.content.0 | urlencode }}" class="word">{{ r.content.0 }}</a>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
+9
-11
@@ -1,16 +1,14 @@
|
||||
{{ define "search" }}
|
||||
{{ template "header" . }}
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content results">
|
||||
{{ if not .Data.Results.Entries }}
|
||||
<h2>{{ .L.T "public.noResultsTitle" }}</h2>
|
||||
{% if not results.entries or results.entries | length == 0 %}
|
||||
<h2>{{ t(key="public.noResultsTitle") }}</h2>
|
||||
<p>
|
||||
{{ .L.T "public.noResults" }}
|
||||
{{ t(key="public.noResults") }}
|
||||
</p>
|
||||
{{ else }}
|
||||
{{ template "results" . }}
|
||||
{{ end }}
|
||||
{% else %}
|
||||
{% include "results.html" %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
||||
{% endblock %}
|
||||
|
||||
+1
-6
@@ -1,8 +1,3 @@
|
||||
// Custom language code mapping for govarnam.
|
||||
window.autoCompLangCodes = {
|
||||
"kannada": "kn"
|
||||
}
|
||||
|
||||
function hasKannadaChar(str) {
|
||||
return /[\u0C80-\u0CFF]/.test(str);
|
||||
}
|
||||
@@ -57,4 +52,4 @@ function hasKannadaChar(str) {
|
||||
defs[i].appendChild(s);
|
||||
}
|
||||
|
||||
})();
|
||||
})();
|
||||
|
||||
+16
-3
@@ -7,6 +7,9 @@ function autocomp(el, options = {}) {
|
||||
|
||||
// Disable browser's default autocomplete behaviour on the input.
|
||||
el.autocomplete = "off";
|
||||
el.setAttribute("role", "combobox");
|
||||
el.setAttribute("aria-autocomplete", "list");
|
||||
el.setAttribute("aria-expanded", "false");
|
||||
|
||||
// Attach all the events required for the interactions in one go.
|
||||
["input", "keydown", "blur"].forEach(k => el.addEventListener(k, handleEvent));
|
||||
@@ -85,7 +88,9 @@ function autocomp(el, options = {}) {
|
||||
});
|
||||
|
||||
box.classList.add("autocomp");
|
||||
box.setAttribute("role", "listbox");
|
||||
el.parentNode.insertBefore(box, el.nextSibling);
|
||||
el.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
|
||||
function renderResults() {
|
||||
@@ -93,6 +98,8 @@ function autocomp(el, options = {}) {
|
||||
items.forEach((item, idx) => {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("autocomp-item");
|
||||
div.setAttribute("role", "option");
|
||||
div.setAttribute("aria-selected", idx === cur ? "true" : "false");
|
||||
|
||||
// If there's a custom renderer callback, use it, else, simply insert the value/text as-is.
|
||||
opt.onRender ? div.appendChild(opt.onRender(item)) : div.innerText = item;
|
||||
@@ -110,11 +117,16 @@ function autocomp(el, options = {}) {
|
||||
|
||||
// Remove the previous item's highlight;
|
||||
const prev = box.querySelector(`:nth-child(${cur + 1})`);
|
||||
prev?.classList.remove("autocomp-sel");
|
||||
if (prev) {
|
||||
prev.classList.remove("autocomp-sel");
|
||||
prev.setAttribute("aria-selected", "false");
|
||||
}
|
||||
|
||||
// Increment the cursor and highlight the next item, cycled between [0, n].
|
||||
cur = (cur + direction + items.length) % items.length;
|
||||
box.querySelector(`:nth-child(${cur + 1})`).classList.add("autocomp-sel");
|
||||
const next = box.querySelector(`:nth-child(${cur + 1})`);
|
||||
next.classList.add("autocomp-sel");
|
||||
next.setAttribute("aria-selected", "true");
|
||||
}
|
||||
|
||||
function select(idx) {
|
||||
@@ -122,7 +134,7 @@ function autocomp(el, options = {}) {
|
||||
return;
|
||||
}
|
||||
|
||||
val = opt.onSelect(items[idx], items);
|
||||
val = opt.onSelect(items[idx]);
|
||||
el.value = val || items[idx];
|
||||
}
|
||||
|
||||
@@ -133,5 +145,6 @@ function autocomp(el, options = {}) {
|
||||
box.remove();
|
||||
box = null;
|
||||
}
|
||||
el.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="#aaa" width="12" height="12" viewBox="0 0 15.867 15.79" xmlns:v="https://vecta.io/nano"><path d="M9.866 2.635l3.228 3.228-8.17 8.17-3.226-3.228zm5.677-.778L14.104.417a1.43 1.43 0 0 0-2.018 0l-1.379 1.379 3.228 3.228 1.608-1.608a1.1 1.1 0 0 0 0-1.559zM.009 15.342c-.059.264.18.501.444.437l3.597-.872-3.226-3.228z"/></svg>
|
||||
|
After Width: | Height: | Size: 366 B |
+1
-2
@@ -1,2 +1 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#aaa" width="18px" height="18px" viewBox="0 0 40 40" xmlns="http://www.w3.org/2000/svg"><path d="M30.3 13.7L25 8.4l-5.3 5.3-1.4-1.4L25 5.6l6.7 6.7z"/><path d="M24 7h2v21h-2z"/><path d="M35 40H15c-1.7 0-3-1.3-3-3V19c0-1.7 1.3-3 3-3h7v2h-7c-.6 0-1 .4-1 1v18c0 .6.4 1 1 1h20c.6 0 1-.4 1-1V19c0-.6-.4-1-1-1h-7v-2h7c1.7 0 3 1.3 3 3v18c0 1.7-1.3 3-3 3z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="#aaa" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M8 10V1.5M5 4l3-2.5L11 4"/><path d="M3 7v6.5h10V7"/></svg>
|
||||
|
Before Width: | Height: | Size: 485 B After Width: | Height: | Size: 242 B |
@@ -1,135 +0,0 @@
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.row {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
.row.nogutter {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
.row.nogutter > .columns {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
.columns {
|
||||
box-sizing: border-box;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
.one {
|
||||
flex-basis: 8.33333333%;
|
||||
}
|
||||
.two {
|
||||
flex-basis: 16.66666667%;
|
||||
}
|
||||
.three {
|
||||
flex-basis: 25%;
|
||||
}
|
||||
.four {
|
||||
flex-basis: 33.3333333333%;
|
||||
}
|
||||
.five {
|
||||
flex-basis: 41.66666667%;
|
||||
}
|
||||
.six {
|
||||
flex-basis: 50%;
|
||||
}
|
||||
.seven {
|
||||
flex-basis: 58.33333333%;
|
||||
}
|
||||
.eight {
|
||||
flex-basis: 66.66666667%;
|
||||
}
|
||||
.nine {
|
||||
flex-basis: 75%;
|
||||
}
|
||||
.ten {
|
||||
flex-basis: 83.33333333%;
|
||||
}
|
||||
.eleven {
|
||||
flex-basis: 91.66666667%;
|
||||
}
|
||||
.twelve {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
.col-offset-0 {
|
||||
margin-left: 0;
|
||||
}
|
||||
.col-offset-1 {
|
||||
margin-left: 8.33333333%;
|
||||
}
|
||||
.col-offset-2 {
|
||||
margin-left: 16.66666667%;
|
||||
}
|
||||
.col-offset-3 {
|
||||
margin-left: 25%;
|
||||
}
|
||||
.col-offset-4 {
|
||||
margin-left: 33.33333333%;
|
||||
}
|
||||
.col-offset-5 {
|
||||
margin-left: 41.66666667%;
|
||||
}
|
||||
.col-offset-6 {
|
||||
margin-left: 50%;
|
||||
}
|
||||
.col-offset-7 {
|
||||
margin-left: 58.33333333%;
|
||||
}
|
||||
.col-offset-8 {
|
||||
margin-left: 66.66666667%;
|
||||
}
|
||||
.col-offset-9 {
|
||||
margin-left: 75%;
|
||||
}
|
||||
.col-offset-10 {
|
||||
margin-left: 83.33333333%;
|
||||
}
|
||||
.col-offset-11 {
|
||||
margin-left: 91.66666667%;
|
||||
}
|
||||
.between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
.end {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.around {
|
||||
justify-content: space-around;
|
||||
}
|
||||
.row-align-center {
|
||||
align-items: center;
|
||||
}
|
||||
.space-right {
|
||||
margin-right: 10px;
|
||||
}
|
||||
.space-left {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.space-bottom {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.space-top {
|
||||
margin-top: 10px;
|
||||
}
|
||||
@media (max-width: 980px) {
|
||||
.columns {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
.offset-0,
|
||||
.offset-1,
|
||||
.offset-2 {
|
||||
margin: unset;
|
||||
}
|
||||
.container {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" xmlns:v="https://vecta.io/nano"><path fill="#aaa" d="M48 0c-4.285 0-8.298 1.654-11.308 4.692L24.678 16.678c-3.037 3.01-4.692 7.051-4.692 11.308 0 5.478 2.766 10.495 7.376 13.478.705.461 1.519.569 2.278.407a3.03 3.03 0 0 0 1.871-1.302c.434-.705.569-1.519.407-2.251-.163-.759-.624-1.437-1.302-1.898-2.902-1.844-4.61-4.99-4.61-8.407a9.95 9.95 0 0 1 2.929-7.078L40.922 8.949C42.82 7.051 45.315 6.02 48 6.02a10.02 10.02 0 0 1 10.007 10.007 9.95 9.95 0 0 1-2.929 7.078L49.383 28.8a3.06 3.06 0 0 0-.895 2.169c0 .759.298 1.492.895 2.061.597.597 1.356.868 2.115.868s1.546-.298 2.115-.868l5.695-5.695A15.94 15.94 0 0 0 64 16c0-8.814-7.186-16-16-16zM36.61 22.536c-.705-.461-1.519-.569-2.278-.407a3.03 3.03 0 0 0-1.871 1.302c-.434.705-.569 1.519-.407 2.251.163.759.624 1.437 1.302 1.898 2.902 1.844 4.61 4.99 4.61 8.407a9.95 9.95 0 0 1-2.929 7.078L23.051 55.078c-1.898 1.898-4.393 2.929-7.078 2.929A10.02 10.02 0 0 1 5.966 48a9.95 9.95 0 0 1 2.929-7.078l5.695-5.695a3.06 3.06 0 0 0 .895-2.169c0-.732-.298-1.492-.895-2.061-.597-.597-1.356-.868-2.115-.868s-1.546.298-2.115.868l-5.695 5.695A15.87 15.87 0 0 0 0 48c0 8.814 7.186 16 16 16 4.285 0 8.298-1.654 11.308-4.692l12.014-11.986c3.037-3.01 4.692-7.051 4.692-11.308-.027-5.478-2.766-10.522-7.403-13.478z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
+84
-120
@@ -1,15 +1,3 @@
|
||||
async function screenshotElement(element) {
|
||||
const canvas = await html2canvas(element);
|
||||
canvas.toBlob(blob => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'screenshot.png';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
}
|
||||
|
||||
(() => {
|
||||
const elForm = document.querySelector("form.search-form");
|
||||
const elQ = document.querySelector("#q");
|
||||
@@ -34,7 +22,7 @@ async function screenshotElement(element) {
|
||||
let val = q.trim();
|
||||
|
||||
const uri = elForm.getAttribute("action");
|
||||
document.location.href = `${uri}/${encodeURIComponent(val).replace(/%20/g, "+")}`;
|
||||
document.location.href = `${uri}/${encodeURIComponent(val).replace(/%20/g, "-")}`;
|
||||
}
|
||||
|
||||
// ===================================
|
||||
@@ -82,7 +70,7 @@ async function screenshotElement(element) {
|
||||
// fetch() content from the dataset data.guid from /api/entry/{guid}
|
||||
// and populate the div.more innerHTML with the content.
|
||||
if (state === "block" && !el.dataset.fetched) {
|
||||
container.innerHTML = `<div class="loader"></div>`;
|
||||
container.innerHTML = `<div aria-busy="true"></div>`;
|
||||
|
||||
fetch(`${window._ROOT_URL}/api/dictionary/entries/${el.dataset.entryGuid}`)
|
||||
.then((resp) => resp.json())
|
||||
@@ -127,52 +115,11 @@ async function screenshotElement(element) {
|
||||
elQ.select();
|
||||
})();
|
||||
|
||||
// Screenshot sharing.
|
||||
(() => {
|
||||
document.querySelectorAll("a.export").forEach((el) => {
|
||||
el.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
const guid = el.dataset.guid;
|
||||
const entryEl = document.querySelector(`.entry[data-guid='${guid}']`);
|
||||
if (!entryEl) {
|
||||
alert("Could not find entry to export");
|
||||
return;
|
||||
}
|
||||
|
||||
const title = entryEl.dataset.head;
|
||||
// Make the filename by stripping spaces from the head word(s).
|
||||
const filename = title.replace(/\s+/g, "_").toLowerCase();
|
||||
|
||||
try {
|
||||
await shareDOM(entryEl, `${title} meaning`, `${localStorage.from_lang} to ${localStorage.to_lang} meaning\n\n${window.location.href}`, `${filename}.png`);
|
||||
} catch (err) {
|
||||
console.error("Error sharing entry:", err);
|
||||
alert(`Error sharing entry: ${err?.message || err}`);
|
||||
}
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
||||
// Play audio.
|
||||
(() => {
|
||||
document.querySelectorAll("a[data-audio]").forEach((el) => {
|
||||
el.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
const audio = new Audio(el.getAttribute("href"));
|
||||
audio.play().catch((err) => {
|
||||
console.error("error playing audio:", err);
|
||||
alert("error playing audio");
|
||||
});
|
||||
};
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
// Submission form.
|
||||
(() => {
|
||||
function filterTypes(e) {
|
||||
// Filter the types select field with elements that are supported by the language.
|
||||
const types = e.target.closest("fieldset").querySelector("select[name=relation_type]");
|
||||
const types = e.target.closest("[data-relation-controls]").querySelector("select[name=relation_type]");
|
||||
types.querySelectorAll("option").forEach((o) => o.style.display = "none");
|
||||
types.querySelectorAll(`option[data-lang=${e.target.value}]`).forEach((o) => o.style.display = "block");
|
||||
types.selectedIndex = 1;
|
||||
@@ -207,64 +154,59 @@ async function screenshotElement(element) {
|
||||
}
|
||||
})();
|
||||
|
||||
// Edit form.
|
||||
// Edit form using ot-dropdown.
|
||||
(() => {
|
||||
document.querySelectorAll(".edit").forEach((o) => {
|
||||
o.onclick = ((e) => {
|
||||
e.preventDefault();
|
||||
const btn = e.target;
|
||||
const tpl = document.querySelector("#tpl-form-comments");
|
||||
if (!tpl) return;
|
||||
|
||||
// Form is already open.
|
||||
if (btn.close) {
|
||||
btn.close();
|
||||
return;
|
||||
}
|
||||
let counter = 0;
|
||||
document.querySelectorAll("[data-edit-from]").forEach((btn) => {
|
||||
const parent = btn.parentNode;
|
||||
|
||||
const form = document.querySelector(".form-comments").cloneNode(true);
|
||||
o.parentNode.appendChild(form);
|
||||
form.style.display = "block";
|
||||
// Clone template content and give the popover a unique ID.
|
||||
const popoverId = `form-comments-${counter++}`;
|
||||
const form = tpl.content.firstElementChild.cloneNode(true);
|
||||
form.id = popoverId;
|
||||
|
||||
const txt = form.querySelector("textarea");
|
||||
txt.focus();
|
||||
txt.onkeydown = (e) => {
|
||||
if (e.key === "Escape" && txt.value === "") {
|
||||
btn.close();
|
||||
}
|
||||
};
|
||||
// Wire the button as the popover trigger.
|
||||
btn.setAttribute("popovertarget", popoverId);
|
||||
|
||||
btn.close = () => {
|
||||
btn.close = null;
|
||||
form.remove();
|
||||
};
|
||||
// Build <ot-dropdown> with children fully assembled before inserting into DOM
|
||||
// so that init() finds [popovertarget] and [popover].
|
||||
const dropdown = document.createElement("ot-dropdown");
|
||||
dropdown.appendChild(btn);
|
||||
dropdown.appendChild(form);
|
||||
parent.appendChild(dropdown);
|
||||
|
||||
// Handle form submission.
|
||||
form.onsubmit = () => {
|
||||
fetch(`${window._ROOT_URL}/api/submissions/comments`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from_guid: btn.dataset.from,
|
||||
to_guid: btn.dataset.to,
|
||||
comments: txt.value
|
||||
})
|
||||
}).catch((err) => {
|
||||
alert(`Error submitting: ${err}`);
|
||||
});
|
||||
const txt = form.querySelector("textarea");
|
||||
|
||||
alert(form.dataset.success);
|
||||
btn.close();
|
||||
};
|
||||
// Handle submission.
|
||||
form.querySelector("button.submit-comment").onclick = () => {
|
||||
fetch(`${window._ROOT_URL}/api/submissions/comments`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
from_guid: btn.dataset.editFrom,
|
||||
to_guid: btn.dataset.editTo,
|
||||
comments: txt.value
|
||||
})
|
||||
}).catch((err) => {
|
||||
alert(`Error submitting: ${err}`);
|
||||
});
|
||||
|
||||
form.querySelector("button.close").onclick = btn.close;
|
||||
});
|
||||
})
|
||||
alert(form.dataset.success);
|
||||
form.hidePopover();
|
||||
};
|
||||
|
||||
form.querySelector("button.close").onclick = () => form.hidePopover();
|
||||
});
|
||||
})();
|
||||
|
||||
// Autocomplete.
|
||||
(() => {
|
||||
if(!autocomp) {
|
||||
if (!autocomp) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -273,37 +215,25 @@ async function screenshotElement(element) {
|
||||
let debounce;
|
||||
|
||||
autocomp(elQ, {
|
||||
autoSelect: elQ.dataset.autocompAutoselect === "true",
|
||||
autoSelect: false,
|
||||
onQuery: async (val) => {
|
||||
const langCode = localStorage.from_lang;
|
||||
|
||||
if (!langCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const shortcode = autoCompLangCodes?.[langCode] ?? langCode;
|
||||
|
||||
clearTimeout(debounce);
|
||||
return new Promise(resolve => {
|
||||
debounce = setTimeout(async () => {
|
||||
const response = await fetch(`${_ROOT_URL}/atl/${shortcode}/${val.toLowerCase()}`);
|
||||
const response = await fetch(`${_ROOT_URL}/api/autocomplete/${langCode}/${val}`);
|
||||
const data = await response.json();
|
||||
|
||||
const a = data.greedy_tokenized.map(item => item.word).slice(0, 3).sort((a, b) => a.length - b.length);
|
||||
const b = data.dictionary_suggestions.map(item => item.word).slice(0, 6).sort((a, b) => a.length - b.length);
|
||||
const suggestions = data.data.map(item => item.content[0]);
|
||||
|
||||
debounce = null;
|
||||
resolve([...new Set(a.concat(b))]);
|
||||
resolve(suggestions);
|
||||
}, 50);
|
||||
});
|
||||
},
|
||||
|
||||
onSelect: (val, items) => {
|
||||
// If the val is English, then pick the first item from items and use that.
|
||||
if (/^[A-Za-z0-9\-,'" ]+$/.test(val) && items.length > 0) {
|
||||
val = items[0];
|
||||
}
|
||||
|
||||
onSelect: (val) => {
|
||||
// autocomp search isn't complete. Use the user's input instead of autocomp selection.
|
||||
if (val) {
|
||||
elQ.value = val;
|
||||
}
|
||||
@@ -313,3 +243,37 @@ async function screenshotElement(element) {
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
// Audio playback.
|
||||
document.querySelectorAll("[data-audio]").forEach((el) => {
|
||||
el.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
const audio = new Audio(el.dataset.src);
|
||||
audio.play().catch((err) => {
|
||||
console.error("error playing audio:", err);
|
||||
alert("error playing audio");
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Scroll to hash on load.
|
||||
window.setTimeout(() => {
|
||||
if (window.location.hash) {
|
||||
document.querySelector(window.location.hash)?.scrollIntoView();
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Screenshot share.
|
||||
document.querySelectorAll("[data-share-entry]").forEach((el) => {
|
||||
el.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
const entryEl = document.getElementById(el.dataset.shareEntry);
|
||||
if (!entryEl) return;
|
||||
try {
|
||||
await shareEntry(entryEl);
|
||||
} catch (err) {
|
||||
console.error("Error sharing entry:", err);
|
||||
alert(`Error sharing: ${err?.message || err}`);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+194
-252
@@ -1,265 +1,207 @@
|
||||
async function screenshotDOM(el, opts = {}) {
|
||||
const {
|
||||
scale = 2,
|
||||
type = 'image/png',
|
||||
quality = 0.92,
|
||||
background = '#fff',
|
||||
// if you want to *allow* splitting (and risk the bug), set to false
|
||||
preventCapsuleSplit = true
|
||||
} = opts;
|
||||
// Canvas-based entry card screenshot and share.
|
||||
|
||||
if (!(el instanceof Element)) throw new TypeError('Expected a DOM Element');
|
||||
function renderEntryCard(entryEl) {
|
||||
const cs = getComputedStyle(document.documentElement);
|
||||
const colors = {
|
||||
bg: '#fff',
|
||||
primary: cs.getPropertyValue('--primary').trim() || '#111',
|
||||
light: cs.getPropertyValue('--light').trim() || '#666',
|
||||
lighter: cs.getPropertyValue('--lighter').trim() || '#aaa',
|
||||
border: '#e6e6e6',
|
||||
};
|
||||
const fontFamily = getComputedStyle(document.body).fontFamily;
|
||||
const font = (size, bold) => `${bold ? 'bold ' : ''}${size}px ${fontFamily}`;
|
||||
|
||||
if (document.fonts && document.fonts.status !== 'loaded') {
|
||||
try { await document.fonts.ready; } catch { }
|
||||
// Extract data from entry DOM.
|
||||
const headword = entryEl.dataset.head || '';
|
||||
const pronun = entryEl.querySelector('.pronun')?.textContent?.trim() || '';
|
||||
|
||||
// Collect definition groups: [{type, defs}]
|
||||
const groups = [];
|
||||
entryEl.querySelectorAll('ol.defs').forEach((ol) => {
|
||||
let typeLabel = '';
|
||||
const defs = [];
|
||||
ol.querySelectorAll(':scope > li').forEach((li) => {
|
||||
if (li.classList.contains('types')) {
|
||||
typeLabel = li.textContent.trim();
|
||||
} else {
|
||||
const defEl = li.querySelector('.def');
|
||||
if (defEl) {
|
||||
let text = '';
|
||||
for (const node of defEl.childNodes) {
|
||||
if (node.matches?.('.more, .more-toggle, .edit')) break;
|
||||
text += node.textContent;
|
||||
}
|
||||
text = text.trim().replace(/\s+/g, ' ');
|
||||
if (text) defs.push(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (defs.length > 0) {
|
||||
groups.push({ type: typeLabel, defs });
|
||||
}
|
||||
});
|
||||
|
||||
// Layout constants.
|
||||
const W = 600, pad = 32, contentW = W - pad * 2;
|
||||
const headSize = 22, pronunSize = 14, typeSize = 13, defSize = 15;
|
||||
const lineHeight = 1.45;
|
||||
const scale = Math.max(2, window.devicePixelRatio || 2);
|
||||
const numIndent = 24;
|
||||
|
||||
// Text wrapping.
|
||||
const mctx = document.createElement('canvas').getContext('2d');
|
||||
const defFont = font(defSize);
|
||||
const defMaxW = contentW - numIndent;
|
||||
|
||||
function wrapText(text) {
|
||||
mctx.font = defFont;
|
||||
const words = text.split(' ');
|
||||
const lines = [];
|
||||
let line = '';
|
||||
for (const word of words) {
|
||||
const test = line ? line + ' ' + word : word;
|
||||
if (mctx.measureText(test).width > defMaxW && line) {
|
||||
lines.push(line);
|
||||
line = word;
|
||||
} else {
|
||||
line = test;
|
||||
}
|
||||
}
|
||||
if (line) lines.push(line);
|
||||
return lines.length ? lines : [''];
|
||||
}
|
||||
|
||||
// Pre-wrap all definitions.
|
||||
const groupLayouts = groups.map((g) => ({
|
||||
type: g.type,
|
||||
defs: g.defs.map(wrapText),
|
||||
}));
|
||||
|
||||
// Unified layout: measures when ctx is null, draws when provided.
|
||||
function doLayout(ctx) {
|
||||
let y = pad;
|
||||
|
||||
if (ctx) {
|
||||
ctx.font = font(headSize, true);
|
||||
ctx.fillStyle = colors.primary;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(headword, pad, y);
|
||||
}
|
||||
y += headSize * lineHeight;
|
||||
|
||||
if (pronun) {
|
||||
if (ctx) {
|
||||
ctx.font = font(pronunSize);
|
||||
ctx.fillStyle = colors.light;
|
||||
ctx.fillText(pronun, pad, y);
|
||||
}
|
||||
y += pronunSize * lineHeight + 2;
|
||||
}
|
||||
y += 12;
|
||||
|
||||
for (const gl of groupLayouts) {
|
||||
if (gl.type) {
|
||||
if (ctx) {
|
||||
ctx.font = font(typeSize, true);
|
||||
ctx.fillStyle = colors.light;
|
||||
const tw = ctx.measureText(gl.type).width;
|
||||
ctx.fillText(gl.type, pad, y);
|
||||
ctx.setLineDash([3, 3]);
|
||||
ctx.strokeStyle = colors.lighter;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(pad, y + typeSize + 2);
|
||||
ctx.lineTo(pad + tw, y + typeSize + 2);
|
||||
ctx.stroke();
|
||||
ctx.setLineDash([]);
|
||||
}
|
||||
y += typeSize * lineHeight + 8;
|
||||
}
|
||||
|
||||
for (let i = 0; i < gl.defs.length; i++) {
|
||||
if (ctx) {
|
||||
ctx.font = defFont;
|
||||
ctx.fillStyle = colors.lighter;
|
||||
ctx.fillText(`${i + 1}.`, pad, y);
|
||||
ctx.fillStyle = colors.primary;
|
||||
}
|
||||
for (const line of gl.defs[i]) {
|
||||
if (ctx) ctx.fillText(line, pad + numIndent, y);
|
||||
y += defSize * lineHeight;
|
||||
}
|
||||
y += 4;
|
||||
}
|
||||
y += 4;
|
||||
}
|
||||
|
||||
const { width, height } = (() => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return { width: Math.ceil(r.width), height: Math.ceil(r.height) };
|
||||
})();
|
||||
if (!width || !height) throw new Error('Element has zero width/height.');
|
||||
return y + pad - 4;
|
||||
}
|
||||
|
||||
const clone = el.cloneNode(true);
|
||||
await inlineEverything(el, clone);
|
||||
// Measure, create canvas, draw.
|
||||
const H = Math.ceil(doLayout(null));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = W * scale;
|
||||
canvas.height = H * scale;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(scale, scale);
|
||||
|
||||
if (background !== 'transparent') clone.style.background = background;
|
||||
if (!clone.getAttribute('xmlns')) clone.setAttribute('xmlns', 'http://www.w3.org/1999/xhtml');
|
||||
// Card background.
|
||||
ctx.fillStyle = colors.bg;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(4, 4, W - 8, H - 8, 8);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = colors.border;
|
||||
ctx.lineWidth = 1.2;
|
||||
ctx.stroke();
|
||||
|
||||
const xhtml = new XMLSerializer().serializeToString(clone);
|
||||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
||||
<foreignObject x="0" y="0" width="100%" height="100%">${xhtml}</foreignObject>
|
||||
</svg>`;
|
||||
doLayout(ctx);
|
||||
|
||||
const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
|
||||
const url = URL.createObjectURL(svgBlob);
|
||||
|
||||
try {
|
||||
const img = await loadImage(url);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.ceil(width * scale);
|
||||
canvas.height = Math.ceil(height * scale);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (background !== 'transparent' || type !== 'image/png') {
|
||||
ctx.fillStyle = background === 'transparent' ? '#0000' : background;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
const blob = await new Promise(res => canvas.toBlob(res, type, quality));
|
||||
if (!blob) throw new Error('Canvas.toBlob returned null.');
|
||||
return blob;
|
||||
} finally {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function loadImage(src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.decoding = 'async';
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = reject;
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
async function inlineEverything(srcNode, dstNode) {
|
||||
copyComputedStyle(srcNode, dstNode);
|
||||
|
||||
// Remove interactive states that may be captured on touch devices
|
||||
if (dstNode instanceof Element) {
|
||||
// Reset outline from :focus state
|
||||
dstNode.style.setProperty('outline', 'none', 'important');
|
||||
// Reset any pointer-events to ensure no hover states
|
||||
const cs = window.getComputedStyle(srcNode);
|
||||
// Only override cursor if it's pointer (indicating interactivity)
|
||||
if (cs.cursor === 'pointer') {
|
||||
dstNode.style.setProperty('cursor', 'default', 'important');
|
||||
}
|
||||
}
|
||||
|
||||
if (srcNode instanceof HTMLTextAreaElement) {
|
||||
dstNode.textContent = srcNode.value;
|
||||
} else if (srcNode instanceof HTMLInputElement) {
|
||||
dstNode.setAttribute('value', srcNode.value);
|
||||
if ((srcNode.type === 'checkbox' || srcNode.type === 'radio') && srcNode.checked) {
|
||||
dstNode.setAttribute('checked', '');
|
||||
}
|
||||
} else if (srcNode instanceof HTMLSelectElement) {
|
||||
const sel = Array.from(srcNode.options).filter(o => o.selected).map(o => o.value);
|
||||
Array.from(dstNode.options).forEach(o => (o.selected = sel.includes(o.value)));
|
||||
}
|
||||
|
||||
if (srcNode instanceof HTMLCanvasElement) {
|
||||
try {
|
||||
const dataURL = srcNode.toDataURL();
|
||||
const img = document.createElement('img');
|
||||
img.src = dataURL;
|
||||
copyBoxSizing(dstNode, img);
|
||||
dstNode.replaceWith(img);
|
||||
dstNode = img;
|
||||
} catch { }
|
||||
}
|
||||
|
||||
// Handle img elements to ensure they render properly
|
||||
if (srcNode instanceof HTMLImageElement && dstNode instanceof HTMLImageElement) {
|
||||
// Convert image to data URL for inlining
|
||||
if (srcNode.complete && srcNode.naturalWidth > 0) {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
canvas.width = srcNode.naturalWidth;
|
||||
canvas.height = srcNode.naturalHeight;
|
||||
ctx.drawImage(srcNode, 0, 0);
|
||||
dstNode.src = canvas.toDataURL('image/png');
|
||||
// Also set width/height to ensure proper rendering
|
||||
const cs = window.getComputedStyle(srcNode);
|
||||
dstNode.style.width = cs.width;
|
||||
dstNode.style.height = cs.height;
|
||||
dstNode.style.objectFit = cs.objectFit;
|
||||
} catch (err) {
|
||||
// If cross-origin or other error, keep original src
|
||||
console.log('Could not inline image:', err);
|
||||
dstNode.src = srcNode.src;
|
||||
}
|
||||
} else {
|
||||
// Image not loaded or has no dimensions, use src as-is
|
||||
dstNode.src = srcNode.src || srcNode.getAttribute('src') || '';
|
||||
}
|
||||
// Remove alt text from being rendered
|
||||
dstNode.removeAttribute('alt');
|
||||
}
|
||||
|
||||
materializePseudo(srcNode, dstNode, '::before');
|
||||
materializePseudo(srcNode, dstNode, '::after');
|
||||
|
||||
const sKids = srcNode.childNodes;
|
||||
const dKids = dstNode.childNodes;
|
||||
for (let i = 0; i < sKids.length; i++) {
|
||||
const s = sKids[i], d = dKids[i];
|
||||
if (s && d && s.nodeType === 1 && d.nodeType === 1) await inlineEverything(s, d);
|
||||
}
|
||||
|
||||
function copyComputedStyle(src, dst) {
|
||||
const cs = window.getComputedStyle(src);
|
||||
let cssText = '';
|
||||
for (const prop of cs) cssText += `${prop}:${cs.getPropertyValue(prop)};`;
|
||||
dst.setAttribute('style', (dst.getAttribute('style') || '') + cssText);
|
||||
|
||||
// Always keep transform origin consistent
|
||||
if (cs.transformOrigin) dst.style.transformOrigin = cs.transformOrigin;
|
||||
|
||||
// Keep each line painting its own decoration (helps some engines)
|
||||
dst.style.setProperty('box-decoration-break', 'clone', 'important');
|
||||
dst.style.setProperty('-webkit-box-decoration-break', 'clone', 'important');
|
||||
|
||||
// --- CAPSULE FIX: prevent decorated inline from splitting across lines ---
|
||||
if (preventCapsuleSplit) {
|
||||
const hasBg = (cs.backgroundImage && cs.backgroundImage !== 'none') ||
|
||||
(cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent');
|
||||
const hasRadius = ['borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius']
|
||||
.some(k => parseFloat(cs[k]) > 0);
|
||||
const hasPad = ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft']
|
||||
.some(k => parseFloat(cs[k]) > 0);
|
||||
const isInlineLevel = cs.display.startsWith('inline');
|
||||
|
||||
if (isInlineLevel && (hasBg || hasRadius || hasPad)) {
|
||||
// Make it behave like a chip for capture: no internal wrapping
|
||||
dst.style.setProperty('display', 'inline-block', 'important');
|
||||
dst.style.setProperty('white-space', 'nowrap', 'important');
|
||||
// In case original allowed breaking long words, emulate by clipping
|
||||
if (cs.overflowWrap === 'break-word' || cs.wordBreak === 'break-all') {
|
||||
dst.style.setProperty('max-width', cs.maxWidth && cs.maxWidth !== 'none' ? cs.maxWidth : '100%', 'important');
|
||||
dst.style.setProperty('overflow', 'hidden', 'important');
|
||||
dst.style.setProperty('text-overflow', 'ellipsis', 'important');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function copyBoxSizing(src, dst) {
|
||||
const cs = window.getComputedStyle(src);
|
||||
dst.style.width = cs.width;
|
||||
dst.style.height = cs.height;
|
||||
dst.style.display = cs.display;
|
||||
dst.style.objectFit = 'contain';
|
||||
}
|
||||
|
||||
function materializePseudo(src, dst, which) {
|
||||
const ps = window.getComputedStyle(src, which);
|
||||
if (!ps || ps.content === '' || ps.content === 'none') return;
|
||||
const span = document.createElement('span');
|
||||
let cssText = '';
|
||||
for (const prop of ps) cssText += `${prop}:${ps.getPropertyValue(prop)};`;
|
||||
span.setAttribute('style', cssText);
|
||||
|
||||
const content = ps.content;
|
||||
const quoted = /^(['"]).*\1$/.test(content);
|
||||
if (quoted) span.textContent = content.slice(1, -1).replace(/\\n/g, '\n');
|
||||
else if (content.startsWith('attr(')) {
|
||||
const attrName = content.slice(5, -1).trim();
|
||||
span.textContent = src.getAttribute(attrName) || '';
|
||||
} else span.textContent = '';
|
||||
|
||||
if (which === '::before') dst.insertBefore(span, dst.firstChild);
|
||||
else dst.appendChild(span);
|
||||
}
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
canvas.toBlob((blob) => resolve(blob), 'image/png');
|
||||
});
|
||||
}
|
||||
|
||||
async function shareDOM(target, title, text, filename) {
|
||||
async function shareEntry(entryEl) {
|
||||
const blob = await renderEntryCard(entryEl);
|
||||
const head = entryEl.dataset.head || 'entry';
|
||||
const filename = `${head.replace(/[^a-zA-Z0-9\u0900-\u097F\u0D00-\u0D7F]/g, '_')}.png`;
|
||||
const file = new File([blob], filename, { type: 'image/png', lastModified: Date.now() });
|
||||
|
||||
// 1. Try Web Share with image file.
|
||||
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
||||
await navigator.share({ files: [file] });
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. Try Web Share with text/URL (no file support).
|
||||
if (navigator.share) {
|
||||
const def = entryEl.querySelector('.def');
|
||||
const url = `${window.location.origin}${window.location.pathname}#${entryEl.id}`;
|
||||
await navigator.share({
|
||||
title: head,
|
||||
text: def?.textContent?.trim() || '',
|
||||
url: url,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Try clipboard.
|
||||
if (navigator.clipboard && window.ClipboardItem) {
|
||||
try {
|
||||
// Remove any active/focus states before capturing
|
||||
// This is especially important on touch devices (Android)
|
||||
if (document.activeElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
|
||||
alert('Screenshot copied to clipboard');
|
||||
return;
|
||||
} catch (e) {
|
||||
console.log('Clipboard write failed, downloading instead:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Wait a brief moment for any :active states to clear
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// Make a crisp image (PNG keeps transparency; use JPEG for smaller files)
|
||||
const blob = await screenshotDOM(target, {
|
||||
scale: Math.max(2, window.devicePixelRatio || 2),
|
||||
type: 'image/png',
|
||||
background: 'white'
|
||||
});
|
||||
|
||||
const file = new File([blob], filename || 'share.png', {
|
||||
type: blob.type,
|
||||
lastModified: Date.now()
|
||||
});
|
||||
|
||||
// Web Share API with files (Android Chrome, iOS/iPadOS Safari 16+).
|
||||
if (navigator.canShare && navigator.canShare({ files: [file] })) {
|
||||
await navigator.share({ title, text, files: [file] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy image to clipboard (desktop Chrome/Edge, some Android)
|
||||
if (navigator.clipboard && window.ClipboardItem) {
|
||||
try {
|
||||
await navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
|
||||
alert('Screenshot copied to clipboard');
|
||||
return;
|
||||
} catch (clipErr) {
|
||||
// Clipboard write failed (common on Android), fall through to download
|
||||
console.log('Clipboard write not allowed, downloading instead:', clipErr);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: prompt a download
|
||||
const dlUrl = URL.createObjectURL(blob);
|
||||
const a = Object.assign(document.createElement('a'), {
|
||||
href: dlUrl,
|
||||
download: 'share.png'
|
||||
});
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(dlUrl);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert(`share failed: ${err?.message || err}`);
|
||||
} finally { }
|
||||
// Fallback: download.
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = Object.assign(document.createElement('a'), { href: url, download: filename });
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
+421
-699
File diff suppressed because it is too large
Load Diff
+39
-41
@@ -1,67 +1,67 @@
|
||||
{{ define "submit-entry" }}
|
||||
{{ template "header" . }}
|
||||
{% extends "base.html" %}
|
||||
|
||||
<h2>{{ .L.T "public.submitEntryTitle" }}</h2>
|
||||
{% block content %}
|
||||
<h2>{{ t(key="public.submitEntry") }}</h2>
|
||||
<hr />
|
||||
<form method="post" action="" class="form-submit">
|
||||
<div>
|
||||
<div class="row">
|
||||
<fieldset class="columns three">
|
||||
<label for="entry-lang">{{ .L.T "public.suggestEntryLang" }}</label>
|
||||
<select name="entry_lang" for="entry-lang">
|
||||
{{ range $id, $l := .Langs }}
|
||||
<option value="{{ $id }}">{{ $l.Name }}</option>
|
||||
{{ end }}
|
||||
<div class="col-3">
|
||||
<label for="entry-lang">{{ t(key="public.suggestEntryLang") }}</label>
|
||||
<select name="entry_lang" id="entry-lang">
|
||||
{% for id, l in langs %}
|
||||
<option value="{{ id }}">{{ l.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</fieldset>
|
||||
<fieldset class="columns one"></fieldset>
|
||||
<fieldset class="columns eight">
|
||||
<label for="entry-content">{{ .L.T "public.suggestContent" }}</label>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
<div class="col-8">
|
||||
<label for="entry-content">{{ t(key="public.suggestContent") }}</label>
|
||||
<textarea name="entry_content" id="entry-content" required minlength="3"></textarea>
|
||||
</fieldset>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset>
|
||||
<label>{{ .L.T "public.suggestPhones" }}</label>
|
||||
<input type="text" name="phones" placeholder="{{ .L.T "public.suggestPhonesPlaceholder" }}" />
|
||||
<label>{{ t(key="public.suggestPhones") }}</label>
|
||||
<input type="text" name="phones" placeholder="{{ t(key="public.suggestPhonesPlaceholder") }}" />
|
||||
</fieldset>
|
||||
|
||||
<br />
|
||||
<h3>{{ .L.T "public.suggestDefsTitle" }}</h3>
|
||||
<ol class="add-relations box">
|
||||
<h3>{{ t(key="public.suggestDefsTitle") }}</h3>
|
||||
<ol class="add-relations card">
|
||||
<li class="row">
|
||||
<fieldset class="columns three">
|
||||
<label>{{ .L.T "public.suggestDefLang" }}</label>
|
||||
<div class="col-3" data-relation-controls>
|
||||
<label>{{ t(key="public.suggestDefLang") }}</label>
|
||||
<select name="relation_lang">
|
||||
{{ range $id, $l := .Langs }}
|
||||
<option value="{{ $id }}">{{ $l.Name }}</option>
|
||||
{{ end }}
|
||||
{% for id, l in langs %}
|
||||
<option value="{{ id }}">{{ l.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<select name="relation_type">
|
||||
{{ range $lid, $l := .Langs }}
|
||||
{{ range $tid, $t := $l.Types }}
|
||||
<option data-lang="{{ $lid }}" value="{{ $tid }}">{{ $t }}</option>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<select name="relation_type" aria-label="Definition type">
|
||||
{% for lid, l in langs %}
|
||||
{% for tid, typ in l.types %}
|
||||
<option data-lang="{{ lid }}" value="{{ tid }}">{{ typ }}</option>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<br />
|
||||
<a href="#" class="btn-remove-relation">{{ .L.T "global.btnDelete" }}</a>
|
||||
</fieldset>
|
||||
<fieldset class="columns one"></fieldset>
|
||||
<fieldset class="columns eight">
|
||||
<label for="definition-content">{{ .L.T "public.suggestContent" }}</label>
|
||||
<a href="#" class="btn-remove-relation">{{ t(key="global.btnDelete") }}</a>
|
||||
</div>
|
||||
<div class="col-1"></div>
|
||||
<div class="col-8">
|
||||
<label for="definition-content">{{ t(key="public.suggestContent") }}</label>
|
||||
<textarea name="relation_content" required minlength="3"></textarea>
|
||||
</fieldset>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<p class="text-right">
|
||||
<a href="#" class="btn-add-relation">+ {{ .L.T "public.suggestAddDefBtn" }}</a>
|
||||
<p class="align-right">
|
||||
<a href="#" class="btn-add-relation">+ {{ t(key="public.suggestAddDefBtn") }}</a>
|
||||
</p>
|
||||
|
||||
<br />
|
||||
<p>
|
||||
<button type="submit">{{ .L.T "public.suggestSubmitBtn" }}</button>
|
||||
<button type="submit">{{ t(key="public.suggestSubmitBtn") }}</button>
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
@@ -69,6 +69,4 @@
|
||||
<script>
|
||||
(() => { window.setTimeout(() => { document.querySelector('#entry-content').focus() }, 10); })();
|
||||
</script>
|
||||
|
||||
{{ template "footer" . }}
|
||||
{{ end }}
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user