#421 geo tag suggestion
This commit is contained in:
parent
1351679a21
commit
14461c362f
@ -12,6 +12,10 @@
|
||||
<h1>@ViewData["Title"]</h1>
|
||||
</div>
|
||||
<script>
|
||||
// full existing-location catalog ({id, name, latitude, longitude}[]), used by the
|
||||
// search-as-you-type location picker on each geo tag - see setupLocationAutocomplete in bk.js
|
||||
var allGeoLocations = @Html.Raw(Model.AllLocationsJson);
|
||||
|
||||
async function checkAllNotCorrect() {
|
||||
var ok = await upConfirm('همه نادرست؟');
|
||||
if (!ok) return;
|
||||
@ -40,10 +44,13 @@
|
||||
}
|
||||
|
||||
function toggleGeoTagNewLocationFields(geoIndex) {
|
||||
var select = document.getElementById('geotag' + geoIndex + '_location_select');
|
||||
var hidden = document.getElementById('geotag' + geoIndex + '_location_select');
|
||||
var fields = document.getElementById('geotag' + geoIndex + '_new_location_fields');
|
||||
if (!select || !fields) return;
|
||||
fields.style.display = (select.value === '0') ? '' : 'none';
|
||||
if (!hidden || !fields) return;
|
||||
// '' (nothing picked yet / cleared while typing) and '0' (the old select's "no change /
|
||||
// suggest as new location" option) both mean "not linked to an existing location"
|
||||
var linkedToExisting = hidden.value && hidden.value !== '0';
|
||||
fields.style.display = linkedToExisting ? 'none' : '';
|
||||
}
|
||||
|
||||
function moderatePoemCorrections(verseCount, correctionId, geoTagCount) {
|
||||
@ -912,22 +919,24 @@ else
|
||||
<p class="up-hint">
|
||||
اگر کاربر در نام مکان یا مختصات آن اشتباه کرده اما پیشنهادش در اصل ارزش تأیید دارد، بهجای رد کردن میتوانید همینجا آن را اصلاح کنید:
|
||||
</p>
|
||||
<label for="geotag@(geoIndex)_location_select">اتصال به مکان موجود در فهرست (در صورت وجود اشتباه در تشخیص مکان):</label>
|
||||
<select id="geotag@(geoIndex)_location_select" name="geotag@(geoIndex)_location_select" onchange="toggleGeoTagNewLocationFields(@geoIndex)">
|
||||
<option value="0">-- بدون تغییر / پیشنهاد بهعنوان مکان جدید --</option>
|
||||
@foreach (var loc in (Model.Locations ?? new List<GanjoorGeoLocation>()))
|
||||
{
|
||||
if (loc.Id == 0) continue;
|
||||
if (geoTag.LocationId == loc.Id)
|
||||
{
|
||||
<option value="@loc.Id" selected="selected">@loc.Name (@loc.Latitude, @loc.Longitude)</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
<option value="@loc.Id">@loc.Name (@loc.Latitude, @loc.Longitude)</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
<label for="geotag@(geoIndex)_location_search">اتصال به مکان موجود در فهرست (در صورت وجود اشتباه در تشخیص مکان):</label>
|
||||
<div class="up-autocomplete-wrap">
|
||||
<input type="text" id="geotag@(geoIndex)_location_search" autocomplete="off" placeholder="نام مکان را تایپ کنید..."
|
||||
value="@(geoTag.LocationId != null ? (Model.Locations ?? new List<GanjoorGeoLocation>()).Where(l => l.Id == geoTag.LocationId).Select(l => l.Name).FirstOrDefault() : null)" />
|
||||
<input type="hidden" id="geotag@(geoIndex)_location_select" name="geotag@(geoIndex)_location_select" value="@(geoTag.LocationId ?? 0)" />
|
||||
<div id="geotag@(geoIndex)_location_results" class="up-autocomplete-results" style="display:none;"></div>
|
||||
</div>
|
||||
<p class="up-hint">برای اتصال به یک مکان موجود، نام آن را تایپ کنید و از فهرست انتخاب کنید؛ برای رها کردن اتصال و پیشنهاد بهعنوان مکان جدید، این فیلد را خالی کنید.</p>
|
||||
<script>
|
||||
setupLocationAutocomplete(
|
||||
document.getElementById('geotag@(geoIndex)_location_search'),
|
||||
document.getElementById('geotag@(geoIndex)_location_select'),
|
||||
document.getElementById('geotag@(geoIndex)_location_results'),
|
||||
allGeoLocations,
|
||||
function (loc) { toggleGeoTagNewLocationFields(@geoIndex); },
|
||||
function (typedText) { toggleGeoTagNewLocationFields(@geoIndex); }
|
||||
);
|
||||
</script>
|
||||
@if (geoTag.LocationId == null)
|
||||
{
|
||||
<div id="geotag@(geoIndex)_new_location_fields">
|
||||
@ -946,7 +955,7 @@ else
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<div class="up-radio-option">
|
||||
<input type="radio" id="geotag@(geoIndex)_review_result_Approved" name="geotag@(geoIndex)_review_result" class="radio-approved" value="@RMuseum.Models.Ganjoor.CorrectionReviewResult.Approved">
|
||||
<input type="radio" id="geotag@(geoIndex)_review_result_Approved" name="geotag@(geoIndex)_review_result" class="radio-approved" value="@RMuseum.Models.Ganjoor.CorrectionReviewResult.Approved" checked="checked">
|
||||
<label for="geotag@(geoIndex)_review_result_Approved">@RMuseum.Services.Implementation.CorrectionReviewResultConvertor.GetString(RMuseum.Models.Ganjoor.CorrectionReviewResult.Approved)</label>
|
||||
</div>
|
||||
<div class="up-radio-option">
|
||||
@ -958,7 +967,7 @@ else
|
||||
<label for="geotag@(geoIndex)_review_result_RejectedBecauseUnnecessaryChange">تغییر سلیقهای یا بی دلیل است</label>
|
||||
</div>
|
||||
<div class="up-radio-option">
|
||||
<input type="radio" id="geotag@(geoIndex)_review_result_Rejected" name="geotag@(geoIndex)_review_result" class="radio-rejected" value="@RMuseum.Models.Ganjoor.CorrectionReviewResult.Rejected" checked="checked">
|
||||
<input type="radio" id="geotag@(geoIndex)_review_result_Rejected" name="geotag@(geoIndex)_review_result" class="radio-rejected" value="@RMuseum.Models.Ganjoor.CorrectionReviewResult.Rejected">
|
||||
<label for="geotag@(geoIndex)_review_result_Rejected">@RMuseum.Services.Implementation.CorrectionReviewResultConvertor.GetString(RMuseum.Models.Ganjoor.CorrectionReviewResult.Rejected)</label>
|
||||
</div>
|
||||
<input type="text" id="geotag@(geoIndex)_review_note" name="geotag@(geoIndex)_review_note" value="" size="50" />
|
||||
|
||||
@ -68,6 +68,17 @@ namespace GanjooRazor.Areas.Admin.Pages
|
||||
/// </summary>
|
||||
public List<GanjoorGeoLocation> Locations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// camelCase JSON of Locations (id/name/latitude/longitude only), used client-side for the
|
||||
/// search-as-you-type location picker on each geo tag (see setupLocationAutocomplete in bk.js) -
|
||||
/// same shape/purpose as Editor.cshtml.cs's AllLocationsJson
|
||||
/// </summary>
|
||||
public string AllLocationsJson =>
|
||||
JsonConvert.SerializeObject(
|
||||
(Locations ?? new List<GanjoorGeoLocation>()).Select(l => new { l.Id, l.Name, l.Latitude, l.Longitude }),
|
||||
new JsonSerializerSettings { ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver() }
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// groups verses into couplets - same logic as SuggestQuoted.cshtml.cs's/Editor.cshtml.cs's GetCouplets,
|
||||
/// duplicated here rather than shared, so this page doesn't take on a cross-file dependency on those
|
||||
|
||||
@ -126,6 +126,42 @@
|
||||
var allGeoLocations = @Html.Raw(Model.AllLocationsJson);
|
||||
$(document).ready(function () {
|
||||
renderPendingGeoDateTags();
|
||||
setupLocationAutocomplete(
|
||||
document.getElementById('geotag-location-search'),
|
||||
document.getElementById('geotag-location-select'),
|
||||
document.getElementById('geotag-location-results'),
|
||||
allGeoLocations,
|
||||
function (loc) {
|
||||
// picking an existing location (or clearing the field) means whatever was
|
||||
// typed into the "new location" panel no longer applies to this suggestion
|
||||
var widget = document.getElementById('new-location-widget');
|
||||
if (loc != null && widget.style.display !== 'none') {
|
||||
toggleNewLocationWidget();
|
||||
}
|
||||
},
|
||||
function (typedText) {
|
||||
var widget = document.getElementById('new-location-widget');
|
||||
if (widget.style.display === 'none' || widget.style.display === '') {
|
||||
toggleNewLocationWidget();
|
||||
}
|
||||
document.getElementById('geotag-new-location-name').value = typedText;
|
||||
}
|
||||
);
|
||||
var directAddLocationSearch = document.getElementById('location-search');
|
||||
if (directAddLocationSearch != null) {
|
||||
// admin-only direct-add row (bypasses the suggestion/moderation flow entirely) -
|
||||
// no "new location" concept here, so an unmatched search just has nothing to pick
|
||||
setupLocationAutocomplete(
|
||||
directAddLocationSearch,
|
||||
document.getElementById('location'),
|
||||
document.getElementById('location-results'),
|
||||
allGeoLocations,
|
||||
null,
|
||||
function (typedText) {
|
||||
upToast('این مکان در فهرست نیست. برای افزودن مکان جدید از «مکانها» در پایین این جدول استفاده کنید.', 'error');
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
var geoDateTagMap = null;
|
||||
var geoDateTagMarker = null;
|
||||
@ -268,7 +304,7 @@
|
||||
var s = geoDateTagSuggestions[i];
|
||||
var status = s.markForDelete ? '🗑️ درخواست حذف' : '➕ پیشنهاد جدید';
|
||||
var coupletText = (s.coupletIndex != null && geoDateTagCoupletTexts[s.coupletIndex]) ? geoDateTagCoupletTexts[s.coupletIndex] : '— کل شعر —';
|
||||
var locationText = s.markForDelete ? '' : (s.locationId ? $('#geotag-location-select option[value="' + s.locationId + '"]').text() : (s.suggestedLocationName || ''));
|
||||
var locationText = s.markForDelete ? '' : (s.locationId ? ((allGeoLocations.filter(function (l) { return l.id === s.locationId; })[0] || {}).name || ('مکان #' + s.locationId)) : (s.suggestedLocationName || ''));
|
||||
html += '<tr><td>' + status + '</td><td>' + coupletText + '</td><td>' + locationText + '</td><td>' + (s.lunarYear || '') + '</td><td>' + (s.lunarMonth || '') + '</td><td>' + (s.lunarDay || '') + '</td>' +
|
||||
'<td><a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="removeGeoDateTagSuggestion(' + i + ')">✕ حذف از فهرست</a></td></tr>';
|
||||
}
|
||||
@ -354,6 +390,8 @@
|
||||
function resetGeoDateTagForm() {
|
||||
$('#geotag-couplet-select').val('');
|
||||
$('#geotag-location-select').val('');
|
||||
$('#geotag-location-search').val('');
|
||||
$('#geotag-location-results').hide().empty();
|
||||
$('#geotag-new-location-name').val('');
|
||||
$('#geotag-new-location-lat').val('');
|
||||
$('#geotag-new-location-lng').val('');
|
||||
@ -2001,14 +2039,13 @@ else
|
||||
</div>
|
||||
|
||||
<div class="up-field">
|
||||
<label for="geotag-location-select">مکان</label>
|
||||
<select id="geotag-location-select">
|
||||
<option value="">— انتخاب نشده —</option>
|
||||
@foreach (var location in Model.Locations)
|
||||
{
|
||||
<option value="@location.Id">@location.Name</option>
|
||||
}
|
||||
</select>
|
||||
<label for="geotag-location-search">مکان</label>
|
||||
<div class="up-autocomplete-wrap">
|
||||
<input type="text" id="geotag-location-search" autocomplete="off" placeholder="نام مکان را تایپ کنید..." />
|
||||
<input type="hidden" id="geotag-location-select" />
|
||||
<div id="geotag-location-results" class="up-autocomplete-results" style="display:none;"></div>
|
||||
</div>
|
||||
<p class="up-hint">شروع به تایپ کنید تا فهرست مکانهای موجود بر اساس نام فیلتر شود؛ اگر مکان مورد نظر پیدا نشد، گزینهٔ تعریف آن بهعنوان مکان جدید نمایش داده میشود.</p>
|
||||
<a role="button" class="up-btn up-btn--sm up-btn--ghost" onclick="toggleNewLocationWidget()">📍 مکان جدید پیشنهاد بده</a>
|
||||
</div>
|
||||
|
||||
@ -2203,14 +2240,11 @@ else
|
||||
}
|
||||
<tr>
|
||||
<td>
|
||||
<select id="location">
|
||||
@{
|
||||
foreach (var location in Model.Locations)
|
||||
{
|
||||
<option value="@location.Id">@location.Name</option>
|
||||
}
|
||||
}
|
||||
</select>
|
||||
<div class="up-autocomplete-wrap">
|
||||
<input type="text" id="location-search" autocomplete="off" placeholder="نام مکان را تایپ کنید..." />
|
||||
<input type="hidden" id="location" />
|
||||
<div id="location-results" class="up-autocomplete-results" style="display:none;"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<input type="text" id="year" />
|
||||
|
||||
@ -218,6 +218,11 @@ namespace GanjooRazor.Areas.User.Pages
|
||||
|
||||
FatalError = "";
|
||||
CanEdit = Request.Cookies["CanEdit"] == "True";
|
||||
// set before any of the early "API call failed" returns below, so the view - which
|
||||
// reads Model.Couplets unconditionally near the top of its script block, before the
|
||||
// FatalError check further down - never sees a null and throws ArgumentNullException
|
||||
// out of Couplets.ToDictionary(...) on a failed request
|
||||
Couplets = Array.Empty<Tuple<int, string>>();
|
||||
|
||||
ShowAdminOps = CanEdit && Request.Query["admin"] == "1";
|
||||
using (HttpClient secureClient = new HttpClient(new GanjoorReloginHandler(Request, Response)))
|
||||
|
||||
@ -612,6 +612,51 @@ body.up-modal-open {
|
||||
box-shadow: 0 0 0 3px rgba(51, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
/* ---------- location search-as-you-type (see setupLocationAutocomplete in bk.js) ---------- */
|
||||
|
||||
.up-autocomplete-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.up-autocomplete-results {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
top: calc(100% + 2px);
|
||||
right: 0;
|
||||
left: 0;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
background: var(--up-surface);
|
||||
border: 1px solid var(--up-border);
|
||||
border-radius: var(--up-radius-sm);
|
||||
box-shadow: var(--up-shadow-sm);
|
||||
}
|
||||
|
||||
.up-autocomplete-item {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--up-border);
|
||||
}
|
||||
|
||||
.up-autocomplete-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.up-autocomplete-item:hover {
|
||||
background: var(--up-bg);
|
||||
}
|
||||
|
||||
.up-autocomplete-item--empty,
|
||||
.up-autocomplete-item--more {
|
||||
color: var(--up-text-muted);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.up-autocomplete-item--empty {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ---------- editable surfaces (contenteditable divs standing in for inputs) ----------
|
||||
Needs a strong, unmistakable affordance since it isn't a native form control -
|
||||
nothing else on the page should look like this. */
|
||||
|
||||
@ -2498,4 +2498,104 @@ function load7DaysVisits(url) {
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// Turns a plain text <input> into a "search as you type" picker over a small
|
||||
// in-memory location catalog ({id, name, latitude, longitude}[]), instead of a
|
||||
// long <select> the user has to scroll/search through by hand. If nothing in
|
||||
// the catalog matches what's typed, the dropdown offers to define it as a new
|
||||
// location instead - see onNotFound.
|
||||
// searchInput - the visible text <input> the user types a location name into
|
||||
// hiddenInput - an existing/hidden <input> that ends up holding the selected
|
||||
// location's Id as a string (cleared, i.e. '', whenever the
|
||||
// text doesn't currently match a selected location) - callers
|
||||
// that used to read a <select>'s .val() can keep doing so
|
||||
// unchanged as long as this element keeps the same id
|
||||
// resultsBox - an empty container element (positioned by .up-autocomplete-results)
|
||||
// right after searchInput, used to render the dropdown
|
||||
// locations - array of {id, name, latitude, longitude}
|
||||
// onSelect(loc) - called with the chosen location object, or null when the
|
||||
// field is cleared/typed into (so any "selected" state a
|
||||
// caller is tracking elsewhere - like a new-location panel
|
||||
// that should hide - can be kept in sync)
|
||||
// onNotFound(text) - called with the trimmed typed text when it matches no
|
||||
// location in the catalog and the user clicks the "define
|
||||
// as new location" row
|
||||
function setupLocationAutocomplete(searchInput, hiddenInput, resultsBox, locations, onSelect, onNotFound) {
|
||||
var MAX_RESULTS = 12;
|
||||
|
||||
function hide() {
|
||||
resultsBox.style.display = 'none';
|
||||
resultsBox.innerHTML = '';
|
||||
}
|
||||
|
||||
function selectLocation(loc) {
|
||||
searchInput.value = loc.name;
|
||||
hiddenInput.value = String(loc.id);
|
||||
hide();
|
||||
if (onSelect) onSelect(loc);
|
||||
}
|
||||
|
||||
function render(typedText) {
|
||||
var typedLower = typedText.toLowerCase();
|
||||
var matches = (locations || []).filter(function (l) {
|
||||
return l.name && l.name.toLowerCase().indexOf(typedLower) !== -1;
|
||||
});
|
||||
|
||||
if (matches.length == 0) {
|
||||
var emptyRow = document.createElement('div');
|
||||
emptyRow.className = 'up-autocomplete-item up-autocomplete-item--empty';
|
||||
emptyRow.textContent = '➕ «' + typedText + '» در فهرست پیدا نشد؛ بهعنوان مکان جدید تعریف شود';
|
||||
emptyRow.addEventListener('click', function () {
|
||||
hide();
|
||||
if (onNotFound) onNotFound(typedText);
|
||||
});
|
||||
resultsBox.innerHTML = '';
|
||||
resultsBox.appendChild(emptyRow);
|
||||
resultsBox.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
resultsBox.innerHTML = '';
|
||||
for (var i = 0; i < Math.min(matches.length, MAX_RESULTS); i++) {
|
||||
var loc = matches[i];
|
||||
var row = document.createElement('div');
|
||||
row.className = 'up-autocomplete-item';
|
||||
row.textContent = loc.name + ' (' + loc.latitude + ', ' + loc.longitude + ')';
|
||||
row.addEventListener('click', function (l) {
|
||||
return function () { selectLocation(l); };
|
||||
}(loc));
|
||||
resultsBox.appendChild(row);
|
||||
}
|
||||
if (matches.length > MAX_RESULTS) {
|
||||
var moreRow = document.createElement('div');
|
||||
moreRow.className = 'up-autocomplete-item up-autocomplete-item--more';
|
||||
moreRow.textContent = '… و ' + (matches.length - MAX_RESULTS) + ' مورد دیگر (برای محدودکردن نتایج بیشتر تایپ کنید)';
|
||||
resultsBox.appendChild(moreRow);
|
||||
}
|
||||
resultsBox.style.display = 'block';
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', function () {
|
||||
hiddenInput.value = ''; // typing invalidates whatever was previously selected
|
||||
var typed = searchInput.value.trim();
|
||||
if (typed == '') {
|
||||
hide();
|
||||
if (onSelect) onSelect(null);
|
||||
return;
|
||||
}
|
||||
render(typed);
|
||||
});
|
||||
|
||||
searchInput.addEventListener('focus', function () {
|
||||
var typed = searchInput.value.trim();
|
||||
if (typed != '' && hiddenInput.value == '') {
|
||||
render(typed); // re-show suggestions for text that hasn't resolved to a pick yet
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target !== searchInput && !resultsBox.contains(e.target)) {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user