From 897e74c7f178dc1f14ded0cbd3c7d42801c46c50 Mon Sep 17 00:00:00 2001 From: Tom Najdek Date: Tue, 23 May 2017 00:10:03 +0100 Subject: [PATCH 1/6] Reactify the Tag Selector --- .babelrc | 1 + .../content/zotero/bindings/tagselector.xml | 1213 ----------------- .../reactUI/containers/tag-selector.jsx | 455 +++++++ chrome/content/zotero/reactUI/tagSelector.xul | 58 + chrome/content/zotero/reactUI/zoteroPane.js | 40 + chrome/content/zotero/reactUI/zoteroPane.xul | 35 + .../content/zotero/standalone/standalone.xul | 3 + chrome/content/zotero/xpcom/notifier.js | 1 + chrome/content/zotero/zoteroPane.js | 107 +- chrome/content/zotero/zoteroPane.xul | 8 +- chrome/skin/default/zotero/zotero.css | 6 - package-lock.json | 326 +---- package.json | 7 +- resource/classnames.js | 1 + resource/prop-types.js | 1 + resource/react-ui/components/form/input.jsx | 155 +++ resource/react-ui/components/tag-selector.jsx | 53 + .../components/tag-selector/tag-list.jsx | 55 + resource/require.js | 41 +- resource/web-library | 1 - scripts/babel-worker.js | 5 +- scripts/build.js | 4 +- scripts/config.js | 13 +- scripts/sass.js | 14 +- scripts/watch.js | 10 +- scss/abstracts/_functions.scss | 4 + scss/abstracts/_mixins.scss | 4 + scss/abstracts/_placeholders.scss | 3 + scss/abstracts/_utilities.scss | 4 + scss/abstracts/_variables.scss | 77 ++ scss/components/_tag-selector.scss | 86 ++ scss/themes/_light.scss | 179 +++ scss/zotero-react-client.scss | 23 + 33 files changed, 1396 insertions(+), 1597 deletions(-) delete mode 100644 chrome/content/zotero/bindings/tagselector.xml create mode 100644 chrome/content/zotero/reactUI/containers/tag-selector.jsx create mode 100644 chrome/content/zotero/reactUI/tagSelector.xul create mode 100644 chrome/content/zotero/reactUI/zoteroPane.js create mode 100644 chrome/content/zotero/reactUI/zoteroPane.xul create mode 120000 resource/classnames.js create mode 120000 resource/prop-types.js create mode 100644 resource/react-ui/components/form/input.jsx create mode 100644 resource/react-ui/components/tag-selector.jsx create mode 100644 resource/react-ui/components/tag-selector/tag-list.jsx delete mode 120000 resource/web-library create mode 100644 scss/abstracts/_functions.scss create mode 100644 scss/abstracts/_mixins.scss create mode 100644 scss/abstracts/_placeholders.scss create mode 100644 scss/abstracts/_utilities.scss create mode 100644 scss/abstracts/_variables.scss create mode 100644 scss/components/_tag-selector.scss create mode 100644 scss/themes/_light.scss create mode 100644 scss/zotero-react-client.scss diff --git a/.babelrc b/.babelrc index c11d720d86..2965585ef6 100644 --- a/.babelrc +++ b/.babelrc @@ -18,6 +18,7 @@ "syntax-jsx", "transform-react-jsx", "transform-react-display-name", + "transform-class-properties", [ "transform-es2015-modules-commonjs", { diff --git a/chrome/content/zotero/bindings/tagselector.xml b/chrome/content/zotero/bindings/tagselector.xml deleted file mode 100644 index 9db67dc840..0000000000 --- a/chrome/content/zotero/bindings/tagselector.xml +++ /dev/null @@ -1,1213 +0,0 @@ - - - - - - - - - - - - - - - - - - - - false - false - null - - null - null - - - "view" - - - - - - - - - - - - - - - - - - - - - null - - - - - - - - - false - null - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Zotero.Promise.check(this.mode)); - - let { div, emptyRegular } = this.createTagsList(this._tags); - - this._emptyRegular = emptyRegular; - tagsBox.innerHTML = ""; - tagsBox.appendChild(div); - this._dirty = false; - this._tagsDiv = null; - } - // Otherwise just update based on visibility - else { - // If only a few tags, regenerate buttons from scratch - if (this.filterToScope && this._scope.size <= 100) { - // If full set is currently displayed, store it for later - if (!this._tagsDiv) { - this._tagsDiv = tagsBox.firstChild; - } - - let tags = []; - this._scope.forEach(function (types, name) { - tags.push(...types.map(type => { - return { - tag: name, - type - }; - })); - }); - let { div, emptyRegular } = this.createTagsList(tags); - tagsBox.replaceChild(div, tagsBox.firstChild); - this._emptyRegular = emptyRegular; - } - // Otherwise swap in the stored buttons from the last full run, - // after updating their visibility - else { - let oldDiv = tagsBox.removeChild(tagsBox.firstChild); - if (!this._tagsDiv) { - this._tagsDiv = oldDiv; - } - - let elems = this._tagsDiv.childNodes; - let tagColors = Zotero.Tags.getColors(this.libraryID); - for (let i = 0; i < elems.length; i++) { - let elem = elems[i]; - let visible = this._updateClickableTag( - elem, elem.textContent, tagColors - ); - if (visible) { - this._emptyRegular = false; - } - } - tagsBox.appendChild(this._tagsDiv); - } - } - - //start tag cloud code - - var tagCloud = Zotero.Prefs.get('tagCloud'); - if (false && tagCloud) { - var labels = tagsBox.getElementsByTagName('label'); - - //loop through displayed labels and find number of linked items - var numlinked= []; - for (var i=0; i - - - - - - tag.tag)); - [...new Set(tagColors.keys())].filter(x => !regularTags.has(x)).forEach((x) => { - tags.push(Zotero.Tags.cleanData({ tag: x })); - }); - - // Sort by name - var t = new Date(); - Zotero.debug("Sorting tags"); - var collation = Zotero.getLocaleCollation(); - tags.sort(function (a, b) { - return collation.compareString(1, a.tag, b.tag); - }); - Zotero.debug(`Sorted tags in ${new Date() - t} ms`); - - var div = document.createElementNS('http://www.w3.org/1999/xhtml', 'div'); - var emptyRegular = true; - var lastTag; - for (let i = 0; i < tags.length; i++) { - let tagData = tags[i]; - - // Only show tags of different types once - if (tagData.tag === lastTag) { - continue; - } - lastTag = tagData.tag; - - let elem = this._insertClickableTag(div, tagData); - let visible = this._updateClickableTag( - elem, tagData.tag, tagColors - ); - if (visible) { - emptyRegular = false; - } - } - return { div, emptyRegular }; - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - val.split("/")[1] == 'tagColors')) { - yield this.refresh(true); - } - return; - } - - // Ignore anything other than deletes in duplicates view - if (this.collectionTreeRow.isDuplicates()) { - switch (event) { - case 'delete': - case 'trash': - break; - - default: - return; - } - } - - // Ignore item events other than 'trash' - if (type == 'item' && event != 'trash') { - return; - } - - var selectionChanged = false; - - // If a selected tag no longer exists, deselect it - if (event == 'delete' || event == 'trash' || event == 'modify') { - // TODO: necessary, or just use notifier value? - this._tags = yield Zotero.Tags.getAll(this.libraryID, this._types); - - for (let tag of this.selection) { - for (let tag2 of this._tags) { - if (tag == tag2) { - var found = true; - break; - } - } - if (!found) { - this.selection.delete(tag); - selectionChanged = true; - } - } - } - - if (event == 'add') { - if (type == 'item-tag') { - let tagObjs = ids - // Get tag name and type - .map(x => extraData[x]) - // Ignore tag adds for items not in the current library, if there is one - .filter(function (x) { - if (!this._libraryID) return true; - return x.libraryID == this._libraryID; - }.bind(this)); - - if (tagObjs.length) { - this.insertSorted(this.id('tags-box').firstChild, tagObjs); - // If full set isn't currently displayed, update it too - if (this._tagsDiv) { - this.insertSorted(this._tagsDiv, tagObjs); - } - } - } - // Don't add anything for item or collection-item; just update scope - - return this.updateScope(); - } - - var t = this.id('tags-search').inputField; - if (t.value) { - this.setSearch(t.value, true); - } - else { - this.setSearch(false, true); - } - this._dirty = true; - - // This is a hack, but set this to run after the refresh, - // since _emptyRegular isn't set until then - this.onRefresh = function () { - // If no regular tags visible after a delete, deselect all. - // This is necessary so that a selected tag that's removed - // from its last item doesn't cause all regular tags to - // disappear without anything being visibly selected. - if ((event == 'remove' || event == 'delete') && - this._emptyRegular && this.getNumSelected()) { - Zotero.debug('No tags visible after delete -- deselecting all'); - return this.clearAll(); - } - }.bind(this); - - // If the selection changed, update the items list - if (selectionChanged && this.onchange) { - return this.onchange(); - } - - // Otherwise, just update the tag selector - return this.updateScope(); - }, this); - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { - Zotero.updateZoteroPaneProgressMeter( - Math.round(progress / progressMax * 100) - ); - } - ); - } - finally { - Zotero.hideZoteroPaneOverlays(); - } - } - }.bind(this))(); - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - = Zotero.Tags.MAX_COLORED_TAGS && !tagColors.has(io.name)) { - var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] - .getService(Components.interfaces.nsIPromptService); - ps.alert(null, "", Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS)); - return; - } - - io.tagColors = tagColors; - - window.openDialog( - 'chrome://zotero/content/tagColorChooser.xul', - "zotero-tagSelector-colorChooser", - "chrome,modal,centerscreen", io - ); - - // Dialog cancel - if (typeof io.color == 'undefined') { - return; - } - - yield Zotero.Tags.setColor(this.libraryID, io.name, io.color, io.position); - }.bind(this)); - ]]> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/chrome/content/zotero/reactUI/containers/tag-selector.jsx b/chrome/content/zotero/reactUI/containers/tag-selector.jsx new file mode 100644 index 0000000000..4085cbac58 --- /dev/null +++ b/chrome/content/zotero/reactUI/containers/tag-selector.jsx @@ -0,0 +1,455 @@ +/* global Zotero: false */ +'use strict'; + +(function() { + +const React = require('react'); +const ReactDom = require('react-dom'); +const TagSelector = require('react-ui/components/tag-selector.js'); +const noop = Promise.resolve(); +const defaults = { + tags: [], + searchString: '', + shouldFocus: false, + onSelection: noop, + viewOnly: false +}; +const { Cc, Ci } = require('chrome'); + +ZoteroPane.React.TagSelector = class TagSelectorContainer extends React.Component { + constructor(props) { + super(props); + this.opts = Object.assign({}, defaults, props); + this.selectedTags = new Set(); + this.updateScope = Promise.resolve; + this.filterToScope = true; + this.showAutomatic = true; + this._notifierID = Zotero.Notifier.registerObserver( + this, + ['collection-item', 'item', 'item-tag', 'tag', 'setting'], + 'tagSelector' + ); + this._scope = null; + this._allTags = null; + this.state = null; + this.update(); + } + + componentWillReceiveProps(nextProps) { + this.opts = Object.assign({}, this.opts, nextProps); + this.update(); + } + + async notify(event, type, ids, extraData) { + if (type === 'setting') { + if (ids.some(val => val.split('/')[1] == 'tagColors')) { + await this.refresh(true); + } + return; + } + + // Ignore anything other than deletes in duplicates view + if (this.collectionTreeRow.isDuplicates()) { + switch (event) { + case 'delete': + case 'trash': + break; + + default: + return; + } + } + + // Ignore item events other than 'trash' + if (type == 'item' && event != 'trash') { + return; + } + + + // If a selected tag no longer exists, deselect it + if (event == 'delete' || event == 'trash' || event == 'modify') { + for (let tag of this.selectedTags) { + if (tag == extraData[ids[0]].old.tag) { + this.selectedTags.delete(tag); + break; + } + } + await this.refresh(true); + } + + if (event == 'add') { + if (type == 'item-tag') { + let tagObjs = ids + // Get tag name and type + .map(x => extraData[x]) + // Ignore tag adds for items not in the current library, if there is one + .filter(x => { + if (!this._libraryID) { + return true; + } + return x.libraryID == this._libraryID; + }); + + if (tagObjs.length) { + this.insertSorted(tagObjs); + } + } + } + + // Otherwise, just update the tag selector + return this.updateScope(); + } + + async refresh(fetch) { + let t = new Date; + + if(this._allTags === null) { + fetch = true; + } + + if (fetch) { + Zotero.debug('Reloading tags selector'); + } else { + Zotero.debug('Refreshing tags selector'); + } + + // If new data, rebuild boxes + if (fetch) { + let tagColors = Zotero.Tags.getColors(this.libraryID); + this._allTags = await Zotero.Tags.getAll(this.libraryID, this.showAutomatic ? [0, 1] : [0]); + // .tap(() => Zotero.Promise.check(this.mode)); + + // Add colored tags that aren't already real tags + let regularTags = new Set(this._allTags.map(tag => tag.tag)); + let coloredTags = Array.from(tagColors.keys()); + + coloredTags.filter(ct => !regularTags.has(ct)).forEach(x => + this._allTags.push(Zotero.Tags.cleanData({ tag: x })) + ); + + // Sort by name + this._allTags.sort(function (a, b) { + return Zotero.getLocaleCollation().compareString(1, a.tag, b.tag); + }); + + } + + this.update(); + + Zotero.debug('Loaded tag selector in ' + (new Date - t) + ' ms'); + + // var event = new Event('refresh'); + // this.dispatchEvent(event); + } + + update() { + var tags; + let flatScopeTags = Array.isArray(this._scope) ? this._scope.map(tag => tag.tag) : []; + + if('libraryID' in this) { + var tagColors = Zotero.Tags.getColors(this.libraryID); + } + + if(this.filterToScope && Array.isArray(this._scope)) { + tags = this._allTags.filter(tag => + flatScopeTags.includes(tag.tag) || (tagColors && tagColors.has(tag.tag)) + ); + } else { + tags = this._allTags ? this._allTags.slice(0) : []; + } + + if(this.opts.searchString) { + tags = tags.filter(tag => !!tag.tag.match(new RegExp(this.opts.searchString, 'i'))); + } + + this.opts.tags = tags.map(t => { + let name = t.tag; + let selected = this.selectedTags.has(name); + let color = tagColors && tagColors.has(name) ? tagColors.get(name).color : ''; + let disabled = !flatScopeTags.includes(name); + return { name, selected, color, disabled }; + }); + this.state ? this.setState(this.opts) : this.state = Object.assign({}, this.opts); + } + + render() { + return {} : this.onTagSelectedHandler.bind(this) } + onTagContext={ this.onTagContextHandler.bind(this) } + onSearch={ this.onSearchHandler.bind(this) } + onSettings={ this.onTagSelectorViewSettingsHandler.bind(this) } + />; + } + + setMode(mode) { + this.setState({viewOnly: mode == 'view'}); + } + + focusTextbox() { + this.opts.shouldFocus = true; + this.update(); + } + + toggle() { + this._isCollapsed = !this._isCollapsed; + } + + unregister() { + ReactDom.unmountComponentAtNode(this.domEl); + if (this._notifierID) { + Zotero.Notifier.unregisterObserver(this._notifierID); + } + } + + uninit() { + this.selectedTags = new Set(); + this.opts.searchString = ''; + this.update(); + } + + onTagContextHandler(tag, ev) { + let tagContextMenu = document.getElementById('tag-menu'); + ev.preventDefault(); + tagContextMenu.openPopup(ev.target, 'end_before', 0, 0, true); + this.contextTag = tag; + } + + onTagSelectorViewSettingsHandler(ev) { + let settingsContextMenu = document.getElementById('tag-selector-view-settings-menu'); + ev.preventDefault(); + settingsContextMenu.openPopup(ev.target, 'end_before', 0, 0, true); + } + + onTagSelectedHandler(tag) { + if(this.selectedTags.has(tag)) { + this.selectedTags.delete(tag); + } else { + this.selectedTags.add(tag); + } + + if('onSelection' in this.opts && typeof(this.opts.onSelection) === 'function') { + this.opts.onSelection(this.selectedTags).then(this.refresh.bind(this)); + } + } + + onSearchHandler(searchString) { + this.opts.searchString = searchString; + this.update(); + } + + getTagSelection() { + return this.selectedTags; + } + + clearTagSelection() { + this.selectedTags = new Set(); + return this.selectedTags; + } + + async openColorPickerWindow() { + var io = { + libraryID: this.libraryID, + name: this.contextTag.name + }; + + var tagColors = Zotero.Tags.getColors(this.libraryID); + if (tagColors.size >= Zotero.Tags.MAX_COLORED_TAGS && !tagColors.has(io.name)) { + var ps = Cc['@mozilla.org/embedcomp/prompt-service;1'] + .getService(Ci.nsIPromptService); + ps.alert(null, '', Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS)); + return; + } + + io.tagColors = tagColors; + + window.openDialog( + 'chrome://zotero/content/tagColorChooser.xul', + 'zotero-tagSelector-colorChooser', + 'chrome,modal,centerscreen', io + ); + + // Dialog cancel + if (typeof io.color == 'undefined') { + return; + } + + await Zotero.Tags.setColor(this.libraryID, io.name, io.color, io.position); + + this.refresh(); + } + + async openRenamePrompt() { + var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1'] + .getService(Ci.nsIPromptService); + + var newName = { value: this.contextTag.name }; + var result = promptService.prompt(window, + Zotero.getString('pane.tagSelector.rename.title'), + Zotero.getString('pane.tagSelector.rename.message'), + newName, '', {}); + + if (!result || !newName.value || this.contextTag.name == newName.value) { + return; + } + + if (this.selectedTags.has(this.contextTag.name)) { + var wasSelected = true; + this.selectedTags.delete(this.contextTag.name); + } + + if (Zotero.Tags.getID(this.contextTag.name)) { + await Zotero.Tags.rename(this.libraryID, this.contextTag.name, newName.value); + } + // Colored tags don't need to exist, so in that case + // just rename the color setting + else { + let color = Zotero.Tags.getColor(this.libraryID, this.contextTag.name); + if (!color) { + throw new Error("Can't rename missing tag"); + } + await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false); + await Zotero.Tags.setColor(this.libraryID, newName, color); + } + + if(wasSelected) { + this.selectedTags.add(newName.value); + } + + this.updateScope(); + } + + async openDeletePrompt() { + var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1'] + .getService(Ci.nsIPromptService); + + var confirmed = promptService.confirm(window, + Zotero.getString('pane.tagSelector.delete.title'), + Zotero.getString('pane.tagSelector.delete.message')); + + if (!confirmed) { + return; + } + + var tagID = Zotero.Tags.getID(this.contextTag.name); + + if (tagID) { + await Zotero.Tags.removeFromLibrary(this.libraryID, tagID); + } + // If only a tag color setting, remove that + else { + await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false); + } + + this.updateScope(); + } + + toggleFilterToScope(newValue) { + this.filterToScope = typeof(newValue) === 'undefined' ? !this.filterToScope : newValue; + this.refresh(); + } + + toggleShowAutomatic(newValue) { + this.showAutomatic = typeof(newValue) === 'undefined' ? !this.showAutomatic : newValue; + this.refresh(true); + } + + deselectAll() { + this.selectedTags = new Set(); + if('onSelection' in this.opts && typeof(this.opts.onSelection) === 'function') { + this.opts.onSelection(this.selectedTags).then(this.refresh.bind(this)); + } + } + + set scope(newScope) { + try { + this._scope = Array.from(newScope); + } catch(e) { + this._scope = null; + } + this.refresh(); + } + + get label() { + let count = this.selectedTags.size; + let mod = count === 1 ? 'singular' : count === 0 ? 'none' : 'plural'; + + return Zotero.getString('pane.tagSelector.numSelected.' + mod, [count]); + } + + onResize() { + const COLLECTIONS_HEIGHT = 32; // minimum height of the collections pane and toolbar + + //Zotero.debug('Updating tag selector size'); + var zoteroPane = document.getElementById('zotero-pane-stack'); + var splitter = document.getElementById('zotero-tags-splitter'); + var tagSelector = document.getElementById('zotero-tag-selector'); + + // Nothing should be bigger than appcontent's height + var max = document.getElementById('appcontent').boxObject.height + - splitter.boxObject.height; + + // Shrink tag selector to appcontent's height + var maxTS = max - COLLECTIONS_HEIGHT; + var tsHeight = parseInt(tagSelector.getAttribute("height")); + if (tsHeight > maxTS) { + //Zotero.debug("Limiting tag selector height to appcontent"); + tagSelector.setAttribute('height', maxTS); + } + tagSelector.style.height = tsHeight + 'px'; + + var height = tagSelector.getBoundingClientRect().height; + + /*Zotero.debug("tagSelector.boxObject.height: " + tagSelector.boxObject.height); + Zotero.debug("tagSelector.getAttribute('height'): " + tagSelector.getAttribute('height')); + Zotero.debug("zoteroPane.boxObject.height: " + zoteroPane.boxObject.height); + Zotero.debug("zoteroPane.getAttribute('height'): " + zoteroPane.getAttribute('height'));*/ + + + // Don't let the Z-pane jump back down to its previous height + // (if shrinking or hiding the tag selector let it clear the min-height) + if (zoteroPane.getAttribute('height') < zoteroPane.boxObject.height) { + //Zotero.debug("Setting Zotero pane height attribute to " + zoteroPane.boxObject.height); + zoteroPane.setAttribute('height', zoteroPane.boxObject.height); + } + + if (tagSelector.getAttribute('collapsed') == 'true') { + // 32px is the default Z pane min-height in overlay.css + height = 32; + } + else { + // tS.boxObject.height doesn't exist at startup, so get from attribute + if (!height) { + height = parseInt(tagSelector.getAttribute('height')); + } + // 121px seems to be enough room for the toolbar and collections + // tree at minimum height + height = height + COLLECTIONS_HEIGHT; + } + + //Zotero.debug('Setting Zotero pane minheight to ' + height); + zoteroPane.setAttribute('minheight', height); + + if (this.isShowing() && !this.isFullScreen()) { + zoteroPane.setAttribute('savedHeight', zoteroPane.boxObject.height); + } + + // Fix bug whereby resizing the Z pane downward after resizing + // the tag selector up and then down sometimes caused the Z pane to + // stay at a fixed size and get pushed below the bottom + tagSelector.height++; + tagSelector.height--; + } + + static init(domEl, opts) { + var ref; + console.log(domEl.style); + ReactDom.render( ref = c } {...opts} />, domEl); + ref.domEl = domEl; + new MutationObserver(ref.onResize).observe(domEl, {attributes: true, attributeFilter: ['height']}); + return ref; + } +} +})(); diff --git a/chrome/content/zotero/reactUI/tagSelector.xul b/chrome/content/zotero/reactUI/tagSelector.xul new file mode 100644 index 0000000000..d1adca8208 --- /dev/null +++ b/chrome/content/zotero/reactUI/tagSelector.xul @@ -0,0 +1,58 @@ + + + %globalDTD; + %zoteroDTD; +]> + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/chrome/content/zotero/reactUI/zoteroPane.js b/chrome/content/zotero/reactUI/zoteroPane.js new file mode 100644 index 0000000000..c77ce511d9 --- /dev/null +++ b/chrome/content/zotero/reactUI/zoteroPane.js @@ -0,0 +1,40 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2018 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://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 ***** +*/ + +'use strict'; + +ZoteroPane.React = { + init() { + var tagSelector = document.getElementById('zotero-tag-selector'); + ZoteroPane_Local.tagSelector = ZoteroPane.React.TagSelector.init(tagSelector, { + onSelection: ZoteroPane_Local.updateTagFilter.bind(ZoteroPane_Local) + }); + }, + + destroy() { + ZoteroPane_Local.tagSelector.unregister(); + } +} + diff --git a/chrome/content/zotero/reactUI/zoteroPane.xul b/chrome/content/zotero/reactUI/zoteroPane.xul new file mode 100644 index 0000000000..056cf67fcd --- /dev/null +++ b/chrome/content/zotero/reactUI/zoteroPane.xul @@ -0,0 +1,35 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/chrome/content/zotero/standalone/standalone.xul b/chrome/content/zotero/standalone/standalone.xul index dd381dc7f6..ad04b99844 100644 --- a/chrome/content/zotero/standalone/standalone.xul +++ b/chrome/content/zotero/standalone/standalone.xul @@ -287,4 +287,7 @@ + + + diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js index 6ffda23b33..11a64f547e 100644 --- a/chrome/content/zotero/xpcom/notifier.js +++ b/chrome/content/zotero/xpcom/notifier.js @@ -73,6 +73,7 @@ Zotero.Notifier = new function(){ if (priority) { msg += " with priority " + priority; } + Zotero.debug(msg); _observers[hash] = { ref: ref, types: types, diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index 1b597f2f6d..e5c1726ef6 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -58,8 +58,6 @@ var ZoteroPane = new function() this.document = document; - const COLLECTIONS_HEIGHT = 32; // minimum height of the collections pane and toolbar - var self = this, _loaded = false, _madeVisible = false, titlebarcolorState, titleState, observerService, @@ -171,11 +169,6 @@ var ZoteroPane = new function() itemsTree.addEventListener("mousedown", ZoteroPane_Local.onTreeMouseDown, true); itemsTree.addEventListener("click", ZoteroPane_Local.onTreeClick, true); - var tagSelector = document.getElementById('zotero-tag-selector'); - tagSelector.onchange = function () { - return ZoteroPane_Local.updateTagFilter(); - }; - Zotero.Keys.windowInit(document); if (Zotero.restoreFromServer) { @@ -240,6 +233,7 @@ var ZoteroPane = new function() } catch (e) {} } + ZoteroPane.React.init(); if (Zotero.openPane) { Zotero.openPane = false; @@ -351,8 +345,7 @@ var ZoteroPane = new function() this.serializePersist(); } - var tagSelector = document.getElementById('zotero-tag-selector'); - tagSelector.unregister(); + ZoteroPane_Local.React.destroy(); if(this.collectionsView) this.collectionsView.unregister(); if(this.itemsView) this.itemsView.unregister(); @@ -1106,85 +1099,17 @@ var ZoteroPane = new function() // and focus filter textbox if (showing) { yield this.setTagScope(); - tagSelector.focusTextbox(); + ZoteroPane_Local.tagSelector.focusTextbox(); } // If hiding, clear selection else { - tagSelector.uninit(); + ZoteroPane_Local.tagSelector.uninit(); } }); this.updateTagSelectorSize = function () { - //Zotero.debug('Updating tag selector size'); - var zoteroPane = document.getElementById('zotero-pane-stack'); - var splitter = document.getElementById('zotero-tags-splitter'); - var tagSelector = document.getElementById('zotero-tag-selector'); - // Nothing should be bigger than appcontent's height - var max = document.getElementById('appcontent').boxObject.height - - splitter.boxObject.height; - - // Shrink tag selector to appcontent's height - var maxTS = max - COLLECTIONS_HEIGHT; - if (parseInt(tagSelector.getAttribute("height")) > maxTS) { - //Zotero.debug("Limiting tag selector height to appcontent"); - tagSelector.setAttribute('height', maxTS); - } - - var height = tagSelector.boxObject.height; - - - /*Zotero.debug("tagSelector.boxObject.height: " + tagSelector.boxObject.height); - Zotero.debug("tagSelector.getAttribute('height'): " + tagSelector.getAttribute('height')); - Zotero.debug("zoteroPane.boxObject.height: " + zoteroPane.boxObject.height); - Zotero.debug("zoteroPane.getAttribute('height'): " + zoteroPane.getAttribute('height'));*/ - - - // Don't let the Z-pane jump back down to its previous height - // (if shrinking or hiding the tag selector let it clear the min-height) - if (zoteroPane.getAttribute('height') < zoteroPane.boxObject.height) { - //Zotero.debug("Setting Zotero pane height attribute to " + zoteroPane.boxObject.height); - zoteroPane.setAttribute('height', zoteroPane.boxObject.height); - } - - if (tagSelector.getAttribute('collapsed') == 'true') { - // 32px is the default Z pane min-height in overlay.css - height = 32; - } - else { - // tS.boxObject.height doesn't exist at startup, so get from attribute - if (!height) { - height = parseInt(tagSelector.getAttribute('height')); - } - // 121px seems to be enough room for the toolbar and collections - // tree at minimum height - height = height + COLLECTIONS_HEIGHT; - } - - //Zotero.debug('Setting Zotero pane minheight to ' + height); - zoteroPane.setAttribute('minheight', height); - - if (this.isShowing() && !this.isFullScreen()) { - zoteroPane.setAttribute('savedHeight', zoteroPane.boxObject.height); - } - - // Fix bug whereby resizing the Z pane downward after resizing - // the tag selector up and then down sometimes caused the Z pane to - // stay at a fixed size and get pushed below the bottom - tagSelector.height++; - tagSelector.height--; - } - - - function getTagSelection() { - var tagSelector = document.getElementById('zotero-tag-selector'); - return tagSelector.selection ? tagSelector.selection : new Set(); - } - - - this.clearTagSelection = function () { - document.getElementById('zotero-tag-selector').deselectAll(); } @@ -1193,7 +1118,7 @@ var ZoteroPane = new function() */ this.updateTagFilter = Zotero.Promise.coroutine(function* () { if (this.itemsView) { - yield this.itemsView.setFilter('tags', getTagSelection()); + yield this.itemsView.setFilter('tags', ZoteroPane_Local.tagSelector.getTagSelection()); } }); @@ -1212,23 +1137,23 @@ var ZoteroPane = new function() * * Passed to the items tree to trigger on changes */ - this.setTagScope = Zotero.Promise.coroutine(function* () { - var collectionTreeRow = this.getCollectionTreeRow(); + this.setTagScope = async function () { + var collectionTreeRow = self.getCollectionTreeRow(); var tagSelector = document.getElementById('zotero-tag-selector'); - if (this.tagSelectorShown()) { + if (self.tagSelectorShown()) { Zotero.debug('Updating tag selector with current tags'); if (collectionTreeRow.editable) { - tagSelector.mode = 'edit'; + ZoteroPane_Local.tagSelector.setMode('edit'); } else { - tagSelector.mode = 'view'; + ZoteroPane_Local.tagSelector.setMode('view'); } - tagSelector.collectionTreeRow = collectionTreeRow; - tagSelector.updateScope = () => this.setTagScope(); - tagSelector.libraryID = collectionTreeRow.ref.libraryID; - tagSelector.scope = yield collectionTreeRow.getChildTags(); + ZoteroPane_Local.tagSelector.collectionTreeRow = collectionTreeRow; + ZoteroPane_Local.tagSelector.updateScope = self.setTagScope; + ZoteroPane_Local.tagSelector.libraryID = collectionTreeRow.ref.libraryID; + ZoteroPane_Local.tagSelector.scope = await collectionTreeRow.getChildTags(); } - }); + }; this.onCollectionSelected = function () { @@ -1280,7 +1205,7 @@ var ZoteroPane = new function() }*/ collectionTreeRow.setSearch(''); - collectionTreeRow.setTags(getTagSelection()); + collectionTreeRow.setTags(ZoteroPane_Local.tagSelector.getTagSelection()); this._updateToolbarIconsForRow(collectionTreeRow); diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul index adff5bfeae..6ba72fabb1 100644 --- a/chrome/content/zotero/zoteroPane.xul +++ b/chrome/content/zotero/zoteroPane.xul @@ -27,6 +27,7 @@ + %globalDTD; @@ -35,7 +36,8 @@ ]> + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml"> - - - + + \ No newline at end of file diff --git a/chrome/content/zotero/containers/tagSelector.jsx b/chrome/content/zotero/containers/tagSelector.jsx new file mode 100644 index 0000000000..858f8ed1fb --- /dev/null +++ b/chrome/content/zotero/containers/tagSelector.jsx @@ -0,0 +1,412 @@ +/* global Zotero: false */ +'use strict'; + +(function() { + +const React = require('react'); +const ReactDOM = require('react-dom'); +const { IntlProvider } = require('react-intl'); +const TagSelector = require('components/tag-selector.js'); +const noop = Promise.resolve(); +const defaults = { + tagColors: new Map(), + tags: [], + showAutomatic: Zotero.Prefs.get('tagSelector.showAutomatic'), + searchString: '', + inScope: new Set(), + loaded: false +}; +const { Cc, Ci } = require('chrome'); + +Zotero.TagSelector = class TagSelectorContainer extends React.Component { + constructor(props) { + super(props); + this._notifierID = Zotero.Notifier.registerObserver( + this, + ['collection-item', 'item', 'item-tag', 'tag', 'setting'], + 'tagSelector' + ); + this.displayAllTags = Zotero.Prefs.get('tagSelector.displayAllTags'); + this.selectedTags = new Set(); + this.state = defaults; + } + + // Update trigger #1 (triggered by ZoteroPane) + async onItemViewChanged({collectionTreeRow, libraryID, tagsInScope}) { + this.collectionTreeRow = collectionTreeRow || this.collectionTreeRow; + + let newState = {loaded: true}; + + if (!this.state.tagColors.length && libraryID && this.libraryID != libraryID) { + newState.tagColors = Zotero.Tags.getColors(libraryID); + } + this.libraryID = libraryID; + + newState.tags = await this.getTags(tagsInScope, + this.state.tagColors.length ? this.state.tagColors : newState.tagColors); + this.setState(newState); + } + + // Update trigger #2 + async notify(event, type, ids, extraData) { + if (type === 'setting') { + if (ids.some(val => val.split('/')[1] == 'tagColors')) { + let tagColors = Zotero.Tags.getColors(this.libraryID); + this.state.tagColors = tagColors; + this.setState({tagColors, tags: await this.getTags(null, tagColors)}); + } + return; + } + + // Ignore anything other than deletes in duplicates view + if (this.collectionTreeRow.isDuplicates()) { + switch (event) { + case 'delete': + case 'trash': + break; + + default: + return; + } + } + + // Ignore item events other than 'trash' + if (type == 'item' && (event == 'trash')) { + return this.setState({tags: await this.getTags()}); + } + + // If a selected tag no longer exists, deselect it + if (type == 'item-tag') { + if (event == 'delete' || event == 'trash' || event == 'modify') { + for (let tag of this.selectedTags) { + if (tag == extraData[ids[0]].old.tag) { + this.selectedTags.delete(tag); + } + } + } + return this.setState({tags: await this.getTags()}); + } + + this.setState({tags: await this.getTags()}); + } + + async getTags(tagsInScope, tagColors) { + if (!tagsInScope) { + tagsInScope = await this.collectionTreeRow.getChildTags(); + } + this.inScope = new Set(tagsInScope.map(t => t.tag)); + let tags; + if (this.displayAllTags) { + tags = await Zotero.Tags.getAll(this.libraryID, [0, 1]); + } else { + tags = tagsInScope + } + + tagColors = tagColors || this.state.tagColors; + + // Add colored tags that aren't already real tags + let regularTags = new Set(tags.map(tag => tag.tag)); + let coloredTags = Array.from(tagColors.keys()); + + coloredTags.filter(ct => !regularTags.has(ct)).forEach(x => + tags.push(Zotero.Tags.cleanData({ tag: x })) + ); + + // Sort by name + tags.sort(function (a, b) { + let aColored = tagColors.has(a.tag), + bColored = tagColors.has(b.tag); + if (aColored && !bColored) return -1; + if (!aColored && bColored) return 1; + return Zotero.getLocaleCollation().compareString(1, a.tag, b.tag); + }); + + return tags; + } + + render() { + let tags = this.state.tags; + let tagMap = new Map(); + if (!this.showAutomatic) { + tags = tags.filter(t => t.type != 1) + } else { + // Ensure no duplicates from auto and manual tags + tags.forEach(t => !tagMap.has() && t.type != 1 && tagMap.set(t.tag, t)); + tags = Array.from(tagMap.values()); + } + if (this.state.searchString) { + tags = tags.filter(tag => !!tag.tag.match(new RegExp(this.state.searchString, 'i'))); + } + tags = tags.map(t => { + let name = t.tag; + return { + name, + selected: this.selectedTags.has(name), + color: this.state.tagColors.has(name) ? this.state.tagColors.get(name).color : '', + disabled: !this.inScope.has(name) + } + }); + return this.focusTextbox = ref && ref.focusTextbox} + searchString={this.state.searchString} + shouldFocus={this.state.shouldFocus} + dragObserver={this.dragObserver} + onSelect={this.state.viewOnly ? () => {} : this.handleTagSelected} + onTagContext={this.handleTagContext} + onSearch={this.handleSearch} + onSettings={this.handleSettings} + loaded={this.state.loaded} + />; + } + + setMode(mode) { + this.state.viewOnly != (mode == 'view') && this.setState({viewOnly: mode == 'view'}); + } + + unregister() { + ReactDOM.unmountComponentAtNode(this.domEl); + if (this._notifierID) { + Zotero.Notifier.unregisterObserver(this._notifierID); + } + } + + uninit() { + this.setState({searchString: ''}); + this.selectedTags = new Set(); + } + + handleTagContext = (tag, ev) => { + let tagContextMenu = document.getElementById('tag-menu'); + ev.preventDefault(); + tagContextMenu.openPopup(null, null, ev.clientX+2, ev.clientY+2); + this.contextTag = tag; + } + + handleSettings = (ev) => { + let settingsContextMenu = document.getElementById('tag-selector-view-settings-menu'); + ev.preventDefault(); + settingsContextMenu.openPopup(ev.target, 'end_before', 0, 0, true); + } + + handleTagSelected = (tag) => { + let selectedTags = this.selectedTags; + if(selectedTags.has(tag)) { + selectedTags.delete(tag); + } else { + selectedTags.add(tag); + } + + if (typeof(this.props.onSelection) === 'function') { + this.props.onSelection(selectedTags); + } + } + + handleSearch = Zotero.Utilities.debounce((searchString) => { + this.setState({searchString}); + }) + + dragObserver = { + onDragOver: function(event) { + if (!event.dataTransfer.getData('zotero/item')) { + return; + } + + var elem = event.target; + + // Ignore drops not on tags + if (elem.localName != 'li') { + return; + } + + // Store the event, because drop event does not have shiftKey attribute set + Zotero.DragDrop.currentEvent = event; + elem.classList.add('dragged-over'); + event.preventDefault(); + event.dataTransfer.dropEffect = "copy"; + }, + onDragExit: function (event) { + Zotero.DragDrop.currentEvent = null; + event.target.classList.remove('dragged-over'); + }, + onDrop: async function(event) { + var elem = event.target; + + // Ignore drops not on tags + if (elem.localName != 'li') { + return; + } + + elem.classList.remove('dragged-over'); + + var dt = event.dataTransfer; + var ids = dt.getData('zotero/item'); + if (!ids) { + return; + } + + return Zotero.DB.executeTransaction(function* () { + ids = ids.split(','); + var items = Zotero.Items.get(ids); + var value = elem.textContent; + + for (let i=0; i= Zotero.Tags.MAX_COLORED_TAGS && !tagColors.has(io.name)) { + var ps = Cc['@mozilla.org/embedcomp/prompt-service;1'] + .getService(Ci.nsIPromptService); + ps.alert(null, '', Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS)); + return; + } + + io.tagColors = tagColors; + + window.openDialog( + 'chrome://zotero/content/tagColorChooser.xul', + 'zotero-tagSelector-colorChooser', + 'chrome,modal,centerscreen', io + ); + + // Dialog cancel + if (typeof io.color == 'undefined') { + return; + } + + await Zotero.Tags.setColor(this.libraryID, io.name, io.color, io.position); + } + + async openRenamePrompt() { + var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1'] + .getService(Ci.nsIPromptService); + + var newName = { value: this.contextTag.name }; + var result = promptService.prompt(window, + Zotero.getString('pane.tagSelector.rename.title'), + Zotero.getString('pane.tagSelector.rename.message'), + newName, '', {}); + + if (!result || !newName.value || this.contextTag.name == newName.value) { + return; + } + + let selectedTags = this.selectedTags; + if (selectedTags.has(this.contextTag.name)) { + var wasSelected = true; + selectedTags.delete(this.contextTag.name); + } + + if (Zotero.Tags.getID(this.contextTag.name)) { + await Zotero.Tags.rename(this.libraryID, this.contextTag.name, newName.value); + } + // Colored tags don't need to exist, so in that case + // just rename the color setting + else { + let color = Zotero.Tags.getColor(this.libraryID, this.contextTag.name); + if (!color) { + throw new Error("Can't rename missing tag"); + } + await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false); + await Zotero.Tags.setColor(this.libraryID, newName.value, color.color); + } + + if (wasSelected) { + selectedTags.add(newName.value); + } + this.setState({tags: await this.getTags()}) + } + + async openDeletePrompt() { + var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1'] + .getService(Ci.nsIPromptService); + + var confirmed = promptService.confirm(window, + Zotero.getString('pane.tagSelector.delete.title'), + Zotero.getString('pane.tagSelector.delete.message')); + + if (!confirmed) { + return; + } + + var tagID = Zotero.Tags.getID(this.contextTag.name); + + if (tagID) { + await Zotero.Tags.removeFromLibrary(this.libraryID, tagID); + } + // If only a tag color setting, remove that + else { + await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false); + } + + this.setState({tags: await this.getTags()}); + } + + async toggleDisplayAllTags(newValue) { + newValue = typeof(newValue) === 'undefined' ? !this.displayAllTags : newValue; + Zotero.Prefs.set('tagSelector.displayAllTags', newValue); + this.displayAllTags = newValue; + this.setState({tags: await this.getTags()}); + } + + toggleShowAutomatic(newValue) { + newValue = typeof(newValue) === 'undefined' ? !this.showAutomatic : newValue; + Zotero.Prefs.set('tagSelector.showAutomatic', newValue); + this.setState({showAutomatic: newValue}); + } + + deselectAll() { + this.selectedTags = new Set(); + if('onSelection' in this.props && typeof(this.props.onSelection) === 'function') { + this.props.onSelection(this.selectedTags); + } + } + + get label() { + let count = this.selectedTags.size; + let mod = count === 1 ? 'singular' : count === 0 ? 'none' : 'plural'; + + return Zotero.getString('pane.tagSelector.numSelected.' + mod, [count]); + } + + get showAutomatic() { + return this.state.showAutomatic; + } + + static init(domEl, opts) { + var ref; + let elem = ( + + ref = c } {...opts} /> + + ); + ReactDOM.render(elem, domEl); + ref.domEl = domEl; + return ref; + } +} +})(); diff --git a/chrome/content/zotero/reactUI/tagSelector.xul b/chrome/content/zotero/containers/tagSelector.xul similarity index 66% rename from chrome/content/zotero/reactUI/tagSelector.xul rename to chrome/content/zotero/containers/tagSelector.xul index d1adca8208..5022281054 100644 --- a/chrome/content/zotero/reactUI/tagSelector.xul +++ b/chrome/content/zotero/containers/tagSelector.xul @@ -30,29 +30,29 @@ - - - + + + + document.getElementById('show-automatic').setAttribute('checked', ZoteroPane.tagSelector.showAutomatic); + document.getElementById('display-all-tags').setAttribute('checked', ZoteroPane.tagSelector.displayAllTags); + document.getElementById('num-selected').label = ZoteroPane.tagSelector.label"> + oncommand="ZoteroPane.tagSelector.deselectAll(); event.stopPropagation();"/> + oncommand="ZoteroPane.tagSelector.toggleShowAutomatic(); event.stopPropagation();"/> \ No newline at end of file diff --git a/chrome/content/zotero/reactUI/containers/tag-selector.jsx b/chrome/content/zotero/reactUI/containers/tag-selector.jsx deleted file mode 100644 index 4085cbac58..0000000000 --- a/chrome/content/zotero/reactUI/containers/tag-selector.jsx +++ /dev/null @@ -1,455 +0,0 @@ -/* global Zotero: false */ -'use strict'; - -(function() { - -const React = require('react'); -const ReactDom = require('react-dom'); -const TagSelector = require('react-ui/components/tag-selector.js'); -const noop = Promise.resolve(); -const defaults = { - tags: [], - searchString: '', - shouldFocus: false, - onSelection: noop, - viewOnly: false -}; -const { Cc, Ci } = require('chrome'); - -ZoteroPane.React.TagSelector = class TagSelectorContainer extends React.Component { - constructor(props) { - super(props); - this.opts = Object.assign({}, defaults, props); - this.selectedTags = new Set(); - this.updateScope = Promise.resolve; - this.filterToScope = true; - this.showAutomatic = true; - this._notifierID = Zotero.Notifier.registerObserver( - this, - ['collection-item', 'item', 'item-tag', 'tag', 'setting'], - 'tagSelector' - ); - this._scope = null; - this._allTags = null; - this.state = null; - this.update(); - } - - componentWillReceiveProps(nextProps) { - this.opts = Object.assign({}, this.opts, nextProps); - this.update(); - } - - async notify(event, type, ids, extraData) { - if (type === 'setting') { - if (ids.some(val => val.split('/')[1] == 'tagColors')) { - await this.refresh(true); - } - return; - } - - // Ignore anything other than deletes in duplicates view - if (this.collectionTreeRow.isDuplicates()) { - switch (event) { - case 'delete': - case 'trash': - break; - - default: - return; - } - } - - // Ignore item events other than 'trash' - if (type == 'item' && event != 'trash') { - return; - } - - - // If a selected tag no longer exists, deselect it - if (event == 'delete' || event == 'trash' || event == 'modify') { - for (let tag of this.selectedTags) { - if (tag == extraData[ids[0]].old.tag) { - this.selectedTags.delete(tag); - break; - } - } - await this.refresh(true); - } - - if (event == 'add') { - if (type == 'item-tag') { - let tagObjs = ids - // Get tag name and type - .map(x => extraData[x]) - // Ignore tag adds for items not in the current library, if there is one - .filter(x => { - if (!this._libraryID) { - return true; - } - return x.libraryID == this._libraryID; - }); - - if (tagObjs.length) { - this.insertSorted(tagObjs); - } - } - } - - // Otherwise, just update the tag selector - return this.updateScope(); - } - - async refresh(fetch) { - let t = new Date; - - if(this._allTags === null) { - fetch = true; - } - - if (fetch) { - Zotero.debug('Reloading tags selector'); - } else { - Zotero.debug('Refreshing tags selector'); - } - - // If new data, rebuild boxes - if (fetch) { - let tagColors = Zotero.Tags.getColors(this.libraryID); - this._allTags = await Zotero.Tags.getAll(this.libraryID, this.showAutomatic ? [0, 1] : [0]); - // .tap(() => Zotero.Promise.check(this.mode)); - - // Add colored tags that aren't already real tags - let regularTags = new Set(this._allTags.map(tag => tag.tag)); - let coloredTags = Array.from(tagColors.keys()); - - coloredTags.filter(ct => !regularTags.has(ct)).forEach(x => - this._allTags.push(Zotero.Tags.cleanData({ tag: x })) - ); - - // Sort by name - this._allTags.sort(function (a, b) { - return Zotero.getLocaleCollation().compareString(1, a.tag, b.tag); - }); - - } - - this.update(); - - Zotero.debug('Loaded tag selector in ' + (new Date - t) + ' ms'); - - // var event = new Event('refresh'); - // this.dispatchEvent(event); - } - - update() { - var tags; - let flatScopeTags = Array.isArray(this._scope) ? this._scope.map(tag => tag.tag) : []; - - if('libraryID' in this) { - var tagColors = Zotero.Tags.getColors(this.libraryID); - } - - if(this.filterToScope && Array.isArray(this._scope)) { - tags = this._allTags.filter(tag => - flatScopeTags.includes(tag.tag) || (tagColors && tagColors.has(tag.tag)) - ); - } else { - tags = this._allTags ? this._allTags.slice(0) : []; - } - - if(this.opts.searchString) { - tags = tags.filter(tag => !!tag.tag.match(new RegExp(this.opts.searchString, 'i'))); - } - - this.opts.tags = tags.map(t => { - let name = t.tag; - let selected = this.selectedTags.has(name); - let color = tagColors && tagColors.has(name) ? tagColors.get(name).color : ''; - let disabled = !flatScopeTags.includes(name); - return { name, selected, color, disabled }; - }); - this.state ? this.setState(this.opts) : this.state = Object.assign({}, this.opts); - } - - render() { - return {} : this.onTagSelectedHandler.bind(this) } - onTagContext={ this.onTagContextHandler.bind(this) } - onSearch={ this.onSearchHandler.bind(this) } - onSettings={ this.onTagSelectorViewSettingsHandler.bind(this) } - />; - } - - setMode(mode) { - this.setState({viewOnly: mode == 'view'}); - } - - focusTextbox() { - this.opts.shouldFocus = true; - this.update(); - } - - toggle() { - this._isCollapsed = !this._isCollapsed; - } - - unregister() { - ReactDom.unmountComponentAtNode(this.domEl); - if (this._notifierID) { - Zotero.Notifier.unregisterObserver(this._notifierID); - } - } - - uninit() { - this.selectedTags = new Set(); - this.opts.searchString = ''; - this.update(); - } - - onTagContextHandler(tag, ev) { - let tagContextMenu = document.getElementById('tag-menu'); - ev.preventDefault(); - tagContextMenu.openPopup(ev.target, 'end_before', 0, 0, true); - this.contextTag = tag; - } - - onTagSelectorViewSettingsHandler(ev) { - let settingsContextMenu = document.getElementById('tag-selector-view-settings-menu'); - ev.preventDefault(); - settingsContextMenu.openPopup(ev.target, 'end_before', 0, 0, true); - } - - onTagSelectedHandler(tag) { - if(this.selectedTags.has(tag)) { - this.selectedTags.delete(tag); - } else { - this.selectedTags.add(tag); - } - - if('onSelection' in this.opts && typeof(this.opts.onSelection) === 'function') { - this.opts.onSelection(this.selectedTags).then(this.refresh.bind(this)); - } - } - - onSearchHandler(searchString) { - this.opts.searchString = searchString; - this.update(); - } - - getTagSelection() { - return this.selectedTags; - } - - clearTagSelection() { - this.selectedTags = new Set(); - return this.selectedTags; - } - - async openColorPickerWindow() { - var io = { - libraryID: this.libraryID, - name: this.contextTag.name - }; - - var tagColors = Zotero.Tags.getColors(this.libraryID); - if (tagColors.size >= Zotero.Tags.MAX_COLORED_TAGS && !tagColors.has(io.name)) { - var ps = Cc['@mozilla.org/embedcomp/prompt-service;1'] - .getService(Ci.nsIPromptService); - ps.alert(null, '', Zotero.getString('pane.tagSelector.maxColoredTags', Zotero.Tags.MAX_COLORED_TAGS)); - return; - } - - io.tagColors = tagColors; - - window.openDialog( - 'chrome://zotero/content/tagColorChooser.xul', - 'zotero-tagSelector-colorChooser', - 'chrome,modal,centerscreen', io - ); - - // Dialog cancel - if (typeof io.color == 'undefined') { - return; - } - - await Zotero.Tags.setColor(this.libraryID, io.name, io.color, io.position); - - this.refresh(); - } - - async openRenamePrompt() { - var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1'] - .getService(Ci.nsIPromptService); - - var newName = { value: this.contextTag.name }; - var result = promptService.prompt(window, - Zotero.getString('pane.tagSelector.rename.title'), - Zotero.getString('pane.tagSelector.rename.message'), - newName, '', {}); - - if (!result || !newName.value || this.contextTag.name == newName.value) { - return; - } - - if (this.selectedTags.has(this.contextTag.name)) { - var wasSelected = true; - this.selectedTags.delete(this.contextTag.name); - } - - if (Zotero.Tags.getID(this.contextTag.name)) { - await Zotero.Tags.rename(this.libraryID, this.contextTag.name, newName.value); - } - // Colored tags don't need to exist, so in that case - // just rename the color setting - else { - let color = Zotero.Tags.getColor(this.libraryID, this.contextTag.name); - if (!color) { - throw new Error("Can't rename missing tag"); - } - await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false); - await Zotero.Tags.setColor(this.libraryID, newName, color); - } - - if(wasSelected) { - this.selectedTags.add(newName.value); - } - - this.updateScope(); - } - - async openDeletePrompt() { - var promptService = Cc['@mozilla.org/embedcomp/prompt-service;1'] - .getService(Ci.nsIPromptService); - - var confirmed = promptService.confirm(window, - Zotero.getString('pane.tagSelector.delete.title'), - Zotero.getString('pane.tagSelector.delete.message')); - - if (!confirmed) { - return; - } - - var tagID = Zotero.Tags.getID(this.contextTag.name); - - if (tagID) { - await Zotero.Tags.removeFromLibrary(this.libraryID, tagID); - } - // If only a tag color setting, remove that - else { - await Zotero.Tags.setColor(this.libraryID, this.contextTag.name, false); - } - - this.updateScope(); - } - - toggleFilterToScope(newValue) { - this.filterToScope = typeof(newValue) === 'undefined' ? !this.filterToScope : newValue; - this.refresh(); - } - - toggleShowAutomatic(newValue) { - this.showAutomatic = typeof(newValue) === 'undefined' ? !this.showAutomatic : newValue; - this.refresh(true); - } - - deselectAll() { - this.selectedTags = new Set(); - if('onSelection' in this.opts && typeof(this.opts.onSelection) === 'function') { - this.opts.onSelection(this.selectedTags).then(this.refresh.bind(this)); - } - } - - set scope(newScope) { - try { - this._scope = Array.from(newScope); - } catch(e) { - this._scope = null; - } - this.refresh(); - } - - get label() { - let count = this.selectedTags.size; - let mod = count === 1 ? 'singular' : count === 0 ? 'none' : 'plural'; - - return Zotero.getString('pane.tagSelector.numSelected.' + mod, [count]); - } - - onResize() { - const COLLECTIONS_HEIGHT = 32; // minimum height of the collections pane and toolbar - - //Zotero.debug('Updating tag selector size'); - var zoteroPane = document.getElementById('zotero-pane-stack'); - var splitter = document.getElementById('zotero-tags-splitter'); - var tagSelector = document.getElementById('zotero-tag-selector'); - - // Nothing should be bigger than appcontent's height - var max = document.getElementById('appcontent').boxObject.height - - splitter.boxObject.height; - - // Shrink tag selector to appcontent's height - var maxTS = max - COLLECTIONS_HEIGHT; - var tsHeight = parseInt(tagSelector.getAttribute("height")); - if (tsHeight > maxTS) { - //Zotero.debug("Limiting tag selector height to appcontent"); - tagSelector.setAttribute('height', maxTS); - } - tagSelector.style.height = tsHeight + 'px'; - - var height = tagSelector.getBoundingClientRect().height; - - /*Zotero.debug("tagSelector.boxObject.height: " + tagSelector.boxObject.height); - Zotero.debug("tagSelector.getAttribute('height'): " + tagSelector.getAttribute('height')); - Zotero.debug("zoteroPane.boxObject.height: " + zoteroPane.boxObject.height); - Zotero.debug("zoteroPane.getAttribute('height'): " + zoteroPane.getAttribute('height'));*/ - - - // Don't let the Z-pane jump back down to its previous height - // (if shrinking or hiding the tag selector let it clear the min-height) - if (zoteroPane.getAttribute('height') < zoteroPane.boxObject.height) { - //Zotero.debug("Setting Zotero pane height attribute to " + zoteroPane.boxObject.height); - zoteroPane.setAttribute('height', zoteroPane.boxObject.height); - } - - if (tagSelector.getAttribute('collapsed') == 'true') { - // 32px is the default Z pane min-height in overlay.css - height = 32; - } - else { - // tS.boxObject.height doesn't exist at startup, so get from attribute - if (!height) { - height = parseInt(tagSelector.getAttribute('height')); - } - // 121px seems to be enough room for the toolbar and collections - // tree at minimum height - height = height + COLLECTIONS_HEIGHT; - } - - //Zotero.debug('Setting Zotero pane minheight to ' + height); - zoteroPane.setAttribute('minheight', height); - - if (this.isShowing() && !this.isFullScreen()) { - zoteroPane.setAttribute('savedHeight', zoteroPane.boxObject.height); - } - - // Fix bug whereby resizing the Z pane downward after resizing - // the tag selector up and then down sometimes caused the Z pane to - // stay at a fixed size and get pushed below the bottom - tagSelector.height++; - tagSelector.height--; - } - - static init(domEl, opts) { - var ref; - console.log(domEl.style); - ReactDom.render( ref = c } {...opts} />, domEl); - ref.domEl = domEl; - new MutationObserver(ref.onResize).observe(domEl, {attributes: true, attributeFilter: ['height']}); - return ref; - } -} -})(); diff --git a/chrome/content/zotero/standalone/standalone.js b/chrome/content/zotero/standalone/standalone.js index 1b34a3acd6..d81330de88 100644 --- a/chrome/content/zotero/standalone/standalone.js +++ b/chrome/content/zotero/standalone/standalone.js @@ -47,7 +47,7 @@ const ZoteroStandalone = new function() { } return Zotero.initializationPromise; }) - .then(function () { + .then(async function () { if (Zotero.Prefs.get('devtools.errorconsole.enabled', true)) { document.getElementById('menu_errorConsole').hidden = false; } @@ -60,6 +60,7 @@ const ZoteroStandalone = new function() { ZoteroStandalone.DebugOutput.init(); Zotero.hideZoteroPaneOverlays(); + await ZoteroPane.Containers.init(); ZoteroPane.init(); ZoteroPane.makeVisible(); diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js index 0503ae68ab..5b2dd654d5 100644 --- a/chrome/content/zotero/xpcom/itemTreeView.js +++ b/chrome/content/zotero/xpcom/itemTreeView.js @@ -1850,7 +1850,7 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i // Clear the quick search and tag selection and try again (once) if (!noRecurse && this.window.ZoteroPane) { let cleared1 = yield this.window.ZoteroPane.clearQuicksearch(); - let cleared2 = this.window.ZoteroPane.clearTagSelection(); + let cleared2 = this.window.ZoteroPane.tagSelector.clearTagSelection(); if (cleared1 || cleared2) { return this.selectItem(id, expand, true); } diff --git a/chrome/content/zotero/xpcom/notifier.js b/chrome/content/zotero/xpcom/notifier.js index 11a64f547e..6ffda23b33 100644 --- a/chrome/content/zotero/xpcom/notifier.js +++ b/chrome/content/zotero/xpcom/notifier.js @@ -73,7 +73,6 @@ Zotero.Notifier = new function(){ if (priority) { msg += " with priority " + priority; } - Zotero.debug(msg); _observers[hash] = { ref: ref, types: types, diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js index 0ba43b1d3f..0805bdbf58 100644 --- a/chrome/content/zotero/xpcom/utilities.js +++ b/chrome/content/zotero/xpcom/utilities.js @@ -144,6 +144,25 @@ var CSL_TYPE_MAPPINGS = { * @class Functions for text manipulation and other miscellaneous purposes */ Zotero.Utilities = { + /** + * Returns a function which will execute `fn` with provided arguments after `delay` milliseconds and not more + * than once, if called multiple times. See + * http://stackoverflow.com/questions/24004791/can-someone-explain-the-debounce-function-in-javascript + * @param fn {Function} function to debounce + * @param delay {Integer} number of miliseconds to delay the function execution + * @returns {Function} + */ + debounce: function(fn, delay=500) { + var timer = null; + return function () { + let args = arguments; + clearTimeout(timer); + timer = setTimeout(function () { + fn.apply(this, args); + }.bind(this), delay); + }; + }, + /** * Fixes author name capitalization. * Currently for all uppercase names only diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js index e5c1726ef6..f66afa2c46 100644 --- a/chrome/content/zotero/zoteroPane.js +++ b/chrome/content/zotero/zoteroPane.js @@ -143,6 +143,8 @@ var ZoteroPane = new function() Zotero.hiDPI = window.devicePixelRatio > 1; Zotero.hiDPISuffix = Zotero.hiDPI ? "@2x" : ""; + ZoteroPane_Local.Containers.loadPane(); + ZoteroPane_Local.setItemsPaneMessage(Zotero.getString('pane.items.loading')); // Add a default progress window @@ -233,7 +235,6 @@ var ZoteroPane = new function() } catch (e) {} } - ZoteroPane.React.init(); if (Zotero.openPane) { Zotero.openPane = false; @@ -345,7 +346,7 @@ var ZoteroPane = new function() this.serializePersist(); } - ZoteroPane_Local.React.destroy(); + ZoteroPane_Local.Containers.destroy(); if(this.collectionsView) this.collectionsView.unregister(); if(this.itemsView) this.itemsView.unregister(); @@ -388,7 +389,6 @@ var ZoteroPane = new function() this.unserializePersist(); this.updateLayout(); this.updateToolbarPosition(); - this.updateTagSelectorSize(); // restore saved row selection (for tab switching) // TODO: Remove now that no tab mode? @@ -1093,7 +1093,6 @@ var ZoteroPane = new function() var showing = tagSelector.getAttribute('collapsed') == 'true'; tagSelector.setAttribute('collapsed', !showing); - this.updateTagSelectorSize(); // If showing, set scope to items in current view // and focus filter textbox @@ -1107,12 +1106,6 @@ var ZoteroPane = new function() } }); - - this.updateTagSelectorSize = function () { - - } - - /* * Sets the tag filter on the items view */ @@ -1139,7 +1132,6 @@ var ZoteroPane = new function() */ this.setTagScope = async function () { var collectionTreeRow = self.getCollectionTreeRow(); - var tagSelector = document.getElementById('zotero-tag-selector'); if (self.tagSelectorShown()) { Zotero.debug('Updating tag selector with current tags'); if (collectionTreeRow.editable) { @@ -1148,10 +1140,11 @@ var ZoteroPane = new function() else { ZoteroPane_Local.tagSelector.setMode('view'); } - ZoteroPane_Local.tagSelector.collectionTreeRow = collectionTreeRow; - ZoteroPane_Local.tagSelector.updateScope = self.setTagScope; - ZoteroPane_Local.tagSelector.libraryID = collectionTreeRow.ref.libraryID; - ZoteroPane_Local.tagSelector.scope = await collectionTreeRow.getChildTags(); + ZoteroPane_Local.tagSelector.onItemViewChanged({ + collectionTreeRow, + libraryID: collectionTreeRow.ref.libraryID, + tagsInScope: await collectionTreeRow.getChildTags() + }); } }; @@ -1219,17 +1212,7 @@ var ZoteroPane = new function() ZoteroPane_Local.displayErrorMessage(); }; this.itemsView.onRefresh.addListener(() => this.setTagScope()); - if (this.tagSelectorShown()) { - let tagSelector = document.getElementById('zotero-tag-selector') - let handler = function () { - tagSelector.removeEventListener('refresh', handler); - Zotero.uiIsReady(); - }; - tagSelector.addEventListener('refresh', handler); - } - else { - this.itemsView.onLoad.addListener(() => Zotero.uiIsReady()); - } + this.itemsView.onLoad.addListener(() => Zotero.uiIsReady()); // If item data not yet loaded for library, load it now. // Other data types are loaded at startup @@ -4835,8 +4818,10 @@ var ZoteroPane = new function() var itemsToolbar = document.getElementById("zotero-items-toolbar"); var itemPane = document.getElementById("zotero-item-pane"); var itemToolbar = document.getElementById("zotero-item-toolbar"); + var tagSelector = document.getElementById("zotero-tag-selector"); collectionsToolbar.style.width = collectionsPane.boxObject.width + 'px'; + tagSelector.style.maxWidth = collectionsPane.boxObject.width + 'px'; if (stackedLayout || itemPane.collapsed) { // The itemsToolbar and itemToolbar share the same space, and it seems best to use some flex attribute from right (because there might be other icons appearing or vanishing). diff --git a/chrome/content/zotero/zoteroPane.xul b/chrome/content/zotero/zoteroPane.xul index 6ba72fabb1..590fa490d2 100644 --- a/chrome/content/zotero/zoteroPane.xul +++ b/chrome/content/zotero/zoteroPane.xul @@ -27,7 +27,7 @@ - + %globalDTD; @@ -316,7 +316,9 @@ TODO: deal with this some other way? --> - + + + =0.10.0" @@ -3047,9 +2995,9 @@ "dev": true }, "js-base64": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.5.tgz", - "integrity": "sha512-aUnNwqMOXw3yvErjMPSQu6qIIzUmT1e5KcU1OZxRDU1g/am6mzBvcrmLAYwzmB59BHPrh5/tKaiF4OPhqRWESQ==", + "version": "2.4.9", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.9.tgz", + "integrity": "sha512-xcinL3AuDJk7VSzsHgb9DvvIXayBbadtMZ4HFPx8rUszbW1MuNMlwYVC4zzCZ6e1sqZpnNS5ZFYOhXqA39T7LQ==", "dev": true }, "js-tokens": { @@ -3061,8 +3009,7 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true + "dev": true }, "jsesc": { "version": "1.3.0", @@ -3076,6 +3023,12 @@ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", "dev": true }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, "json-stable-stringify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz", @@ -3124,12 +3077,6 @@ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, "jspath": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/jspath/-/jspath-0.4.0.tgz", @@ -3137,23 +3084,15 @@ "dev": true }, "jsprim": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", - "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", "dev": true, "requires": { "assert-plus": "1.0.0", - "extsprintf": "1.0.2", + "extsprintf": "1.3.0", "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } + "verror": "1.10.0" } }, "jsx-ast-utils": { @@ -3231,9 +3170,9 @@ } }, "lodash": { - "version": "4.17.4", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", + "version": "4.17.11", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", + "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, "lodash._baseassign": { @@ -3359,13 +3298,13 @@ } }, "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.4.tgz", + "integrity": "sha512-EPstzZ23znHUVLKj+lcXO1KvZkrlw+ZirdwvOmnAnA/1PB4ggyXJ77LRkCqkff+ShQ+cqoxCxLQOh4cKITO5iA==", "dev": true, "requires": { "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "yallist": "^3.0.2" } }, "map-obj": { @@ -3374,6 +3313,12 @@ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", "dev": true }, + "math-random": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.1.tgz", + "integrity": "sha1-izqsWIuKZuSXXjzepn97sylgH6w=", + "dev": true + }, "md5.js": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", @@ -3454,18 +3399,18 @@ } }, "mime-db": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", - "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", "dev": true }, "mime-types": { - "version": "2.1.15", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", - "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "version": "2.1.21", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", + "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", "dev": true, "requires": { - "mime-db": "~1.27.0" + "mime-db": "~1.37.0" } }, "minimalistic-assert": { @@ -3524,6 +3469,15 @@ "supports-color": "3.1.2" }, "dependencies": { + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, "glob": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", @@ -3594,8 +3548,7 @@ "version": "2.10.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "dev": true, - "optional": true + "dev": true }, "nise": { "version": "1.3.2", @@ -3614,15 +3567,16 @@ "version": "1.7.1", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.1.tgz", "integrity": "sha512-j8XsFGCLw79vWXkZtMSmmLaOk9z5SQ9bV/tkbZVCqvgwzrjAGq66igobLofHtF63NvMTp2WjytpsNTGKa+XRIQ==", + "dev": true, "requires": { "encoding": "^0.1.11", "is-stream": "^1.0.1" } }, "node-gyp": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.7.0.tgz", - "integrity": "sha512-qDQE/Ft9xXP6zphwx4sD0t+VhwV7yFaloMpfbL2QnnDZcyaiakWlLdtFGGQfTAwpFHdpbRhRxVhIHN1OKAjgbg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", "dev": true, "requires": { "fstream": "^1.0.0", @@ -3632,17 +3586,25 @@ "nopt": "2 || 3", "npmlog": "0 || 1 || 2 || 3 || 4", "osenv": "0", - "request": ">=2.9.0 <2.82.0", + "request": "^2.87.0", "rimraf": "2", "semver": "~5.3.0", "tar": "^2.0.0", "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + } } }, "node-sass": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.9.0.tgz", - "integrity": "sha512-QFHfrZl6lqRU3csypwviz2XLgGNOoWQbo2GOvtsfQqOfL4cy1BtWnhx/XUeAO9LT3ahBzSRXcEO6DdvAH9DzSg==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.11.0.tgz", + "integrity": "sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==", "dev": true, "requires": { "async-foreach": "^0.1.3", @@ -3658,78 +3620,12 @@ "meow": "^3.7.0", "mkdirp": "^0.5.1", "nan": "^2.10.0", - "node-gyp": "^3.3.1", + "node-gyp": "^3.8.0", "npmlog": "^4.0.0", - "request": "~2.79.0", + "request": "^2.88.0", "sass-graph": "^2.2.4", "stdout-stream": "^1.4.0", "true-case-path": "^1.0.2" - }, - "dependencies": { - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", - "dev": true - }, - "har-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", - "dev": true, - "requires": { - "chalk": "^1.1.1", - "commander": "^2.9.0", - "is-my-json-valid": "^2.12.4", - "pinkie-promise": "^2.0.0" - } - }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "dev": true - }, - "qs": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", - "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", - "dev": true - }, - "request": { - "version": "2.79.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", - "dev": true, - "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.11.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", - "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~2.0.6", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "qs": "~6.3.0", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "~0.4.1", - "uuid": "^3.0.0" - } - }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", - "dev": true - } } }, "nopt": { @@ -3763,9 +3659,9 @@ } }, "npmlog": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", - "integrity": "sha512-ocolIkZYZt8UveuiDS0yAkkIjid1o7lPG8cYm05yNYzBn8ykQtaiPMEGp8fY9tKdDgm8okpdKzkvu1y9hUYugA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", "dev": true, "requires": { "are-we-there-yet": "~1.1.2", @@ -3781,9 +3677,9 @@ "dev": true }, "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, "object-assign": { @@ -3844,9 +3740,9 @@ "dev": true }, "osenv": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", - "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", "dev": true, "requires": { "os-homedir": "^1.0.0", @@ -3983,9 +3879,9 @@ } }, "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, "pify": { @@ -4037,6 +3933,7 @@ "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, "requires": { "asap": "~2.0.3" } @@ -4056,6 +3953,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", + "dev": true + }, "public-encrypt": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.0.tgz", @@ -4076,9 +3979,9 @@ "dev": true }, "qs": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", - "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, "querystring": { @@ -4093,47 +3996,6 @@ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", "dev": true }, - "randomatic": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", - "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, "randombytes": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.5.tgz", @@ -4154,26 +4016,37 @@ } }, "react": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz", - "integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=", + "version": "16.6.3", + "resolved": "https://registry.npmjs.org/react/-/react-16.6.3.tgz", + "integrity": "sha512-zCvmH2vbEolgKxtqXL2wmGCUxUyNheYn/C+PD1YAjfxHC54+MhdruyhO7QieQrYsYeTxrn93PM2y0jRH1zEExw==", "requires": { - "create-react-class": "^15.6.0", - "fbjs": "^0.8.9", "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.11.2" } }, "react-dom": { - "version": "15.6.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz", - "integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=", + "version": "16.6.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.6.3.tgz", + "integrity": "sha512-8ugJWRCWLGXy+7PmNh8WJz3g1TaTUt1XyoIcFN+x0Zbkoz+KKdUyx1AQLYJdbFXjuF41Nmjn5+j//rxvhFjgSQ==", "requires": { - "fbjs": "^0.8.9", "loose-envify": "^1.1.0", - "object-assign": "^4.1.0", - "prop-types": "^15.5.10" + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.11.2" + } + }, + "react-intl": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-intl/-/react-intl-2.7.2.tgz", + "integrity": "sha512-3dcNGLqEw2FKkX+1L2WYLgjP0MVJkvWuVd1uLcnwifIQe8JQvnd9Bss4hb4Gvg/YhBIRcs4LM6C2bAgyklucjw==", + "requires": { + "hoist-non-react-statics": "^2.5.5", + "intl-format-cache": "^2.0.5", + "intl-messageformat": "^2.1.0", + "intl-relativeformat": "^2.1.0", + "invariant": "^2.1.1" } }, "read-only-stream": { @@ -4287,33 +4160,39 @@ } }, "request": { - "version": "2.81.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", - "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "dev": true, "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~4.2.1", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "performance-now": "^0.2.0", - "qs": "~6.4.0", - "safe-buffer": "^5.0.1", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", - "uuid": "^3.0.0" + "uuid": "^3.3.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + } } }, "require-directory": { @@ -4338,9 +4217,9 @@ } }, "rimraf": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", - "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", "dev": true, "requires": { "glob": "^7.0.5" @@ -4362,6 +4241,12 @@ "integrity": "sha512-aSLEDudu6OoRr/2rU609gRmnYboRLxgDG1z9o2Q0os7236FwvcqIOO8r8U5JUEwivZOhDaKlFO4SbPTJYyBEyQ==", "dev": true }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "samsam": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", @@ -4380,6 +4265,15 @@ "yargs": "^7.0.0" } }, + "scheduler": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.11.2.tgz", + "integrity": "sha512-+WCP3s3wOaW4S7C1tl3TEXp4l9lJn0ZK8G3W3WKRWmw77Z2cIFUW2MiNTMHn5sCjxN+t7N43HAOOgMjyAg5hlg==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, "scss-tokenizer": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/scss-tokenizer/-/scss-tokenizer-0.2.3.tgz", @@ -4402,9 +4296,9 @@ } }, "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", + "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==", "dev": true }, "set-blocking": { @@ -4422,7 +4316,8 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true }, "sha.js": { "version": "2.4.9", @@ -4506,15 +4401,6 @@ "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", "dev": true }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -4531,9 +4417,9 @@ } }, "spdx-correct": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", - "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.2.tgz", + "integrity": "sha512-q9hedtzyXHr5S0A1vEPoK/7l8NpfkFYTq6iCY+Pno2ZbdZR6WexZFtqeVGkGxW3TEJMN914Z55EnAGMmenlIQQ==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -4541,9 +4427,9 @@ } }, "spdx-exceptions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", - "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", + "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==", "dev": true }, "spdx-expression-parse": { @@ -4557,15 +4443,15 @@ } }, "spdx-license-ids": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", - "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.2.tgz", + "integrity": "sha512-qky9CVt0lVIECkEsYbNILVnPvycuEBkXoMFLRWsREkomQLevYhtRKC+R91a5TOAQ3bCMjikRwhyaRqj1VYatYg==", "dev": true }, "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", + "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", "dev": true, "requires": { "asn1": "~0.2.3", @@ -4575,21 +4461,14 @@ "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } } }, "stdout-stream": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz", - "integrity": "sha1-osfIWH5U2UJ+qe2zrD8s1SLfN4s=", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", + "integrity": "sha512-j4emi03KXqJWcIeF8eIXkjMFN1Cmb8gUlDYGeBALLPo5qdyTfA9bOtl8m33lRoC+vFMkP3gl0WsDr6+gzxbbTA==", "dev": true, "requires": { "readable-stream": "^2.0.1" @@ -4666,12 +4545,6 @@ } } }, - "stringstream": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", - "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", - "dev": true - }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -4786,11 +4659,12 @@ "dev": true }, "tough-cookie": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", - "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "dev": true, "requires": { + "psl": "^1.1.24", "punycode": "^1.4.1" } }, @@ -4807,27 +4681,12 @@ "dev": true }, "true-case-path": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.2.tgz", - "integrity": "sha1-fskRMJJHZsf1c74wIMNPj9/QDWI=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", "dev": true, "requires": { - "glob": "^6.0.4" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } + "glob": "^7.1.2" } }, "tty-browserify": { @@ -4849,8 +4708,7 @@ "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true + "dev": true }, "type-detect": { "version": "4.0.3", @@ -4882,6 +4740,23 @@ "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", "dev": true }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + } + } + }, "url": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", @@ -4924,15 +4799,15 @@ "dev": true }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "dev": true }, "validate-npm-package-license": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz", - "integrity": "sha512-63ZOUnL4SIXj4L0NixR3L1lcjO38crAbgrTpl28t8jjrfuiOBL5Iygm+60qPs/KsZGzPNg6Smnc/oY16QTjF0g==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, "requires": { "spdx-correct": "^3.0.0", @@ -4940,12 +4815,14 @@ } }, "verror": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", - "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "dev": true, "requires": { - "extsprintf": "1.0.2" + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" } }, "vm-browserify": { @@ -4960,7 +4837,8 @@ "whatwg-fetch": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz", - "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=" + "integrity": "sha1-nITsLc9oGH/wC8ZOEnS0QhduHIQ=", + "dev": true }, "which": { "version": "1.3.1", @@ -4978,12 +4856,12 @@ "dev": true }, "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", "dev": true, "requires": { - "string-width": "^1.0.2" + "string-width": "^1.0.2 || 2" } }, "wrap-ansi": { @@ -5015,9 +4893,9 @@ "dev": true }, "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", "dev": true }, "yargs": { diff --git a/package.json b/package.json index 8fe98fbcfe..0163ffdd7a 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,12 @@ "bluebird": "^3.5.1", "classnames": "^2.2.6", "prop-types": "^15.6.2", - "react": "^15.6.2", - "react-dom": "^15.6.2" + "react": "^16.6.3", + "react-dom": "^16.6.3", + "react-intl": "^2.7.2" }, "devDependencies": { - "babel-core": "^6.26.0", + "babel-core": "^6.26.3", "babel-plugin-syntax-async-generators": "^6.13.0", "babel-plugin-syntax-decorators": "^6.13.0", "babel-plugin-syntax-do-expressions": "^6.13.0", @@ -31,7 +32,7 @@ "babel-plugin-syntax-jsx": "^6.13.0", "babel-plugin-syntax-object-rest-spread": "^6.13.0", "babel-plugin-transform-class-properties": "^6.24.1", - "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", + "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", "babel-preset-react": "^6.16.0", "browserify": "^14.5.0", "chai": "^4.1.2", @@ -45,7 +46,7 @@ "jspath": "^0.4.0", "mocha": "^3.5.3", "multimatch": "^2.1.0", - "node-sass": "^4.9.0", + "node-sass": "^4.11.0", "sinon": "^4.5.0", "universalify": "^0.1.1" } diff --git a/resource/react-dom.js b/resource/react-dom.js index 7b95c1e019..dcca22f2ff 120000 --- a/resource/react-dom.js +++ b/resource/react-dom.js @@ -1 +1 @@ -../node_modules/react-dom/dist/react-dom.js \ No newline at end of file +../node_modules/react-dom/umd/react-dom.development.js \ No newline at end of file diff --git a/resource/react-intl.js b/resource/react-intl.js new file mode 120000 index 0000000000..ef64c43adc --- /dev/null +++ b/resource/react-intl.js @@ -0,0 +1 @@ +../node_modules/react-intl/dist/react-intl.js \ No newline at end of file diff --git a/resource/react-ui/components/tag-selector/tag-list.jsx b/resource/react-ui/components/tag-selector/tag-list.jsx deleted file mode 100644 index 9c14b078dd..0000000000 --- a/resource/react-ui/components/tag-selector/tag-list.jsx +++ /dev/null @@ -1,55 +0,0 @@ -const React = require('react'); -const PropTypes = require('prop-types'); -const cx = require('classnames'); - -class TagList extends React.PureComponent { - renderTag(index) { - const { tags } = this.props; - const tag = index < tags.length ? - tags[index] : { - tag: "", - }; - - const className = cx('tag-selector-item', { - selected: tag.selected, - colored: tag.color, - }); - - let props = { - className, - onClick: ev => this.props.onSelect(tag.name, ev), - onContextMenu: ev => this.props.onTagContext(tag, ev), - }; - - if(tag.color) { - props['style'] = { - color: tag.color, - }; - } - - - return ( -
  • - { tag.name } -
  • - ); - } - - render() { - const totalTagCount = this.props.tags.length; - return ( -
    { this.container = ref } }> -
      - { - [...Array(totalTagCount).keys()].map(index => this.renderTag(index)) - } -
    -
    - ) - - } -} - -module.exports = TagList; diff --git a/resource/react.js b/resource/react.js index 3382a7499b..1af854d8eb 120000 --- a/resource/react.js +++ b/resource/react.js @@ -1 +1 @@ -../node_modules/react/dist/react.js \ No newline at end of file +../node_modules/react/umd/react.development.js \ No newline at end of file diff --git a/resource/require.js b/resource/require.js index ed8dc4603e..d0c51069f4 100644 --- a/resource/require.js +++ b/resource/require.js @@ -8,9 +8,9 @@ var require = (function() { var { Loader, Require, Module } = Components.utils.import('resource://gre/modules/commonjs/toolkit/loader.js'); var requirer = Module('/', '/'); var _runningTimers = {}; - var window = {}; + var win = {}; - window.setTimeout = function (func, ms) { + win.setTimeout = function (func, ms) { var id = Math.floor(Math.random() * (1000000000000 - 1)) + 1 var useMethodjit = Components.utils.methodjit; var timer = Components.classes["@mozilla.org/timer;1"] @@ -47,7 +47,7 @@ var require = (function() { return id; }; - window.clearTimeout = function (id) { + win.clearTimeout = function (id) { var timer = _runningTimers[id]; if (timer) { timer.cancel(); @@ -55,7 +55,7 @@ var require = (function() { delete _runningTimers[id]; }; - window.debug = function (msg) { + win.debug = function (msg) { dump(msg + "\n\n"); }; @@ -83,15 +83,18 @@ var require = (function() { document: typeof document !== 'undefined' && document || {}, console: cons, navigator: typeof navigator !== 'undefined' && navigator || {}, - window, - setTimeout: window.setTimeout, - clearTimeout: window.clearTimeout, + setTimeout: win.setTimeout, + clearTimeout: win.clearTimeout, }; Object.defineProperty(globals, 'Zotero', { get: getZotero }); + Object.defineProperty(globals, 'window', { get: function() { + return typeof window != 'undefined' ? window : win; + } }); var loader = Loader({ id: 'zotero/require', paths: { '': 'resource://zotero/', + 'components/': 'chrome://zotero/content/components/' }, globals }); diff --git a/scripts/babel-worker.js b/scripts/babel-worker.js index 66e70515e7..219776f5d1 100644 --- a/scripts/babel-worker.js +++ b/scripts/babel-worker.js @@ -26,9 +26,14 @@ async function babelWorker(ev) { try { let contents = await fs.readFile(sourcefile, 'utf8'); - if (sourcefile === 'resource/react-dom.js') { + if (sourcefile === 'resource/react.js') { // patch react - transformed = contents.replace(/ownerDocument\.createElement\((.*?)\)/gi, 'ownerDocument.createElementNS(DOMNamespaces.html, $1)') + transformed = contents.replace('instanceof Error', '.constructor.name == "Error"') + } else if (sourcefile === 'resource/react-dom.js') { + // and react-dom + transformed = contents.replace(/ ownerDocument\.createElement\((.*?)\)/gi, 'ownerDocument.createElementNS(HTML_NAMESPACE, $1)') + .replace('element instanceof win.HTMLIFrameElement', + 'typeof element != "undefined" && element.tagName.toLowerCase() == "iframe"') .replace("isInputEventSupported = false", 'isInputEventSupported = true'); } else if ('ignore' in options && options.ignore.some(ignoreGlob => multimatch(sourcefile, ignoreGlob).length)) { transformed = contents; diff --git a/scripts/build.js b/scripts/build.js index 871ab56374..07fe74dbf7 100644 --- a/scripts/build.js +++ b/scripts/build.js @@ -16,6 +16,7 @@ if (require.main === module) { const symlinks = symlinkFiles .concat(dirs.map(d => `${d}/**`)) .concat([`!${formatDirsForMatcher(dirs)}/**/*.js`]) + .concat([`!${formatDirsForMatcher(dirs)}/**/*.jsx`]) .concat([`!${formatDirsForMatcher(copyDirs)}/**`]) const signatures = await getSignatures(); diff --git a/scripts/config.js b/scripts/config.js index 14adc04fbc..3687bbc8fa 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -4,7 +4,6 @@ const dirs = [ 'components', 'defaults', 'resource', - 'resource/web-library', 'test', 'test/resource/chai', 'test/resource/chai-as-promised', diff --git a/scripts/js.js b/scripts/js.js index 3fc752efa4..d3f4b35725 100644 --- a/scripts/js.js +++ b/scripts/js.js @@ -27,7 +27,7 @@ async function getJS(source, options, signatures) { var f; while ((f = matchingJSFiles.pop()) != null) { const newFileSignature = await getFileSignature(f); - const dest = path.join('build', f); + const dest = path.join('build', f.replace('.jsx', '.js')); f = path.normalize(f); if (f in signatures) { if (compareSignatures(newFileSignature, signatures[f])) { diff --git a/scripts/sass.js b/scripts/sass.js index 25c28dd176..fe94bcfe4d 100644 --- a/scripts/sass.js +++ b/scripts/sass.js @@ -28,7 +28,8 @@ async function getSass(source, options, signatures={}) { if (compareSignatures(newFileSignature, signatures[f])) { try { await fs.access(dest, fs.constants.F_OK); - continue; + // TODO: Doesn't recompile on partial scss file changes, so temporarily disabled + // continue; } catch (_) { // file does not exists in build, fallback to browserifing } diff --git a/scripts/watch.js b/scripts/watch.js index 41ab2e9b3c..b9e215c682 100644 --- a/scripts/watch.js +++ b/scripts/watch.js @@ -34,6 +34,7 @@ const source = [ const symlinks = symlinkFiles .concat(dirs.map(d => `${d}/**`)) .concat([`!${formatDirsForMatcher(dirs)}/**/*.js`]) + .concat([`!${formatDirsForMatcher(dirs)}/**/*.jsx`]) .concat([`!${formatDirsForMatcher(copyDirs)}/**`]); var signatures; diff --git a/scss/abstracts/_variables.scss b/scss/abstracts/_variables.scss index b979d91409..5b874d3fb8 100644 --- a/scss/abstracts/_variables.scss +++ b/scss/abstracts/_variables.scss @@ -46,23 +46,27 @@ $headings-line-height: 1.2; // Components // -------------------------------------------------- -$padding-x-sm: $space-sm; -$padding-x-md: $space-md; +$padding-base-vertical: 2px; -$default-padding-x: $space-sm; -$default-padding-x-touch: $space-md; +$padding-base-horizontal: $space-xs; +$padding-large-horizontal: $space-sm; -$border-radius: 4px; -$border-radius-sm: 3px; -$border-radius-lg: 6px; +$border-radius-small: 3px; +$border-radius-base: 4px; +$border-radius-large: 6px; $border-width: 1px; $separator-width: 1px; -// Z-index master list +// Buttons // -------------------------------------------------- +$btn-icon-padding: $space-min + 1px; +$btn-disabled-opacity: 0.5; + +// Z-index master list +// -------------------------------------------------- $z-index-mobile-nav: 0; $z-index-navbar-bg: 10; diff --git a/scss/components/_button.scss b/scss/components/_button.scss new file mode 100644 index 0000000000..d12a11d7a4 --- /dev/null +++ b/scss/components/_button.scss @@ -0,0 +1,42 @@ +// +// Button +// -------------------------------------------------- + +.btn { + font: { + family: inherit; + size: inherit; + } + line-height: inherit; + color: inherit; + text-align: center; + -moz-appearance: toolbarbutton; + + &[disabled], + &.disabled { + opacity: $btn-disabled-opacity; + } +} + +.btn-icon { + padding: $btn-icon-padding; + + .icon { + &:first-child { + margin-left: -5px; + } + &:last-child { + margin-right: -5px; + } + } + svg, img { + vertical-align: middle; + } + span.menu-marker { + -moz-appearance: toolbarbutton-dropdown; + display: inline-block; + vertical-align: middle; + margin-right: -5px; + } +} + diff --git a/scss/components/_tag-selector.scss b/scss/components/_tag-selector.scss index b02438491f..96a86825fd 100644 --- a/scss/components/_tag-selector.scss +++ b/scss/components/_tag-selector.scss @@ -2,23 +2,26 @@ // Tag selector // -------------------------------------------------- -.library .tag-selector { - height: 160px; -} - .tag-selector { display: flex; - flex: 1 0 100%; + flex: 1 0; flex-direction: column; overflow: hidden; - background-color: $tag-selector-bg; } .tag-selector-container { flex: 1 1 auto; justify-content: space-between; overflow: auto; - height: auto; + height: 100px; + background-color: $tag-selector-bg; +} + +.tag-selector-message { + display: flex; + align-items: center; + justify-content: center; + height: 100%; } .tag-selector-filter-container { @@ -26,7 +29,7 @@ flex: 0 0 1em; display: flex; flex-direction: row; - padding: 0.25em; + padding: 0.125em 0 0.125em 0.5em; } .tag-selector-list { @@ -42,21 +45,21 @@ } .tag-selector-actions { - flex: 0 0 32px; + flex: 0 0 42px; + display: block; + white-space: nowrap; + background-color: inherit; } .tag-selector-item { - border-radius: 1em; - border: 1px solid transparent; cursor: pointer; display: inline-block; - padding: .05em .5em; - margin: 0; - - &.selected { - background-color: $secondary; - border: 1px solid $secondary; - } + margin: .15em .05em .15em .3em; + padding: 0 .25em 0 .25em; + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; &.colored { font-weight: bold; @@ -64,14 +67,17 @@ &.disabled { opacity: .6; - cursor: auto; - pointer-events: none; + cursor: default; } + + &.dragged-over { + color: $shade-0; + background: $shade-6; + } +} - &:not(.disabled):hover { - background-color: lighten($secondary, 15%); - border: 1px solid $secondary; - } +#zotero-tag-selector-container { + display: flex; } #zotero-tag-selector { diff --git a/scss/mac/_tag-selector.scss b/scss/mac/_tag-selector.scss new file mode 100644 index 0000000000..d8a33b0d96 --- /dev/null +++ b/scss/mac/_tag-selector.scss @@ -0,0 +1,20 @@ +// +// Tag selector +// -------------------------------------------------- + +.tag-selector-filter-container { + padding: 0.25em 0 0.25em 0.5em; + border-top: 1px solid $shade-3; +} + +.tag-selector-filter { + -moz-appearance: searchfield; + height: 24px; +} + +.tag-selector-actions { + flex: none; + border: 0; + margin-right: 3px; + padding: 0 6px; +} diff --git a/scss/themes/_light.scss b/scss/themes/_light.scss index b2709b52e6..e0658486ce 100644 --- a/scss/themes/_light.scss +++ b/scss/themes/_light.scss @@ -7,7 +7,7 @@ // -------------------------------------------------- $red: #cc2936; -$blue: #2e4de5; // 230° 80 90 +$blue: rgb(89, 139, 236); $asphalt: #7a8799; $asphalt-light: #dadee3; @@ -69,6 +69,13 @@ $main-bg: $shade-0; // Sidebar $sidebar-bg: $shade-1; +// Clicky +$clicky-hover-bg-color: rgb(187, 206, 241); +$clicky-active-bg-color: $secondary; +$clicky-border-radius: 6px; +$clicky-border-color: rgb(109, 149, 224); +$clicky-active-color: $shade-0; + // Components // -------------------------------------------------- @@ -129,20 +136,10 @@ $metadata-separator-color: $shade-3; // Button $btn-primary-color: $shade-0; $btn-primary-bg: $asphalt; -$btn-secondary-color: $secondary; $btn-default-color: $text-color; +$btn-border: $shade-3; $btn-default-bg: $shade-0; $btn-default-active-color: rgba($btn-default-color, 0.5); -$btn-icon-bg: $transparent; -$btn-icon-focus-color: $shade-0; -$btn-icon-focus-bg: $asphalt; -$btn-icon-focus-active-color: rgba($shade-0, 0.5); -$btn-icon-active-color: $shade-7; -$btn-icon-active-bg: $shade-2; -$twisty-color: $shade-6; -$twisty-focus-color: $asphalt; -$twisty-selected-color: $shade-7; -$twisty-dnd-target-color: $shade-0; // Forms $input-color: $text-color; diff --git a/scss/zotero-react-client.scss b/scss/zotero-react-client.scss index 0d4c31f39b..5d5c4402f0 100644 --- a/scss/zotero-react-client.scss +++ b/scss/zotero-react-client.scss @@ -21,3 +21,4 @@ // -------------------------------------------------- @import "components/tag-selector"; +@import "components/button"; diff --git a/test/content/support.js b/test/content/support.js index 1513ea4c73..a5702c72ad 100644 --- a/test/content/support.js +++ b/test/content/support.js @@ -213,12 +213,15 @@ var waitForTagSelector = function (win) { var zp = win.ZoteroPane; var deferred = Zotero.Promise.defer(); if (zp.tagSelectorShown()) { - var tagSelector = win.document.getElementById('zotero-tag-selector'); - var onRefresh = () => { - tagSelector.removeEventListener('refresh', onRefresh); + let tagSelector = zp.tagSelector; + let componentDidUpdate = tagSelector.componentDidUpdate; + tagSelector.componentDidUpdate = function() { deferred.resolve(); - }; - tagSelector.addEventListener('refresh', onRefresh); + tagSelector.componentDidUpdate = componentDidUpdate; + if (typeof componentDidUpdate == 'function') { + componentDidUpdate.call(this, arguments); + } + } } else { deferred.resolve(); diff --git a/test/tests/tagSelectorTest.js b/test/tests/tagSelectorTest.js index 8ba26d239c..454b48ba31 100644 --- a/test/tests/tagSelectorTest.js +++ b/test/tests/tagSelectorTest.js @@ -1,7 +1,7 @@ "use strict"; describe("Tag Selector", function () { - var win, doc, collectionsView, tagSelector; + var win, doc, collectionsView, tagSelectorElem, tagSelector; var clearTagColors = Zotero.Promise.coroutine(function* (libraryID) { var tagColors = Zotero.Tags.getColors(libraryID); @@ -11,27 +11,13 @@ describe("Tag Selector", function () { }); function getColoredTags() { - var tagsBox = tagSelector.id('tags-box'); - var elems = tagsBox.getElementsByTagName('button'); - var names = []; - for (let i = 0; i < elems.length; i++) { - if (elems[i].style.order < 0) { - names.push(elems[i].textContent); - } - } - return names; + var elems = Array.from(tagSelectorElem.querySelectorAll('.tag-selector-item.colored')); + return elems.map(elem => elem.textContent); } function getRegularTags() { - var tagsBox = tagSelector.id('tags-box'); - var elems = tagsBox.getElementsByTagName('button'); - var names = []; - for (let i = 0; i < elems.length; i++) { - if (elems[i].style.order >= 0 && elems[i].style.display != 'none') { - names.push(elems[i].textContent); - } - } - return names; + var elems = Array.from(tagSelectorElem.querySelectorAll('.tag-selector-item:not(.colored)')); + return elems.map(elem => elem.textContent); } @@ -39,7 +25,8 @@ describe("Tag Selector", function () { win = yield loadZoteroPane(); doc = win.document; collectionsView = win.ZoteroPane.collectionsView; - tagSelector = doc.getElementById('zotero-tag-selector'); + tagSelectorElem = doc.getElementById('zotero-tag-selector'); + tagSelector = win.ZoteroPane.tagSelector; // Wait for things to settle yield Zotero.Promise.delay(100); @@ -49,16 +36,37 @@ describe("Tag Selector", function () { var libraryID = Zotero.Libraries.userLibraryID; yield clearTagColors(libraryID); // Default "Display All Tags in This Library" off - tagSelector.filterToScope = true; - tagSelector.setSearch(''); - yield tagSelector.refresh(true); - }) + tagSelector.displayAllTags = false; + tagSelector.selectedTags = new Set(); + tagSelector.handleSearch(''); + tagSelector.onItemViewChanged({libraryID}); + }); after(function () { win.close(); }); - describe("#setSearch()", function () { + it('should not display duplicate tags when automatic and manual tag with same name exists', async function () { + var collection = await createDataObject('collection'); + var item1 = createUnsavedDataObject('item', { collections: [collection.id] }); + item1.setTags([{ + tag: "A", + type: 1 + }]); + var item2 = createUnsavedDataObject('item', { collections: [collection.id] }); + item2.setTags(["A", "B"]); + var promise = waitForTagSelector(win); + await Zotero.DB.executeTransaction(async function () { + await item1.save(); + await item2.save(); + }); + await promise; + + var tags = getRegularTags(); + assert.sameMembers(tags, ['A', 'B']); + }); + + describe("#handleSearch()", function () { it("should filter to tags matching the search", function* () { var collection = yield createDataObject('collection'); var item = createUnsavedDataObject('item', { collections: [collection.id] }); @@ -67,20 +75,23 @@ describe("Tag Selector", function () { yield item.saveTx(); yield promise; - var tagsSearch = doc.getElementById('tags-search'); - tagsSearch.value = 'a'; - tagsSearch.doCommand(); + promise = waitForTagSelector(win); + tagSelector.handleSearch('a'); + yield Zotero.Promise.delay(500); + + yield promise; var tags = getRegularTags(); assert.sameMembers(tags, ['a']); + + tagSelector.handleSearch(''); + yield Zotero.Promise.delay(500); - tagsSearch.value = ''; - tagsSearch.doCommand(); yield item.eraseTx(); }); }); - describe("#refresh()", function () { + describe("#handleTagSelected()", function () { it("should remove tags not on matching items on tag click", function* () { var collection = yield createDataObject('collection'); var item1 = createUnsavedDataObject('item', { collections: [collection.id] }); @@ -112,13 +123,8 @@ describe("Tag Selector", function () { }); yield promise; - var buttons = tagSelector.id('tags-box').getElementsByTagName('button'); - var spy = sinon.spy(win.ZoteroPane, "updateTagFilter"); - buttons[0].click(); - - yield spy.returnValues[0]; - - spy.restore(); + tagSelector.handleTagSelected('A'); + yield waitForTagSelector(win); var tags = getRegularTags(); assert.sameMembers(tags, ['A', 'B']); @@ -126,9 +132,9 @@ describe("Tag Selector", function () { }); - describe("#filterToScope", function () { - it("should show all tags in library when false", function* () { - tagSelector.filterToScope = false; + describe("#displayAllTags", function () { + it("should show all tags in library when true", function* () { + tagSelector.displayAllTags = true; var collection = yield createDataObject('collection'); var item1 = createUnsavedDataObject('item'); @@ -165,7 +171,7 @@ describe("Tag Selector", function () { describe("#notify()", function () { it("should add a tag when added to an item in the library root", function* () { - var promise, tagSelector; + var promise; if (collectionsView.selection.currentIndex != 0) { promise = waitForTagSelector(win); @@ -256,11 +262,14 @@ describe("Tag Selector", function () { it("should show a colored tag at the top of the list even when linked to no items", function* () { var libraryID = Zotero.Libraries.userLibraryID; - var tagElems = tagSelector.id('tags-box').getElementsByTagName('button'); + var tagElems = tagSelectorElem.querySelectorAll('.tag-selector-item'); var count = tagElems.length; - + + var promise = waitForTagSelector(win); yield Zotero.Tags.setColor(libraryID, "Top", '#AAAAAA'); - + yield promise; + + tagElems = tagSelectorElem.querySelectorAll('.tag-selector-item'); assert.equal(tagElems.length, count + 1); }); @@ -288,16 +297,16 @@ describe("Tag Selector", function () { var promise = waitForTagSelector(win); yield item.saveTx(); yield promise; - - var tagElems = tagSelector.id('tags-box').getElementsByTagName('button'); + + var tagElems = tagSelectorElem.querySelectorAll('.tag-selector-item'); // Make sure the colored tags are still in the right position var tags = new Map(); for (let i = 0; i < tagElems.length; i++) { - tags.set(tagElems[i].textContent, tagElems[i].style.order); + tags.set(tagElems[i].textContent, i); } - assert.isBelow(parseInt(tags.get("B")), 0); - assert.isBelow(parseInt(tags.get("B")), parseInt(tags.get("A"))); + assert.isAbove(tags.get("B"), 0); + assert.isAbove(tags.get("B"), tags.get("A")); }) it("should remove a tag when an item is removed from a collection", function* () { @@ -325,7 +334,7 @@ describe("Tag Selector", function () { promise = waitForTagSelector(win); yield item.saveTx(); yield promise; - + // Tag selector shouldn't show the removed item's tag assert.equal(getRegularTags().length, 0); }) @@ -379,8 +388,9 @@ describe("Tag Selector", function () { // Remove tag from library promise = waitForTagSelector(win); - var dialogPromise = waitForDialog(); - yield tagSelector.deleteTag("A"); + waitForDialog(); + tagSelector.contextTag = {name: "A"}; + yield tagSelector.openDeletePrompt(); yield promise; // Tag selector shouldn't show the deleted item's tag @@ -388,7 +398,7 @@ describe("Tag Selector", function () { }) }) - describe("#rename()", function () { + describe("#openRenamePrompt", function () { it("should rename a tag and update the tag selector", function* () { yield selectLibrary(win); @@ -409,7 +419,8 @@ describe("Tag Selector", function () { dialog.document.getElementById('loginTextbox').value = newTag; dialog.document.documentElement.acceptDialog(); }) - yield tagSelector.rename(tag); + tagSelector.contextTag = {name: tag}; + yield tagSelector.openRenamePrompt(); yield promise; var tags = getRegularTags(); @@ -428,11 +439,12 @@ describe("Tag Selector", function () { yield promise; promise = waitForTagSelector(win); - var promptPromise = waitForWindow("chrome://global/content/commonDialog.xul", function (dialog) { + waitForWindow("chrome://global/content/commonDialog.xul", function (dialog) { dialog.document.getElementById('loginTextbox').value = newTag; dialog.document.documentElement.acceptDialog(); - }) - yield tagSelector.rename(oldTag); + }); + tagSelector.contextTag = {name: oldTag}; + yield tagSelector.openRenamePrompt(); yield promise; var tags = getColoredTags(); @@ -441,7 +453,7 @@ describe("Tag Selector", function () { }); }) - describe("#_openColorPickerWindow()", function () { + describe("#openColorPickerWindow()", function () { it("should assign a color to a tag", function* () { yield selectLibrary(win); var tag = "b " + Zotero.Utilities.randomString(); @@ -463,7 +475,8 @@ describe("Tag Selector", function () { var dialogPromise = waitForDialog(false, undefined, 'chrome://zotero/content/tagColorChooser.xul'); var tagSelectorPromise = waitForTagSelector(win); - yield tagSelector._openColorPickerWindow(tag); + tagSelector.contextTag = {name: tag}; + yield tagSelector.openColorPickerWindow(); yield dialogPromise; yield tagSelectorPromise; From 90a70f7c31a08c0deef2abb0c1a2c7e2686822d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Fri, 14 Dec 2018 14:11:42 +0200 Subject: [PATCH 3/6] Fix a /saveItems session management race condition bug --- chrome/content/zotero/xpcom/connector/server_connector.js | 7 ++++--- chrome/content/zotero/xpcom/translation/translate_item.js | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/chrome/content/zotero/xpcom/connector/server_connector.js b/chrome/content/zotero/xpcom/connector/server_connector.js index dd891125ed..c80293b25a 100644 --- a/chrome/content/zotero/xpcom/connector/server_connector.js +++ b/chrome/content/zotero/xpcom/connector/server_connector.js @@ -742,9 +742,10 @@ Zotero.Server.Connector.SaveItems.prototype = { session, targetID, requestData, - function (topLevelItems) { + function (jsonItems, items) { + session.addItems(items); // Only return the properties the connector needs - topLevelItems = topLevelItems.map((item) => { + jsonItems = jsonItems.map((item) => { let o = { id: item.id, title: item.title, @@ -764,7 +765,7 @@ Zotero.Server.Connector.SaveItems.prototype = { }; return o; }); - resolve([201, "application/json", JSON.stringify({items: topLevelItems})]); + resolve([201, "application/json", JSON.stringify({items: jsonItems})]); } ) // Add items to session once all attachments have been saved diff --git a/chrome/content/zotero/xpcom/translation/translate_item.js b/chrome/content/zotero/xpcom/translation/translate_item.js index c9fe751858..afafa0380f 100644 --- a/chrome/content/zotero/xpcom/translation/translate_item.js +++ b/chrome/content/zotero/xpcom/translation/translate_item.js @@ -179,7 +179,7 @@ Zotero.Translate.ItemSaver.prototype = { }.bind(this)); if (itemsDoneCallback) { - itemsDoneCallback(items.map(item => jsonByItem.get(item))); + itemsDoneCallback(items.map(item => jsonByItem.get(item)), items); } // Save standalone attachments From f5c4fb06e1ac1c304c0e7d13725a65ed18335f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Mon, 21 Jan 2019 11:01:04 +0200 Subject: [PATCH 4/6] Ensure high-dpi icons are loaded in React components --- chrome/content/zotero/components/icons.jsx | 2 +- chrome/content/zotero/include.js | 7 +- .../content/zotero/standalone/standalone.xul | 3 +- components/zotero-service.js | 8 +- resource/concurrentCaller.js | 7 +- resource/require.js | 115 +++++++++--------- scss/components/_button.scss | 7 +- 7 files changed, 83 insertions(+), 66 deletions(-) diff --git a/chrome/content/zotero/components/icons.jsx b/chrome/content/zotero/components/icons.jsx index 2647ba4dd5..909cae95e2 100644 --- a/chrome/content/zotero/components/icons.jsx +++ b/chrome/content/zotero/components/icons.jsx @@ -26,7 +26,7 @@ function i(name, svgOrSrc) { const { className } = this.props if (typeof svgOrSrc == 'string') { - if (window.devicePixelRatio >= 0.75) { + if (window.devicePixelRatio >= 1.25) { let parts = svgOrSrc.split('.'); parts[parts.length-2] = parts[parts.length-2] + '@2x'; svgOrSrc = parts.join('.') diff --git a/chrome/content/zotero/include.js b/chrome/content/zotero/include.js index 45e4fd4973..fcb0858de6 100644 --- a/chrome/content/zotero/include.js +++ b/chrome/content/zotero/include.js @@ -7,4 +7,9 @@ var Zotero = Components.classes['@zotero.org/Zotero;1'] .getService(Components.interfaces.nsISupports) .wrappedJSObject; -Components.utils.import('resource://zotero/require.js'); \ No newline at end of file +// Components.utils.import('resource://zotero/require.js'); +// Not using Cu.import here since we don't want the require module to be cached +// for includes within ZoteroPane or other code, where we want the window instance available to modules. +Components.classes["@mozilla.org/moz/jssubscript-loader;1"] + .getService(Components.interfaces.mozIJSSubScriptLoader) + .loadSubScript('resource://zotero/require.js'); diff --git a/chrome/content/zotero/standalone/standalone.xul b/chrome/content/zotero/standalone/standalone.xul index ad04b99844..ff548bee21 100644 --- a/chrome/content/zotero/standalone/standalone.xul +++ b/chrome/content/zotero/standalone/standalone.xul @@ -46,7 +46,8 @@ windowtype="navigator:browser" title="&brandShortName;" width="1000" height="600" - persist="screenX screenY width height sizemode"> + persist="screenX screenY width height sizemode"> +