diff --git a/sdkjs-plugins/content/zotero/index.html b/sdkjs-plugins/content/zotero/index.html index cac79464..1083efbf 100644 --- a/sdkjs-plugins/content/zotero/index.html +++ b/sdkjs-plugins/content/zotero/index.html @@ -58,7 +58,8 @@ - + + @@ -134,12 +135,12 @@
-
- -
- -
- - -
-
+
+ + + diff --git a/sdkjs-plugins/content/zotero/resources/css/components.css b/sdkjs-plugins/content/zotero/resources/css/components.css index f2484c5f..0793545f 100644 --- a/sdkjs-plugins/content/zotero/resources/css/components.css +++ b/sdkjs-plugins/content/zotero/resources/css/components.css @@ -59,6 +59,7 @@ html { .custom-button { position: relative; display: inline-flex; + width: 100%; align-items: center; justify-content: center; gap: 8px; @@ -419,6 +420,7 @@ body.theme-dark .custom-button-container { font-size: 22px; line-height: 0; transition: all 0.2s ease; + outline: none; } /* Counter */ diff --git a/sdkjs-plugins/content/zotero/resources/css/plugin_style.css b/sdkjs-plugins/content/zotero/resources/css/plugin_style.css index 519b4ba6..ed7f2520 100644 --- a/sdkjs-plugins/content/zotero/resources/css/plugin_style.css +++ b/sdkjs-plugins/content/zotero/resources/css/plugin_style.css @@ -136,22 +136,17 @@ input[type="text"] { #searchWrapper { display: flex; + gap: 4px; justify-content: space-between; position: relative; margin-top: 13px; } - #searchWrapper .input-field-container-searchField { - width: 207px; - } #searchWrapper .selectbox-container { position: absolute; } #searchWrapper .selectbox-container .selectbox-header { opacity: 0; } -#searchField { - width: 207px; -} #controlsHolder { padding-top: 4px; @@ -160,8 +155,8 @@ input[type="text"] { #controlsHolder button { margin-top: 8px; } - #controlsHolder .custom-button-primary { - width: 207px; + #controlsHolder .custom-button-container:first-child { + width: calc(100% - 28px); } #controlsHolder .custom-button-secondary { width: 100%; @@ -256,11 +251,6 @@ input[type="text"] { transform: rotateZ(-45deg); } - -#styleLangList { - right: 0px; -} - .selectList { display: flex; flex-direction: column; @@ -342,7 +332,7 @@ input[type="text"] { box-sizing: border-box; margin-top: 6px; padding: 4px 6px 6px 6px; - height: 51px; + max-height: 51px; overflow: hidden; } .page0 > .doc:first-child { @@ -390,8 +380,7 @@ input[type="text"] { } .doc.doc-open { - height: auto; - overflow: visible; + max-height: none; } .doc-open .selectbox-arrow { transform: rotate(180deg); diff --git a/sdkjs-plugins/content/zotero/scripts/code.js b/sdkjs-plugins/content/zotero/scripts/code.js index c93287b9..413aecc1 100644 --- a/sdkjs-plugins/content/zotero/scripts/code.js +++ b/sdkjs-plugins/content/zotero/scripts/code.js @@ -23,26 +23,19 @@ /// /// /// -/// -/// /// /// /// /// -/// -/// /// -/// +/// +/// (function () { var counter = 0; // счетчик отправленных запросов (используется чтобы знать показывать "not found" или нет) var displayNoneClass = "hidden"; var blurClass = "blur"; - //var loadingStyle = false; - //var loadingLocale = false; - var bNumFormat = false; - // TODO добавить ещё обработку событий (удаление линков) их не нужно удалять // из библиографии автоматически (это делать только при обновлении библиографии // или refresh), но их точно нужно удалить из formatter! @@ -52,7 +45,7 @@ // TODO сейчас всегда делаем полный refresh при каждом действии // (обновлении, вставке линков, вставке библиографии), потому что мы не знаем // что поменялось без событий (потом добавить ещё сравнение контента) - // TODO ms меняет линки (если стиль с нумерацией bNumFormat) делает их по порядку + // TODO ms меняет линки (если стиль с нумерацией settings._bNumFormat) делает их по порядку // как документе (для этого нужно знать где именно в документе мы вставляем цитату, // какая цитата сверху и снизу от текущего курсора) @@ -60,12 +53,10 @@ var router; /** @type {ZoteroSdk} */ var sdk; - /** @type {ConnectingToApi} */ - var connectingToApi; - /** @type {CslStylesManager} */ - var cslStylesManager; - /** @type {LocalesManager} */ - var localesManager; + + /** @type {SettingsPage} */ + var settings; + /** @type {CitationService} */ var citationService; @@ -86,8 +77,6 @@ /** @type {Button} */ var insertLinkBtn; /** @type {Button} */ - var settingsBtn; - /** @type {Button} */ var insertBibBtn; /** @type {Button} */ var refreshBtn; @@ -111,62 +100,16 @@ throw new Error("contentHolder not found"); } - const loginState = document.getElementById("loginState"); - if (!loginState) { - throw new Error("loginState not found"); - } const mainState = document.getElementById("mainState"); if (!mainState) { throw new Error("mainState not found"); } - const styleWrapper = document.getElementById("styleWrapper"); - if (!styleWrapper) { - throw new Error("styleWrapper not found"); - } - const styleSelectList = document.getElementById("styleSelectList"); - if (!styleSelectList) { - throw new Error("styleSelectList not found"); - } - const styleSelectListOther = document.getElementById( - "styleSelectedListOther" - ); - if (!styleSelectListOther) { - throw new Error("styleSelectListOther not found"); - } - const styleSelect = document.getElementById("styleSelect"); - if (!styleSelect) { - throw new Error("styleSelect not found"); - } - const styleLang = document.getElementById("styleLang"); - if (!styleLang) { - throw new Error("styleLang not found"); - } - const styleLangList = document.getElementById("styleLangList"); - if (!styleLangList) { - throw new Error("styleLangList not found"); - } - const notesStyleWrapper = document.getElementById("notesStyle"); - if (!notesStyleWrapper) { - throw new Error("notesStyleWrapper not found"); - } - const footNotes = document.getElementById("footNotes"); - if (!footNotes) { - throw new Error("footNotes not found"); - } - const endNotes = document.getElementById("endNotes"); - if (!endNotes) { - throw new Error("endNotes not found"); - } const checkOmitAuthor = document.getElementById("omitAuthor"); if (!checkOmitAuthor) { throw new Error("checkOmitAuthor not found"); } - const cslFileInput = document.getElementById("cslFileInput"); - if (!cslFileInput) { - throw new Error("cslFileInput not found"); - } searchFilter = new SearchFilterComponents(); selectCitation = new SelectCitationsComponent( displayNoneClass, @@ -179,10 +122,6 @@ insertLinkBtn = new Button("insertLinkBtn", { disabled: true, }); - settingsBtn = new Button("settingsBtn", { - variant: "icon-only", - size: "small", - }); insertBibBtn = new Button("insertBibBtn", { variant: "secondary", }); @@ -195,22 +134,10 @@ error: error, contentHolder: contentHolder, - loginState: loginState, mainState: mainState, - styleWrapper: styleWrapper, - styleSelectList: styleSelectList, - styleSelectListOther: styleSelectListOther, - styleSelect: styleSelect, - styleLang: styleLang, - styleLangList: styleLangList, - notesStyleWrapper: notesStyleWrapper, - footNotes: footNotes, - endNotes: endNotes, - checkOmitAuthor: checkOmitAuthor, - cslFileInput: cslFileInput, }; } @@ -220,116 +147,36 @@ router = new Router(); sdk = new ZoteroSdk(); - connectingToApi = new ConnectingToApi(router, sdk); - cslStylesManager = new CslStylesManager(); - localesManager = new LocalesManager(); + const loginPage = new LoginPage(router, sdk); + settings = new SettingsPage(router, displayNoneClass); citationService = new CitationService( - localesManager, - cslStylesManager, + settings.getLocalesManager(), + settings.getStyleManager(), sdk ); addEventListeners(); - initSelectBoxes(); - const connector = connectingToApi.init(); - connector.onOpen(function () { - showLoader(false); - }); - connector.onChangeState(function (apis) { - cslStylesManager.setDesktopApiAvailable(apis.desktop); - cslStylesManager.setRestApiAvailable(apis.online); - localesManager.setDesktopApiAvailable(apis.desktop); - localesManager.setRestApiAvailable(apis.online); - }); - connector.onAuthorized(function (apis) { - showLoader(true); - - Promise.all([ - loadGroups(), - loadStyles(), - preloadLastStyle(), - initLanguageSelect(), - ]).then(function () { + loginPage + .init() + .onOpen(function () { showLoader(false); - }); + }) + .onChangeState(function (apis) { + settings.setDesktopApiAvailable(apis.desktop); + settings.setRestApiAvailable(apis.online); + }) + .onAuthorized(function (apis) { + showLoader(true); - addStylesEventListeners(); - }); + Promise.all([loadGroups(), settings.init()]).then(function () { + showLoader(false); + }); + }); window.Asc.plugin.onTranslate = applyTranslations; }; - function preloadLastStyle() { - var lastStyle = cslStylesManager.getLastUsedStyleId() || "ieee"; - return cslStylesManager.getStyle(lastStyle); - } - - /** - * @returns {Promise} - */ - function initLanguageSelect() { - const savedLang = localesManager.getLastUsedLanguage(); - const option = elements.styleLangList.querySelector( - '[data-value="' + savedLang + '"]' - ); - if (!option || !(elements.styleLang instanceof HTMLInputElement)) { - console.error("initLanguageSelect: no option"); - return Promise.resolve(null); - } - option.setAttribute("selected", ""); - elements.styleLang.value = option.textContent; - elements.styleLang.setAttribute("data-value", savedLang); - return localesManager.loadLocale(savedLang); - } - - /** @returns {Promise} */ - function loadStyles() { - return cslStylesManager - .getStylesInfo() - .then( - /** @param {Array} stylesInfo*/ function ( - stylesInfo - ) { - var openOtherStyleList = function ( - /** @type {HTMLElement} */ list - ) { - return function (/** @type {MouseEvent} */ ev) { - elements.styleSelectListOther.style.width = - elements.styleWrapper.clientWidth - 2 + "px"; - ev.stopPropagation(); - openList(list); - }; - }; - - addStylesToList(stylesInfo); - - const el = document.createElement("hr"); - elements.styleSelectList.appendChild(el); - - if (elements.styleSelectListOther.children.length > 0) { - var other = document.createElement("span"); - other.textContent = "More Styles..."; - elements.styleSelectList.appendChild(other); - other.onclick = openOtherStyleList( - elements.styleSelectListOther - ); - } - - var custom = document.createElement("span"); - custom.setAttribute("class", "select-file"); - var label = document.createElement("label"); - label.setAttribute("for", "cslFileInput"); - label.textContent = "Add custom style..."; - custom.appendChild(label); - elements.styleSelectList.appendChild(custom); - } - ) - .catch(function (err) { - console.error(err); - }); - } - /** @returns {Promise} */ function loadGroups() { return sdk @@ -339,121 +186,9 @@ }); } - /** - * @param {Array} stylesInfo - */ - function addStylesToList(stylesInfo) { - var lastStyle = cslStylesManager.getLastUsedStyleIdOrDefault(); - const styleSelect = elements.styleSelect; - if (styleSelect instanceof HTMLInputElement === false) { - console.error("styleSelect is not an input element"); - return; - } - - /** - * @param {HTMLElement} list - the list of styles where the element is added. - * @param {HTMLElement} other - the list of styles where the element is removed. - */ - var onStyleSelectOther = function (list, other) { - return function (/** @type {MouseEvent} */ ev) { - let tmpEl = list.removeChild( - list.children[list.children.length - 3] - ); - var newEl = document.createElement("span"); - newEl.setAttribute( - "data-value", - String(tmpEl.getAttribute("data-value")) - ); - newEl.textContent = tmpEl.textContent; - other.appendChild(newEl); - newEl.onclick = onStyleSelectOther( - elements.styleSelectList, - elements.styleSelectListOther - ); - - if (ev.target instanceof HTMLElement === false) { - console.error("ev.target is not an HTMLElement"); - return; - } - tmpEl = other.removeChild(ev.target); - newEl = document.createElement("span"); - newEl.setAttribute( - "data-value", - String(tmpEl.getAttribute("data-value")) - ); - newEl.textContent = tmpEl.textContent; - list.insertBefore(newEl, list.firstElementChild); - newEl.onclick = onClickListElement( - elements.styleSelectList, - styleSelect - ); - var event = new Event("click"); - newEl.dispatchEvent(event); - closeList(); - }; - }; - - for (var i = 0; i < stylesInfo.length; i++) { - var el = document.createElement("span"); - el.setAttribute("data-value", stylesInfo[i].name); - el.textContent = stylesInfo[i].title; - if ( - cslStylesManager.isStyleDefault(stylesInfo[i].name) || - stylesInfo[i].name == lastStyle - ) { - if (stylesInfo.length == 1) - elements.styleSelectList.insertBefore( - el, - elements.styleSelectList.firstElementChild - ); - else elements.styleSelectList.appendChild(el); - el.onclick = onClickListElement( - elements.styleSelectList, - styleSelect - ); - } else { - elements.styleSelectListOther.appendChild(el); - el.onclick = onStyleSelectOther( - elements.styleSelectList, - elements.styleSelectListOther - ); - } - if (stylesInfo[i].name == lastStyle) { - el.setAttribute("selected", ""); - selectInput(styleSelect, el, elements.styleSelectList, false); - } - } - } - function addEventListeners() { selectCitation.subscribe(checkSelected); - elements.cslFileInput.onchange = function (e) { - if (!(e.target instanceof HTMLInputElement)) return; - /** @type {HTMLInputElement} */ - const target = e.target; - if (!target.files) return; - var file = target.files[0]; - if (!file) { - console.error("No file selected"); - return; - } - //showLoader(true); - - cslStylesManager - .addCustomStyle(file) - .then(function (styleValue) { - addStylesToList([styleValue]); - }) - .catch(function (error) { - console.error(error); - showError(translate("Failed to upload file")); - }) - .finally(function () { - showLoader(false); - }); - }; - /** * @param {string} text * @param {Array} selectedGroups @@ -538,11 +273,11 @@ if (event.type !== "button:click") { return; } - if (!cslStylesManager.getLastUsedStyleId()) { + if (!settings.getLastUsedStyleId()) { showError(translate("Style is not selected")); return; } - if (!localesManager.getLocale()) { + if (!settings.getLocale()) { showError(translate("Language is not selected")); return; } @@ -566,11 +301,11 @@ if (event.type !== "button:click") { return; } - if (!cslStylesManager.getLastUsedStyleId()) { + if (!settings.getLastUsedStyleId()) { showError(translate("Style is not selected")); return; } - if (!localesManager.getLocale()) { + if (!settings.getLocale()) { showError(translate("Language is not selected")); return; } @@ -596,11 +331,11 @@ if (event.type !== "button:click") { return; } - if (!cslStylesManager.getLastUsedStyleId()) { + if (!settings.getLastUsedStyleId()) { showError(translate("Style is not selected")); return; } - if (!localesManager.getLocale()) { + if (!settings.getLocale()) { showError(translate("Language is not selected")); return; } @@ -643,91 +378,12 @@ showLoader(false); }); }); - } - function addStylesEventListeners() { - /** - * @param {Event|null} e - The input event. - * @param {String} [filter] - The filter to apply on the style options. - */ - elements.styleSelect.oninput = function (e, filter) { - var input = elements.styleSelect; - if (!(input instanceof HTMLInputElement)) return; - filter = filter !== undefined ? filter : input.value.toLowerCase(); - var list = elements.styleSelectList.classList.contains( - displayNoneClass - ) - ? elements.styleSelectListOther - : elements.styleSelectList; - - for (var i = 0; i < list.children.length; i++) { - const child = list.children[i]; - if (child instanceof HTMLElement === false) { - continue; - } - var text = child.textContent || child.innerText; - var hide = true; - if (!filter || text.toLowerCase().indexOf(filter) > -1) { - hide = false; - } - - switchClass(child, displayNoneClass, hide); - } - }; - - /** - * @param {Event} inp - The input event. - * @param {String} styleName - The name of the selected style. - * @param {Boolean} isClick - Whether the style was selected manually or not. - */ - elements.styleSelect.onselectchange = function ( - inp, - styleName, - isClick - ) { - isClick && showLoader(true); - elements.styleSelect.oninput(inp, ""); - - return cslStylesManager - .getStyle(styleName) - .then(function (style) { - onStyleChange(); - if (isClick) { - return citationService.updateCslItems( - true, - true, - false - ); - } - }) - .catch(function (err) { - console.error(err); - if (typeof err === "string") { - showError(err); - } - }) - .finally(function () { - isClick && showLoader(false); - }); - }; - - /** - * @param {Event} inp - The select change event. - * @param {String} val - The value of the selected language. - * @param {Boolean} isClick - Whether the language was selected manually or not. - */ - elements.styleLang.onselectchange = function (inp, val, isClick) { + settings.onChangeState(function (/** @type {Promise} */ promise) { showLoader(true); - localesManager.saveLastUsedLanguage(val); - localesManager - .loadLocale(val) + promise .then(function () { - if (isClick) - return citationService.updateCslItems( - true, - true, - false - ); + return citationService.updateCslItems(true, true, false); }) .catch(function (error) { console.error(error); @@ -738,27 +394,6 @@ .finally(function () { showLoader(false); }); - }; - - elements.styleSelectList.onopen = function () { - elements.styleSelectList.style.width = - elements.styleWrapper.clientWidth - 2 + "px"; - }; - - [elements.footNotes, elements.endNotes].forEach(function (el) { - el.addEventListener("change", function (event) { - if ( - event.target instanceof HTMLInputElement && - event.target.checked - ) { - const value = event.target.value; - if (value === "endnotes" || value === "footnotes") { - cslStylesManager.saveLastUsedNotesStyle(value); - } else { - console.error("Unknown notes style: " + value); - } - } - }); }); } @@ -827,160 +462,6 @@ body.classList.add("theme-" + themeType); }; - /** @type {HTMLElement[]} */ - var selectLists = []; - function initSelectBoxes() { - var select = document.getElementsByClassName("control select"); - for (var i = 0; i < select.length; i++) { - var input = select[i]; - var holder = input.parentElement; - if (!(input instanceof HTMLInputElement) || !holder) { - console.error("initSelectBoxes: no input or holder"); - continue; - } - - var arrow = document.createElement("span"); - arrow.classList.add("selectArrow"); - arrow.appendChild(document.createElement("span")); - arrow.appendChild(document.createElement("span")); - holder.appendChild(arrow); - - for ( - var k = 0; - k < holder.getElementsByClassName("selectList").length; - k++ - ) { - var list = holder.getElementsByClassName("selectList")[k]; - if (list.children.length > 0) { - for (var j = 0; j < list.children.length; j++) { - const child = list.children[j]; - if (child instanceof HTMLElement === false) { - continue; - } - child.onclick = onClickListElement(list, input); - } - // selectInput(input, list.children[0], list, false); - } - - /** - * @param {HTMLElement} list - * @param {HTMLElement} input - * @returns {function} - */ - var fOpen = function (list, input) { - return function (/** @type {MouseEvent} */ ev) { - ev.stopPropagation(); - if ( - !elements.styleSelectListOther.classList.contains( - displayNoneClass - ) - ) - return true; - - if (list.onopen) { - list.onopen(); - } - if (!input.hasAttribute("readonly")) { - input.select(); - } - openList(list); - - return true; - }; - }; - - if (k !== 1) { - input.onclick = fOpen(list, input); - arrow.onclick = fOpen(list, input); - } - selectLists.push(list); - } - } - } - - /** - * @param {HTMLElement} el - */ - function openList(el) { - switchClass(el, displayNoneClass, false); - window.addEventListener("click", closeList); - } - - function closeList() { - window.removeEventListener("click", closeList); - for (var i = 0; i < selectLists.length; i++) { - if (selectLists[i] === elements.styleSelectList) - elements.styleSelect.oninput(null, ""); - switchClass(selectLists[i], displayNoneClass, true); - } - } - - /** - * @param {HTMLInputElement} input - * @param {HTMLElement} el - * @param {HTMLElement} list - * @param {boolean} isClick - */ - function selectInput(input, el, list, isClick) { - input.value = el.textContent; - var val = el.getAttribute("data-value") || ""; - input.setAttribute("data-value", val); - input.setAttribute("title", el.textContent); - if (input.onselectchange) { - input.onselectchange(input, val, isClick); - } - switchClass(list, displayNoneClass, true); - } - - /** - * @param {Element} list - * @param {HTMLInputElement} input - */ - function onClickListElement(list, input) { - return function (/** @type {MouseEvent} */ ev) { - if (!ev.target || !(ev.target instanceof HTMLElement)) { - console.error("onClickListElement: no target"); - return; - } - var sel = ev.target.getAttribute("data-value"); - for (var i = 0; i < list.children.length; i++) { - const temp = list.children[i]; - if (temp instanceof HTMLElement === false) continue; - /** @type {HTMLElement} */ - const child = temp; - if (list.children[i].getAttribute("data-value") == sel) { - list.children[i].setAttribute("selected", ""); - - selectInput(input, child, list, true); - } else { - if (list.children[i].hasAttribute("selected")) { - list.children[i].attributes.removeNamedItem("selected"); - } - } - } - }; - } - - function onStyleChange() { - let styleFormat = cslStylesManager.getLastUsedFormat(); - citationService.setStyleFormat(styleFormat); - bNumFormat = styleFormat == "numeric"; - if ("note" === styleFormat) { - elements.notesStyleWrapper.classList.remove(displayNoneClass); - } else { - elements.notesStyleWrapper.classList.add(displayNoneClass); - } - - let notesStyle = cslStylesManager.getLastUsedNotesStyle(); - citationService.setNotesStyle(notesStyle); - const notesAs = elements.notesStyleWrapper.querySelector( - 'input[name="notesAs"][value="' + notesStyle + '"]' - ); - if (notesAs && notesAs instanceof HTMLInputElement) { - notesAs.checked = true; - } - } - function applyTranslations() { var elements = document.getElementsByClassName("i18n"); diff --git a/sdkjs-plugins/content/zotero/scripts/connection.js b/sdkjs-plugins/content/zotero/scripts/login.js similarity index 93% rename from sdkjs-plugins/content/zotero/scripts/connection.js rename to sdkjs-plugins/content/zotero/scripts/login.js index 611d3c4d..abe61440 100644 --- a/sdkjs-plugins/content/zotero/scripts/connection.js +++ b/sdkjs-plugins/content/zotero/scripts/login.js @@ -13,7 +13,7 @@ * @param {Router} router * @param {ZoteroSdk} sdk */ -function ConnectingToApi(router, sdk) { +function LoginPage(router, sdk) { this._router = router; this._sdk = sdk; this._apiKeyLoginField = new InputField("apiKeyField", { @@ -48,7 +48,7 @@ function ConnectingToApi(router, sdk) { this._onOpen = function () {}; } -ConnectingToApi.prototype.init = function () { +LoginPage.prototype.init = function () { const self = this; this._addEventListeners(); let hasFirstAnswer = false; @@ -90,29 +90,34 @@ ConnectingToApi.prototype.init = function () { } }); - return { + const triggers = { /** - * @param {function(AvailableApis): void} callbackFn + * @param {function(): void} callbackFn */ - onAuthorized: function (callbackFn) { - self._onAuthorized = callbackFn; + onOpen: function (callbackFn) { + self._onOpen = callbackFn; + return triggers; }, /** * @param {function(AvailableApis): void} callbackFn */ onChangeState: function (callbackFn) { self._onChangeState = callbackFn; + return triggers; }, /** - * @param {function(): void} callbackFn + * @param {function(AvailableApis): void} callbackFn */ - onOpen: function (callbackFn) { - self._onOpen = callbackFn; + onAuthorized: function (callbackFn) { + self._onAuthorized = callbackFn; + return triggers; }, }; + + return triggers; }; -ConnectingToApi.prototype._addEventListeners = function () { +LoginPage.prototype._addEventListeners = function () { const self = this; this._apiKeyLoginField.subscribe(function (event) { if (event.type !== "inputfield:input") { @@ -176,22 +181,19 @@ ConnectingToApi.prototype._addEventListeners = function () { }; }; -ConnectingToApi.prototype._hideAllMessages = function () { +LoginPage.prototype._hideAllMessages = function () { this._apiKeyMessage.close(); }; -/** @param {string} apiKey */ -ConnectingToApi.prototype._onClickSave = function (apiKey) {}; - /** @param {boolean} [bShowLogoutLink] */ -ConnectingToApi.prototype._hide = function (bShowLogoutLink) { +LoginPage.prototype._hide = function (bShowLogoutLink) { this._router.openMain(); if (bShowLogoutLink) { this._logoutLink.classList.remove("hidden"); } }; -ConnectingToApi.prototype._show = function () { +LoginPage.prototype._show = function () { this._router.openLogin(); this._logoutLink.classList.add("hidden"); }; diff --git a/sdkjs-plugins/content/zotero/scripts/router.js b/sdkjs-plugins/content/zotero/scripts/router.js index 5233d6af..e0a38c3e 100644 --- a/sdkjs-plugins/content/zotero/scripts/router.js +++ b/sdkjs-plugins/content/zotero/scripts/router.js @@ -1,9 +1,9 @@ // @ts-check function Router() { - this._states = ["mainState", "loginState"]; - this._routes = ["main", "login"]; - /** @type {"main"|"login"} */ + this._states = ["mainState", "loginState", "settingsState"]; + this._routes = ["main", "login", "settings"]; + /** @type {"main"|"login"|"settings"} */ this._currentRoute = "login"; this._currentRouteIndex = 1; this._containers = this._states.map(function (route) { @@ -13,12 +13,12 @@ function Router() { }); } -/** @returns {"main"|"login"} */ +/** @returns {"main"|"login"|"settings"} */ Router.prototype.getRoute = function () { return this._currentRoute; }; -/** @param {"main"|"login"} route */ +/** @param {"main"|"login"|"settings"} route */ Router.prototype._setCurrentRoute = function (route) { this._containers[this._currentRouteIndex].classList.add("hidden"); this._currentRoute = route; @@ -33,3 +33,7 @@ Router.prototype.openMain = function () { Router.prototype.openLogin = function () { this._setCurrentRoute("login"); }; + +Router.prototype.openSettings = function () { + this._setCurrentRoute("settings"); +}; diff --git a/sdkjs-plugins/content/zotero/scripts/settings.js b/sdkjs-plugins/content/zotero/scripts/settings.js new file mode 100644 index 00000000..275420e8 --- /dev/null +++ b/sdkjs-plugins/content/zotero/scripts/settings.js @@ -0,0 +1,669 @@ +// @ts-check + +/// +/// +/// +/// +/// +/// +/// +/// + +/** + * @param {Router} router + * @param {string} displayNoneClass + */ +function SettingsPage(router, displayNoneClass) { + this._router = router; + this._displayNoneClass = displayNoneClass; + + this._openSettingsBtn = new Button("settingsBtn", { + variant: "icon-only", + size: "small", + }); + this._saveBtn = new Button("saveSettingsBtn", { + variant: "primary", + }); + this._cancelBtn = new Button("cancelBtn", { + variant: "secondary", + }); + + this._styleWrapper = document.getElementById("styleWrapper"); + if (!this._styleWrapper) { + throw new Error("styleWrapper not found"); + } + this._styleSelectList = document.getElementById("styleSelectList"); + if (!this._styleSelectList) { + throw new Error("styleSelectList not found"); + } + this._styleSelectListOther = document.getElementById( + "styleSelectedListOther" + ); + if (!this._styleSelectListOther) { + throw new Error("styleSelectListOther not found"); + } + this._styleSelect = document.getElementById("styleSelect"); + if (!this._styleSelect) { + throw new Error("styleSelect not found"); + } + this._notesStyleWrapper = document.getElementById("notesStyle"); + if (!this._notesStyleWrapper) { + throw new Error("notesStyleWrapper not found"); + } + this._footNotes = document.getElementById("footNotes"); + if (!this._footNotes) { + throw new Error("footNotes not found"); + } + this._endNotes = document.getElementById("endNotes"); + if (!this._endNotes) { + throw new Error("endNotes not found"); + } + + this._cslFileInput = document.getElementById("cslFileInput"); + if (!this._cslFileInput) { + throw new Error("cslFileInput not found"); + } + + this._languageSelect = new SelectBox("styleLangList", { + placeholder: "Select language", + }); + + this._cslStylesManager = new CslStylesManager(); + this._localesManager = new LocalesManager(); + + /** @type {HTMLElement[]} */ + this._selectLists = []; + /** + * @param {Promise} promise + */ + this._onChangeState = function (promise) {}; + this._apiKeyMessage = new Message("apiKeyMessage", { type: "error" }); + /** @type {Array<[string, string]>} */ + this._LANGUAGES = [ + ["af-ZA", "Afrikaans"], + ["ar", "Arabic"], + ["bg-BG", "Bulgarian"], + ["ca-AD", "Catalan"], + ["cs-CZ", "Czech"], + ["cy-GB", "Welsh"], + ["da-DK", "Danish"], + ["de-AT", "German (Austria)"], + ["de-CH", "German (Switzerland)"], + ["de-DE", "German (Germany)"], + ["el-GR", "Greek"], + ["en-GB", "English (UK)"], + ["en-US", "English (US)"], + ["es-CL", "Spanish (Chile)"], + ["es-ES", "Spanish (Spain)"], + ["es-MX", "Spanish (Mexico)"], + ["et-EE", "Estonian"], + ["eu", "Basque"], + ["fa-IR", "Persian"], + ["fi-FI", "Finnish"], + ["fr-CA", "French (Canada)"], + ["fr-FR", "French (France)"], + ["he-IL", "Hebrew"], + ["hr-HR", "Croatian"], + ["hu-HU", "Hungarian"], + ["id-ID", "Indonesian"], + ["is-IS", "Icelandic"], + ["it-IT", "Italian"], + ["ja-JP", "Japanese"], + ["km-KH", "Khmer"], + ["ko-KR", "Korean"], + ["la", "Latin"], + ["lt-LT", "Lithuanian"], + ["lv-LV", "Latvian"], + ["mn-MN", "Mongolian"], + ["nb-NO", "Norwegian (Bokmål)"], + ["nl-NL", "Dutch"], + ["nn-NO", "Norwegian (Nynorsk)"], + ["pl-PL", "Polish"], + ["pt-BR", "Portuguese (Brazil)"], + ["pt-PT", "Portuguese (Portugal)"], + ["ro-RO", "Romanian"], + ["ru-RU", "Russian"], + ["sk-SK", "Slovak"], + ["sl-SI", "Slovenian"], + ["sr-RS", "Serbian"], + ["sv-SE", "Swedish"], + ["th-TH", "Thai"], + ["tr-TR", "Turkish"], + ["uk-UA", "Ukrainian"], + ["vi-VN", "Vietnamese"], + ["zh-CN", "Chinese (PRC)"], + ["zh-TW", "Chinese (Taiwan)"], + ]; + + this._bNumFormat = false; + + this._initSelectBoxes(); +} + +/** + * @returns {LocalesManager} + */ +SettingsPage.prototype.getLocalesManager = function () { + return this._localesManager; +}; + +/** + * @returns {CslStylesManager} + */ +SettingsPage.prototype.getStyleManager = function () { + return this._cslStylesManager; +}; + +/** + * @returns {string|null} + */ +SettingsPage.prototype.getLocale = function () { + return this._localesManager.getLocale(); +}; + +/** + * @returns {string|null} + */ +SettingsPage.prototype.getLastUsedStyleId = function () { + return this._cslStylesManager.getLastUsedStyleId(); +}; + +/** + * @returns + */ +SettingsPage.prototype.init = function () { + const self = this; + var lastStyle = this._cslStylesManager.getLastUsedStyleId() || "ieee"; + const savedLang = this._localesManager.getLastUsedLanguage(); + this._addEventListeners(); + this._languageSelect.addItems(this._LANGUAGES, savedLang); + + const promises = [ + this._cslStylesManager.getStyle(lastStyle), + this._localesManager.loadLocale(savedLang), + this._loadStyles(), + ]; + + return Promise.all(promises); +}; + +/** + * @param {function(Promise): void} callbackFn + */ +SettingsPage.prototype.onChangeState = function (callbackFn) { + this._onChangeState = callbackFn; +}; + +/** + * @param {boolean} isAvailable + */ +SettingsPage.prototype.setDesktopApiAvailable = function (isAvailable) { + this._localesManager.setDesktopApiAvailable(isAvailable); + this._cslStylesManager.setDesktopApiAvailable(isAvailable); +}; + +/** + * @param {boolean} isAvailable + */ +SettingsPage.prototype.setRestApiAvailable = function (isAvailable) { + this._localesManager.setRestApiAvailable(isAvailable); + this._cslStylesManager.setRestApiAvailable(isAvailable); +}; + +SettingsPage.prototype._addEventListeners = function () { + const self = this; + + this._openSettingsBtn.subscribe(function (event) { + if (event.type !== "button:click") { + return; + } + self._show(); + }); + this._saveBtn.subscribe(function (event) { + if (event.type !== "button:click") { + return; + } + const selectedLang = self._languageSelect.getSelectedValue(); + if (selectedLang === null) { + console.error("No language selected"); + return; + } + const promises = []; + if (self._localesManager.getLastUsedLanguage() !== selectedLang) { + self._localesManager.saveLastUsedLanguage(selectedLang); + promises.push(self._localesManager.loadLocale(selectedLang)); + } + + if (promises.length) { + self._onChangeState( + Promise.all(promises).then(function () { + self._hide(); + }) + ); + } + }); + this._cancelBtn.subscribe(function (event) { + if (event.type !== "button:click") { + return; + } + self._hide(); + }); + + this._cslFileInput.onchange = function (e) { + if (!(e.target instanceof HTMLInputElement)) return; + /** @type {HTMLInputElement} */ + const target = e.target; + if (!target.files) return; + var file = target.files[0]; + if (!file) { + console.error("No file selected"); + return; + } + //showLoader(true); + + self._cslStylesManager + .addCustomStyle(file) + .then(function (styleValue) { + self._addStylesToList([styleValue]); + }) + .catch(function (error) { + console.error(error); + showError(translate("Failed to upload file")); + }) + .finally(function () { + showLoader(false); + }); + }; + + /** + * @param {Event|null} e - The input event. + * @param {String} [filter] - The filter to apply on the style options. + */ + this._styleSelect.oninput = function (e, filter) { + var input = self._styleSelect; + if (!(input instanceof HTMLInputElement)) return; + filter = filter !== undefined ? filter : input.value.toLowerCase(); + var list = self._styleSelectList.classList.contains( + self._displayNoneClass + ) + ? self._styleSelectListOther + : self._styleSelectList; + + for (var i = 0; i < list.children.length; i++) { + const child = list.children[i]; + if (child instanceof HTMLElement === false) { + continue; + } + var text = child.textContent || child.innerText; + if (!filter || text.toLowerCase().indexOf(filter) > -1) { + child.classList.remove(self._displayNoneClass); + } else { + child.classList.add(self._displayNoneClass); + } + } + }; + + /** + * @param {Event} inp - The input event. + * @param {String} styleName - The name of the selected style. + * @param {Boolean} isClick - Whether the style was selected manually or not. + */ + this._styleSelect.onselectchange = function (inp, styleName, isClick) { + isClick && self._showLoader(true); + self._styleSelect.oninput(inp, ""); + + return self._cslStylesManager + .getStyle(styleName) + .then(function (style) { + self._onStyleChange(); + if (isClick) { + return self._citationService.updateCslItems( + true, + true, + false + ); + } + }) + .catch(function (err) { + console.error(err); + if (typeof err === "string") { + self._showError(err); + } + }) + .finally(function () { + isClick && self._showLoader(false); + }); + }; + + this._styleSelectList.onopen = function () { + self._styleSelectList.style.width = + self._styleWrapper.clientWidth - 2 + "px"; + }; + + [this._footNotes, this._endNotes].forEach(function (el) { + el.addEventListener("change", function (event) { + if ( + event.target instanceof HTMLInputElement && + event.target.checked + ) { + const value = event.target.value; + if (value === "endnotes" || value === "footnotes") { + self._cslStylesManager.saveLastUsedNotesStyle(value); + } else { + console.error("Unknown notes style: " + value); + } + } + }); + }); +}; + +SettingsPage.prototype._hideAllMessages = function () { + this._apiKeyMessage.close(); +}; + +SettingsPage.prototype._hide = function () { + this._router.openMain(); +}; + +SettingsPage.prototype._show = function () { + this._router.openSettings(); +}; + +/** @returns {Promise} */ +SettingsPage.prototype._loadStyles = function () { + const self = this; + return this._cslStylesManager + .getStylesInfo() + .then( + /** @param {Array} stylesInfo*/ function (stylesInfo) { + var openOtherStyleList = function ( + /** @type {HTMLElement} */ list + ) { + return function (/** @type {MouseEvent} */ ev) { + self._styleSelectListOther.style.width = + self._styleWrapper.clientWidth - 2 + "px"; + ev.stopPropagation(); + self._openList(list); + }; + }; + + self._addStylesToList(stylesInfo); + + const el = document.createElement("hr"); + self._styleSelectList.appendChild(el); + + if (self._styleSelectListOther.children.length > 0) { + var other = document.createElement("span"); + other.textContent = "More Styles..."; + self._styleSelectList.appendChild(other); + other.onclick = openOtherStyleList( + self._styleSelectListOther + ); + } + + var custom = document.createElement("span"); + custom.setAttribute("class", "select-file"); + var label = document.createElement("label"); + label.setAttribute("for", "cslFileInput"); + label.textContent = "Add custom style..."; + custom.appendChild(label); + self._styleSelectList.appendChild(custom); + } + ) + .catch(function (err) { + console.error(err); + }); +}; + +/** + * @param {Array} stylesInfo + */ +SettingsPage.prototype._addStylesToList = function (stylesInfo) { + const self = this; + var lastStyle = this._cslStylesManager.getLastUsedStyleIdOrDefault(); + const styleSelect = this._styleSelect; + if (styleSelect instanceof HTMLInputElement === false) { + console.error("styleSelect is not an input element"); + return; + } + + /** + * @param {HTMLElement} list - the list of styles where the element is added. + * @param {HTMLElement} other - the list of styles where the element is removed. + */ + var onStyleSelectOther = function (list, other) { + return function (/** @type {MouseEvent} */ ev) { + let tmpEl = list.removeChild( + list.children[list.children.length - 3] + ); + var newEl = document.createElement("span"); + newEl.setAttribute( + "data-value", + String(tmpEl.getAttribute("data-value")) + ); + newEl.textContent = tmpEl.textContent; + other.appendChild(newEl); + newEl.onclick = onStyleSelectOther( + self._styleSelectList, + self._styleSelectListOther + ); + + if (ev.target instanceof HTMLElement === false) { + console.error("ev.target is not an HTMLElement"); + return; + } + tmpEl = other.removeChild(ev.target); + newEl = document.createElement("span"); + newEl.setAttribute( + "data-value", + String(tmpEl.getAttribute("data-value")) + ); + newEl.textContent = tmpEl.textContent; + list.insertBefore(newEl, list.firstElementChild); + newEl.onclick = self._onClickListElement( + self._styleSelectList, + styleSelect + ); + var event = new Event("click"); + newEl.dispatchEvent(event); + self._closeList(); + }; + }; + + for (var i = 0; i < stylesInfo.length; i++) { + var el = document.createElement("span"); + el.setAttribute("data-value", stylesInfo[i].name); + el.textContent = stylesInfo[i].title; + if ( + self._cslStylesManager.isStyleDefault(stylesInfo[i].name) || + stylesInfo[i].name == lastStyle + ) { + if (stylesInfo.length == 1) + self._styleSelectList.insertBefore( + el, + self._styleSelectList.firstElementChild + ); + else self._styleSelectList.appendChild(el); + el.onclick = self._onClickListElement( + self._styleSelectList, + styleSelect + ); + } else { + self._styleSelectListOther.appendChild(el); + el.onclick = onStyleSelectOther( + self._styleSelectList, + self._styleSelectListOther + ); + } + if (stylesInfo[i].name == lastStyle) { + el.setAttribute("selected", ""); + self._selectInput(styleSelect, el, self._styleSelectList, false); + } + } +}; + +SettingsPage.prototype._onStyleChange = function () { + let styleFormat = this._cslStylesManager.getLastUsedFormat(); + this._citationService.setStyleFormat(styleFormat); + this._bNumFormat = styleFormat == "numeric"; + if ("note" === styleFormat) { + this._notesStyleWrapper.classList.remove(this._displayNoneClass); + } else { + this._notesStyleWrapper.classList.add(this._displayNoneClass); + } + + let notesStyle = this._cslStylesManager.getLastUsedNotesStyle(); + this._citationService.setNotesStyle(notesStyle); + const notesAs = this._notesStyleWrapper.querySelector( + 'input[name="notesAs"][value="' + notesStyle + '"]' + ); + if (notesAs && notesAs instanceof HTMLInputElement) { + notesAs.checked = true; + } +}; + +/** + * @param {HTMLElement} el + */ +SettingsPage.prototype._openList = function (el) { + const self = this; + el.classList.remove(this._displayNoneClass); + const f = function () { + self._closeList(); + window.removeEventListener("click", f); + }; + window.addEventListener("click", f); +}; + +SettingsPage.prototype._closeList = function () { + for (var i = 0; i < this._selectLists.length; i++) { + if (this._selectLists[i] === this._styleSelectList) + this._styleSelect.oninput(null, ""); + this._selectLists[i].classList.add(this._displayNoneClass); + } +}; + +SettingsPage.prototype._initSelectBoxes = function () { + const self = this; + var select = document.getElementsByClassName("control select"); + for (var i = 0; i < select.length; i++) { + var input = select[i]; + var holder = input.parentElement; + if (!(input instanceof HTMLInputElement) || !holder) { + console.error("initSelectBoxes: no input or holder"); + continue; + } + + var arrow = document.createElement("span"); + arrow.classList.add("selectArrow"); + arrow.appendChild(document.createElement("span")); + arrow.appendChild(document.createElement("span")); + holder.appendChild(arrow); + + const holderElement = holder.getElementsByClassName("selectList"); + + for (var k = 0; k < holderElement.length; k++) { + var temp = holderElement[k]; + var list; + if (temp instanceof HTMLElement) { + list = temp; + } else { + console.error( + "initSelectBoxes: holderElement is not HTMLElement" + ); + continue; + } + + if (list.children.length > 0) { + for (var j = 0; j < list.children.length; j++) { + const child = list.children[j]; + if (child instanceof HTMLElement === false) { + continue; + } + child.onclick = self._onClickListElement(list, input); + } + // selectInput(input, list.children[0], list, false); + } + + /** + * @param {HTMLElement} list + * @param {HTMLElement} input + * @returns {function} + */ + var fOpen = function (list, input) { + return function (/** @type {MouseEvent} */ ev) { + ev.stopPropagation(); + if ( + !self._styleSelectListOther.classList.contains( + self._displayNoneClass + ) + ) + return true; + + if (list.onopen) { + list.onopen(); + } + if (!input.hasAttribute("readonly")) { + input.select(); + } + self._openList(list); + + return true; + }; + }; + + if (k !== 1) { + input.onclick = fOpen(list, input); + arrow.onclick = fOpen(list, input); + } + self._selectLists.push(list); + } + } +}; + +/** + * @param {HTMLElement} list + * @param {HTMLInputElement} input + */ +SettingsPage.prototype._onClickListElement = function (list, input) { + const self = this; + return function (/** @type {MouseEvent} */ ev) { + if (!ev.target || !(ev.target instanceof HTMLElement)) { + console.error("onClickListElement: no target"); + return; + } + var sel = ev.target.getAttribute("data-value"); + for (var i = 0; i < list.children.length; i++) { + const temp = list.children[i]; + if (temp instanceof HTMLElement === false) continue; + /** @type {HTMLElement} */ + const child = temp; + if (list.children[i].getAttribute("data-value") == sel) { + list.children[i].setAttribute("selected", ""); + + self._selectInput(input, child, list, true); + } else { + if (list.children[i].hasAttribute("selected")) { + list.children[i].attributes.removeNamedItem("selected"); + } + } + } + }; +}; + +/** + * @param {HTMLInputElement} input + * @param {HTMLElement} el + * @param {HTMLElement} list + * @param {boolean} isClick + */ +SettingsPage.prototype._selectInput = function (input, el, list, isClick) { + input.value = el.textContent; + var val = el.getAttribute("data-value") || ""; + input.setAttribute("data-value", val); + input.setAttribute("title", el.textContent); + if (input.onselectchange) { + input.onselectchange(input, val, isClick); + } + list.classList.add(this._displayNoneClass); +}; diff --git a/sdkjs-plugins/content/zotero/scripts/shared/components/select-citation.js b/sdkjs-plugins/content/zotero/scripts/shared/components/select-citation.js index 27cebda1..440c57ef 100644 --- a/sdkjs-plugins/content/zotero/scripts/shared/components/select-citation.js +++ b/sdkjs-plugins/content/zotero/scripts/shared/components/select-citation.js @@ -51,7 +51,7 @@ function SelectCitationsComponent( ["volume", "Volume"], ]; - this._cancelBtn = document.getElementById("cancelBtn"); + this._cancelSelectBtn = document.getElementById("cancelSelectBtn"); this._docsHolder = document.getElementById("docsHolder"); this._docsThumb = document.getElementById("docsThumb"); @@ -89,8 +89,8 @@ function SelectCitationsComponent( SelectCitationsComponent.prototype._init = function () { const self = this; - if (this._cancelBtn) { - this._cancelBtn.onclick = function (e) { + if (this._cancelSelectBtn) { + this._cancelSelectBtn.onclick = function (e) { var ids = []; for (var id in self._items) { ids.push(id); diff --git a/sdkjs-plugins/content/zotero/scripts/shared/ui/selectbox.js b/sdkjs-plugins/content/zotero/scripts/shared/ui/selectbox.js index e9087fc0..3aa43367 100644 --- a/sdkjs-plugins/content/zotero/scripts/shared/ui/selectbox.js +++ b/sdkjs-plugins/content/zotero/scripts/shared/ui/selectbox.js @@ -538,6 +538,36 @@ SelectBox.prototype.addItem = function (value, text, selected) { this._updateSelectedText(); }; +/** + * @param {Array<[string,string]>} values + * @param {string} [selectedValue] + */ +SelectBox.prototype.addItems = function (values, selectedValue) { + const self = this; + values.forEach(function (pair, index) { + const isSelected = selectedValue + ? pair[0] === selectedValue + : index === 0; + + if (isSelected) { + if (self._options.multiple) { + self._selectedValues.add(pair[0]); + } else { + self._selectedValues.clear(); + self._selectedValues.add(pair[0]); + } + } + + self._items.push({ + value: pair[0], + text: pair[1], + selected: isSelected, + }); + }, this); + + this._updateSelectedText(); +}; + SelectBox.prototype.addSeparator = function () { this._items.push(null); }; @@ -556,6 +586,21 @@ SelectBox.prototype.removeItem = function (value) { this._updateSelectedText(); }; +/** + * @return {null | string} + */ +SelectBox.prototype.getSelectedValue = function () { + if (this._options.multiple) { + console.error( + "Method getSelectedValue is only available for single-select boxes." + ); + return null; + } else { + var values = Array.from(this._selectedValues); + return values.length > 0 ? values[0] : null; + } +}; + /** * @return {null | string | Array} */