diff --git a/chrome/content/zotero/containers/tagSelectorContainer.jsx b/chrome/content/zotero/containers/tagSelectorContainer.jsx
index 467ca59263..a7e529ac5c 100644
--- a/chrome/content/zotero/containers/tagSelectorContainer.jsx
+++ b/chrome/content/zotero/containers/tagSelectorContainer.jsx
@@ -642,6 +642,46 @@ Zotero.TagSelector = class TagSelectorContainer extends React.PureComponent {
await Zotero.Tags.setColor(this.libraryID, io.name, io.color, io.position);
}
+ async openTagSplitterWindow() {
+ const oldTagName = this.contextTag.name; // contextTag contains { name, width, color }
+ const dataIn = {
+ oldTag: this.contextTag.name,
+ isLongTag: false
+ };
+ const dataOut = { result: null };
+
+ window.openDialog(
+ 'chrome://zotero/content/longTagFixer.xhtml',
+ '',
+ 'chrome,modal,centerscreen',
+ dataIn, dataOut
+ );
+
+ if (!dataOut.result) {
+ return;
+ }
+
+ const oldTagID = Zotero.Tags.getID(oldTagName);
+
+ if (dataOut.result.op === 'split') {
+ const itemIDs = await Zotero.Tags.getTagItems(this.libraryID, oldTagID);
+ await Zotero.DB.executeTransaction(async () => {
+ for (const itemID of itemIDs) {
+ const item = await Zotero.Items.getAsync(itemID);
+ const tagType = item.getTagType(oldTagName);
+ for (const newTagName of dataOut.result.tags) {
+ item.addTag(newTagName, tagType);
+ }
+ item.removeTag(oldTagName);
+ await item.save();
+ }
+ await Zotero.Tags.purge(oldTagID);
+ });
+ } else {
+ throw new Error('Unsupported op: ' + dataOut.result.op);
+ }
+ }
+
async openRenamePrompt() {
var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1']
.getService(Ci.nsIPromptService);
diff --git a/chrome/content/zotero/longTagFixer.js b/chrome/content/zotero/longTagFixer.js
index 9acb1fd9b3..d2dd331c83 100644
--- a/chrome/content/zotero/longTagFixer.js
+++ b/chrome/content/zotero/longTagFixer.js
@@ -1,191 +1,212 @@
/*
***** BEGIN LICENSE BLOCK *****
-
- Copyright © 2009 Center for History and New Media
- George Mason University, Fairfax, Virginia, USA
- http://zotero.org
-
+
+ Copyright © 2022 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ https://www.zotero.org
+
This file is part of Zotero.
-
+
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
-
+
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
-
+
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see .
-
+
***** END LICENSE BLOCK *****
*/
+const HTML_NS = 'http://www.w3.org/1999/xhtml';
-var Zotero_Long_Tag_Fixer = new function () {
- var _oldTag = window.arguments[0];
- var _dataOut = window.arguments[1];
+var Zotero_Long_Tag_Fixer = new function () { // eslint-disable-line camelcase, no-unused-vars
+ const { oldTag, isLongTag } = window.arguments?.[0] ?? { isLongTag: true, oldTag: '' };
+ const dataOut = window.arguments?.[1] || {};
this.init = function () {
- document.getElementById('zotero-old-tag').value = _oldTag;
- document.getElementById('zotero-old-tag-delimiter').nextSibling.value = Zotero.getString('general.character.singular');
-
- var delimiter = Zotero.Prefs.get('lastLongTagDelimiter');
- document.getElementById('zotero-old-tag-delimiter').value = delimiter;
-
- var lastMode = Zotero.Prefs.get('lastLongTagMode');
- if (!lastMode) {
- lastMode = 0;
- }
- this.switchMode(lastMode);
- }
+ const lastMode = Zotero.Prefs.get('lastLongTagMode') || 0;
+ const delimiter = Zotero.Prefs.get('lastLongTagDelimiter');
+
+ this.dialog = document.getElementById('zotero-long-tag-fixer');
+ this.intro = document.getElementById('intro');
+ this.tabs = document.getElementById('zotero-new-tag-actions');
+ this.oldTagInput = document.getElementById('zotero-old-tag');
+ this.oldTag = document.getElementById('zotero-old-tag');
+ this.delimiterLabel = document.getElementById('delimiter-label');
+ this.oldTagDelimiter = document.getElementById('zotero-old-tag-delimiter');
+ this.listbox = document.getElementById('zotero-new-tag-list');
+ this.newTagInput = document.getElementById('zotero-new-tag-editor');
+ this.newTagCharacterCount = document.getElementById('zotero-new-tag-character-count');
+ this.zoteroNewTagInfo = document.getElementById('zotero-new-tag-characters');
+
+ document.addEventListener('dialogaccept', () => this.accept());
+ document.addEventListener('dialogcancel', () => this.cancel());
+ this.tabs.addEventListener('select', (ev) => {
+ if (ev.target === this.tabs.querySelector('tabpanels')) {
+ this.switchMode(ev.currentTarget.selectedIndex);
+ }
+ });
+
+ this.dialog.classList.toggle('is-long-tag', isLongTag);
+
+ this.oldTagDelimiter.addEventListener('input', () => this.onUpdateDelimiter());
+ this.newTagInput.addEventListener('input', ev => this.updateEditLength(ev.currentTarget.value.length));
+
+ this.oldTagInput.value = oldTag;
+ this.oldTagDelimiter.value = delimiter;
+
+ this.updateLabel();
+ this.switchMode(isLongTag ? lastMode : 0);
+ };
this.switchMode = function (index) {
- var dialog = document.getElementById('zotero-long-tag-fixer');
-
- document.getElementById('zotero-new-tag-actions').selectedIndex = index;
+ this.tabs.selectedIndex = index;
+ let buttonLabel = "";
switch (index) {
+ default:
case 0:
- var buttonLabel = "saveTags";
+ buttonLabel = 'saveTags';
this.updateTagList();
- document.getElementById('zotero-old-tag-delimiter').select();
+ this.oldTagDelimiter.select();
break;
case 1:
- var buttonLabel = "saveTag";
- document.getElementById('zotero-new-tag-editor').value = _oldTag;
- this.updateEditLength(_oldTag.length)
+ buttonLabel = 'saveTag';
+ this.newTagInput.value = oldTag;
+ this.updateEditLength(oldTag.length);
break;
case 2:
- var buttonLabel = "deleteTag";
- dialog.getButton('accept').disabled = false;
+ buttonLabel = 'deleteTag';
+ this.dialog.getButton('accept').disabled = false;
break;
}
- document.getElementById('zotero-long-tag-fixer').getButton('accept').label = Zotero.getString('sync.longTagFixer.' + buttonLabel);
+ this.dialog.getButton('accept').label = Zotero.getString('sync.longTagFixer.' + buttonLabel);
window.sizeToContent();
- Zotero.Prefs.set('lastLongTagMode', index);
- }
-
+ if (isLongTag) {
+ Zotero.Prefs.set('lastLongTagMode', index);
+ }
+ };
/**
* Split tags and populate list
*/
this.updateTagList = function () {
- var listbox = document.getElementById('zotero-new-tag-list');
- while (listbox.childNodes.length) {
- listbox.removeChild(listbox.lastChild);
- }
+ let tags = [];
- var delimiter = document.getElementById('zotero-old-tag-delimiter').value;
+ const delimiter = document.getElementById('zotero-old-tag-delimiter').value;
if (delimiter) {
Zotero.Prefs.set('lastLongTagDelimiter', delimiter);
- var re = new RegExp("\\s*" + delimiter.replace(/([\.\-\[\]\(\)\?\*\+])/g, "\\$1") + "\\s*");
- var tags = _oldTag.split(re);
+ const re = new RegExp("\\s*" + delimiter.replace(/([\.\-\[\]\(\)\?\*\+])/g, "\\$1") + "\\s*");
+ tags = [...new Set(oldTag.split(re).filter(t => t.length > 0))];
}
- var acceptButton = document.getElementById('zotero-long-tag-fixer').getButton('accept');
+ const acceptButton = document.getElementById('zotero-long-tag-fixer').getButton('accept');
if (!delimiter || tags.length < 2) {
acceptButton.disabled = true;
- return;
+ // return;
}
else {
acceptButton.disabled = false;
}
tags.sort();
- for (var i=0; i {
+ const li = document.createElement('richlistitem');
+ const div = document.createElement('div');
+ const checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.checked = true;
+ checkbox.id = 'tag-' + tag;
+ const label = document.createElement('label');
+ label.setAttribute('for', 'tag-' + tag);
+ label.textContent = tag;
+ // Don't toggle checkbox for single-click on label
+
+ div.appendChild(checkbox);
+ div.appendChild(label);
+ li.appendChild(div);
+ this.listbox.append(li);
+ });
window.sizeToContent();
- }
+ };
+
+ this.updateLabel = function () {
+ this.delimiterLabel.innerHTML = this.oldTagDelimiter.value.length > 1
+ ? Zotero.getString('general.character.plural')
+ : Zotero.getString('general.character.singular');
+ };
+
+ this.onUpdateDelimiter = function () {
+ this.updateLabel();
+ this.updateTagList();
+ };
this.deselectAll = function () {
- var lis = document.getElementById('zotero-new-tag-list').getElementsByTagName('listitem');
- for (var i=0; i checkbox.checked = false);
+ };
this.selectAll = function () {
- var lis = document.getElementById('zotero-new-tag-list').getElementsByTagName('listitem');
- for (var i=0; i checkbox.checked = true);
+ };
this.updateEditLength = function (len) {
- document.getElementById('zotero-new-tag-character-count').value = len;
- var invalid = len == 0 || len > Zotero.Tags.MAX_SYNC_LENGTH;
- document.getElementById('zotero-new-tag-characters').setAttribute('invalid', invalid);
- document.getElementById('zotero-long-tag-fixer').getButton('accept').disabled = invalid;
- }
+ this.newTagCharacterCount.innerText = len;
+ const invalid = len == 0 || len > Zotero.Tags.MAX_SYNC_LENGTH;
+ this.zoteroNewTagInfo.classList.toggle('invalid', invalid);
+ this.dialog.getButton('accept').disabled = invalid;
+ };
this.cancel = function () {
- _dataOut.result = false;
- }
+ dataOut.result = false;
+ };
- this.save = function () {
+ this.accept = function () {
try {
-
- var result = {};
-
- var index = document.getElementById('zotero-new-tag-actions').selectedIndex;
-
- switch (index) {
- // Split
- case 0:
- // Get checked tags
- var listbox = document.getElementById('zotero-new-tag-list');
- var len = listbox.childElementCount;
- var newTags = [];
- for (var i=0; i c.checked)
+ .map(n => n.nextSibling.textContent);
+ break;
+ // Edit
+ case 1:
+ result.op = 'edit';
+ result.tag = this.newTagInput.value;
+ break;
- result.op = 'split';
- result.tags = newTags;
- break;
-
- // Edit
- case 1:
- result.op = 'edit';
- result.tag = document.getElementById('zotero-new-tag-editor').value;
- break;
-
- // Delete
- case 2:
- result.op = 'delete';
- break;
- }
-
- _dataOut.result = result;
-
+ // Delete
+ case 2:
+ result.op = 'delete';
+ break;
+ }
+ dataOut.result = result;
}
catch (e) {
Zotero.debug(e);
throw (e);
}
- }
-}
+ };
+};
diff --git a/chrome/content/zotero/longTagFixer.xhtml b/chrome/content/zotero/longTagFixer.xhtml
new file mode 100644
index 0000000000..ca40793638
--- /dev/null
+++ b/chrome/content/zotero/longTagFixer.xhtml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/chrome/content/zotero/longTagFixer.xul b/chrome/content/zotero/longTagFixer.xul
deleted file mode 100644
index 2954190c4b..0000000000
--- a/chrome/content/zotero/longTagFixer.xul
+++ /dev/null
@@ -1,76 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/chrome/content/zotero/xpcom/sync/syncRunner.js b/chrome/content/zotero/xpcom/sync/syncRunner.js
index 4ca1f8dbaf..fa0ef0aa5c 100644
--- a/chrome/content/zotero/xpcom/sync/syncRunner.js
+++ b/chrome/content/zotero/xpcom/sync/syncRunner.js
@@ -1214,62 +1214,54 @@ Zotero.Sync.Runner_Module = function (options = {}) {
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
- // Open long tag fixer for every long tag in every editable library we're syncing
- var editableLibraries = options.libraries
- .filter(x => Zotero.Libraries.get(x).editable);
- for (let libraryID of editableLibraries) {
- let oldTagIDs = yield Zotero.Tags.getLongTagsInLibrary(libraryID);
- for (let oldTagID of oldTagIDs) {
- let oldTag = Zotero.Tags.getName(oldTagID);
- let dataOut = { result: null };
- lastWin.openDialog(
- 'chrome://zotero/content/longTagFixer.xul',
- '',
- 'chrome,modal,centerscreen',
- oldTag,
- dataOut
- );
- // If dialog was cancelled, stop
- if (!dataOut.result) {
- return;
- }
- switch (dataOut.result.op) {
+ // Open long tag fixer for library we're syncing
+ let oldTagIDs = yield Zotero.Tags.getLongTagsInLibrary(object.libraryID);
+
+ for (let oldTagID of oldTagIDs) {
+ let oldTag = Zotero.Tags.getName(oldTagID);
+ let dataOut = { result: null };
+ lastWin.openDialog(
+ 'chrome://zotero/content/longTagFixer.xhtml',
+ '',
+ 'chrome,modal,centerscreen',
+ { oldTag, isLongTag: true },
+ dataOut
+ );
+ // If dialog was cancelled, stop
+ if (!dataOut.result) {
+ return;
+ }
+ const itemIDs = yield Zotero.Tags.getTagItems(object.libraryID, oldTagID);
+
+ switch (dataOut.result.op) {
case 'split':
- for (let libraryID of editableLibraries) {
- let itemIDs = yield Zotero.Tags.getTagItems(libraryID, oldTagID);
- yield Zotero.DB.executeTransaction(async function () {
- for (let itemID of itemIDs) {
- let item = await Zotero.Items.getAsync(itemID);
- for (let tag of dataOut.result.tags) {
- item.addTag(tag);
- }
- item.removeTag(oldTag);
- await item.save();
+ yield Zotero.DB.executeTransaction(async function () {
+ for (let itemID of itemIDs) {
+ let item = await Zotero.Items.getAsync(itemID);
+ let tagType = item.getTagType(oldTag);
+ for (let tag of dataOut.result.tags) {
+ item.addTag(tag, tagType);
}
- await Zotero.Tags.purge(oldTagID);
- });
- }
+ item.removeTag(oldTag);
+ await item.save();
+ }
+ await Zotero.Tags.purge(oldTagID);
+ });
break;
case 'edit':
- for (let libraryID of editableLibraries) {
- let itemIDs = yield Zotero.Tags.getTagItems(libraryID, oldTagID);
- yield Zotero.DB.executeTransaction(async function () {
- for (let itemID of itemIDs) {
- let item = await Zotero.Items.getAsync(itemID);
- item.replaceTag(oldTag, dataOut.result.tag);
- await item.save();
- }
- });
- }
+ yield Zotero.DB.executeTransaction(async function () {
+ for (let itemID of itemIDs) {
+ let item = await Zotero.Items.getAsync(itemID);
+ item.replaceTag(oldTag, dataOut.result.tag);
+ await item.save();
+ }
+ });
break;
case 'delete':
- for (let libraryID of editableLibraries) {
- yield Zotero.Tags.removeFromLibrary(libraryID, oldTagID);
- }
+ yield Zotero.Tags.removeFromLibrary(object.libraryID, oldTagID);
break;
- }
}
}
diff --git a/chrome/content/zotero/zoteroPane.xhtml b/chrome/content/zotero/zoteroPane.xhtml
index 458318386a..351dcb52fc 100644
--- a/chrome/content/zotero/zoteroPane.xhtml
+++ b/chrome/content/zotero/zoteroPane.xhtml
@@ -1202,6 +1202,8 @@
oncommand="ZoteroPane.tagSelector.openColorPickerWindow(); event.stopPropagation();"/>
+
diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd
index e545cd8b95..e8e6b3de3e 100644
--- a/chrome/locale/en-US/zotero/zotero.dtd
+++ b/chrome/locale/en-US/zotero/zotero.dtd
@@ -139,6 +139,7 @@
+
diff --git a/chrome/skin/default/zotero/longTagFixer.css b/chrome/skin/default/zotero/longTagFixer.css
deleted file mode 100644
index b8dde18916..0000000000
--- a/chrome/skin/default/zotero/longTagFixer.css
+++ /dev/null
@@ -1,8 +0,0 @@
-#zotero-new-tag-characters[invalid="true"] {
- color: red;
-}
-
-#zotero-new-tag-character-count {
- font-weight: bold;
- margin-right: 0;
-}
diff --git a/scss/_zotero-react-client.scss b/scss/_zotero-react-client.scss
index f2b9f3ac97..bd0ceb0c9d 100644
--- a/scss/_zotero-react-client.scss
+++ b/scss/_zotero-react-client.scss
@@ -25,11 +25,14 @@
@import "components/autosuggest";
@import "components/button";
@import "components/clicky";
+@import "components/collection-tree";
@import "components/createParent";
@import "components/dictionaryManager";
@import "components/editable";
@import "components/exportOptions";
@import "components/icons";
+@import "components/item-tree";
+@import "components/longTagFixer";
@import "components/mainWindow";
@import "components/notesList";
@import "components/progressMeter";
@@ -38,9 +41,7 @@
@import "components/tabBar";
@import "components/tagsBox";
@import "components/tagSelector";
-@import "components/collection-tree";
@import "components/virtualized-table";
-@import "components/item-tree";
// Elements
// --------------------------------------------------
diff --git a/scss/abstracts/_utilities.scss b/scss/abstracts/_utilities.scss
index d8e3164bb6..34d7b23067 100644
--- a/scss/abstracts/_utilities.scss
+++ b/scss/abstracts/_utilities.scss
@@ -2,3 +2,8 @@
// Utilities
// --------------------------------------------------
+@mixin text-truncate($text-overflow: ellipsis) {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: $text-overflow;
+}
\ No newline at end of file
diff --git a/scss/components/_longTagFixer.scss b/scss/components/_longTagFixer.scss
new file mode 100644
index 0000000000..df0e01e17a
--- /dev/null
+++ b/scss/components/_longTagFixer.scss
@@ -0,0 +1,112 @@
+#zotero-long-tag-fixer {
+ min-width: 400px; // with intro disabled, dialog tends to get to narrow
+
+ tab {
+ display: none;
+
+ @include variant("#zotero-long-tag-fixer.is-long-tag") {
+ display: revert;
+ }
+ }
+
+ tabpanels {
+ padding-top: 15px;
+
+ @include variant("#zotero-long-tag-fixer.is-long-tag") {
+ padding-top: 33px;
+ }
+ }
+
+ tab[visuallyselected="true"]:not(:-moz-window-inactive) {
+ color: unset;
+ }
+
+ #intro {
+ display: none;
+ flex-direction: column;
+
+ @include variant("#zotero-long-tag-fixer.is-long-tag") {
+ display: flex;
+ }
+
+ > label {
+ margin: .5em 0 0 0;
+ }
+
+ > textarea {
+ margin: 1em;
+ height: 50px;
+ border: none;
+ padding: .5em;
+ background-color: $shade-1;
+ appearance: none;
+ outline: none;
+ }
+ }
+
+ #zotero-new-tag-actions {
+ margin-top: 1em;
+ }
+
+ .split-tab {
+ .tag-list {
+ height: calc(8 * (1em + 10px));
+ overflow-x: hidden;
+ overflow-y: scroll;
+
+ > richlistitem {
+ display: block;
+ height: calc(1em + 10px);
+ overflow: hidden;
+ }
+
+ div {
+ display: flex;
+ align-items: center;
+ }
+
+ label {
+ // width of the label is set to the width of the viewport minus sum of widths of
+ // paddings, margins and a checkbox to produce ellipsis truncation.
+ width: calc(100vw - 100px);
+ @include text-truncate;
+ }
+ }
+
+ .delimiter-input {
+ width: 20px;
+ }
+
+ .delimiter-input-wrap {
+ display: flex;
+ align-items: center;
+ }
+
+ .tag-list + label {
+ padding: 0.5em 0;
+ }
+ }
+
+ .edit-tab {
+ display: flex;
+ flex-direction: column;
+
+ #zotero-new-tag-editor {
+ flex: 1 1 auto;
+ }
+
+ #zotero-new-tag-characters {
+ flex: 0 1 auto;
+ padding: .5em 0;
+ }
+ }
+
+ .invalid {
+ color: $red;
+ }
+
+ #zotero-new-tag-character-count {
+ font-weight: bold;
+ margin-right: 0;
+ }
+}
\ No newline at end of file
diff --git a/test/tests/syncRunnerTest.js b/test/tests/syncRunnerTest.js
index 6a5bd72746..86f24cf0bc 100644
--- a/test/tests/syncRunnerTest.js
+++ b/test/tests/syncRunnerTest.js
@@ -1321,7 +1321,7 @@ describe("Zotero.Sync.Runner", function () {
}
});
- waitForDialog(null, 'accept', 'chrome://zotero/content/longTagFixer.xul');
+ waitForDialog(null, 'accept', 'chrome://zotero/content/longTagFixer.xhtml');
yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] });
assert.isFalse(Zotero.Tags.getID(tag));
@@ -1391,9 +1391,9 @@ describe("Zotero.Sync.Runner", function () {
}
});
- waitForDialog(function (dialog) {
- dialog.Zotero_Long_Tag_Fixer.switchMode(2);
- }, 'accept', 'chrome://zotero/content/longTagFixer.xul');
+ waitForDialog(function (window) {
+ window.Zotero_Long_Tag_Fixer.switchMode(2);
+ }, 'accept', 'chrome://zotero/content/longTagFixer.xhtml');
yield runner.sync({ libraries: [Zotero.Libraries.userLibraryID] });
assert.isFalse(Zotero.Tags.getID(tag));