UNPKG

zotero-web-library

Version:

Web library from zotero.org

587 lines (538 loc) 19.3 kB
'use strict'; var log = require('libzotero/lib/Log').Logger('zotero-web-library:items'); var React = require('react'); var LoadingSpinner = require('./LoadingSpinner.js'); var LoadingError = require('./LoadingError.js'); Zotero.ui.getItemsConfig = function (library) { var effectiveUrlVars = ['tag', 'collectionKey', 'order', 'sort', 'q', 'qmode']; var urlConfigVals = {}; effectiveUrlVars.forEach(function (value) { var t = Zotero.state.getUrlVar(value); if (t) { urlConfigVals[value] = t; } }); var defaultConfig = { libraryID: library.libraryID, libraryType: library.libraryType, target: 'items', targetModifier: 'top', limit: library.preferences.getPref('itemsPerPage') }; var userPreferencesApiArgs = { order: Zotero.preferences.getPref('order'), sort: Zotero.preferences.getPref('sort'), limit: library.preferences.getPref('itemsPerPage') }; //Build config object that should be displayed next and compare to currently displayed var newConfig = Z.extend({}, defaultConfig, userPreferencesApiArgs, urlConfigVals); //don't allow ordering by group only columns if user library if (library.libraryType == 'user' && Zotero.Library.prototype.groupOnlyColumns.indexOf(newConfig.order) != -1) { newConfig.order = 'title'; } if (!newConfig.sort) { newConfig.sort = Zotero.config.sortOrdering[newConfig.order]; } //don't pass top if we are searching for tags (or query?) if (newConfig.tag || newConfig.q) { delete newConfig.targetModifier; } return newConfig; }; var Items = React.createClass({ displayName: 'Items', componentWillMount: function componentWillMount() { var reactInstance = this; var library = this.props.library; library.listen('changeItemSorting', reactInstance.resortTriggered); library.listen('displayedItemsChanged', reactInstance.loadItems, {}); library.listen('displayedItemChanged', reactInstance.selectDisplayed); Zotero.listen('selectedItemsChanged', function () { reactInstance.setState({ selectedItemKeys: Zotero.state.getSelectedItemKeys() }); }); library.listen('selectedItemsChanged', function () { reactInstance.setState({ selectedItemKeys: Zotero.state.getSelectedItemKeys() }); }); library.listen('selectedCollectionChanged', function () { Zotero.state.selectedItemKeys = []; library.trigger('selectedItemsChanged', { selectedItemKeys: [] }); }); library.listen('loadMoreItems', reactInstance.loadMoreItems, {}); library.trigger('displayedItemsChanged'); var displayFields = library.preferences.getPref('listDisplayedFields'); this.setState({ displayFields: displayFields }); }, getDefaultProps: function getDefaultProps() { return { narrow: false }; }, getInitialState: function getInitialState() { return { moreloading: false, allItemsLoaded: false, errorLoading: false, items: [], selectedItemKeys: [], allSelected: false, displayFields: ['title', 'creator', 'dateModified'], order: 'title', sort: 'asc' }; }, loadItems: function loadItems() { log.debug('Items.loadItems', 3); var reactInstance = this; var library = this.props.library; var newConfig = Zotero.ui.getItemsConfig(library); //clear contents and show spinner while loading this.setState({ items: [], moreloading: true }); var p = library.loadItems(newConfig).then(function (response) { if (!response.loadedItems) { log.error('expected loadedItems on response not present'); throw 'Expected response to have loadedItems'; } library.items.totalResults = response.totalResults; library.trigger('totalResultsLoaded'); var allLoaded = response.totalResults == response.loadedItems.length; reactInstance.setState({ items: response.loadedItems, moreloading: false, sort: newConfig.sort, order: newConfig.order, allItemsLoaded: allLoaded }); }).catch(function (response) { log.error(response); reactInstance.setState({ errorLoading: true, moreloading: false, sort: newConfig.sort, order: newConfig.order }); }); return p; }, loadMoreItems: function loadMoreItems() { log.debug('Items.loadMoreItems', 3); var reactInstance = this; var library = this.props.library; //bail out if we're already fetching more items if (reactInstance.state.moreloading) { return; } //bail out if we're done loading all items if (reactInstance.state.allItemsLoaded) { return; } reactInstance.setState({ moreloading: true }); var library = reactInstance.props.library; var newConfig = Zotero.ui.getItemsConfig(library); var newStart = reactInstance.state.items.length; newConfig.start = newStart; var p = library.loadItems(newConfig).then(function (response) { if (!response.loadedItems) { log.error('expected loadedItems on response not present'); throw 'Expected response to have loadedItems'; } var allitems = reactInstance.state.items.concat(response.loadedItems); reactInstance.setState({ items: allitems, moreloading: false }); //see if we're displaying as many items as there are in results var itemsDisplayed = allitems.length; if (response.totalResults == itemsDisplayed) { reactInstance.setState({ allItemsLoaded: true }); } }).catch(function (response) { log.error(response); reactInstance.setState({ errorLoading: true, moreloading: false }); }); }, resortItems: function resortItems(evt) { //handle click on the item table header to re-sort items //if it is the currently sorted field, simply flip the sort order //if it is not the currently sorted field, set it to be the currently sorted //field and set the default ordering for that field log.debug('.field-table-header clicked', 3); evt.preventDefault(); var reactInstance = this; var library = this.props.library; var currentSortField = this.state.order; var currentSortOrder = this.state.sort; var newSortField = evt.target.getAttribute('data-columnfield'); var newSortOrder; if (newSortField != currentSortField) { newSortOrder = Zotero.config.sortOrdering[newSortField]; //default for column } else { //swap sort order if (currentSortOrder == 'asc') { newSortOrder = 'desc'; } else { newSortOrder = 'asc'; } } //only allow ordering by the fields we have if (library.sortableColumns.indexOf(newSortField) == -1) { return false; } //problem if there was no sort column mapped to the header that got clicked if (!newSortField) { Zotero.ui.jsNotificationMessage('no order field mapped to column'); return false; } //update the url with the new values Zotero.state.pathVars['order'] = newSortField; Zotero.state.pathVars['sort'] = newSortOrder; Zotero.state.pushState(); //set new order as preference and save it to use www prefs library.preferences.setPref('sortField', newSortField); library.preferences.setPref('sortOrder', newSortOrder); library.preferences.setPref('order', newSortField); library.preferences.setPref('sort', newSortOrder); Zotero.preferences.setPref('order', newSortField); Zotero.preferences.setPref('sort', newSortOrder); }, resortTriggered: function resortTriggered(evt) { //re-sort triggered from another widget var reactInstance = this; var library = this.props.library; var currentSortField = this.state.order; var currentSortOrder = this.state.sort; var newSortField = evt.data.newSortField; var newSortOrder = evt.data.newSortOrder; //only allow ordering by the fields we have if (library.sortableColumns.indexOf(newSortField) == -1) { return false; } //problem if there was no sort column mapped to the header that got clicked if (!newSortField) { Zotero.ui.jsNotificationMessage('no order field mapped to column'); return false; } //update the url with the new values Zotero.state.pathVars['order'] = newSortField; Zotero.state.pathVars['sort'] = newSortOrder; Zotero.state.pushState(); //set new order as preference and save it to use www prefs library.preferences.setPref('sortField', newSortField); library.preferences.setPref('sortOrder', newSortOrder); library.preferences.setPref('order', newSortField); library.preferences.setPref('sort', newSortOrder); Zotero.preferences.setPref('order', newSortField); Zotero.preferences.setPref('sort', newSortOrder); }, //select and highlight in the itemlist the item that is displayed //in the item details widget selectDisplayed: function selectDisplayed() { log.debug('widgets.items.selectDisplayed', 3); Zotero.state.selectedItemKeys = []; this.setState({ selectedItemKeys: Zotero.state.getSelectedItemKeys(), allSelected: false }); }, fixTableHeaders: function fixTableHeaders() { if (document.getElementsByTagName('body')[0].className.indexOf('lib-body') != -1) { var tableEl = J(this.refs.itemsTable); tableEl.floatThead({ top: function top() { var searchContainerEl = J('.library-search-box-container:visible'); var primaryNavEl = J('#primarynav'); return searchContainerEl.height() ? primaryNavEl.height() + searchContainerEl.height() + 'px' : 0; } }); } }, handleSelectAllChange: function handleSelectAllChange(evt) { var library = this.props.library; var nowselected = []; var allSelected = false; if (evt.target.checked) { allSelected = true; //select all items this.state.items.forEach(function (item) { nowselected.push(item.get('key')); }); } else { var selectedItemKey = Zotero.state.getUrlVar('itemKey'); if (selectedItemKey) { nowselected.push(selectedItemKey); } } Zotero.state.selectedItemKeys = nowselected; this.setState({ selectedItemKeys: nowselected, allSelected: allSelected }); library.trigger('selectedItemsChanged', { selectedItemKeys: nowselected }); //if deselected all, reselect displayed item row if (nowselected.length === 0) { library.trigger('displayedItemChanged'); } }, openSortingDialog: function openSortingDialog(evt) { var library = this.props.library; library.trigger('chooseSortingDialog'); }, componentDidMount: function componentDidMount() { this.fixTableHeaders(); }, componentDidUpdate: function componentDidUpdate() { this.fixTableHeaders(); }, render: function render() { log.debug('Items.render', 3); var reactInstance = this; var library = this.props.library; var narrow = this.props.narrow; var order = this.state.order; var sort = this.state.sort; var selectedItemKeys = this.state.selectedItemKeys; var selectedItemKeyMap = {}; selectedItemKeys.forEach(function (itemKey) { selectedItemKeyMap[itemKey] = true; }); var sortIcon; if (sort == 'desc') { sortIcon = React.createElement('span', { className: 'glyphicon fonticon glyphicon-chevron-down pull-right' }); } else { sortIcon = React.createElement('span', { className: 'glyphicon fonticon glyphicon-chevron-up pull-right' }); } var headers = [React.createElement( 'th', { key: 'checkbox-header' }, React.createElement('input', { type: 'checkbox', className: 'itemlist-editmode-checkbox all-checkbox', name: 'selectall', checked: this.state.allSelected, onChange: this.handleSelectAllChange }) )]; if (narrow) { headers.push(React.createElement( 'th', { key: 'single-cell-header', onClick: reactInstance.openSortingDialog, className: 'clickable' }, Zotero.Item.prototype.fieldMap[order], sortIcon )); } else { var fieldHeaders = this.state.displayFields.map(function (header, ind) { var sortable = Zotero.Library.prototype.sortableColumns.indexOf(header) != -1; var selectedClass = header == order ? 'selected-order sort-' + sort + ' ' : ''; var sortspan = null; if (header == order) { sortspan = sortIcon; } return React.createElement( 'th', { key: header, onClick: reactInstance.resortItems, className: 'field-table-header ' + selectedClass + (sortable ? 'clickable ' : ''), 'data-columnfield': header }, Zotero.Item.prototype.fieldMap[header] ? Zotero.Item.prototype.fieldMap[header] : header, sortspan ); }); headers = headers.concat(fieldHeaders); } var displayFields = this.state.displayFields; var itemRows = this.state.items.map(function (item) { var selected = selectedItemKeyMap.hasOwnProperty(item.get('key')) ? true : false; var p = { itemsReactInstance: reactInstance, library: library, key: item.get('key'), item: item, selected: selected, narrow: narrow, displayFields: displayFields }; return React.createElement(ItemRow, p); }); if (itemRows.length == 0) { var tds = this.state.displayFields.map(function (header) { return React.createElement('td', { key: header }); }); tds = [React.createElement('td', { key: 'check' })].concat(tds); itemRows = React.createElement( 'tr', null, tds ); } return React.createElement( 'div', { id: 'library-items-div', className: 'library-items-div row', ref: 'topdiv' }, React.createElement( 'form', { className: 'item-select-form', method: 'POST' }, React.createElement( 'table', { id: 'field-table', ref: 'itemsTable', className: 'wide-items-table table table-striped' }, React.createElement( 'thead', null, React.createElement( 'tr', null, headers ) ), React.createElement( 'tbody', null, itemRows ) ), React.createElement(LoadingError, { errorLoading: this.state.errorLoading }), React.createElement(LoadingSpinner, { loading: this.state.moreloading }), React.createElement( 'div', { hidden: this.state.allItemsLoaded, id: 'load-more-items-div', className: 'row' }, React.createElement( 'button', { onClick: this.loadMoreItems, type: 'button', id: 'load-more-items-button', className: 'btn btn-default' }, 'Load More Items' ) ) ) ); } }); var ItemRow = React.createClass({ displayName: 'ItemRow', getDefaultProps: function getDefaultProps() { return { displayFields: ['title', 'creatorSummary', 'dateModified'], selected: false, item: {}, narrow: false }; }, handleSelectChange: function handleSelectChange(ev) { var reactInstance = this; var library = this.props.library; var itemKey = this.props.item.get('key'); Zotero.state.toggleItemSelected(itemKey); var selected = Zotero.state.getSelectedItemKeys(); library.trigger('selectedItemsChanged', { selectedItemKeys: selected }); }, handleItemLinkClick: function handleItemLinkClick(evt) { evt.preventDefault(); var itemKey = evt.target.getAttribute('data-itemkey'); if (evt.ctrlKey) { //add item to selected, but don't deselect others Zotero.state.toggleItemSelected(itemKey); var selected = Zotero.state.getSelectedItemKeys(); var library = this.props.library; library.trigger('selectedItemsChanged', { selectedItemKeys: selected }); return; } Zotero.state.pathVars.itemKey = itemKey; Zotero.state.pushState(); }, render: function render() { var reactInstance = this; var item = this.props.item; var selected = this.props.selected; if (!this.props.narrow) { var fields = this.props.displayFields.map(function (field) { var ctags = null; if (field == 'title') { ctags = React.createElement(ColoredTags, { item: item }); } return React.createElement( 'td', { onClick: reactInstance.handleItemLinkClick, key: field, className: field, 'data-itemkey': item.get('key') }, ctags, React.createElement( 'a', { onClick: reactInstance.handleItemLinkClick, className: 'item-select-link', 'data-itemkey': item.get('key'), href: Zotero.url.itemHref(item), title: item.get(field) }, Zotero.format.itemField(field, item, true) ) ); }); return React.createElement( 'tr', { className: selected ? 'highlighed' : '' }, React.createElement( 'td', { className: 'edit-checkbox-td', 'data-itemkey': item.get('key') }, React.createElement('input', { type: 'checkbox', onChange: this.handleSelectChange, checked: selected, className: 'itemlist-editmode-checkbox itemKey-checkbox', name: 'selectitem-' + item.get('key'), 'data-itemkey': item.get('key') }) ), fields ); } else { return React.createElement( 'tr', { className: selected ? 'highlighed' : '', 'data-itemkey': item.get('key') }, React.createElement( 'td', { className: 'edit-checkbox-td', 'data-itemkey': item.get('key') }, React.createElement('input', { type: 'checkbox', className: 'itemlist-editmode-checkbox itemKey-checkbox', name: 'selectitem-' + item.get('key'), 'data-itemkey': item.get('key') }) ), React.createElement(SingleCellItemField, { onClick: reactInstance.handleItemLinkClick, item: item, displayFields: this.props.displayFields }) ); } } }); var SingleCellItemField = React.createClass({ displayName: 'SingleCellItemField', render: function render() { var item = this.props.item; var field = this.props.field; var pps = []; this.props.displayFields.forEach(function (field) { var fieldDisplayName = Zotero.Item.prototype.fieldMap[field] ? Zotero.Item.prototype.fieldMap[field] + ':' : ''; if (field == 'title') { pps.push(React.createElement('span', { key: 'itemTypeIcon', className: 'sprite-icon pull-left sprite-treeitem-' + item.itemTypeImageClass() })); pps.push(React.createElement(ColoredTags, { key: 'coloredTags', item: item })); pps.push(React.createElement( 'b', { key: 'title' }, Zotero.format.itemField(field, item, true) )); } else if (field === 'dateAdded' || field === 'dateModified') { pps.push(React.createElement('p', { key: field, title: item.get(field), dangerouslySetInnerHtml: { __html: fieldDisplayName + Zotero.format.itemDateField(field, item, true) } })); } else { pps.push(React.createElement( 'p', { key: field, title: item.get(field) }, fieldDisplayName, Zotero.format.itemField(field, item, true) )); } }); return React.createElement( 'td', { onClick: this.props.onClick, className: 'single-cell-item', 'data-itemkey': item.get('key') }, React.createElement( 'a', { className: 'item-select-link', 'data-itemkey': item.get('key'), href: Zotero.url.itemHref(item) }, pps ) ); } }); var ColoredTags = React.createClass({ displayName: 'ColoredTags', render: function render() { var item = this.props.item; var library = item.owningLibrary; var coloredTags = library.matchColoredTags(item.apiObj._supplement.tagstrings); var ctags = coloredTags.map(function (color) { return React.createElement(ColoredTag, { key: color, color: color }); }); return React.createElement( 'span', { className: 'coloredTags' }, ctags ); } }); var ColoredTag = React.createClass({ displayName: 'ColoredTag', render: function render() { var styleObj = { color: this.props.color }; //styleObj.color += " !important"; return React.createElement( 'span', { style: styleObj }, React.createElement('span', { style: styleObj, className: 'glyphicons fonticon glyphicons-tag' }) ); } }); module.exports = Items;