/* ***** BEGIN LICENSE BLOCK ***** Copyright © 2020 Corporation for Digital Scholarship Vienna, Virginia, USA https://digitalscholar.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 ***** */ // TODO: Fix import/require related issues that might be // related with `require` not reusing the context var React = require('react'); var ReactDOM = require('react-dom'); var NotesList = require('components/itemPane/notesList').default; var ZoteroContextPane = new function () { var _tabCover; var _contextPane; var _contextPaneInner; var _contextPaneSplitter; var _contextPaneSplitterStacked; var _itemToggle; var _notesToggle; var _panesDeck; var _itemPaneDeck; var _notesPaneDeck; var _splitButton; var _itemPaneToggle; var _notesPaneToggle; var _tabToolbar; var _itemContexts = []; var _notesContexts = []; // Using attribute instead of property to set 'selectedIndex' // is more reliable this.update = _update; this.getActiveEditor = _getActiveEditor; this.focus = _focus; this.init = function () { if (!Zotero) { return; } _tabCover = document.getElementById('zotero-tab-cover'); _itemToggle = document.getElementById('zotero-tb-toggle-item-pane'); _notesToggle = document.getElementById('zotero-tb-toggle-notes-pane'); _contextPane = document.getElementById('zotero-context-pane'); _contextPaneInner = document.getElementById('zotero-context-pane-inner'); _contextPaneSplitter = document.getElementById('zotero-context-splitter'); _contextPaneSplitterStacked = document.getElementById('zotero-context-splitter-stacked'); _splitButton = document.getElementById('zotero-tb-split'); _itemPaneToggle = document.getElementById('zotero-tb-toggle-item-pane'); _notesPaneToggle = document.getElementById('zotero-tb-toggle-notes-pane'); _tabToolbar = document.getElementById('zotero-tab-toolbar'); if (Zotero.rtl) { _tabToolbar.style.left = 0; _splitButton.style.transform = 'scaleX(-1)'; } else { _tabToolbar.style.right = 0; } // vbox var vbox = document.createXULElement('vbox'); vbox.setAttribute('flex', '1'); _contextPaneInner.append(vbox); // Toolbar extension var toolbarExtension = document.createXULElement('box'); toolbarExtension.style.height = '32px'; toolbarExtension.id = 'zotero-context-toolbar-extension'; _panesDeck = document.createXULElement('deck'); _panesDeck.setAttribute('flex', 1); _panesDeck.setAttribute('selectedIndex', 0); vbox.append(toolbarExtension, _panesDeck); // Item pane deck _itemPaneDeck = document.createXULElement('deck'); // Notes pane deck _notesPaneDeck = document.createXULElement('deck'); _notesPaneDeck.style.backgroundColor = 'white'; _notesPaneDeck.setAttribute('flex', 1); _notesPaneDeck.className = 'notes-pane-deck'; _panesDeck.append(_itemPaneDeck, _notesPaneDeck); this._notifierID = Zotero.Notifier.registerObserver(this, ['item', 'tab'], 'contextPane'); window.addEventListener('resize', _update); _itemToggle.addEventListener('click', _toggleItemButton); _notesToggle.addEventListener('click', _toggleNotesButton); Zotero.Reader.onChangeSidebarWidth = _updatePaneWidth; Zotero.Reader.onChangeSidebarOpen = _updatePaneWidth; }; this.destroy = function () { _itemToggle.removeEventListener('click', _toggleItemButton); _notesToggle.removeEventListener('click', _toggleNotesButton); window.removeEventListener('resize', _update); Zotero.Notifier.unregisterObserver(this._notifierID); Zotero.Reader.onChangeSidebarWidth = () => {}; Zotero.Reader.onChangeSidebarOpen = () => {}; _contextPaneInner.innerHTML = ''; _itemContexts = []; _notesContexts = []; }; this.notify = function (action, type, ids, extraData) { if (type == 'item') { // Update, remove or re-create item panes for (let context of _itemContexts.slice()) { let item = Zotero.Items.get(context.itemID); if (!item) { _removeItemContext(context.tabID); } else if (item.parentID != context.parentID) { _removeItemContext(context.tabID); _addItemContext(context.tabID, context.itemID); } else { context.update(); } } // Update notes lists for affected libraries let libraryIDs = []; for (let id of ids) { let item = Zotero.Items.get(id); if (item && (item.isNote() || item.isRegularItem())) { libraryIDs.push(item.libraryID); } else if (action == 'delete') { libraryIDs.push(extraData[id].libraryID); } } for (let context of _notesContexts) { if (libraryIDs.includes(context.libraryID)) { context.affectedIDs = new Set([...context.affectedIDs, ...ids]); context.update(); } } } else if (type == 'tab') { if (action == 'add') { _addItemContext(ids[0], extraData[ids[0]].itemID); } else if (action == 'close') { _removeItemContext(ids[0]); if (Zotero_Tabs.deck.children.length == 1) { _notesContexts.forEach(x => x.notesListRef.current.setExpanded(false)); } } else if (action == 'select') { // It seems that changing `hidden` or `collapsed` values might // be related with significant slow down when there are too many // DOM nodes (i.e. 10k notes) if (Zotero_Tabs.selectedType == 'library') { _contextPaneSplitter.setAttribute('hidden', true); _contextPane.setAttribute('collapsed', true); _tabToolbar.hidden = true; _tabCover.classList.add('hidden'); } else if (Zotero_Tabs.selectedType == 'reader') { var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID); if (reader) { _tabCover.classList.remove('hidden'); (async () => { await reader._initPromise; _tabCover.classList.add('hidden'); // Focus reader pages view if context pane note editor is not selected if (Zotero_Tabs.selectedID == reader.tabID && (!document.activeElement || !document.activeElement.closest('.context-node iframe[anonid="editor-view"]'))) { reader.focus(); } var attachment = await Zotero.Items.getAsync(reader.itemID); if (attachment) { _selectNotesContext(attachment.libraryID); var notesContext = _getNotesContext(attachment.libraryID); notesContext.updateFromCache(); } })(); } _contextPaneSplitter.setAttribute('hidden', false); // It seems that on heavy load (i.e. syncing) the line below doesn't set the correct value setTimeout(() => { _contextPane.setAttribute('collapsed', !(_contextPaneSplitter.getAttribute('state') != 'collapsed')); }); _tabToolbar.hidden = false; } _selectItemContext(ids[0]); _update(); } } }; function _toggleItemButton() { _togglePane(0); } function _toggleNotesButton() { _togglePane(1); } function _getActiveEditor() { var splitter; if (Zotero.Prefs.get('layout') == 'stacked') { splitter = _contextPaneSplitterStacked; } else { splitter = _contextPaneSplitter; } if (splitter.getAttribute('state') != 'collapsed') { if (_panesDeck.selectedIndex == 1) { var node = _notesPaneDeck.selectedPanel; if (node.selectedIndex == 1) { return node.querySelector('note-editor'); } } } } function _focus() { var splitter; if (Zotero.Prefs.get('layout') == 'stacked') { splitter = _contextPaneSplitterStacked; } else { splitter = _contextPaneSplitter; } if (splitter.getAttribute('state') != 'collapsed') { if (_panesDeck.selectedIndex == 0) { var node = _itemPaneDeck.selectedPanel; node.querySelector('tab[selected]').focus(); return true; } else { var node = _notesPaneDeck.selectedPanel; if (node.selectedIndex == 0) { node.querySelector('search-textbox').focus(); return true; } else { node.querySelector('note-editor').focusFirst(); return true; } } } return false; } function _updateAddToNote() { var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID); if (reader) { var editor = _getActiveEditor(); var libraryReadOnly = editor && editor.item && _isLibraryReadOnly(editor.item.libraryID); var noteReadOnly = editor && editor.item && (editor.item.deleted || editor.item.parentItem && editor.item.parentItem.deleted); reader.enableAddToNote(!!editor && !libraryReadOnly && !noteReadOnly); } } function _updatePaneWidth() { var stacked = Zotero.Prefs.get('layout') == 'stacked'; var width = Zotero.Reader.getSidebarWidth() + 'px'; if (!Zotero.Reader.getSidebarOpen()) { width = 0; } if (Zotero.rtl) { _contextPane.style.left = 0; _contextPane.style.right = stacked ? width : 'unset'; } else { _contextPane.style.left = stacked ? width : 'unset'; _contextPane.style.right = 0; } } function _updateToolbarWidth() { var stacked = Zotero.Prefs.get('layout') == 'stacked'; var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID); if (reader) { if ((stacked || _contextPaneSplitter.getAttribute('state') == 'collapsed')) { reader.setToolbarPlaceholderWidth(_tabToolbar.offsetWidth); } else { reader.setToolbarPlaceholderWidth(0); } } } function _update() { if (Zotero_Tabs.selectedIndex == 0) { return; } var splitter; var stacked = Zotero.Prefs.get('layout') == 'stacked'; if (stacked) { _contextPaneSplitterStacked.setAttribute('hidden', false); _contextPaneSplitter.setAttribute('state', 'open'); _contextPaneSplitter.setAttribute('hidden', true); _contextPane.classList.add('stacked'); _contextPane.classList.remove('standard'); splitter = _contextPaneSplitterStacked; } else { _contextPaneSplitter.setAttribute('hidden', false); _contextPaneSplitterStacked.setAttribute('hidden', true); _contextPaneSplitterStacked.setAttribute('state', 'open'); _contextPane.classList.add('standard'); _contextPane.classList.remove('stacked'); splitter = _contextPaneSplitter; } var collapsed = splitter.getAttribute('state') == 'collapsed'; var selectedIndex = _panesDeck.selectedIndex; if (!collapsed && selectedIndex == 0) { _itemPaneToggle.classList.add('toggled'); } else { _itemPaneToggle.classList.remove('toggled'); } if (!collapsed && selectedIndex == 1) { _notesPaneToggle.classList.add('toggled'); } else { _notesPaneToggle.classList.remove('toggled'); } if (Zotero_Tabs.selectedIndex > 0) { var height = 0; if (Zotero.Prefs.get('layout') == 'stacked' && _contextPane.getAttribute('collapsed') != 'true') { height = _contextPaneInner.getBoundingClientRect().height; } Zotero.Reader.setBottomPlaceholderHeight(height); } _updatePaneWidth(); _updateToolbarWidth(); _updateAddToNote(); } function _togglePane(paneIndex) { var splitter = Zotero.Prefs.get('layout') == 'stacked' ? _contextPaneSplitterStacked : _contextPaneSplitter; var isOpen = splitter.getAttribute('state') != 'collapsed'; var hide = false; var currentPane = _panesDeck.selectedIndex; if (isOpen && currentPane == paneIndex) { hide = true; } else { _panesDeck.setAttribute('selectedIndex', paneIndex); } splitter.setAttribute('state', hide ? 'collapsed' : 'open'); _update(); if (!hide) { ZoteroContextPane.focus(); } } function _getCurrentAttachment() { var reader = Zotero.Reader.getByTabID(Zotero_Tabs.selectedID); if (reader) { return Zotero.Items.get(reader.itemID); } } function _addNotesContext(libraryID) { let readOnly = _isLibraryReadOnly(libraryID); var list = document.createXULElement('vbox'); list.setAttribute('flex', 1); list.className = 'zotero-context-notes-list'; var noteContainer = document.createXULElement('vbox'); var title = document.createXULElement('vbox'); title.className = 'zotero-context-pane-editor-parent-line'; var editor = new (customElements.get('note-editor')); editor.className = 'zotero-context-pane-pinned-note'; editor.setAttribute('flex', 1); noteContainer.append(title, editor); let contextNode = document.createXULElement('deck'); contextNode.append(list, noteContainer); _notesPaneDeck.append(contextNode); contextNode.className = 'context-node'; contextNode.setAttribute('selectedIndex', 0); editor.returnHandler = () => { contextNode.setAttribute('selectedIndex', 0); _updateAddToNote(); }; var head = document.createXULElement('hbox'); head.style.display = 'flex'; async function _createNoteFromAnnotations(child) { var attachment = _getCurrentAttachment(); if (!attachment) { return; } var note = await Zotero.EditorInstance.createNoteFromAnnotations( attachment.getAnnotations().filter(x => x.annotationType != 'ink'), child && attachment.parentID ); _updateAddToNote(); input.value = ''; _updateNotesList(); _setPinnedNote(note.id); } function _createNote(child) { contextNode.setAttribute('selectedIndex', 1); var item = new Zotero.Item('note'); item.libraryID = libraryID; if (child) { var attachment = _getCurrentAttachment(); if (!attachment) { return; } item.parentID = attachment.parentID; } editor.mode = 'edit'; editor.item = item; editor.parentItem = null; editor.focus(); _updateAddToNote(); input.value = ''; _updateNotesList(); } var vbox = document.createXULElement('vbox'); vbox.style.flex = '1'; var input = document.createXULElement('search-textbox'); input.style.margin = '4px 7px'; input.setAttribute('type', 'search'); input.setAttribute('timeout', '250'); input.addEventListener('command', () => { notesListRef.current.setExpanded(false); _updateNotesList(); }); vbox.append(input); head.append(vbox); var listBox = document.createXULElement('vbox'); listBox.style.display = 'flex'; listBox.setAttribute('flex', '1'); var listInner = document.createElement('div'); listInner.className = 'notes-list-container'; // Otherwise it can be focused with tab listInner.tabIndex = -1; listBox.append(listInner); list.append(head, listBox); var notesListRef = React.createRef(); function _isVisible() { let splitter = Zotero.Prefs.get('layout') == 'stacked' ? _contextPaneSplitterStacked : _contextPaneSplitter; return Zotero_Tabs.selectedID != 'zotero-pane' && _panesDeck.selectedIndex == 1 && context.node.selectedIndex == 0 && splitter.getAttribute('state') != 'collapsed'; } async function _updateNotesList(useCached) { var query = input.value; var notes; // Calls itself and debounces until notes list becomes // visible, and then updates if (!useCached && !_isVisible()) { context.update(); return; } if (useCached && context.cachedNotes.length) { notes = context.cachedNotes; } else { await Zotero.Schema.schemaUpdatePromise; var s = new Zotero.Search(); s.addCondition('libraryID', 'is', libraryID); s.addCondition('itemType', 'is', 'note'); if (query) { let parts = Zotero.SearchConditions.parseSearchString(query); for (let part of parts) { s.addCondition('note', 'contains', part.text); } } notes = await s.search(); notes = Zotero.Items.get(notes); if (Zotero.Prefs.get('sortNotesChronologically.reader')) { notes.sort((a, b) => { a = a.dateModified; b = b.dateModified; return (a > b ? -1 : (a < b ? 1 : 0)); }); } else { let collation = Zotero.getLocaleCollation(); notes.sort((a, b) => { let aTitle = Zotero.Items.getSortTitle(a.getNoteTitle()); let bTitle = Zotero.Items.getSortTitle(b.getNoteTitle()); return collation.compareString(1, aTitle, bTitle); }); } let cachedNotesIndex = new Map(); for (let cachedNote of context.cachedNotes) { cachedNotesIndex.set(cachedNote.id, cachedNote); } notes = notes.map(note => { var parentItem = note.parentItem; // If neither note nor parent item is affected try to return the cached note if (!context.affectedIDs.has(note.id) && (!parentItem || !context.affectedIDs.has(parentItem.id))) { let cachedNote = cachedNotesIndex.get(note.id); if (cachedNote) { return cachedNote; } } var text = note.note; text = Zotero.Utilities.unescapeHTML(text); text = text.trim(); text = text.slice(0, 500); var parts = text.split('\n').map(x => x.trim()).filter(x => x.length); var title = parts[0] && parts[0].slice(0, Zotero.Notes.MAX_TITLE_LENGTH); var date = Zotero.Date.sqlToDate(note.dateModified, true); date = Zotero.Date.toFriendlyDate(date); return { id: note.id, title: title || Zotero.getString('pane.item.notes.untitled'), body: parts[1] || '', date, parentID: note.parentID, parentItemType: parentItem && parentItem.itemType, parentTitle: parentItem && parentItem.getDisplayTitle() }; }); context.cachedNotes = notes; context.affectedIDs = new Set(); } var attachment = _getCurrentAttachment(); var parentID = attachment && attachment.parentID; notesListRef.current.setHasParent(!!parentID); notesListRef.current.setNotes(notes.map(note => ({ ...note, isCurrentChild: parentID && note.parentID == parentID }))); } var context = { libraryID, node: contextNode, editor, notesListRef, cachedNotes: [], affectedIDs: new Set(), update: Zotero.Utilities.throttle(_updateNotesList, 1000, { leading: false }), updateFromCache: () => _updateNotesList(true) }; function _handleListPopupClick(id, event) { switch (event.originalTarget.id) { case 'context-pane-list-show-in-library': ZoteroPane_Local.selectItem(id); Zotero_Tabs.select('zotero-pane'); break; case 'context-pane-list-edit-in-window': ZoteroPane_Local.openNoteWindow(id); break; case 'context-pane-list-move-to-trash': if (!readOnly) { Zotero.Items.trashTx(id); context.cachedNotes = context.cachedNotes.filter(x => x.id != id); _updateNotesList(true); } break; default: } } function _handleAddChildNotePopupClick(event) { if (readOnly) { return; } switch (event.originalTarget.id) { case 'context-pane-add-child-note': _createNote(true); break; case 'context-pane-add-child-note-from-annotations': _createNoteFromAnnotations(true); break; default: } } function _handleAddStandaloneNotePopupClick(event) { if (readOnly) { return; } switch (event.originalTarget.id) { case 'context-pane-add-standalone-note': _createNote(); break; case 'context-pane-add-standalone-note-from-annotations': _createNoteFromAnnotations(); break; default: } } ReactDOM.render( { _setPinnedNote(id); }} onContextMenu={(id, event) => { document.getElementById('context-pane-list-move-to-trash').setAttribute('disabled', readOnly); var popup = document.getElementById('context-pane-list-popup'); popup.onclick = (event) => _handleListPopupClick(id, event); popup.openPopupAtScreen(event.screenX, event.screenY); }} onAddChildButtonDown={(event) => { document.getElementById('context-pane-add-child-note').setAttribute('disabled', readOnly); document.getElementById('context-pane-add-child-note-from-annotations').setAttribute('disabled', readOnly); var popup = document.getElementById('context-pane-add-child-note-button-popup'); popup.onclick = _handleAddChildNotePopupClick; popup.openPopup(event.target, 'after_end'); }} onAddStandaloneButtonDown={(event) => { document.getElementById('context-pane-add-standalone-note').setAttribute('disabled', readOnly); document.getElementById('context-pane-add-standalone-note-from-annotations').setAttribute('disabled', readOnly); var popup = document.getElementById('context-pane-add-standalone-note-button-popup'); popup.onclick = _handleAddStandaloneNotePopupClick; popup.openPopup(event.target, 'after_end'); }} />, listInner, () => { _updateNotesList(); } ); _notesContexts.push(context); return context; } function _getNotesContext(libraryID) { var context = _notesContexts.find(x => x.libraryID == libraryID); if (!context) { context = _addNotesContext(libraryID); } return context; } function _selectNotesContext(libraryID) { let context = _getNotesContext(libraryID); _notesPaneDeck.setAttribute('selectedIndex', Array.from(_notesPaneDeck.children).findIndex(x => x == context.node)); } function _removeNotesContext(libraryID) { var context = _notesContexts.find(x => x.libraryID == libraryID); context.node.remove(); _notesContexts = _notesContexts.filter(x => x.libraryID != libraryID); } function _isLibraryReadOnly(libraryID) { return !Zotero.Libraries.get(libraryID).editable; } function _setPinnedNote(itemID) { var item = Zotero.Items.get(itemID); if (!item) { return; } var readOnly = _isLibraryReadOnly(item.libraryID); var context = _getNotesContext(item.libraryID); if (context) { var { editor, node } = context; node.setAttribute('selectedIndex', 1); editor.mode = readOnly ? 'view' : 'edit'; editor.item = item; editor.parentItem = null; node.querySelector('.zotero-context-pane-editor-parent-line').innerHTML = ''; var parentItem = item.parentItem; if (parentItem) { var container = document.createElement('div'); var img = document.createElement('img'); img.src = Zotero.ItemTypes.getImageSrc(parentItem.itemType); img.className = 'parent-item-type'; var title = document.createElement('div'); title.append(parentItem.getDisplayTitle()); title.className = 'parent-title'; container.append(img, title); node.querySelector('.zotero-context-pane-editor-parent-line').append(container); } _updateAddToNote(); } } function _removeItemContext(tabID) { document.getElementById(tabID + '-context').remove(); _itemContexts = _itemContexts.filter(x => x.tabID != tabID); } function _selectItemContext(tabID) { let selectedIndex = Array.from(_itemPaneDeck.children).findIndex(x => x.id == tabID + '-context'); if (selectedIndex != -1) { _itemPaneDeck.setAttribute('selectedIndex', selectedIndex); } } async function _addItemContext(tabID, itemID) { var container = document.createXULElement('vbox'); container.id = tabID + '-context'; container.className = 'zotero-item-pane-content'; _itemPaneDeck.appendChild(container); var { libraryID } = Zotero.Items.getLibraryAndKeyFromID(itemID); var library = Zotero.Libraries.get(libraryID); await library.waitForDataLoad('item'); var item = Zotero.Items.get(itemID); if (!item) { return; } var libraryID = item.libraryID; var readOnly = _isLibraryReadOnly(libraryID); var parentID = item.parentID; var context = { tabID, itemID, parentID, libraryID, update: () => {} }; _itemContexts.push(context); if (!parentID) { var vbox = document.createXULElement('vbox'); vbox.setAttribute('flex', '1'); vbox.setAttribute('align', 'center'); vbox.setAttribute('pack', 'center'); var description = document.createXULElement('description'); vbox.append(description); description.append(Zotero.getString('pane.context.noParent')); container.append(vbox); return; } var parentItem = Zotero.Items.get(item.parentID); // Dynamically create item pane tabs and panels as in itemPane.xul. // Keep the code below in sync with itemPane.xul // tabbox var tabbox = document.createXULElement('tabbox'); tabbox.setAttribute('flex', '1'); tabbox.className = 'zotero-view-tabbox'; container.append(tabbox); // tabs var tabs = document.createXULElement('tabs'); tabs.className = 'zotero-editpane-tabs'; // tabpanels var tabpanels = document.createXULElement('tabpanels'); tabpanels.setAttribute('flex', '1'); tabpanels.className = 'zotero-view-item'; tabpanels.addEventListener('select', () => { _updateAddToNote(); }); // Info tab var tabInfo = document.createXULElement('tab'); tabInfo.setAttribute('label', Zotero.Intl.strings['zotero.tabs.info.label']); // Tags tab var tabTags = document.createXULElement('tab'); tabTags.setAttribute('label', Zotero.Intl.strings['zotero.tabs.tags.label']); // Related tab var tabRelated = document.createXULElement('tab'); tabRelated.setAttribute('label', Zotero.Intl.strings['zotero.tabs.related.label']); tabs.append(tabInfo, tabTags, tabRelated); tabbox.append(tabs, tabpanels); // Info panel var panelInfo = document.createXULElement('tabpanel'); panelInfo.setAttribute('flex', '1'); panelInfo.className = 'zotero-editpane-item-box'; var itemBox = new (customElements.get('item-box')); itemBox.setAttribute('flex', '1'); panelInfo.append(itemBox); // Tags panel var panelTags = document.createXULElement('tabpanel'); var tagsBox = new (customElements.get('tags-box')); tagsBox.setAttribute('flex', '1'); tagsBox.className = 'zotero-editpane-tags'; panelTags.append(tagsBox); // Related panel var panelRelated = document.createXULElement('tabpanel'); var relatedBox = new (customElements.get('related-box')); relatedBox.setAttribute('flex', '1'); relatedBox.className = 'zotero-editpane-related'; panelRelated.addEventListener('click', (event) => { if (event.originalTarget.closest('.zotero-clicky')) { Zotero_Tabs.select('zotero-pane'); } }); panelRelated.append(relatedBox); tabpanels.append(panelInfo, panelTags, panelRelated); tabbox.selectedIndex = 0; itemBox.mode = readOnly ? 'view' : 'edit'; itemBox.item = parentItem; tagsBox.mode = readOnly ? 'view' : 'edit'; tagsBox.item = parentItem; relatedBox.mode = readOnly ? 'view' : 'edit'; relatedBox.item = parentItem; } };