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 {