diff --git a/tools/cldr-apps/js/src/esm/cldrAddValue.mjs b/tools/cldr-apps/js/src/esm/cldrAddValue.mjs
new file mode 100644
index 00000000000..237f184fba4
--- /dev/null
+++ b/tools/cldr-apps/js/src/esm/cldrAddValue.mjs
@@ -0,0 +1,96 @@
+/*
+ * cldrAddValue: enable submitting a new value for a path
+ */
+import * as cldrNotify from "./cldrNotify.mjs";
+import * as cldrSurvey from "./cldrSurvey.mjs";
+import * as cldrTable from "./cldrTable.mjs";
+import * as cldrVote from "./cldrVote.mjs";
+import * as cldrVue from "./cldrVue.mjs";
+
+import AddValue from "../views/AddValue.vue";
+
+/**
+ * Is the "Add Value" form currently visible?
+ */
+let formIsVisible = false;
+
+let originalTrClassName = "";
+
+function isFormVisible() {
+ return formIsVisible;
+}
+
+function setFormIsVisible(visible, xpstrid) {
+ formIsVisible = visible;
+ const tr = getTrFromXpathStringId(xpstrid);
+ if (tr) {
+ if (visible) {
+ originalTrClassName = tr.className;
+ }
+ tr.className = visible ? "tr_submit" : originalTrClassName;
+ }
+}
+
+function addButton(containerEl, xpstrid) {
+ try {
+ const AddValueWrapper = cldrVue.mount(AddValue, containerEl);
+ AddValueWrapper.setXpathStringId(xpstrid);
+ } catch (e) {
+ console.error(
+ "Error loading Add Value Button vue " + e.message + " / " + e.name
+ );
+ cldrNotify.exception(e, "while loading AddValue");
+ }
+}
+
+function getEnglish(xpstrid) {
+ const theRow = getTheRowFromXpathStringId(xpstrid);
+ return theRow?.displayName || "";
+}
+
+function getWinning(xpstrid) {
+ const theRow = getTheRowFromXpathStringId(xpstrid);
+ if (!theRow) {
+ return "";
+ }
+ let theValue = cldrTable.getValidWinningValue(theRow);
+ if (theValue === cldrSurvey.INHERITANCE_MARKER || theValue === null) {
+ theValue = theRow.inheritedDisplayValue;
+ }
+ return theValue || "";
+}
+
+function sendRequest(xpstrid, newValue) {
+ const tr = getTrFromXpathStringId(xpstrid);
+ if (!tr) {
+ return;
+ }
+ tr.inputTd = tr.querySelector(".othercell");
+ const protoButton = document.getElementById("proto-button");
+ cldrVote.handleWiredClick(
+ tr,
+ tr.theRow,
+ "",
+ newValue,
+ cldrSurvey.cloneAnon(protoButton)
+ );
+}
+
+function getTheRowFromXpathStringId(xpstrid) {
+ const tr = getTrFromXpathStringId(xpstrid);
+ return tr?.theRow;
+}
+
+function getTrFromXpathStringId(xpstrid) {
+ const rowId = cldrTable.makeRowId(xpstrid);
+ return document.getElementById(rowId);
+}
+
+export {
+ addButton,
+ getEnglish,
+ getWinning,
+ isFormVisible,
+ sendRequest,
+ setFormIsVisible,
+};
diff --git a/tools/cldr-apps/js/src/esm/cldrComponents.mjs b/tools/cldr-apps/js/src/esm/cldrComponents.mjs
index 8a70afa6838..7fd83745174 100644
--- a/tools/cldr-apps/js/src/esm/cldrComponents.mjs
+++ b/tools/cldr-apps/js/src/esm/cldrComponents.mjs
@@ -29,6 +29,7 @@ import {
Form,
Input,
List,
+ Modal,
Popover,
Progress,
Radio,
@@ -71,6 +72,7 @@ function setup(app) {
app.component("a-list-item-meta", List.Item.Meta);
app.component("a-list-item", List.Item);
app.component("a-list", List);
+ app.component("a-modal", Modal);
app.component("a-popover", Popover);
app.component("a-progress", Progress);
app.component("a-radio-group", Radio.Group);
diff --git a/tools/cldr-apps/js/src/esm/cldrNotify.mjs b/tools/cldr-apps/js/src/esm/cldrNotify.mjs
index 8c6e0a3d86f..9651e5432af 100644
--- a/tools/cldr-apps/js/src/esm/cldrNotify.mjs
+++ b/tools/cldr-apps/js/src/esm/cldrNotify.mjs
@@ -14,6 +14,10 @@
* such as 8 seconds here, 10 seconds there; top-left here and top-right there; ...
*/
+import * as cldrVue from "./cldrVue.mjs";
+
+import NotificationPopup from "../views/NotificationPopup.vue";
+
// Reference: https://www.antdv.com/components/notification
import { notification } from "ant-design-vue";
import { datadogLogs } from "@datadog/browser-logs";
@@ -119,4 +123,31 @@ function exception(e, context) {
});
}
-export { error, errorWithCallback, exception, open, warning };
+/**
+ * Display an error notification, possibly containing HTML, with a custom dialog
+ *
+ * @param {String} message the title, displayed at the top (plain text)
+ * @param {String} description a description of the problem, possibly HTML
+ */
+function openWithHtml(message, description) {
+ if (hasDataDog) {
+ datadogLogs.logger.error(message, { description });
+ }
+ try {
+ const NotificationPopupWrapper = cldrVue.mount(
+ NotificationPopup,
+ document.body
+ );
+ NotificationPopupWrapper.openWithMessageAndDescription(
+ message,
+ description
+ );
+ } catch (e) {
+ console.error(
+ "Error loading Notification Popup vue " + e.message + " / " + e.name
+ );
+ exception(e, "while loading NotificationPopup");
+ }
+}
+
+export { error, errorWithCallback, exception, open, openWithHtml, warning };
diff --git a/tools/cldr-apps/js/src/esm/cldrSurvey.mjs b/tools/cldr-apps/js/src/esm/cldrSurvey.mjs
index e7e0a0e706f..2c3f656fc7c 100644
--- a/tools/cldr-apps/js/src/esm/cldrSurvey.mjs
+++ b/tools/cldr-apps/js/src/esm/cldrSurvey.mjs
@@ -1,6 +1,7 @@
/**
* cldrSurvey: encapsulate miscellaneous Survey Tool functions
*/
+import * as cldrAddValue from "./cldrAddValue.mjs";
import * as cldrAjax from "./cldrAjax.mjs";
import * as cldrCoverage from "./cldrCoverage.mjs";
import * as cldrCoverageReset from "./cldrCoverageReset.mjs";
@@ -107,18 +108,7 @@ function getXpathMap() {
* @return true if busy
*/
function isInputBusy() {
- if (cldrForum.isFormVisible()) {
- return true;
- }
- const sel = window.getSelection ? window.getSelection() : null;
- if (sel?.anchorNode?.className) {
- // "popover-content" identifies the little input window, created using bootstrap, that appears when the
- // user clicks an add ("+") button.
- if (sel.anchorNode.className.indexOf("popover-content") != -1) {
- return true;
- }
- }
- return false;
+ return cldrAddValue.isFormVisible() || cldrForum.isFormVisible();
}
function createGravatar(user) {
diff --git a/tools/cldr-apps/js/src/esm/cldrTable.mjs b/tools/cldr-apps/js/src/esm/cldrTable.mjs
index 94006a25211..6f27c4ff05a 100644
--- a/tools/cldr-apps/js/src/esm/cldrTable.mjs
+++ b/tools/cldr-apps/js/src/esm/cldrTable.mjs
@@ -9,6 +9,7 @@
*/
import * as cldrAddAlt from "./cldrAddAlt.mjs";
+import * as cldrAddValue from "./cldrAddValue.mjs";
import * as cldrAjax from "./cldrAjax.mjs";
import * as cldrCoverage from "./cldrCoverage.mjs";
import * as cldrDashContext from "./cldrDashContext.mjs";
@@ -502,12 +503,10 @@ function reallyUpdateRow(tr, theRow) {
const comparisonCell = tr.querySelector(".comparisoncell");
const proposedCell = tr.querySelector(".proposedcell");
const otherCell = tr.querySelector(".othercell");
- const addCell = tr.canModify ? tr.querySelector(".addcell") : null;
-
- /*
- * "Add" button, potentially used by updateRowOthersCell and/or by otherCell or addCell
- */
- const formAdd = document.createElement("form");
+ const addCell =
+ tr.canChange && !theRow.fixedCandidates
+ ? tr.querySelector(".addcell")
+ : null;
/*
* Update the "status cell", a.k.a. the "A" column.
@@ -557,17 +556,15 @@ function reallyUpdateRow(tr, theRow) {
* Set up the "other cell", a.k.a. the "Others" column.
*/
if (otherCell) {
- updateRowOthersCell(tr, theRow, otherCell, protoButton, formAdd);
+ updateRowOthersCell(tr, theRow, otherCell, protoButton);
}
/*
* If the user can make changes, add "+" button for adding new candidate item.
*/
- if (tr.canChange) {
- if (addCell) {
- cldrDom.removeAllChildNodes(addCell);
- addCell.appendChild(formAdd);
- }
+ if (addCell) {
+ cldrDom.removeAllChildNodes(addCell);
+ cldrAddValue.addButton(addCell, theRow.xpstrid);
}
/*
@@ -841,127 +838,14 @@ function updateRowProposedWinningCell(tr, theRow, cell, protoButton) {
* @param theRow the data from the server for this row
* @param cell the table cell
* @param protoButton
- * @param formAdd
*
* Called by updateRow.
*/
-function updateRowOthersCell(tr, theRow, cell, protoButton, formAdd) {
+function updateRowOthersCell(tr, theRow, cell, protoButton) {
let hadOtherItems = false;
cldrDom.removeAllChildNodes(cell); // other
cldrSurvey.setLang(cell);
- if (tr.canModify && !tr.theRow.fixedCandidates) {
- formAdd.role = "form";
- formAdd.className = "form-inline";
- const buttonAdd = document.createElement("div");
- const btn = document.createElement("button");
- buttonAdd.className = "button-add form-group";
-
- toAddVoteButton(btn);
-
- buttonAdd.appendChild(btn);
- formAdd.appendChild(buttonAdd);
-
- const input = document.createElement("input");
- let popup;
- input.className = "form-control input-add";
- input.id = "input-add-translation";
- cldrSurvey.setLang(input);
- input.placeholder = "Add a translation";
- const copyWinning = document.createElement("button");
- copyWinning.className = "copyWinning btn btn-info btn-xs";
- copyWinning.title = "Copy Winning";
- copyWinning.type = "button";
- copyWinning.innerHTML =
- ' Winning';
- copyWinning.onclick = function (e) {
- let theValue = getValidWinningValue(theRow);
- if (theValue === cldrSurvey.INHERITANCE_MARKER || theValue === null) {
- theValue = theRow.inheritedDisplayValue;
- }
- input.value = theValue || null;
- input.focus();
- };
- const copyEnglish = document.createElement("button");
- copyEnglish.className = "copyEnglish btn btn-info btn-xs";
- copyEnglish.title = "Copy English";
- copyEnglish.type = "button";
- copyEnglish.innerHTML =
- ' English';
- copyEnglish.onclick = function (e) {
- input.value = theRow.displayName || null;
- input.focus();
- };
- btn.onclick = function (e) {
- //if no input, add one
- if ($(buttonAdd).parent().find("input").length == 0) {
- //hide other
- $.each($("button.vote-submit"), function () {
- toAddVoteButton(this);
- });
-
- //transform the button
- toSubmitVoteButton(btn);
- $(buttonAdd)
- .popover({
- content: " ",
- })
- .popover("show");
- popup = $(buttonAdd).parent().find(".popover-content");
- popup.append(input);
- if (theRow.displayName) {
- popup.append(copyEnglish);
- }
- const winVal = getValidWinningValue(theRow);
- if (winVal || theRow.inheritedValue) {
- popup.append(copyWinning);
- }
- popup
- .closest(".popover")
- .css("top", popup.closest(".popover").position().top - 19);
- input.focus();
-
- //enter pressed
- $(input).keydown(function (e) {
- const newValue = $(this).val();
- if (e.keyCode == 13) {
- //enter pressed
- if (newValue) {
- addValueVote(
- cell,
- tr,
- theRow,
- newValue,
- cldrSurvey.cloneAnon(protoButton)
- );
- } else {
- toAddVoteButton(btn);
- }
- } else if (e.keyCode === 27) {
- toAddVoteButton(btn);
- }
- });
- } else {
- const newValue = input.value;
-
- if (newValue) {
- addValueVote(
- cell,
- tr,
- theRow,
- newValue,
- cldrSurvey.cloneAnon(protoButton)
- );
- } else {
- toAddVoteButton(btn);
- }
- cldrEvent.stopPropagation(e);
- return false;
- }
- cldrEvent.stopPropagation(e);
- return false;
- };
- }
/*
* Add the other vote info -- that is, vote info for the "Others" column.
*/
@@ -1192,51 +1076,6 @@ function appendExample(parent, text, loc) {
return el;
}
-/**
- * Handle new value submission
- *
- * @param td
- * @param tr
- * @param theRow
- * @param newValue
- * @param newButton
- */
-function addValueVote(td, tr, theRow, newValue, newButton) {
- tr.inputTd = td; // cause the proposed item to show up in the right box
- cldrVote.handleWiredClick(tr, theRow, "", newValue, newButton);
-}
-
-/**
- * Transform input + submit button to the add button for the "add translation"
- *
- * @param btn
- */
-function toAddVoteButton(btn) {
- btn.className = "btn btn-primary";
- btn.title = "Add";
- btn.type = "submit";
- btn.innerHTML = '';
- $(btn).parent().popover("destroy");
- $(btn).tooltip("destroy").tooltip();
- $(btn).closest("form").next(".subSpan").show();
- $(btn).parent().children("input").remove();
-}
-
-/**
- * Transform the add button to a submit
- *
- * @param btn the button
- * @return the transformed button (return value is ignored by caller)
- */
-function toSubmitVoteButton(btn) {
- btn.innerHTML = '';
- btn.className = "btn btn-success vote-submit";
- btn.title = "Submit";
- $(btn).tooltip("destroy").tooltip();
- $(btn).closest("form").next(".subSpan").hide();
- return btn;
-}
-
/**
* Update the "no cell", a.k.a, the "Abstain" column, of this row
* Also possibly make changes to the "proposed" (winning) cell
@@ -1429,6 +1268,7 @@ export {
getPageUrl,
getRowApprovalStatusClass,
getStatusIcon,
+ getValidWinningValue,
goToHeaderId,
insertRows,
isHeaderId,
diff --git a/tools/cldr-apps/js/src/esm/cldrVote.mjs b/tools/cldr-apps/js/src/esm/cldrVote.mjs
index 684509ecfe8..ef2ec15014c 100644
--- a/tools/cldr-apps/js/src/esm/cldrVote.mjs
+++ b/tools/cldr-apps/js/src/esm/cldrVote.mjs
@@ -342,20 +342,8 @@ function showProposedItem(inTd, tr, theRow, value, tests, json) {
];
}
- var input = $(inTd).closest("tr").find(".input-add");
- if (input) {
- input.closest(".form-group").addClass("has-error");
- input
- .popover("destroy")
- .popover({
- placement: "bottom",
- html: true,
- content: cldrSurvey.testsToHtml(tests),
- trigger: "hover",
- })
- .popover("show");
- if (tr.myProposal) tr.myProposal.style.display = "none";
- }
+ const description = cldrSurvey.testsToHtml(tests);
+ cldrNotify.openWithHtml("Response to voting", description);
if (ourItem || (replaceErrors && value === "") /* Abstain */) {
const message = cldrText.sub(
"StatusAction_msg",
diff --git a/tools/cldr-apps/js/src/views/AddValue.vue b/tools/cldr-apps/js/src/views/AddValue.vue
new file mode 100644
index 00000000000..1be79c5c940
--- /dev/null
+++ b/tools/cldr-apps/js/src/views/AddValue.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
diff --git a/tools/cldr-apps/js/src/views/NotificationPopup.vue b/tools/cldr-apps/js/src/views/NotificationPopup.vue
new file mode 100644
index 00000000000..43dc2de4593
--- /dev/null
+++ b/tools/cldr-apps/js/src/views/NotificationPopup.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
diff --git a/tools/cldr-apps/src/main/webapp/surveytool.css b/tools/cldr-apps/src/main/webapp/surveytool.css
index c6e1f08ee28..a182524f8ab 100644
--- a/tools/cldr-apps/src/main/webapp/surveytool.css
+++ b/tools/cldr-apps/src/main/webapp/surveytool.css
@@ -2237,12 +2237,13 @@ tr.tr_err {
}
/*
- ready to submit
- TODO: when/where/why is this tr_submit used? Or is it unused?
- */
-
+ * tr_submit highlights a row when a dialog is open for submitting a new value.
+ * Usually the position of the dialog is a good indicator of the row to which it corresponds. However,
+ * if the row is near the top or bottom of the window, the dialog may be positioned higher or lower in
+ * order to be completely visible. The distinctive style of the row may then help to avoid confusion.
+ */
tr.tr_submit {
- background-color: #dfd;
+ background-color: #dfd; /* pale green */
}
div.v-oldVotes-subDiv {