From fc15439fd6002e0c29e2b917ae4706f221875f9b Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Wed, 24 Apr 2019 10:31:05 +0300 Subject: [PATCH 01/56] Keep custom metadata field values in array. --- src/article/converter/jats/FigurePanelConverter.js | 6 +++--- src/article/models/CustomMetadataField.js | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/article/converter/jats/FigurePanelConverter.js b/src/article/converter/jats/FigurePanelConverter.js index 8095be804..3d2e2dc79 100644 --- a/src/article/converter/jats/FigurePanelConverter.js +++ b/src/article/converter/jats/FigurePanelConverter.js @@ -59,11 +59,11 @@ export default class FigurePanelConverter { let kwdEls = kwdGroupEl.findAll('kwd') let labelEl = kwdGroupEl.find('label') let name = labelEl ? labelEl.textContent : '' - let value = kwdEls.map(kwdEl => kwdEl.textContent).join(', ') + let values = kwdEls.map(kwdEl => kwdEl.textContent) return doc.create({ type: 'custom-metadata-field', name, - value + values }).id }) } @@ -109,7 +109,7 @@ export default class FigurePanelConverter { let kwdGroupEl = $$('kwd-group').append( $$('label').text(field.name) ) - let kwdEls = field.value.split(',').map(str => { + let kwdEls = field.values.map(str => { return $$('kwd').text(str.trim()) }) kwdGroupEl.append(kwdEls) diff --git a/src/article/models/CustomMetadataField.js b/src/article/models/CustomMetadataField.js index 534a0e83f..ca33ef026 100644 --- a/src/article/models/CustomMetadataField.js +++ b/src/article/models/CustomMetadataField.js @@ -1,4 +1,4 @@ -import { DocumentNode, STRING } from 'substance' +import { DocumentNode, STRING, STRING_ARRAY } from 'substance' export default class CustomMetadataField extends DocumentNode { static getTemplate () { @@ -14,8 +14,5 @@ export default class CustomMetadataField extends DocumentNode { CustomMetadataField.schema = { type: 'custom-metadata-field', name: STRING, - // ATTENTION: for now a field consist only of one plain-text value - // user may use ',' to separate values - // later on we might opt for a structural approach - value: STRING + values: STRING_ARRAY } From bd84c0bd62346f5a35636119ac1d82ea5927bf3f Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 25 Apr 2019 10:55:52 +0300 Subject: [PATCH 02/56] Use keyword input for keyword editing. --- .../shared/CustomMetadataFieldComponent.js | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/article/shared/CustomMetadataFieldComponent.js b/src/article/shared/CustomMetadataFieldComponent.js index 6e4da3215..67abf6542 100644 --- a/src/article/shared/CustomMetadataFieldComponent.js +++ b/src/article/shared/CustomMetadataFieldComponent.js @@ -1,12 +1,26 @@ import { NodeComponent } from '../../kit' +import KeywordInput from '../../kit/ui/KeywordInput' export default class CustomMetadataFieldComponent extends NodeComponent { render ($$) { let el = $$('div').addClass('sc-custom-metadata-field') - el.append( - this._renderValue($$, 'name', { placeholder: this.getLabel('enter-custom-field-name') }).addClass('se-field-name'), - this._renderValue($$, 'value', { placeholder: this.getLabel('enter-custom-field-value') }) - ) + const node = this._getNode() + + if (this.context.editable) { + el.append( + this._renderValue($$, 'name', { placeholder: this.getLabel('enter-custom-field-name') }).addClass('se-field-name'), + $$(KeywordInput, { + values: node.values, + placeholder: this.getLabel('enter-custom-field-value'), + overlayId: node.id + }).addClass('se-field-values') + ) + } else { + el.append( + $$('div').addClass('se-field-name').append(node.name), + $$('div').addClass('se-field-values').append(node.values) + ) + } return el } } From ec9a508fce7208bc3080450f93d48ab1a7961f29 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 25 Apr 2019 10:56:47 +0300 Subject: [PATCH 03/56] Initial keyword editor version. --- src/kit/ui/KeywordInput.js | 76 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/kit/ui/KeywordInput.js diff --git a/src/kit/ui/KeywordInput.js b/src/kit/ui/KeywordInput.js new file mode 100644 index 000000000..9c75ba54f --- /dev/null +++ b/src/kit/ui/KeywordInput.js @@ -0,0 +1,76 @@ +import { Component } from 'substance' +import OverlayMixin from './OverlayMixin' + +export default class KeywordInput extends OverlayMixin(Component) { + getInitialState () { + return { + isExpanded: this._canShowOverlay() + } + } + + willReceiveProps () { + this.extendState(this.getInitialState()) + } + + render ($$) { + const values = this.props.values + const isEmpty = values.length === 0 + const isExpanded = this.state.isExpanded + + const el = $$('div').addClass('sc-keyword-input') + if (isEmpty) el.addClass('sm-empty') + el.addClass(isExpanded ? 'sm-expanded' : 'sm-collapsed') + el.append( + $$('div').addClass('se-label').text(values.join(', ')) + ) + if (isExpanded) { + el.addClass('sm-active') + el.append( + this._renderEditor($$) + ) + } + el.on('click', this._onClick) + .on('dblclick', this._stopAndPreventDefault) + .on('mousedown', this._stopAndPreventDefault) + + return el + } + + _renderEditor ($$) { + const values = this.props.values + const editorEl = $$('div').ref('editor').addClass('se-keyword-editor') + values.forEach(value => { + editorEl.append( + $$('div').addClass('se-keyword').append( + $$('div').addClass('se-keyword-input').append(value), + this.context.iconProvider.renderIcon($$, 'remove-button').addClass('se-icon') + .on('click', this._removeKeyword) + ) + ) + }) + return editorEl + } + + _getOverlayId () { + return this.props.overlayId || this.getId() + } + + _onClick (event) { + this._stopAndPreventDefault(event) + super._toggleOverlay() + } + + _onOverlayIdHasChanged () { + let overlayId = this.context.appState.overlayId + let id = this._getOverlayId() + let needUpdate = false + if (this.state.isExpanded) { + needUpdate = (overlayId !== id) + } else { + needUpdate = (overlayId === id) + } + if (needUpdate) { + this.extendState(this.getInitialState()) + } + } +} From 01ad89088dbe9be681e331d99d33ed9617251ebc Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 25 Apr 2019 10:58:32 +0300 Subject: [PATCH 04/56] Keep event _stopAndPreventDefault helper in overlay mixin. --- src/kit/ui/OverlayMixin.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/kit/ui/OverlayMixin.js b/src/kit/ui/OverlayMixin.js index 5da1e3e24..185d2e649 100644 --- a/src/kit/ui/OverlayMixin.js +++ b/src/kit/ui/OverlayMixin.js @@ -30,6 +30,11 @@ export default function (Component) { // console.log('Rerendering overlay component because overlay id has changed', this._getOverlayId()') this.rerender() } + + _stopAndPreventDefault (event) { + event.stopPropagation() + event.preventDefault() + } } return OverlayComponent } From de23d498f2c539c013f6667559c90d5808169221 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Thu, 25 Apr 2019 14:15:33 +0300 Subject: [PATCH 05/56] Fix typo. --- src/article/shared/FigureMetadataComponent.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/article/shared/FigureMetadataComponent.js b/src/article/shared/FigureMetadataComponent.js index 37ba37a9b..a92136f04 100644 --- a/src/article/shared/FigureMetadataComponent.js +++ b/src/article/shared/FigureMetadataComponent.js @@ -15,7 +15,7 @@ export default class FigureMetadataComponent extends ValueComponent { } _renderMetadataField ($$, metadataField) { - let MetdataFieldComponent = this.getComponent(metadataField.type) - return $$(MetdataFieldComponent, { node: metadataField }).ref(metadataField.id) + let MetadataFieldComponent = this.getComponent(metadataField.type) + return $$(MetadataFieldComponent, { node: metadataField }).ref(metadataField.id) } } From 378b70879d6f036a4f29c3ba0598e78f8dd74539 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Thu, 25 Apr 2019 14:16:02 +0300 Subject: [PATCH 06/56] WIP: keyword input widget. --- src/article/manuscript/ManuscriptPackage.js | 1 + .../shared/CustomMetadataFieldComponent.js | 27 +++++++++++--- src/kit/ui/KeywordInput.js | 36 ++++++++++++++++--- src/kit/ui/index.js | 1 + 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/src/article/manuscript/ManuscriptPackage.js b/src/article/manuscript/ManuscriptPackage.js index 2780e58a7..87561cf5c 100644 --- a/src/article/manuscript/ManuscriptPackage.js +++ b/src/article/manuscript/ManuscriptPackage.js @@ -306,6 +306,7 @@ export default { config.addLabel('article-info', 'Article Information') config.addLabel('article-record', 'Article Record') config.addLabel('contributors', 'Authors & Contributors') + config.addLabel('create', 'Create') config.addLabel('create-unordered-list', 'Bulleted list') config.addLabel('create-ordered-list', 'Numbered list') config.addLabel('edit-ref', 'Edit Reference') diff --git a/src/article/shared/CustomMetadataFieldComponent.js b/src/article/shared/CustomMetadataFieldComponent.js index 67abf6542..a5a53ab1b 100644 --- a/src/article/shared/CustomMetadataFieldComponent.js +++ b/src/article/shared/CustomMetadataFieldComponent.js @@ -1,26 +1,43 @@ -import { NodeComponent } from '../../kit' -import KeywordInput from '../../kit/ui/KeywordInput' +import { createValueModel, KeywordInput, NodeComponent } from '../../kit' export default class CustomMetadataFieldComponent extends NodeComponent { + getActionHandlers () { + return { + updateValues: this._updateValues + } + } + render ($$) { let el = $$('div').addClass('sc-custom-metadata-field') const node = this._getNode() + const valuesModel = this._getValuesModel() if (this.context.editable) { el.append( this._renderValue($$, 'name', { placeholder: this.getLabel('enter-custom-field-name') }).addClass('se-field-name'), $$(KeywordInput, { - values: node.values, + values: valuesModel.getValue(), + model: valuesModel, placeholder: this.getLabel('enter-custom-field-value'), - overlayId: node.id + overlayId: valuesModel.id }).addClass('se-field-values') ) } else { el.append( $$('div').addClass('se-field-name').append(node.name), - $$('div').addClass('se-field-values').append(node.values) + $$('div').addClass('se-field-values').append(valuesModel.getValue()) ) } return el } + + _updateValues (values) { + const model = this._getValuesModel() + model.setValue(values) + } + + _getValuesModel () { + const node = this._getNode() + return createValueModel(this.context.api, [node.id, 'values']) + } } diff --git a/src/kit/ui/KeywordInput.js b/src/kit/ui/KeywordInput.js index 9c75ba54f..a159fb63b 100644 --- a/src/kit/ui/KeywordInput.js +++ b/src/kit/ui/KeywordInput.js @@ -38,19 +38,37 @@ export default class KeywordInput extends OverlayMixin(Component) { _renderEditor ($$) { const values = this.props.values + const Button = this.getComponent('button') + const Input = this.getComponent('input') const editorEl = $$('div').ref('editor').addClass('se-keyword-editor') - values.forEach(value => { + values.forEach((value, idx) => { editorEl.append( $$('div').addClass('se-keyword').append( - $$('div').addClass('se-keyword-input').append(value), - this.context.iconProvider.renderIcon($$, 'remove-button').addClass('se-icon') - .on('click', this._removeKeyword) + $$('div').addClass('se-keyword-input').append( + $$(Input, { + path: this.props.model.getPath().concat(idx) + }) + ), + this._renderIcon($$, 'trash').on('click', this._removeKeyword.bind(this, idx)) ) ) }) + editorEl.append( + $$('div').addClass('se-keyword-input').append(''), + $$(Button, { + label: this.getLabel('create') + }).addClass('se-create-value') + .on('click', this._addKeyword) + ) return editorEl } + _renderIcon ($$, iconName) { + return $$('div').addClass('se-icon').append( + this.context.iconProvider.renderIcon($$, iconName) + ) + } + _getOverlayId () { return this.props.overlayId || this.getId() } @@ -73,4 +91,14 @@ export default class KeywordInput extends OverlayMixin(Component) { this.extendState(this.getInitialState()) } } + + _addKeyword () { + + } + + _removeKeyword (idx) { + const values = this.props.values + values.splice(idx, 1) + this.send('updateValues', values) + } } diff --git a/src/kit/ui/index.js b/src/kit/ui/index.js index d298e8053..fa6fcd847 100644 --- a/src/kit/ui/index.js +++ b/src/kit/ui/index.js @@ -17,6 +17,7 @@ export { default as getComponentForModel } from './getComponentForModel' export { default as InputWithButton } from './InputWithButton' export { default as IsolatedInlineNodeComponent } from './_IsolatedInlineNodeComponent' export { default as IsolatedNodeComponent } from './_IsolatedNodeComponent' +export { default as KeywordInput } from './KeywordInput' export { default as Managed } from './Managed' export { default as ManyRelationshipComponent } from './ManyRelationshipComponent' export { default as ModalDialog } from './ModalDialog' From 3d5d4121433aad7983c36ea24ad651eddd271ca0 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 26 Apr 2019 13:00:46 +0300 Subject: [PATCH 07/56] Comment out current overlay resetting strategy. --- src/kit/app/EditorSession.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/kit/app/EditorSession.js b/src/kit/app/EditorSession.js index d5052f074..983832383 100644 --- a/src/kit/app/EditorSession.js +++ b/src/kit/app/EditorSession.js @@ -197,18 +197,18 @@ export default class EditorSession extends AbstractEditorSession { } _resetOverlayId () { - const overlayId = this.editorState.overlayId - // overlayId === getKeyForPath(path) => if selection is value && - // Overlays of value components (ManyRelationshipComponent, SingleRelationship) - // need to remain open if the selection is a value selection - let sel = this.getSelection() - if (sel && sel.customType === 'value') { - let valueId = getKeyForPath(sel.data.path) - if (overlayId !== valueId) { - this.editorState.set('overlayId', valueId) - } - } else { - this.editorState.set('overlayId', null) - } + // const overlayId = this.editorState.overlayId + // // overlayId === getKeyForPath(path) => if selection is value && + // // Overlays of value components (ManyRelationshipComponent, SingleRelationship) + // // need to remain open if the selection is a value selection + // let sel = this.getSelection() + // if (sel && sel.customType === 'value') { + // let valueId = getKeyForPath(sel.data.path) + // if (overlayId !== valueId) { + // this.editorState.set('overlayId', valueId) + // } + // } else { + // this.editorState.set('overlayId', null) + // } } } From 5717f0bbeac1c54fcbef55a4673123a6b6def388 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 26 Apr 2019 13:01:03 +0300 Subject: [PATCH 08/56] Use surface for keyword editing. --- .../shared/CustomMetadataFieldComponent.js | 1 - src/kit/ui/KeywordInput.js | 19 +++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/article/shared/CustomMetadataFieldComponent.js b/src/article/shared/CustomMetadataFieldComponent.js index a5a53ab1b..d79c61353 100644 --- a/src/article/shared/CustomMetadataFieldComponent.js +++ b/src/article/shared/CustomMetadataFieldComponent.js @@ -16,7 +16,6 @@ export default class CustomMetadataFieldComponent extends NodeComponent { el.append( this._renderValue($$, 'name', { placeholder: this.getLabel('enter-custom-field-name') }).addClass('se-field-name'), $$(KeywordInput, { - values: valuesModel.getValue(), model: valuesModel, placeholder: this.getLabel('enter-custom-field-value'), overlayId: valuesModel.id diff --git a/src/kit/ui/KeywordInput.js b/src/kit/ui/KeywordInput.js index a159fb63b..c3abf74f9 100644 --- a/src/kit/ui/KeywordInput.js +++ b/src/kit/ui/KeywordInput.js @@ -1,5 +1,6 @@ -import { Component } from 'substance' +import { Component, getKeyForPath } from 'substance' import OverlayMixin from './OverlayMixin' +import TextInput from './TextInput' export default class KeywordInput extends OverlayMixin(Component) { getInitialState () { @@ -13,7 +14,8 @@ export default class KeywordInput extends OverlayMixin(Component) { } render ($$) { - const values = this.props.values + const model = this.props.model + const values = model.getValue() const isEmpty = values.length === 0 const isExpanded = this.state.isExpanded @@ -37,16 +39,21 @@ export default class KeywordInput extends OverlayMixin(Component) { } _renderEditor ($$) { - const values = this.props.values + const model = this.props.model + const values = model.getValue() + const placeholder = this.props.placeholder const Button = this.getComponent('button') - const Input = this.getComponent('input') const editorEl = $$('div').ref('editor').addClass('se-keyword-editor') values.forEach((value, idx) => { + const path = model.getPath().concat(idx) + const name = getKeyForPath(path) editorEl.append( $$('div').addClass('se-keyword').append( $$('div').addClass('se-keyword-input').append( - $$(Input, { - path: this.props.model.getPath().concat(idx) + $$(TextInput, { + name, + path, + placeholder }) ), this._renderIcon($$, 'trash').on('click', this._removeKeyword.bind(this, idx)) From dd7ab8000b01214e7c054bead1a4864e5faf925b Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 26 Apr 2019 17:31:40 +0300 Subject: [PATCH 09/56] Introduce Popup component. --- src/kit/styles/_index.css | 2 + .../styles/_multi-select-input.css | 41 ------------------- src/kit/styles/_popup.css | 40 ++++++++++++++++++ src/kit/ui/MultiSelectInput.js | 10 ++--- src/kit/ui/Popup.js | 24 +++++++++++ src/kit/ui/index.js | 1 + 6 files changed, 70 insertions(+), 48 deletions(-) rename src/{article/shared => kit}/styles/_multi-select-input.css (50%) create mode 100644 src/kit/styles/_popup.css create mode 100644 src/kit/ui/Popup.js diff --git a/src/kit/styles/_index.css b/src/kit/styles/_index.css index 7e1157376..f371cb46e 100644 --- a/src/kit/styles/_index.css +++ b/src/kit/styles/_index.css @@ -3,9 +3,11 @@ @import './_find-and-replace.css'; @import './_input-with-button.css'; @import './_isolated-node.css'; +@import './_multi-select-input.css'; @import './_overlay-canvas.css'; @import './_pinned-msg.css'; @import './_placeholder.css'; +@import './_popup.css'; @import './_scroll-pane.css'; @import './_surface.css'; @import './_text-input.css'; diff --git a/src/article/shared/styles/_multi-select-input.css b/src/kit/styles/_multi-select-input.css similarity index 50% rename from src/article/shared/styles/_multi-select-input.css rename to src/kit/styles/_multi-select-input.css index cb7d609b8..cd2c076b6 100644 --- a/src/article/shared/styles/_multi-select-input.css +++ b/src/kit/styles/_multi-select-input.css @@ -18,25 +18,6 @@ font-weight: var(--t-normal-font-weight); } -.sc-multi-select-input .se-select-editor { - position: absolute; - top: 55px; - left: 200px; - background: #fff; - border: 1px solid #8a8a8a; - padding: 10px 20px; - max-width: 100%; - box-shadow: 0px 3px 7px 1px #777; - z-index: 99; -} - -.sc-multi-select-input .se-select-editor .se-select-label { - text-align: center; - font-size: 12px; - color: #8a8a8a; - margin-bottom: 10px; -} - .sc-multi-select-input .se-select-editor .se-select-item { display: flex; cursor: pointer; @@ -49,26 +30,4 @@ .sc-multi-select-input .se-select-editor .se-select-item .se-item-label { margin-left: 10px; -} - -.sc-multi-select-input .se-select-editor .se-arrow { - width: 60px; - height: 16px; - overflow: hidden; - position: absolute; - left: 50%; - margin-left: -30px; - top: -16px; -} - -.sc-multi-select-input .se-select-editor .se-arrow:after { - content: ""; - position: absolute; - left: 20px; - top: 10px; - width: 20px; - height: 20px; - transform: rotate(45deg); - border: 1px solid #8a8a8a; - background: #fff; } \ No newline at end of file diff --git a/src/kit/styles/_popup.css b/src/kit/styles/_popup.css new file mode 100644 index 000000000..091f0d84e --- /dev/null +++ b/src/kit/styles/_popup.css @@ -0,0 +1,40 @@ +.sc-popup { + position: absolute; + top: 55px; + left: 200px; + background: #fff; + border: 1px solid #8a8a8a; + padding: 10px 20px; + max-width: 100%; + box-shadow: 0px 3px 7px 1px #777; + z-index: 99; +} + +.sc-popup .se-popup-label { + text-align: center; + font-size: 12px; + color: #8a8a8a; + margin-bottom: 10px; +} + +.sc-popup .se-popup-arrow { + width: 60px; + height: 16px; + overflow: hidden; + position: absolute; + left: 50%; + margin-left: -30px; + top: -16px; +} + +.sc-popup .se-popup-arrow:after { + content: ""; + position: absolute; + left: 20px; + top: 10px; + width: 20px; + height: 20px; + transform: rotate(45deg); + border: 1px solid #8a8a8a; + background: #fff; +} \ No newline at end of file diff --git a/src/kit/ui/MultiSelectInput.js b/src/kit/ui/MultiSelectInput.js index 0a1424e07..c4bc2d3f5 100644 --- a/src/kit/ui/MultiSelectInput.js +++ b/src/kit/ui/MultiSelectInput.js @@ -1,5 +1,5 @@ import { Component } from 'substance' -import OverlayMixin from './OverlayMixin' +import { OverlayMixin, Popup } from '../../kit' export default class MultiSelectInput extends OverlayMixin(Component) { getInitialState () { @@ -43,11 +43,7 @@ export default class MultiSelectInput extends OverlayMixin(Component) { const selected = this.props.selected const selectedIdx = selected.map(item => item.id) const options = this._getOptions() - const editorEl = $$('div').ref('options').addClass('se-select-editor').append( - $$('div').addClass('se-arrow'), - $$('div').addClass('se-select-label') - .append(label) - ) + const editorEl = $$('div').ref('options').addClass('se-select-editor') options.forEach(option => { const isSelected = selectedIdx.indexOf(option.id) > -1 const icon = isSelected ? 'checked-item' : 'unchecked-item' @@ -60,7 +56,7 @@ export default class MultiSelectInput extends OverlayMixin(Component) { ).on('click', this._onToggleItem.bind(this, option)) ) }) - return editorEl + return $$(Popup, { label }).append(editorEl) } _getOverlayId () { diff --git a/src/kit/ui/Popup.js b/src/kit/ui/Popup.js new file mode 100644 index 000000000..bc3c73b09 --- /dev/null +++ b/src/kit/ui/Popup.js @@ -0,0 +1,24 @@ +import { Component } from 'substance' + +export default class Popup extends Component { + render ($$) { + const label = this.props.label + const children = this.props.children + + const el = $$('div').addClass('sc-popup').append( + $$('div').addClass('se-popup-arrow') + ) + + if (label) { + el.append( + $$('div').addClass('se-popup-label').append(label) + ) + } + + el.append( + $$('div').addClass('se-popup-content').append(children) + ) + + return el + } +} diff --git a/src/kit/ui/index.js b/src/kit/ui/index.js index fa6fcd847..97774dcb8 100644 --- a/src/kit/ui/index.js +++ b/src/kit/ui/index.js @@ -31,6 +31,7 @@ export { default as ObjectComponent } from './ObjectComponent' export { default as OverlayCanvas } from './OverlayCanvas' export { default as OverlayMixin } from './OverlayMixin' export { default as PinnedMessage } from './PinnedMessage' +export { default as Popup } from './Popup' export { default as renderModel } from './_renderModel' export { default as renderNode } from './_renderNode' export { default as renderValue } from './_renderValue' From 182d8a8bc6130cf880ae3ad6950063d0ebe95a1b Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 26 Apr 2019 21:56:33 +0300 Subject: [PATCH 10/56] Improve popup styling. --- src/kit/styles/_popup.css | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/kit/styles/_popup.css b/src/kit/styles/_popup.css index 091f0d84e..d3fb8bd76 100644 --- a/src/kit/styles/_popup.css +++ b/src/kit/styles/_popup.css @@ -1,20 +1,20 @@ .sc-popup { position: absolute; - top: 55px; - left: 200px; - background: #fff; - border: 1px solid #8a8a8a; - padding: 10px 20px; + left: 50%; + transform: translate(-50%, 10%); + background: var(--t-background-color); + border: var(--t-default-border); + border-radius: var(--t-border-radius); max-width: 100%; - box-shadow: 0px 3px 7px 1px #777; + box-shadow: var(--t-popup-box-shadow); z-index: 99; } .sc-popup .se-popup-label { text-align: center; - font-size: 12px; - color: #8a8a8a; - margin-bottom: 10px; + font-size: var(--t-tiny-font-size); + color: var(--t-light-text-color); + margin-bottom: var(--t-half-spacing); } .sc-popup .se-popup-arrow { @@ -35,6 +35,7 @@ width: 20px; height: 20px; transform: rotate(45deg); - border: 1px solid #8a8a8a; - background: #fff; + border: var(--t-default-border); + background: var(--t-background-color); + box-shadow: var(--t-popup-box-shadow); } \ No newline at end of file From 801ca6ab5c4073f7efe47f90fdebf3a304145535 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 26 Apr 2019 21:57:13 +0300 Subject: [PATCH 11/56] Add more style variables. --- src/styles/_texture.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/styles/_texture.css b/src/styles/_texture.css index 4fcd3fa87..e46d85e17 100644 --- a/src/styles/_texture.css +++ b/src/styles/_texture.css @@ -68,6 +68,8 @@ --t-placeholder-text-color: #ccc; /* Used for focus border, e.g. selected card, or text input */ --t-focus-color: rgb(145, 189, 240); + /* Used for focus border, e.g. selected card, or text input */ + --t-focus-background-color: #218df312; /* E.g. citations of references, figures, etc. */ --t-action-color: #2e72ea; /* Used to display warning icons */ @@ -97,7 +99,7 @@ --t-input-default-border: 2px solid transparent; --t-input-outline-border: 2px solid var(--t-border-color); - --t-input-focus-border: 2px solid rgb(145, 189, 240); + --t-input-focus-border: 2px solid var(--t-focus-color); --t-negative-input-padding: -6px; /* This must be the negative of input padding + default border width */ --t-negative-list-padding: -4px; /* Same, but without borders. Used in comma-separated lists. */ --t-border-radius: 5px; /* Default border radius for rounded corners */ @@ -108,7 +110,7 @@ /* ----------------------------------------------------------------------*/ --t-default-box-shadow: 0 0 0 0.75pt #d1d1d1, 0 0 3pt 0.75pt #ccc; - --t-popup-box-shadow: 0 2px 10px -2px rgba(0,0,0,0.8); + --t-popup-box-shadow: 0 2px 4px 0 var(--t-border-color); /* Substance Styles */ /* ----------------------------------------------------------------------*/ From a6e9e6bfc8ad2160bd2aac02f595d189beb4c6fa Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 26 Apr 2019 21:57:18 +0300 Subject: [PATCH 12/56] Improve multi sleect input styling. --- src/kit/styles/_multi-select-input.css | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/kit/styles/_multi-select-input.css b/src/kit/styles/_multi-select-input.css index cd2c076b6..ca750e899 100644 --- a/src/kit/styles/_multi-select-input.css +++ b/src/kit/styles/_multi-select-input.css @@ -10,7 +10,7 @@ .sc-multi-select-input.sm-active { border: var(--t-input-focus-border); - background: #218df312; + background: var(--t-focus-background-color); } .sc-multi-select-input.sm-empty .se-label { @@ -18,6 +18,10 @@ font-weight: var(--t-normal-font-weight); } +.sc-multi-select-input .se-select-editor { + padding: var(--t-half-spacing) var(--t-default-spacing); +} + .sc-multi-select-input .se-select-editor .se-select-item { display: flex; cursor: pointer; @@ -29,5 +33,5 @@ } .sc-multi-select-input .se-select-editor .se-select-item .se-item-label { - margin-left: 10px; + margin-left: var(--t-half-spacing); } \ No newline at end of file From 5a1a661f4eb29b331ad7057ffc44360fc8c258d7 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 26 Apr 2019 21:57:56 +0300 Subject: [PATCH 13/56] Improve keyword input styling. --- .../shared/styles/_custom-metadata-field.css | 3 +- src/kit/styles/_index.css | 1 + src/kit/styles/_keyword-input.css | 38 +++++++++++++++++++ src/kit/ui/KeywordInput.js | 23 ++++++----- 4 files changed, 54 insertions(+), 11 deletions(-) create mode 100644 src/kit/styles/_keyword-input.css diff --git a/src/article/shared/styles/_custom-metadata-field.css b/src/article/shared/styles/_custom-metadata-field.css index bd46e46d8..c207b25a3 100644 --- a/src/article/shared/styles/_custom-metadata-field.css +++ b/src/article/shared/styles/_custom-metadata-field.css @@ -4,7 +4,8 @@ font-size: var(--t-small-font-size); } -.sc-custom-metadata-field > .sc-string { +.sc-custom-metadata-field > .sc-string, +.sc-custom-metadata-field > .sc-keyword-input { flex: 1; } diff --git a/src/kit/styles/_index.css b/src/kit/styles/_index.css index f371cb46e..e81400d9b 100644 --- a/src/kit/styles/_index.css +++ b/src/kit/styles/_index.css @@ -3,6 +3,7 @@ @import './_find-and-replace.css'; @import './_input-with-button.css'; @import './_isolated-node.css'; +@import './_keyword-input.css'; @import './_multi-select-input.css'; @import './_overlay-canvas.css'; @import './_pinned-msg.css'; diff --git a/src/kit/styles/_keyword-input.css b/src/kit/styles/_keyword-input.css new file mode 100644 index 000000000..8d23c8871 --- /dev/null +++ b/src/kit/styles/_keyword-input.css @@ -0,0 +1,38 @@ +.sc-keyword-input { + position: relative; + width: 100%; + line-height: var(--t-input-line-height); + cursor: pointer; + padding: var(--t-input-padding); + margin: var(--t-negative-input-padding); + border-radius: var(--t-border-radius); + border: var(--t-input-default-border); +} + +.sc-keyword-input.sm-active { + border: var(--t-input-focus-border); + background: var(--t-focus-background-color); +} + +.sc-keyword-input .se-keyword { + display: flex; + align-items: center; + border-bottom: var(--t-default-border); +} + +.sc-keyword-input .se-keyword > * { + margin-right: var(--t-half-spacing); +} + +.sc-keyword-input .se-keyword .se-keyword-input { + flex: 1 1 auto; +} + +.sc-keyword-input .sc-text-input .se-input { + border-radius: 0; + margin: 0; +} + +.sc-keyword-input .sc-text-input:focus .se-input { + border: var(--t-input-default-border); +} \ No newline at end of file diff --git a/src/kit/ui/KeywordInput.js b/src/kit/ui/KeywordInput.js index c3abf74f9..243d76d8f 100644 --- a/src/kit/ui/KeywordInput.js +++ b/src/kit/ui/KeywordInput.js @@ -1,6 +1,5 @@ import { Component, getKeyForPath } from 'substance' -import OverlayMixin from './OverlayMixin' -import TextInput from './TextInput' +import { InputWithButton, OverlayMixin, Popup, TextInput } from '../../kit' export default class KeywordInput extends OverlayMixin(Component) { getInitialState () { @@ -39,10 +38,12 @@ export default class KeywordInput extends OverlayMixin(Component) { } _renderEditor ($$) { - const model = this.props.model + const { model, placeholder } = this.props const values = model.getValue() - const placeholder = this.props.placeholder + const Button = this.getComponent('button') + const Input = this.getComponent('input') + const editorEl = $$('div').ref('editor').addClass('se-keyword-editor') values.forEach((value, idx) => { const path = model.getPath().concat(idx) @@ -61,13 +62,15 @@ export default class KeywordInput extends OverlayMixin(Component) { ) }) editorEl.append( - $$('div').addClass('se-keyword-input').append(''), - $$(Button, { - label: this.getLabel('create') - }).addClass('se-create-value') - .on('click', this._addKeyword) + $$('div').addClass('se-keyword-input').append( + $$(Input, { placeholder }).ref('urlInput'), + $$(Button).append( + this.getLabel('create') + ).addClass('se-create-value') + .on('click', this._addKeyword) + ) ) - return editorEl + return $$(Popup).append(editorEl) } _renderIcon ($$, iconName) { From 07b70edac5365ef6a4697369e9f3bdc9163cc9a4 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Sat, 27 Apr 2019 18:46:16 +0300 Subject: [PATCH 14/56] Set lower z-index. --- src/kit/styles/_popup.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kit/styles/_popup.css b/src/kit/styles/_popup.css index d3fb8bd76..1ed85966e 100644 --- a/src/kit/styles/_popup.css +++ b/src/kit/styles/_popup.css @@ -7,7 +7,7 @@ border-radius: var(--t-border-radius); max-width: 100%; box-shadow: var(--t-popup-box-shadow); - z-index: 99; + z-index: 10; } .sc-popup .se-popup-label { From 0d631845ef1508128ec3f439344932998b843a0c Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Sat, 27 Apr 2019 18:46:38 +0300 Subject: [PATCH 15/56] Improve keyword input styling. --- src/kit/styles/_keyword-input.css | 25 +++++++++++++++++++++++++ src/kit/ui/KeywordInput.js | 6 ++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/kit/styles/_keyword-input.css b/src/kit/styles/_keyword-input.css index 8d23c8871..90dd7dc6f 100644 --- a/src/kit/styles/_keyword-input.css +++ b/src/kit/styles/_keyword-input.css @@ -20,6 +20,10 @@ border-bottom: var(--t-default-border); } +.sc-keyword-input .se-keyword:last-child { + border-bottom: none; +} + .sc-keyword-input .se-keyword > * { margin-right: var(--t-half-spacing); } @@ -35,4 +39,25 @@ .sc-keyword-input .sc-text-input:focus .se-input { border: var(--t-input-default-border); +} + +.sc-keyword-input .se-keyword-input > .sc-input { + width: 100%; + height: var(--t-input-height); + padding: var(--t-input-padding); + font-size: var(--t-small-font-size); + line-height: var(--t-input-line-height); + border: var(--t-input-default-border); + border-radius: var(--t-border-radius); +} + +.sc-keyword-input .se-keyword-input > .sc-input::placeholder { + color: var(--t-placeholder-text-color); + font-weight: var(--t-normal-font-weight); +} +.sc-keyword-input .se-create-value { + background: var(--t-action-background-color); + color: var(--t-inverted-text-color); + border-radius: var(--t-border-radius); + padding: 2px 6px; } \ No newline at end of file diff --git a/src/kit/ui/KeywordInput.js b/src/kit/ui/KeywordInput.js index 243d76d8f..5f4672c69 100644 --- a/src/kit/ui/KeywordInput.js +++ b/src/kit/ui/KeywordInput.js @@ -62,8 +62,10 @@ export default class KeywordInput extends OverlayMixin(Component) { ) }) editorEl.append( - $$('div').addClass('se-keyword-input').append( - $$(Input, { placeholder }).ref('urlInput'), + $$('div').addClass('se-keyword').append( + $$('div').addClass('se-keyword-input').append( + $$(Input, { placeholder }).ref('urlInput') + ), $$(Button).append( this.getLabel('create') ).addClass('se-create-value') From 3a2fd58e49686541060065a300cdb5189f9e81de Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Sat, 27 Apr 2019 18:47:05 +0300 Subject: [PATCH 16/56] Set width of keyword input for metadata fields. --- src/article/shared/styles/_custom-metadata-field.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/article/shared/styles/_custom-metadata-field.css b/src/article/shared/styles/_custom-metadata-field.css index c207b25a3..9aa5ef87f 100644 --- a/src/article/shared/styles/_custom-metadata-field.css +++ b/src/article/shared/styles/_custom-metadata-field.css @@ -9,6 +9,10 @@ flex: 1; } +.sc-custom-metadata-field .se-keyword-editor { + width: 300px; +} + .sc-custom-metadata-field > .se-field-name { font-weight: var(--t-bold-font-weight); } From 085827846e73cfca9e7fbe54da72b5a909abe738 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Sat, 27 Apr 2019 19:48:49 +0300 Subject: [PATCH 17/56] Add missing labels and icons for keyword input. --- src/article/manuscript/ManuscriptPackage.js | 1 + src/article/metadata/MetadataPackage.js | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/article/manuscript/ManuscriptPackage.js b/src/article/manuscript/ManuscriptPackage.js index 87561cf5c..98694f16b 100644 --- a/src/article/manuscript/ManuscriptPackage.js +++ b/src/article/manuscript/ManuscriptPackage.js @@ -333,6 +333,7 @@ export default { config.addLabel('enter-custom-field-value', 'Enter value') config.addLabel('add-action', 'Add') config.addLabel('enter-url-placeholder', 'Enter url') + config.addLabel('enter-keyword', 'Enter keyword') // Icons config.addIcon('create-unordered-list', { 'fontawesome': 'fa-list-ul' }) diff --git a/src/article/metadata/MetadataPackage.js b/src/article/metadata/MetadataPackage.js index 7ef504105..94cd52c51 100644 --- a/src/article/metadata/MetadataPackage.js +++ b/src/article/metadata/MetadataPackage.js @@ -359,11 +359,13 @@ export default { config.addLabel('subtitle', 'Subtitle') config.addLabel('empty-figure-metadata', 'No fields specified') config.addLabel('open-link', 'Open Link') + config.addLabel('enter-keyword', 'Enter keyword') // Icons config.addIcon('input-error', { 'fontawesome': 'fa-exclamation-circle' }) config.addIcon('input-loading', { 'fontawesome': 'fa-spinner fa-spin' }) config.addIcon('move-down-figure-panel', { 'fontawesome': 'fa-caret-square-o-down' }) config.addIcon('open-link', { 'fontawesome': 'fa-external-link' }) + config.addIcon('trash', { 'fontawesome': 'fa-trash' }) // TODO: need to rethink this a some point registerCollectionCommand(config, 'author', ['metadata', 'authors'], { keyboardShortcut: 'CommandOrControl+Alt+A', nodeType: 'person' }) From e46ea79e7a521297e0934029a441ea3605782b04 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Sat, 27 Apr 2019 19:49:16 +0300 Subject: [PATCH 18/56] Provide correct label for keyword input. --- src/article/shared/CustomMetadataFieldComponent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/article/shared/CustomMetadataFieldComponent.js b/src/article/shared/CustomMetadataFieldComponent.js index d79c61353..a0ffb4baa 100644 --- a/src/article/shared/CustomMetadataFieldComponent.js +++ b/src/article/shared/CustomMetadataFieldComponent.js @@ -17,7 +17,7 @@ export default class CustomMetadataFieldComponent extends NodeComponent { this._renderValue($$, 'name', { placeholder: this.getLabel('enter-custom-field-name') }).addClass('se-field-name'), $$(KeywordInput, { model: valuesModel, - placeholder: this.getLabel('enter-custom-field-value'), + placeholder: this.getLabel('enter-keyword'), overlayId: valuesModel.id }).addClass('se-field-values') ) From 995d53e3519a54962e7ff87a2c0de64d3794ea56 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Sat, 27 Apr 2019 19:49:32 +0300 Subject: [PATCH 19/56] Limit keyword input width. --- src/article/shared/styles/_custom-metadata-field.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/article/shared/styles/_custom-metadata-field.css b/src/article/shared/styles/_custom-metadata-field.css index 9aa5ef87f..b21fb7aa3 100644 --- a/src/article/shared/styles/_custom-metadata-field.css +++ b/src/article/shared/styles/_custom-metadata-field.css @@ -11,6 +11,7 @@ .sc-custom-metadata-field .se-keyword-editor { width: 300px; + max-width: 100%; } .sc-custom-metadata-field > .se-field-name { From 0477da811486d2c0ed5ab50ed50d79dfed347a8d Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Sat, 27 Apr 2019 19:49:56 +0300 Subject: [PATCH 20/56] Keyword inout imrovements. --- src/kit/ui/KeywordInput.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/kit/ui/KeywordInput.js b/src/kit/ui/KeywordInput.js index 5f4672c69..e190ee5af 100644 --- a/src/kit/ui/KeywordInput.js +++ b/src/kit/ui/KeywordInput.js @@ -1,5 +1,5 @@ import { Component, getKeyForPath } from 'substance' -import { InputWithButton, OverlayMixin, Popup, TextInput } from '../../kit' +import { OverlayMixin, Popup, TextInput } from '../../kit' export default class KeywordInput extends OverlayMixin(Component) { getInitialState () { @@ -23,6 +23,7 @@ export default class KeywordInput extends OverlayMixin(Component) { el.addClass(isExpanded ? 'sm-expanded' : 'sm-collapsed') el.append( $$('div').addClass('se-label').text(values.join(', ')) + .on('click', this._onClick) ) if (isExpanded) { el.addClass('sm-active') @@ -30,8 +31,7 @@ export default class KeywordInput extends OverlayMixin(Component) { this._renderEditor($$) ) } - el.on('click', this._onClick) - .on('dblclick', this._stopAndPreventDefault) + el.on('dblclick', this._stopAndPreventDefault) .on('mousedown', this._stopAndPreventDefault) return el @@ -40,7 +40,7 @@ export default class KeywordInput extends OverlayMixin(Component) { _renderEditor ($$) { const { model, placeholder } = this.props const values = model.getValue() - + const Button = this.getComponent('button') const Input = this.getComponent('input') @@ -64,7 +64,7 @@ export default class KeywordInput extends OverlayMixin(Component) { editorEl.append( $$('div').addClass('se-keyword').append( $$('div').addClass('se-keyword-input').append( - $$(Input, { placeholder }).ref('urlInput') + $$(Input, { placeholder }).ref('keywordInput') ), $$(Button).append( this.getLabel('create') @@ -105,11 +105,16 @@ export default class KeywordInput extends OverlayMixin(Component) { } _addKeyword () { - + const model = this.props.model + const values = model.getValue() + const keyword = this.refs.keywordInput.val() + values.push(keyword) + this.send('updateValues', values) } _removeKeyword (idx) { - const values = this.props.values + const model = this.props.model + const values = model.getValue() values.splice(idx, 1) this.send('updateValues', values) } From 7489ba30f32f29c938fa885e56d8238c89305b60 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Sat, 27 Apr 2019 20:07:11 +0300 Subject: [PATCH 21/56] Repair affiliation select editor styling. --- src/article/shared/styles/_custom-metadata-field.css | 5 ----- src/kit/styles/_multi-select-input.css | 1 + src/kit/styles/_popup.css | 3 ++- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/article/shared/styles/_custom-metadata-field.css b/src/article/shared/styles/_custom-metadata-field.css index b21fb7aa3..c207b25a3 100644 --- a/src/article/shared/styles/_custom-metadata-field.css +++ b/src/article/shared/styles/_custom-metadata-field.css @@ -9,11 +9,6 @@ flex: 1; } -.sc-custom-metadata-field .se-keyword-editor { - width: 300px; - max-width: 100%; -} - .sc-custom-metadata-field > .se-field-name { font-weight: var(--t-bold-font-weight); } diff --git a/src/kit/styles/_multi-select-input.css b/src/kit/styles/_multi-select-input.css index ca750e899..82ac6a80e 100644 --- a/src/kit/styles/_multi-select-input.css +++ b/src/kit/styles/_multi-select-input.css @@ -1,4 +1,5 @@ .sc-multi-select-input { + position: relative; width: 100%; line-height: var(--t-input-line-height); cursor: pointer; diff --git a/src/kit/styles/_popup.css b/src/kit/styles/_popup.css index 1ed85966e..29eafc346 100644 --- a/src/kit/styles/_popup.css +++ b/src/kit/styles/_popup.css @@ -1,5 +1,6 @@ .sc-popup { position: absolute; + width: 100%; left: 50%; transform: translate(-50%, 10%); background: var(--t-background-color); @@ -14,7 +15,7 @@ text-align: center; font-size: var(--t-tiny-font-size); color: var(--t-light-text-color); - margin-bottom: var(--t-half-spacing); + margin-top: var(--t-half-spacing); } .sc-popup .se-popup-arrow { From 7038c1bae08a43c7b0956486ef084741db026832 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Sat, 27 Apr 2019 20:15:51 +0300 Subject: [PATCH 22/56] Silence. --- src/kit/app/EditorSession.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/kit/app/EditorSession.js b/src/kit/app/EditorSession.js index 983832383..79296a846 100644 --- a/src/kit/app/EditorSession.js +++ b/src/kit/app/EditorSession.js @@ -66,7 +66,7 @@ export default class EditorSession extends AbstractEditorSession { // EXPERIMENTAL: hook that records changes triggered via node state updates doc.on('document:changed', this._onDocumentChange, this) // EXPERIMENTAL: registering a 'reducer' that resets overlayId whenever the selection changes - editorState.addObserver(['selection'], this._resetOverlayId, this, { stage: 'update' }) + // editorState.addObserver(['selection'], this._resetOverlayId, this, { stage: 'update' }) } initialize () { @@ -197,18 +197,18 @@ export default class EditorSession extends AbstractEditorSession { } _resetOverlayId () { - // const overlayId = this.editorState.overlayId - // // overlayId === getKeyForPath(path) => if selection is value && - // // Overlays of value components (ManyRelationshipComponent, SingleRelationship) - // // need to remain open if the selection is a value selection - // let sel = this.getSelection() - // if (sel && sel.customType === 'value') { - // let valueId = getKeyForPath(sel.data.path) - // if (overlayId !== valueId) { - // this.editorState.set('overlayId', valueId) - // } - // } else { - // this.editorState.set('overlayId', null) - // } + const overlayId = this.editorState.overlayId + // overlayId === getKeyForPath(path) => if selection is value && + // Overlays of value components (ManyRelationshipComponent, SingleRelationship) + // need to remain open if the selection is a value selection + let sel = this.getSelection() + if (sel && sel.customType === 'value') { + let valueId = getKeyForPath(sel.data.path) + if (overlayId !== valueId) { + this.editorState.set('overlayId', valueId) + } + } else { + this.editorState.set('overlayId', null) + } } } From 6cff250c1f65db723a4592016fa1f238c10c0c5f Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Mon, 29 Apr 2019 10:25:22 +0300 Subject: [PATCH 23/56] Remove keyword via command. --- src/article/manuscript/ManuscriptPackage.js | 2 ++ src/article/shared/RemoveKeywordCommand.js | 16 ++++++++++++++++ src/kit/ui/KeywordInput.js | 7 +++---- 3 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 src/article/shared/RemoveKeywordCommand.js diff --git a/src/article/manuscript/ManuscriptPackage.js b/src/article/manuscript/ManuscriptPackage.js index 98694f16b..d285f6775 100644 --- a/src/article/manuscript/ManuscriptPackage.js +++ b/src/article/manuscript/ManuscriptPackage.js @@ -57,6 +57,7 @@ import { import InsertTableTool from './InsertTableTool' import OpenFigurePanelImageTool from '../shared/OpenFigurePanelImageTool' import RemoveItemCommand from '../shared/RemoveItemCommand' +import RemoveKeywordCommand from '../shared/RemoveKeywordCommand' import ReplaceFigurePanelTool from '../shared/ReplaceFigurePanelTool' import ReplaceSupplementaryFileCommand from './ReplaceSupplementaryFileCommand' import ReplaceSupplementaryFileTool from './ReplaceSupplementaryFileTool' @@ -241,6 +242,7 @@ export default { nodeType: 'footnote', commandGroup: 'footnote' }) + config.addCommand('remove-keyword', RemoveKeywordCommand) config.addCommand('replace-figure-panel-image', ReplaceFigurePanelImageCommand, { commandGroup: 'figure-panel' }) diff --git a/src/article/shared/RemoveKeywordCommand.js b/src/article/shared/RemoveKeywordCommand.js new file mode 100644 index 000000000..926e0e781 --- /dev/null +++ b/src/article/shared/RemoveKeywordCommand.js @@ -0,0 +1,16 @@ +import { Command, documentHelpers } from 'substance' + +export default class RemoveKeywordCommand extends Command { + getCommandState (params, context) { + return { disabled: false } + } + + execute (params, context) { + const { path, idx } = params + const editorSession = context.editorSession + editorSession.transaction(tx => { + documentHelpers.remove(tx, path, idx) + tx.selection = null + }) + } +} diff --git a/src/kit/ui/KeywordInput.js b/src/kit/ui/KeywordInput.js index e190ee5af..f822b755b 100644 --- a/src/kit/ui/KeywordInput.js +++ b/src/kit/ui/KeywordInput.js @@ -64,7 +64,7 @@ export default class KeywordInput extends OverlayMixin(Component) { editorEl.append( $$('div').addClass('se-keyword').append( $$('div').addClass('se-keyword-input').append( - $$(Input, { placeholder }).ref('keywordInput') + $$(Input, { placeholder }).attr({ tabindex: '2' }).ref('keywordInput') ), $$(Button).append( this.getLabel('create') @@ -114,8 +114,7 @@ export default class KeywordInput extends OverlayMixin(Component) { _removeKeyword (idx) { const model = this.props.model - const values = model.getValue() - values.splice(idx, 1) - this.send('updateValues', values) + const path = model.getPath() + this.send('executeCommand', 'remove-keyword', { path, idx }) } } From e22ae388b38a1ad3f9a8757a7a6df8a37285848f Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Mon, 29 Apr 2019 10:36:37 +0300 Subject: [PATCH 24/56] Improve remove keyword command execution. --- src/article/shared/RemoveKeywordCommand.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/article/shared/RemoveKeywordCommand.js b/src/article/shared/RemoveKeywordCommand.js index 926e0e781..661daba33 100644 --- a/src/article/shared/RemoveKeywordCommand.js +++ b/src/article/shared/RemoveKeywordCommand.js @@ -9,8 +9,7 @@ export default class RemoveKeywordCommand extends Command { const { path, idx } = params const editorSession = context.editorSession editorSession.transaction(tx => { - documentHelpers.remove(tx, path, idx) - tx.selection = null + documentHelpers.removeAt(tx, path, idx) }) } } From e5bc4f6777b67e8c3c6c23cf74df0f22fcd75e05 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Mon, 29 Apr 2019 10:37:07 +0300 Subject: [PATCH 25/56] Add palceholder for custom field value. --- src/article/manuscript/ManuscriptPackage.js | 1 + src/article/metadata/MetadataPackage.js | 2 ++ src/article/shared/CustomMetadataFieldComponent.js | 2 +- src/kit/ui/KeywordInput.js | 6 ++++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/article/manuscript/ManuscriptPackage.js b/src/article/manuscript/ManuscriptPackage.js index d285f6775..576726d41 100644 --- a/src/article/manuscript/ManuscriptPackage.js +++ b/src/article/manuscript/ManuscriptPackage.js @@ -336,6 +336,7 @@ export default { config.addLabel('add-action', 'Add') config.addLabel('enter-url-placeholder', 'Enter url') config.addLabel('enter-keyword', 'Enter keyword') + config.addLabel('enter-keywords', 'Click to add keywords') // Icons config.addIcon('create-unordered-list', { 'fontawesome': 'fa-list-ul' }) diff --git a/src/article/metadata/MetadataPackage.js b/src/article/metadata/MetadataPackage.js index 94cd52c51..cb1f58fa3 100644 --- a/src/article/metadata/MetadataPackage.js +++ b/src/article/metadata/MetadataPackage.js @@ -360,6 +360,8 @@ export default { config.addLabel('empty-figure-metadata', 'No fields specified') config.addLabel('open-link', 'Open Link') config.addLabel('enter-keyword', 'Enter keyword') + config.addLabel('enter-keywords', 'Click to add keywords') + // Icons config.addIcon('input-error', { 'fontawesome': 'fa-exclamation-circle' }) config.addIcon('input-loading', { 'fontawesome': 'fa-spinner fa-spin' }) diff --git a/src/article/shared/CustomMetadataFieldComponent.js b/src/article/shared/CustomMetadataFieldComponent.js index a0ffb4baa..5f1e4b12a 100644 --- a/src/article/shared/CustomMetadataFieldComponent.js +++ b/src/article/shared/CustomMetadataFieldComponent.js @@ -17,7 +17,7 @@ export default class CustomMetadataFieldComponent extends NodeComponent { this._renderValue($$, 'name', { placeholder: this.getLabel('enter-custom-field-name') }).addClass('se-field-name'), $$(KeywordInput, { model: valuesModel, - placeholder: this.getLabel('enter-keyword'), + placeholder: this.getLabel('enter-keywords'), overlayId: valuesModel.id }).addClass('se-field-values') ) diff --git a/src/kit/ui/KeywordInput.js b/src/kit/ui/KeywordInput.js index f822b755b..bb0c5225b 100644 --- a/src/kit/ui/KeywordInput.js +++ b/src/kit/ui/KeywordInput.js @@ -17,12 +17,13 @@ export default class KeywordInput extends OverlayMixin(Component) { const values = model.getValue() const isEmpty = values.length === 0 const isExpanded = this.state.isExpanded + const label = isEmpty ? this.props.placeholder : values.join(', ') const el = $$('div').addClass('sc-keyword-input') if (isEmpty) el.addClass('sm-empty') el.addClass(isExpanded ? 'sm-expanded' : 'sm-collapsed') el.append( - $$('div').addClass('se-label').text(values.join(', ')) + $$('div').addClass('se-label').text(label) .on('click', this._onClick) ) if (isExpanded) { @@ -38,8 +39,9 @@ export default class KeywordInput extends OverlayMixin(Component) { } _renderEditor ($$) { - const { model, placeholder } = this.props + const model = this.props.model const values = model.getValue() + const placeholder = this.getLabel('enter-keyword') const Button = this.getComponent('button') const Input = this.getComponent('input') From cc69279f35842f6ac9e56b856422bfcb7f19b2c5 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Tue, 30 Apr 2019 13:42:53 +0300 Subject: [PATCH 26/56] Move KeywordInput to article shared components folder. --- src/article/shared/CustomMetadataFieldComponent.js | 3 ++- src/{kit/ui => article/shared}/KeywordInput.js | 0 src/kit/ui/index.js | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{kit/ui => article/shared}/KeywordInput.js (100%) diff --git a/src/article/shared/CustomMetadataFieldComponent.js b/src/article/shared/CustomMetadataFieldComponent.js index 5f1e4b12a..f7ef36684 100644 --- a/src/article/shared/CustomMetadataFieldComponent.js +++ b/src/article/shared/CustomMetadataFieldComponent.js @@ -1,4 +1,5 @@ -import { createValueModel, KeywordInput, NodeComponent } from '../../kit' +import { createValueModel, NodeComponent } from '../../kit' +import KeywordInput from './KeywordInput' export default class CustomMetadataFieldComponent extends NodeComponent { getActionHandlers () { diff --git a/src/kit/ui/KeywordInput.js b/src/article/shared/KeywordInput.js similarity index 100% rename from src/kit/ui/KeywordInput.js rename to src/article/shared/KeywordInput.js diff --git a/src/kit/ui/index.js b/src/kit/ui/index.js index 97774dcb8..642746f9a 100644 --- a/src/kit/ui/index.js +++ b/src/kit/ui/index.js @@ -17,7 +17,6 @@ export { default as getComponentForModel } from './getComponentForModel' export { default as InputWithButton } from './InputWithButton' export { default as IsolatedInlineNodeComponent } from './_IsolatedInlineNodeComponent' export { default as IsolatedNodeComponent } from './_IsolatedNodeComponent' -export { default as KeywordInput } from './KeywordInput' export { default as Managed } from './Managed' export { default as ManyRelationshipComponent } from './ManyRelationshipComponent' export { default as ModalDialog } from './ModalDialog' From 57c0f729214905fa665b8252cef363be83b353bb Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Tue, 30 Apr 2019 13:57:36 +0300 Subject: [PATCH 27/56] Import modules separatly. --- src/kit/ui/MultiSelectInput.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/kit/ui/MultiSelectInput.js b/src/kit/ui/MultiSelectInput.js index c4bc2d3f5..b4d368920 100644 --- a/src/kit/ui/MultiSelectInput.js +++ b/src/kit/ui/MultiSelectInput.js @@ -1,5 +1,6 @@ import { Component } from 'substance' -import { OverlayMixin, Popup } from '../../kit' +import OverlayMixin from './OverlayMixin' +import Popup from './Popup' export default class MultiSelectInput extends OverlayMixin(Component) { getInitialState () { From d9f936523c036d99f81dcba3fd54aac4a3453047 Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Tue, 30 Apr 2019 20:16:22 +0200 Subject: [PATCH 28/56] Use substance.domHelper --- src/article/shared/KeywordInput.js | 9 +++++---- src/kit/ui/OverlayMixin.js | 5 ----- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index bb0c5225b..1263b4e44 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -1,4 +1,4 @@ -import { Component, getKeyForPath } from 'substance' +import { Component, getKeyForPath, domHelpers } from 'substance' import { OverlayMixin, Popup, TextInput } from '../../kit' export default class KeywordInput extends OverlayMixin(Component) { @@ -32,8 +32,9 @@ export default class KeywordInput extends OverlayMixin(Component) { this._renderEditor($$) ) } - el.on('dblclick', this._stopAndPreventDefault) - .on('mousedown', this._stopAndPreventDefault) + el.on('dblclick', domHelpers.stopAndPrevent) + .on('mousedown', domHelpers.stopAndPrevent) + .on('mouseup', domHelpers.stopAndPrevent) return el } @@ -88,7 +89,7 @@ export default class KeywordInput extends OverlayMixin(Component) { } _onClick (event) { - this._stopAndPreventDefault(event) + domHelpers.stopAndPrevent(event) super._toggleOverlay() } diff --git a/src/kit/ui/OverlayMixin.js b/src/kit/ui/OverlayMixin.js index 185d2e649..5da1e3e24 100644 --- a/src/kit/ui/OverlayMixin.js +++ b/src/kit/ui/OverlayMixin.js @@ -30,11 +30,6 @@ export default function (Component) { // console.log('Rerendering overlay component because overlay id has changed', this._getOverlayId()') this.rerender() } - - _stopAndPreventDefault (event) { - event.stopPropagation() - event.preventDefault() - } } return OverlayComponent } From fd948c4f2beb3f0c3cda04e6cf633b96afee0ebe Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Tue, 30 Apr 2019 20:18:33 +0200 Subject: [PATCH 29/56] Move generalized _getOverlayId() into OverlayMixin. --- src/article/shared/KeywordInput.js | 4 ---- src/kit/ui/MultiSelectInput.js | 4 ---- src/kit/ui/OverlayMixin.js | 2 +- 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index 1263b4e44..d6b6c446c 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -84,10 +84,6 @@ export default class KeywordInput extends OverlayMixin(Component) { ) } - _getOverlayId () { - return this.props.overlayId || this.getId() - } - _onClick (event) { domHelpers.stopAndPrevent(event) super._toggleOverlay() diff --git a/src/kit/ui/MultiSelectInput.js b/src/kit/ui/MultiSelectInput.js index b4d368920..9cae0faee 100644 --- a/src/kit/ui/MultiSelectInput.js +++ b/src/kit/ui/MultiSelectInput.js @@ -60,10 +60,6 @@ export default class MultiSelectInput extends OverlayMixin(Component) { return $$(Popup, { label }).append(editorEl) } - _getOverlayId () { - return this.props.overlayId || this.getId() - } - _getOptions () { return this.getParent().getAvailableOptions() } diff --git a/src/kit/ui/OverlayMixin.js b/src/kit/ui/OverlayMixin.js index 5da1e3e24..2d8f96076 100644 --- a/src/kit/ui/OverlayMixin.js +++ b/src/kit/ui/OverlayMixin.js @@ -15,7 +15,7 @@ export default function (Component) { } _getOverlayId () { - return this.getId() + return this.props.overlayId || this.getId() } _canShowOverlay () { From 95cbea05cad5746e1ad0f15dd24c979fcca86e3e Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Tue, 30 Apr 2019 20:18:54 +0200 Subject: [PATCH 30/56] Expose overlay instance via child context. --- src/kit/ui/OverlayMixin.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/kit/ui/OverlayMixin.js b/src/kit/ui/OverlayMixin.js index 2d8f96076..56e36e0fd 100644 --- a/src/kit/ui/OverlayMixin.js +++ b/src/kit/ui/OverlayMixin.js @@ -1,5 +1,9 @@ export default function (Component) { class OverlayComponent extends Component { + getChildContext () { + return { overlay: this } + } + didMount () { super.didMount() From 32022d7dc86378f9b1790e2024598e45e3349dab Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Tue, 30 Apr 2019 20:17:37 +0200 Subject: [PATCH 31/56] Enable reducer for editorState.overlayId --- src/kit/app/EditorSession.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/kit/app/EditorSession.js b/src/kit/app/EditorSession.js index 79296a846..3dbb70aa9 100644 --- a/src/kit/app/EditorSession.js +++ b/src/kit/app/EditorSession.js @@ -66,7 +66,7 @@ export default class EditorSession extends AbstractEditorSession { // EXPERIMENTAL: hook that records changes triggered via node state updates doc.on('document:changed', this._onDocumentChange, this) // EXPERIMENTAL: registering a 'reducer' that resets overlayId whenever the selection changes - // editorState.addObserver(['selection'], this._resetOverlayId, this, { stage: 'update' }) + editorState.addObserver(['selection'], this._resetOverlayId, this, { stage: 'update' }) } initialize () { @@ -208,7 +208,21 @@ export default class EditorSession extends AbstractEditorSession { this.editorState.set('overlayId', valueId) } } else { - this.editorState.set('overlayId', null) + // EXPERIMENTAL: OverlayMixin leaves context.overlay + // which can be used to detect if the focused surface is inside an overlay + let focusedSurface = this.getSurface(sel.surfaceId) + if (focusedSurface) { + let overlay = focusedSurface.context.overlay + if (overlay) { + if (overlayId !== overlay._getOverlayId()) { + this.editorState.set('overlayId', overlay._getOverlayId()) + } + } else { + this.editorState.set('overlayId', null) + } + } else { + this.editorState.set('overlayId', null) + } } } } From 0f4475be222e8ef49142344ec254054ea44e2150 Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Tue, 30 Apr 2019 22:06:58 +0200 Subject: [PATCH 32/56] Focus KeywordInput's new field when shown. --- src/article/shared/KeywordInput.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index d6b6c446c..03b294498 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -12,6 +12,18 @@ export default class KeywordInput extends OverlayMixin(Component) { this.extendState(this.getInitialState()) } + didMount () { + super.didMount() + + this._focusNewKeyworkInput() + } + + didUpdate (oldProps, oldState) { + if (!oldState.isExpanded) { + this._focusNewKeyworkInput() + } + } + render ($$) { const model = this.props.model const values = model.getValue() @@ -67,7 +79,7 @@ export default class KeywordInput extends OverlayMixin(Component) { editorEl.append( $$('div').addClass('se-keyword').append( $$('div').addClass('se-keyword-input').append( - $$(Input, { placeholder }).attr({ tabindex: '2' }).ref('keywordInput') + $$(Input, { placeholder }).attr({ tabindex: '2' }).ref('newKeywordInput') ), $$(Button).append( this.getLabel('create') @@ -106,7 +118,7 @@ export default class KeywordInput extends OverlayMixin(Component) { _addKeyword () { const model = this.props.model const values = model.getValue() - const keyword = this.refs.keywordInput.val() + const keyword = this.refs.newKeywordInput.val() values.push(keyword) this.send('updateValues', values) } @@ -116,4 +128,10 @@ export default class KeywordInput extends OverlayMixin(Component) { const path = model.getPath() this.send('executeCommand', 'remove-keyword', { path, idx }) } + + _focusNewKeyworkInput () { + if (this.state.isExpanded) { + this.refs['newKeywordInput'].focus() + } + } } From f9118e2309c3fb7eb1ff18be077351b7ec9021de Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Tue, 30 Apr 2019 21:54:22 +0200 Subject: [PATCH 33/56] Use the unminified substance bundle in web-demo. --- builds/demo/index.html | 2 +- make.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/builds/demo/index.html b/builds/demo/index.html index 27404acae..bad861a90 100644 --- a/builds/demo/index.html +++ b/builds/demo/index.html @@ -16,7 +16,7 @@ height: 100%; } - + diff --git a/make.js b/make.js index 22f011991..5a06adf83 100644 --- a/make.js +++ b/make.js @@ -104,6 +104,7 @@ b.task('build:assets', function () { b.copy('./node_modules/inter-ui', DIST + 'lib/inter-ui') b.copy('./node_modules/katex/dist', DIST + 'lib/katex') b.copy('./node_modules/substance/dist/*.css*', DIST + 'lib/substance/') + b.copy('./node_modules/substance/dist/substance.js*', DIST + 'lib/substance/') b.copy('./node_modules/substance/dist/substance.min.js*', DIST + 'lib/substance/') b.copy('./node_modules/texture-plugin-jats/dist', DIST + 'plugins/texture-plugin-jats') b.css('texture.css', DIST + 'texture.css') From a70cc660da78ea8b002dd396c42c10cb322cbb1e Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Tue, 30 Apr 2019 22:05:35 +0200 Subject: [PATCH 34/56] Working on KeywordInput implementation. The required solution is new, so this implementation is our first prototype. --- src/article/shared/KeywordInput.js | 73 ++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 9 deletions(-) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index 03b294498..186d46f4d 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -1,23 +1,44 @@ -import { Component, getKeyForPath, domHelpers } from 'substance' +import { Component, getKeyForPath, domHelpers, keys } from 'substance' import { OverlayMixin, Popup, TextInput } from '../../kit' +/** + * Experimental: this is the first example of an overlay that itself + * hosts Surfaces, similar to an IsolatedNodeComponent. + * Thus we are applying the same strategy regarding selections and surfaceId. + */ export default class KeywordInput extends OverlayMixin(Component) { + constructor (...args) { + super(...args) + + this._surfaceId = this.context.parentSurfaceId + '/' + this._getOverlayId() + } getInitialState () { return { isExpanded: this._canShowOverlay() } } - willReceiveProps () { - this.extendState(this.getInitialState()) + getChildContext () { + return Object.assign(super.getChildContext(), { + parentSurfaceId: this._surfaceId + }) } didMount () { super.didMount() + let appState = this.context.appState + appState.addObserver(['selection'], this._onSelectionHasChanged, this, { stage: 'render' }) + this._focusNewKeyworkInput() } + dispose () { + super.dispose() + + this.context.appState.removeObserver(this) + } + didUpdate (oldProps, oldState) { if (!oldState.isExpanded) { this._focusNewKeyworkInput() @@ -44,9 +65,10 @@ export default class KeywordInput extends OverlayMixin(Component) { this._renderEditor($$) ) } - el.on('dblclick', domHelpers.stopAndPrevent) - .on('mousedown', domHelpers.stopAndPrevent) - .on('mouseup', domHelpers.stopAndPrevent) + el.on('mousedown', domHelpers.stop) + .on('mouseup', domHelpers.stop) + .on('click', domHelpers.stop) + .on('dblclick', domHelpers.stop) return el } @@ -60,16 +82,18 @@ export default class KeywordInput extends OverlayMixin(Component) { const Input = this.getComponent('input') const editorEl = $$('div').ref('editor').addClass('se-keyword-editor') + let lastIdx = values.length - 1 values.forEach((value, idx) => { const path = model.getPath().concat(idx) const name = getKeyForPath(path) editorEl.append( $$('div').addClass('se-keyword').append( $$('div').addClass('se-keyword-input').append( - $$(TextInput, { + $$(_HackedTextInput, { name, path, - placeholder + placeholder, + isLast: idx === lastIdx }) ), this._renderIcon($$, 'trash').on('click', this._removeKeyword.bind(this, idx)) @@ -79,7 +103,9 @@ export default class KeywordInput extends OverlayMixin(Component) { editorEl.append( $$('div').addClass('se-keyword').append( $$('div').addClass('se-keyword-input').append( - $$(Input, { placeholder }).attr({ tabindex: '2' }).ref('newKeywordInput') + $$(Input, { placeholder }).ref('newKeywordInput') + .attr({ tabindex: '2' }) + .on('keydown', this._onNewKeywordKeydown) ), $$(Button).append( this.getLabel('create') @@ -101,6 +127,15 @@ export default class KeywordInput extends OverlayMixin(Component) { super._toggleOverlay() } + _onNewKeywordKeydown (event) { + // TODO: maybe use the parseKeyEvent + parseKeyCombo trick to have exactly only reaction on ENTER without modifiers + if (event.keyCode === keys.ENTER) { + event.stopPropagation() + event.preventDefault() + this._addKeyword() + } + } + _onOverlayIdHasChanged () { let overlayId = this.context.appState.overlayId let id = this._getOverlayId() @@ -115,6 +150,17 @@ export default class KeywordInput extends OverlayMixin(Component) { } } + _onSelectionHasChanged (sel) { + let surfaceId = sel.surfaceId + if (surfaceId && surfaceId.startsWith(this._surfaceId)) { + if (!this.state.isExpanded) { + this.extendState({ isExpanded: true }) + } + } else if (this.state.isExpanded) { + this.extendState({ isExpanded: false }) + } + } + _addKeyword () { const model = this.props.model const values = model.getValue() @@ -135,3 +181,12 @@ export default class KeywordInput extends OverlayMixin(Component) { } } } + +class _HackedTextInput extends TextInput { + _handleTabKey (event) { + event.stopPropagation() + if (!event.shiftKey && !this.props.isLast) { + this.__handleTab(event) + } + } +} From df8b36af00979396f1fa43e9a69ce67d1cc98b7d Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Wed, 1 May 2019 18:00:34 +0300 Subject: [PATCH 35/56] Keep keyword values in custom-metadata-value. --- src/article/models/CustomMetadataField.js | 4 ++-- src/article/models/CustomMetadataValue.js | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 src/article/models/CustomMetadataValue.js diff --git a/src/article/models/CustomMetadataField.js b/src/article/models/CustomMetadataField.js index ca33ef026..7232222e8 100644 --- a/src/article/models/CustomMetadataField.js +++ b/src/article/models/CustomMetadataField.js @@ -1,4 +1,4 @@ -import { DocumentNode, STRING, STRING_ARRAY } from 'substance' +import { CHILDREN, DocumentNode, STRING } from 'substance' export default class CustomMetadataField extends DocumentNode { static getTemplate () { @@ -14,5 +14,5 @@ export default class CustomMetadataField extends DocumentNode { CustomMetadataField.schema = { type: 'custom-metadata-field', name: STRING, - values: STRING_ARRAY + values: CHILDREN('custom-metadata-value') } diff --git a/src/article/models/CustomMetadataValue.js b/src/article/models/CustomMetadataValue.js new file mode 100644 index 000000000..0b9d60bc8 --- /dev/null +++ b/src/article/models/CustomMetadataValue.js @@ -0,0 +1,17 @@ +import { DocumentNode, STRING } from 'substance' + +export default class CustomMetadataValue extends DocumentNode { + static getTemplate () { + return { + type: 'custom-metadata-value' + } + } + + isEmpty () { + return this.length === 0 + } +} +CustomMetadataValue.schema = { + type: 'custom-metadata-value', + content: STRING +} From 58d0973c45f7dfe5382a48198611abbf231b4175 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Wed, 1 May 2019 18:26:37 +0300 Subject: [PATCH 36/56] Add CustomMetadataValue model to article model package. --- src/article/models/ArticleModelPackage.js | 3 ++- src/article/models/index.js | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/article/models/ArticleModelPackage.js b/src/article/models/ArticleModelPackage.js index 1fb437e70..51b73f22f 100644 --- a/src/article/models/ArticleModelPackage.js +++ b/src/article/models/ArticleModelPackage.js @@ -11,6 +11,7 @@ import ChapterRef from './ChapterRef' import ConferencePaperRef from './ConferencePaperRef' import CustomAbstract from './CustomAbstract' import CustomMetadataField from './CustomMetadataField' +import CustomMetadataValue from './CustomMetadataValue' import DataPublicationRef from './DataPublicationRef' import ExternalLink from './ExternalLink' import Figure from './Figure' @@ -65,7 +66,7 @@ export default { ;[ Abstract, Article, ArticleRef, BlockFormula, BlockQuote, Body, Bold, BookRef, Break, ChapterRef, ConferencePaperRef, - CustomAbstract, CustomMetadataField, DataPublicationRef, ExternalLink, Figure, FigurePanel, + CustomAbstract, CustomMetadataField, CustomMetadataValue, DataPublicationRef, ExternalLink, Figure, FigurePanel, Footnote, Funder, Graphic, Group, Heading, InlineFormula, InlineGraphic, Italic, Keyword, JournalArticleRef, List, ListItem, MagazineArticleRef, Metadata, Monospace, NewspaperArticleRef, Organisation, Overline, Paragraph, PatentRef, Permission, diff --git a/src/article/models/index.js b/src/article/models/index.js index 22b703a89..a4529027d 100644 --- a/src/article/models/index.js +++ b/src/article/models/index.js @@ -11,6 +11,7 @@ export { default as ChapterRef } from './ChapterRef' export { default as ConferencePaperRef } from './ConferencePaperRef' export { default as CustomAbstract } from './CustomAbstract' export { default as CustomMetadataField } from './CustomMetadataField' +export { default as CustomMetadataValue } from './CustomMetadataValue' export { default as DataPublicationRef } from './DataPublicationRef' export { default as ExternalLink } from './ExternalLink' export { default as Figure } from './Figure' From 2312c600a9a426b26567ce554b42cf27dbb1cd91 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Wed, 1 May 2019 18:27:31 +0300 Subject: [PATCH 37/56] Use CustomMetadataValue in figure panel converter. --- src/article/converter/jats/FigurePanelConverter.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/article/converter/jats/FigurePanelConverter.js b/src/article/converter/jats/FigurePanelConverter.js index 3d2e2dc79..c91f5e1cc 100644 --- a/src/article/converter/jats/FigurePanelConverter.js +++ b/src/article/converter/jats/FigurePanelConverter.js @@ -59,7 +59,12 @@ export default class FigurePanelConverter { let kwdEls = kwdGroupEl.findAll('kwd') let labelEl = kwdGroupEl.find('label') let name = labelEl ? labelEl.textContent : '' - let values = kwdEls.map(kwdEl => kwdEl.textContent) + let values = kwdEls.map(kwdEl => { + return doc.create({ + type: 'custom-metadata-value', + content: kwdEl.textContent + }).id + }) return doc.create({ type: 'custom-metadata-field', name, @@ -109,8 +114,8 @@ export default class FigurePanelConverter { let kwdGroupEl = $$('kwd-group').append( $$('label').text(field.name) ) - let kwdEls = field.values.map(str => { - return $$('kwd').text(str.trim()) + let kwdEls = field.resolve('values').map(keyword => { + return $$('kwd').text(keyword.content) }) kwdGroupEl.append(kwdEls) return kwdGroupEl From d83311f2799e56fd4516d833ab78df66be952aed Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Wed, 1 May 2019 21:42:41 +0300 Subject: [PATCH 38/56] Use dedicated model for custom metadata keywords. --- .../shared/CustomMetadataFieldComponent.js | 8 +++---- src/article/shared/KeywordInput.js | 22 +++++++++---------- src/article/shared/RemoveKeywordCommand.js | 4 ++-- 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/article/shared/CustomMetadataFieldComponent.js b/src/article/shared/CustomMetadataFieldComponent.js index f7ef36684..6f40b166c 100644 --- a/src/article/shared/CustomMetadataFieldComponent.js +++ b/src/article/shared/CustomMetadataFieldComponent.js @@ -4,7 +4,7 @@ import KeywordInput from './KeywordInput' export default class CustomMetadataFieldComponent extends NodeComponent { getActionHandlers () { return { - updateValues: this._updateValues + addValue: this._addValue } } @@ -31,9 +31,9 @@ export default class CustomMetadataFieldComponent extends NodeComponent { return el } - _updateValues (values) { - const model = this._getValuesModel() - model.setValue(values) + _addValue (value) { + const node = this._getNode() + this.context.api._appendChild([node.id, 'values'], { type: 'custom-metadata-value', content: value }) } _getValuesModel () { diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index 186d46f4d..a3b760fc9 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -46,11 +46,12 @@ export default class KeywordInput extends OverlayMixin(Component) { } render ($$) { + const doc = this.context.doc const model = this.props.model const values = model.getValue() const isEmpty = values.length === 0 const isExpanded = this.state.isExpanded - const label = isEmpty ? this.props.placeholder : values.join(', ') + const label = isEmpty ? this.props.placeholder : values.map(v => doc.get([v, 'content'])).join(', ') const el = $$('div').addClass('sc-keyword-input') if (isEmpty) el.addClass('sm-empty') @@ -75,16 +76,16 @@ export default class KeywordInput extends OverlayMixin(Component) { _renderEditor ($$) { const model = this.props.model - const values = model.getValue() + const metadataValues = model.getValue() const placeholder = this.getLabel('enter-keyword') const Button = this.getComponent('button') const Input = this.getComponent('input') const editorEl = $$('div').ref('editor').addClass('se-keyword-editor') - let lastIdx = values.length - 1 - values.forEach((value, idx) => { - const path = model.getPath().concat(idx) + let lastIdx = metadataValues.length - 1 + metadataValues.forEach((value, idx) => { + const path = [value, 'content'] const name = getKeyForPath(path) editorEl.append( $$('div').addClass('se-keyword').append( @@ -96,7 +97,7 @@ export default class KeywordInput extends OverlayMixin(Component) { isLast: idx === lastIdx }) ), - this._renderIcon($$, 'trash').on('click', this._removeKeyword.bind(this, idx)) + this._renderIcon($$, 'trash').on('click', this._removeKeyword.bind(this, value)) ) ) }) @@ -162,17 +163,14 @@ export default class KeywordInput extends OverlayMixin(Component) { } _addKeyword () { - const model = this.props.model - const values = model.getValue() const keyword = this.refs.newKeywordInput.val() - values.push(keyword) - this.send('updateValues', values) + this.send('addValue', keyword) } - _removeKeyword (idx) { + _removeKeyword (value) { const model = this.props.model const path = model.getPath() - this.send('executeCommand', 'remove-keyword', { path, idx }) + this.send('executeCommand', 'remove-keyword', { path, value }) } _focusNewKeyworkInput () { diff --git a/src/article/shared/RemoveKeywordCommand.js b/src/article/shared/RemoveKeywordCommand.js index 661daba33..0e7862ea0 100644 --- a/src/article/shared/RemoveKeywordCommand.js +++ b/src/article/shared/RemoveKeywordCommand.js @@ -6,10 +6,10 @@ export default class RemoveKeywordCommand extends Command { } execute (params, context) { - const { path, idx } = params + const { path, value } = params const editorSession = context.editorSession editorSession.transaction(tx => { - documentHelpers.removeAt(tx, path, idx) + documentHelpers.remove(tx, path, value) }) } } From 204508ae1126408eae6622661bb5d643224d7db6 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Thu, 2 May 2019 12:21:56 +0300 Subject: [PATCH 39/56] Improve kwyword removing command. --- src/article/shared/KeywordInput.js | 2 +- src/article/shared/RemoveKeywordCommand.js | 22 ++++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index a3b760fc9..2ee40b653 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -170,7 +170,7 @@ export default class KeywordInput extends OverlayMixin(Component) { _removeKeyword (value) { const model = this.props.model const path = model.getPath() - this.send('executeCommand', 'remove-keyword', { path, value }) + this.send('executeCommand', 'remove-keyword', { path, nodeId: value, surfaceId: this._surfaceId }) } _focusNewKeyworkInput () { diff --git a/src/article/shared/RemoveKeywordCommand.js b/src/article/shared/RemoveKeywordCommand.js index 0e7862ea0..01f549e53 100644 --- a/src/article/shared/RemoveKeywordCommand.js +++ b/src/article/shared/RemoveKeywordCommand.js @@ -6,10 +6,28 @@ export default class RemoveKeywordCommand extends Command { } execute (params, context) { - const { path, value } = params + const { path, nodeId, surfaceId } = params const editorSession = context.editorSession editorSession.transaction(tx => { - documentHelpers.remove(tx, path, value) + const index = tx.get(path).indexOf(nodeId) + const size = tx.get(path).length + if (index === -1) return false + + documentHelpers.removeAt(tx, path, index) + documentHelpers.deepDeleteNode(nodeId) + // TODO: after removing selection should be + // moved to the next ‘reasonable’ place + if (index < size - 1) { + tx.setSelection({ + type: 'property', + path: [tx.get(path)[index], 'content'], + surfaceId: surfaceId, + startOffset: 0, + endOffset: 0 + }) + } else { + tx.selection = null + } }) } } From de69be3200517786f9153365c533e7e5bc91c94f Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Fri, 3 May 2019 13:06:21 +0200 Subject: [PATCH 40/56] Remove obsolete API documentation. --- docs/TextureAPI.md | 347 --------------------------------------------- 1 file changed, 347 deletions(-) delete mode 100644 docs/TextureAPI.md diff --git a/docs/TextureAPI.md b/docs/TextureAPI.md deleted file mode 100644 index d3c59e105..000000000 --- a/docs/TextureAPI.md +++ /dev/null @@ -1,347 +0,0 @@ -# Texture API - -A Javascript API to modify content in a Texture article. This is particularly useful for collecting metadata, such as authors, affiliations, references, keywords etc. This is a format-agnostic abstraction. So it means the data can be serialised in different ways, so Texture is not tightly bound to JATS. We could also use HTML + JSON to represent an article and its metadata. - -* [Affiliations](#affiliations) -* [Authors](#authors) -* [Editors](#editors) -* [Awards](#awards) -* [References](#references) -* [Keywords](#keywords) -* [Subjects](#subjects) - -## Contribs - -Manage contributor data, such as authors, editors, affiliations, awards. - -```js -let contribs = api.getContribsModel() -``` - -### Affiliations - -Print affiliations, depending on the order of authors. If you haven't assigned any affiliations, this list will be empty. - -```js -contribs.getAffiliations() -``` - -```js -[{ id: 'aff1', ... }, ...] -``` - -Add new affiliation: - -```js -contribs.addAffiliation({ - id: 'aff1', - name: 'German Primate Center GmbH', - division1: 'Neurobiology Laboratory', - city: 'Göttingen', - country: 'Germany' -}) -``` - -Get affiliation: - -```js -contribs.getAffiliation('aff1') -``` - -Update an affiliation: - -```js -contribs.updateAffiliation('aff1', {...}) -``` - -Remove an affiliation: - -```js -contribs.deleteAffiliation('aff1') -``` - -### Authors - -Get all authors in order: - -```js -contribs.getAuthors() -``` - - -```js -[ - { - id: 'author1', - type: 'person', - surname: 'Schaffelhofer', - givenNames: 'Stefan', - suffix: 'Phd', - email: 'stefan@schaffelhofer.com', - // affiliations related to this paper - affiliations: ['org1'], - presentAffiliation: ['org1'], - // awards related to this paper - awards: ['fund1'], - equalContrib: true, - corresp: true, - }, - // Groups are considered one independent entity (not reusing person entry for members) - // When updating via API, a whole new record is written - { - id: 'author2', - type: 'group', - affiliations: ['org2'], - presentAffiliation: ['org2'], - awards: ['fund1'], - members: [ - { - surname: 'Kelly', - givenNames: 'Laura A.', - email: 'stefan@schaffelhofer.com', - affiliations: ['org2'], - awards: ['fund1'], - role: 'Writing Group' - }, - { - surname: 'Randall', - givenNames: 'Daniel Lee', - suffix: 'Jr.', - email: 'stefan@schaffelhofer.com', - affiliations: ['org3'], - awards: ['fund1'], - role: 'Lab Group' - } - ] - } -] -``` - -To add a new author: - -```js -contribs.addAuthor({...}) -``` - -Update an author: - -```js -contribs.updateAuthor('author1', {...}) -``` - -Delete an author: - -```js -contribs.deleteAuthor('author3') -``` - -To change the position of an author in the author list: - -```js -contribs.moveAuthor('author4', 0) // move to position 0 -``` - -### Editors - -Pretty much the same as with authors, except there's no type attribute needed. Use the following methods: - -```js -contribs.getEditors(data) -contribs.addEditor(data) -contribs.updateEditor(id, data) -contribs.moveEditor(id, pos) -contribs.deleteEditor(id) -``` - -### Awards - -Add an award (grant), which can then be referenced from an author using the `awards` property. - -```js -contribs.addAward({ - id: 'fund1', - institution: 'Howard Huges Medical Institute', - awardId: 'F32 GM089018' -}) -``` - -## References - -```js -let references = this.context.api.getReferences() -``` - -### Get Reference - -```js -references.getReference('r1') -``` - -Result: - -```js -{ - "type": "book", - "id": "r1", - "authors": [ - { - "givenNames": "JA", - "surname": "Coyne" - }, - { - "givenNames": "HA", - "surname": "Orr" - } - ], - "translators": [], - "title": "Speciation and its consequences", - "volume": "", - "edition": "", - "publisherLoc": "Sunderland, MA", - "publisherName": "Sinauer Associates", - "year": "1989" -} -``` - -### Add Reference - -```js -references.addReference({ - id: "r2", - type: "journal-article", - "title": "....", - ... -}) -``` - -### Update a Reference - -```js -references.updateReference('r2', { - "title": "....", - ... -}) -``` - -### Get label for a reference: - -```js -references.getLabel('r2') // => e.g. [2] -``` - -### Render reference - -Returns the rendered HTML string (without label) - -```js -references.renderReference('r2') -``` - - -### Get bibliography - -```js -references.getBibliography() -``` - -Result: - -```js -[ - { - label: '[1]', - data: {id: 'r1', type: 'book', ...} - } -] -``` - - -## Metadata - -```js -let meta = api.getMeta() -``` - -### Keywords - -Add a keyword: - -```js -meta.addKeyword('optogenetics', 'author-keyword') -meta.addKeyword('two-photon', 'author-keyword') -meta.addKeyword('Mouse', 'research-organism') -``` - -List all available keyword categories: - -```js -meta.getKeywordCategories() -``` - -Result: - -```js -['author-keyword', 'research-organism'] -``` - -List keywords for a given category: - -```js -meta.getKeywords('author-keyword') -``` - -```js -['optogenetics', 'two-photon'] -``` - -### Subjects - -Add a subject: - -```js -meta.addSubject('Research Article', 'article-type') -meta.addSubject('Computational and Systems Biology', 'research-subject') -meta.addSubject('Epidemiology and Global Health', 'research-subject') -``` - -List all available keyword categories: - -```js -meta.getSubjectCategories() -``` - -Result: - -```js -['article-type', 'research-subject'] -``` - -List keywords for a given category: - -```js -meta.getSubjects('research-subject') -``` - -```js -['Computational and Systems Biology', 'Epidemiology and Global Health'] -``` - -### Publication Dates - -Set or overwrite publication date (month and day are optional) - -```js -meta.setPubDate(2016, 3, 1) -``` - -Add publication history record: - -```js -meta.addPubHistoryRecord('received', 2016, 3, 1) -``` - -Remove publication history record: - -```js -meta.clearPubHistoryRecord('received') -``` From eb2fc5d24db59c34a39b0434beec99884a4ed65e Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Fri, 3 May 2019 13:06:52 +0200 Subject: [PATCH 41/56] Allow to disable ESC handling of Input component. --- src/kit/ui/Input.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/kit/ui/Input.js b/src/kit/ui/Input.js index b3aace7d4..72811bd75 100644 --- a/src/kit/ui/Input.js +++ b/src/kit/ui/Input.js @@ -60,9 +60,11 @@ export default class Input extends Component { switch (combo) { // ESCAPE reverts the current pending change case ESCAPE: { - event.stopPropagation() - event.preventDefault() - this.el.val(this._getDocumentValue()) + if (this.props.handleEscape !== false) { + event.stopPropagation() + event.preventDefault() + this.el.val(this._getDocumentValue()) + } break } default: From f707ef2189a9262a8b6628b0fa11a1ada8e57969 Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Fri, 3 May 2019 13:07:23 +0200 Subject: [PATCH 42/56] Trying a selection based approach for KeywordInput. --- src/article/shared/KeywordInput.js | 242 ++++++++++++++++++++++------- 1 file changed, 186 insertions(+), 56 deletions(-) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index 2ee40b653..700c7e177 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -1,27 +1,25 @@ -import { Component, getKeyForPath, domHelpers, keys } from 'substance' -import { OverlayMixin, Popup, TextInput } from '../../kit' +import { CustomSurface, getKeyForPath, domHelpers, keys, last } from 'substance' +import { Popup, TextInput } from '../../kit' /** * Experimental: this is the first example of an overlay that itself * hosts Surfaces, similar to an IsolatedNodeComponent. * Thus we are applying the same strategy regarding selections and surfaceId. */ -export default class KeywordInput extends OverlayMixin(Component) { - constructor (...args) { - super(...args) - - this._surfaceId = this.context.parentSurfaceId + '/' + this._getOverlayId() - } - getInitialState () { +export default class KeywordInput extends CustomSurface { + // HACK: keyboard handling between TextInputs and the + // native input is pretty hacky, and should be approached differently + getActionHandlers () { return { - isExpanded: this._canShowOverlay() + select: this._select } } - getChildContext () { - return Object.assign(super.getChildContext(), { - parentSurfaceId: this._surfaceId - }) + getInitialState () { + return { + isSelected: false, + isExpanded: false + } } didMount () { @@ -29,8 +27,6 @@ export default class KeywordInput extends OverlayMixin(Component) { let appState = this.context.appState appState.addObserver(['selection'], this._onSelectionHasChanged, this, { stage: 'render' }) - - this._focusNewKeyworkInput() } dispose () { @@ -39,44 +35,42 @@ export default class KeywordInput extends OverlayMixin(Component) { this.context.appState.removeObserver(this) } - didUpdate (oldProps, oldState) { - if (!oldState.isExpanded) { - this._focusNewKeyworkInput() - } - } - render ($$) { const doc = this.context.doc const model = this.props.model const values = model.getValue() const isEmpty = values.length === 0 + const isSelected = this.state.isSelected const isExpanded = this.state.isExpanded const label = isEmpty ? this.props.placeholder : values.map(v => doc.get([v, 'content'])).join(', ') const el = $$('div').addClass('sc-keyword-input') if (isEmpty) el.addClass('sm-empty') + if (isSelected) { + el.addClass('sm-active') + } el.addClass(isExpanded ? 'sm-expanded' : 'sm-collapsed') el.append( $$('div').addClass('se-label').text(label) .on('click', this._onClick) ) if (isExpanded) { - el.addClass('sm-active') el.append( this._renderEditor($$) ) } - el.on('mousedown', domHelpers.stop) - .on('mouseup', domHelpers.stop) - .on('click', domHelpers.stop) - .on('dblclick', domHelpers.stop) + el.on('mousedown', domHelpers.stopAndPrevent) + .on('mouseup', domHelpers.stopAndPrevent) + .on('click', domHelpers.stopAndPrevent) + .on('dblclick', domHelpers.stopAndPrevent) + .on('keydown', this._onKeydown) return el } _renderEditor ($$) { const model = this.props.model - const metadataValues = model.getValue() + const metadataValues = model.getItems() const placeholder = this.getLabel('enter-keyword') const Button = this.getComponent('button') @@ -85,17 +79,21 @@ export default class KeywordInput extends OverlayMixin(Component) { const editorEl = $$('div').ref('editor').addClass('se-keyword-editor') let lastIdx = metadataValues.length - 1 metadataValues.forEach((value, idx) => { - const path = [value, 'content'] + const path = [value.id, 'content'] const name = getKeyForPath(path) + let isFirst = idx === 0 + let isLast = idx === lastIdx + let input = $$(_HackedTextInput, { + name, + path, + placeholder, + isFirst, + isLast + }).ref(value.id) editorEl.append( $$('div').addClass('se-keyword').append( $$('div').addClass('se-keyword-input').append( - $$(_HackedTextInput, { - name, - path, - placeholder, - isLast: idx === lastIdx - }) + input ), this._renderIcon($$, 'trash').on('click', this._removeKeyword.bind(this, value)) ) @@ -104,7 +102,10 @@ export default class KeywordInput extends OverlayMixin(Component) { editorEl.append( $$('div').addClass('se-keyword').append( $$('div').addClass('se-keyword-input').append( - $$(Input, { placeholder }).ref('newKeywordInput') + $$(Input, { + placeholder, + handleEscape: false + }).ref('newKeywordInput') .attr({ tabindex: '2' }) .on('keydown', this._onNewKeywordKeydown) ), @@ -123,48 +124,112 @@ export default class KeywordInput extends OverlayMixin(Component) { ) } + _getCustomResourceId () { + return this.props.model.id + } + _onClick (event) { domHelpers.stopAndPrevent(event) - super._toggleOverlay() + if (!this.state.isSelected || !this.state.isExpanded) { + this._select(true) + } else if (this.state.isExpanded) { + this._select(false) + } + } + + _onKeydown (event) { + console.log('### _onKeydown()', event) + event.stopPropagation() + if (event.keyCode === keys.ESCAPE) { + event.preventDefault() + this._ensureIsCollapsed() + } } _onNewKeywordKeydown (event) { + console.log('### _onNewKeywordKeydown()', event) // TODO: maybe use the parseKeyEvent + parseKeyCombo trick to have exactly only reaction on ENTER without modifiers if (event.keyCode === keys.ENTER) { event.stopPropagation() event.preventDefault() this._addKeyword() + this._select(true) + } else if (event.keyCode === keys.UP) { + event.stopPropagation() + event.preventDefault() + this._selectLast() + } else if (event.keyCode === keys.TAB) { + event.stopPropagation() + event.preventDefault() + if (event.shiftKey) { + this._selectLast() + } + } else if (event.keyCode === keys.ESCAPE) { + event.preventDefault() + this.refs.newKeywordInput.val('') } } - _onOverlayIdHasChanged () { - let overlayId = this.context.appState.overlayId - let id = this._getOverlayId() - let needUpdate = false - if (this.state.isExpanded) { - needUpdate = (overlayId !== id) + _onSelectionHasChanged (sel) { + let surfaceId = sel.surfaceId + if (surfaceId && surfaceId.startsWith(this._surfaceId)) { + if (sel.isCustomSelection()) { + let isExpanded = sel.data.isExpanded + if (!this.state.isSelected) { + this.extendState({ + isSelected: true, + isExpanded + }) + } else if (this.state.isExpanded !== isExpanded) { + this.extendState({ + isExpanded + }) + } + } else { + this._ensureIsExpanded() + } + } else if (this.state.isSelected) { + this.extendState({ isSelected: false, isExpanded: false }) + } + } + + rerenderDOMSelection () { + let sel = this.context.appState.selection + if (sel.isCustomSelection()) { + if (sel.data.isExpanded) { + this._ensureIsExpanded() + this._focusNewKeyworkInput() + } else { + this._ensureIsCollapsed() + } } else { - needUpdate = (overlayId === id) + this._ensureIsExpanded() + this.context.parentSurface.rerenderDOMSelection() } - if (needUpdate) { - this.extendState(this.getInitialState()) + } + + _ensureIsExpanded () { + if (!this.state.isExpanded) { + this.extendState({ + isExpanded: true + }) } } - _onSelectionHasChanged (sel) { - let surfaceId = sel.surfaceId - if (surfaceId && surfaceId.startsWith(this._surfaceId)) { - if (!this.state.isExpanded) { - this.extendState({ isExpanded: true }) - } - } else if (this.state.isExpanded) { - this.extendState({ isExpanded: false }) + _ensureIsCollapsed () { + if (this.state.isExpanded) { + this.extendState({ + isExpanded: false + }) } } _addKeyword () { const keyword = this.refs.newKeywordInput.val() - this.send('addValue', keyword) + this.refs.newKeywordInput.val('') + if (keyword) { + this.send('addValue', keyword) + } } _removeKeyword (value) { @@ -178,13 +243,78 @@ export default class KeywordInput extends OverlayMixin(Component) { this.refs['newKeywordInput'].focus() } } + + _select (isExpanded) { + const model = this.props.model + this.context.editorSession.setSelection({ + type: 'custom', + customType: 'keywordInput', + nodeId: model._path[0], + data: { isExpanded }, + surfaceId: this._surfaceId + }) + } + + _selectLast () { + let model = this.props.model + let ids = model.getValue() + let lastId = last(ids) + let surface = this.refs[lastId] + if (surface) { + this.context.editorSession.setSelection({ + type: 'property', + path: surface.props.path, + startOffset: 0, + surfaceId: surface.getSurfaceId() + }) + } + } } +// TODO: try to make TextInput better customizable so +// that it is easier to override specific keyboard handlers +// The biggest problem we have is, that some fields are Surfaces +// and the one for the new keyword is just a plain input. +// I tried using a Surface for that too, but this requires the +// value to exist in the model. Maybe, it would still be interesting +// create something that is Surface compatible, but does not need +// a real value in the model, or allows to bind it to a 'volatile' one. class _HackedTextInput extends TextInput { _handleTabKey (event) { event.stopPropagation() - if (!event.shiftKey && !this.props.isLast) { + if (this.props.isLast) { + if (event.shiftKey) { + this.__handleTab(event) + } else { + event.preventDefault() + this._select(true) + } + } else if (this.props.isFirst) { + if (event.shiftKey) { + event.preventDefault() + } else { + this.__handleTab(event) + } + } else { this.__handleTab(event) } } + + _handleUpOrDownArrowKey (event) { + event.stopPropagation() + if (this.props.isLast && event.keyCode === keys.DOWN) { + // skip so that the cursor stays within this overlay + event.preventDefault() + this._select(true) + } else if (this.props.isFirst && event.keyCode === keys.UP) { + // skip so that the cursor stays within this overlay + event.preventDefault() + } else { + super._handleUpOrDownArrowKey(event) + } + } + + _select (isExpanded) { + this.send('select', isExpanded) + } } From 5fed50811a9bcfc485646d53f9c1be708f191714 Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Fri, 3 May 2019 13:11:32 +0200 Subject: [PATCH 43/56] Silence. --- src/article/shared/KeywordInput.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index 700c7e177..9b7124576 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -138,7 +138,7 @@ export default class KeywordInput extends CustomSurface { } _onKeydown (event) { - console.log('### _onKeydown()', event) + // console.log('### _onKeydown()', event) event.stopPropagation() if (event.keyCode === keys.ESCAPE) { event.preventDefault() @@ -147,8 +147,7 @@ export default class KeywordInput extends CustomSurface { } _onNewKeywordKeydown (event) { - console.log('### _onNewKeywordKeydown()', event) - // TODO: maybe use the parseKeyEvent + parseKeyCombo trick to have exactly only reaction on ENTER without modifiers + // console.log('### _onNewKeywordKeydown()', event) if (event.keyCode === keys.ENTER) { event.stopPropagation() event.preventDefault() From acda28fb7ceb5fa446f03aaeb2250fb3447c89dc Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 3 May 2019 15:18:14 +0300 Subject: [PATCH 44/56] Fix keyword removing. --- src/article/shared/KeywordInput.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index 9b7124576..d5f836579 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -95,7 +95,7 @@ export default class KeywordInput extends CustomSurface { $$('div').addClass('se-keyword-input').append( input ), - this._renderIcon($$, 'trash').on('click', this._removeKeyword.bind(this, value)) + this._renderIcon($$, 'trash').on('click', this._removeKeyword.bind(this, value.id)) ) ) }) @@ -231,10 +231,10 @@ export default class KeywordInput extends CustomSurface { } } - _removeKeyword (value) { + _removeKeyword (nodeId) { const model = this.props.model const path = model.getPath() - this.send('executeCommand', 'remove-keyword', { path, nodeId: value, surfaceId: this._surfaceId }) + this.send('executeCommand', 'remove-keyword', { path, nodeId, surfaceId: this._surfaceId }) } _focusNewKeyworkInput () { From 269c6a2d898f4c71b6ed979f22ab456d76c9faa0 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 3 May 2019 17:02:49 +0300 Subject: [PATCH 45/56] Move selection after keyword removing. --- src/article/shared/KeywordInput.js | 8 +++++++- src/article/shared/RemoveKeywordCommand.js | 20 +++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index d5f836579..9798f6b55 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -234,7 +234,13 @@ export default class KeywordInput extends CustomSurface { _removeKeyword (nodeId) { const model = this.props.model const path = model.getPath() - this.send('executeCommand', 'remove-keyword', { path, nodeId, surfaceId: this._surfaceId }) + const values = model.getValue() + const valueIndex = values.indexOf(nodeId) + const isLast = valueIndex === values.length - 1 + // NOTE: current strartegy it to put selection to a next keyword. + // To do it we need to pass next input surfaceId or new keyword input surfaceId + const surfaceId = isLast ? this._surfaceId : this.refs[values[valueIndex + 1]].getSurfaceId() + this.send('executeCommand', 'remove-keyword', { path, nodeId, surfaceId }) } _focusNewKeyworkInput () { diff --git a/src/article/shared/RemoveKeywordCommand.js b/src/article/shared/RemoveKeywordCommand.js index 01f549e53..a18a2d559 100644 --- a/src/article/shared/RemoveKeywordCommand.js +++ b/src/article/shared/RemoveKeywordCommand.js @@ -15,18 +15,24 @@ export default class RemoveKeywordCommand extends Command { documentHelpers.removeAt(tx, path, index) documentHelpers.deepDeleteNode(nodeId) - // TODO: after removing selection should be - // moved to the next ‘reasonable’ place + // NOTE: After removing keyword selection should be moved to + // the next reasonable place: next keyword value or new keyword input if (index < size - 1) { + const nextNodeId = tx.get(path)[index] tx.setSelection({ type: 'property', - path: [tx.get(path)[index], 'content'], - surfaceId: surfaceId, - startOffset: 0, - endOffset: 0 + path: [nextNodeId, 'content'], + surfaceId, + startOffset: 0 }) } else { - tx.selection = null + tx.setSelection({ + type: 'custom', + customType: 'keywordInput', + nodeId, + data: { isExpanded: true }, + surfaceId + }) } }) } From f7e6fca9394e0097e7ad69985df04ff098916571 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 3 May 2019 18:33:48 +0300 Subject: [PATCH 46/56] Get document via editor session. --- src/article/shared/KeywordInput.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index 9798f6b55..097ae4b85 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -36,13 +36,16 @@ export default class KeywordInput extends CustomSurface { } render ($$) { - const doc = this.context.doc + const editorSession = this.context.editorSession + const doc = editorSession.getDocument() const model = this.props.model const values = model.getValue() const isEmpty = values.length === 0 const isSelected = this.state.isSelected const isExpanded = this.state.isExpanded - const label = isEmpty ? this.props.placeholder : values.map(v => doc.get([v, 'content'])).join(', ') + const label = isEmpty ? this.props.placeholder : values.map(v => { + return doc.get([v, 'content']) + }).join(', ') const el = $$('div').addClass('sc-keyword-input') if (isEmpty) el.addClass('sm-empty') From 69f52ac1d919059e5418c71ae12bf3f0697281a9 Mon Sep 17 00:00:00 2001 From: Daniel Beilinson Date: Fri, 3 May 2019 18:34:29 +0300 Subject: [PATCH 47/56] Change tests accordingly to new metadata keyword values input. --- test/Figure.test.js | 12 ++++++++---- test/FigureMetadata.test.js | 24 ++++++++++++++---------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/test/Figure.test.js b/test/Figure.test.js index 2de8b7333..82d582506 100644 --- a/test/Figure.test.js +++ b/test/Figure.test.js @@ -24,7 +24,8 @@ const xrefListItemSelector = '.sc-edit-xref-tool .se-option .sc-preview' const figurePanelPreviousSelector = '.sc-figure .se-control.sm-previous' const figurePanelNextSelector = '.sc-figure .se-control.sm-next' const currentPanelSelector = '.sc-figure .se-current-panel .sc-figure-panel' -const figureCustomMetadataFieldInputSelector = '.sc-custom-metadata-field .sc-string' +const figureCustomMetadataFieldNameSelector = '.sc-custom-metadata-field .sc-string' +const figureCustomMetadataFieldValuesSelector = '.sc-custom-metadata-field .sc-keyword-input' const FIGURE_WITH_TWO_PANELS = ` @@ -462,9 +463,12 @@ test('Figure: replicate first panel structure', t => { t.ok(insertFigurePanelTool.el.click(), 'clicking on the insert figure panel button should not throw error') insertFigurePanelTool.onFileSelect(new PseudoFileEvent()) _gotoNext() - const fields = editor.findAll(figureCustomMetadataFieldInputSelector) - t.equal(fields[0].getTextContent(), 'Field I', 'shoud be replicated keyword label inside custom field name') - t.equal(fields[1].getTextContent(), '', 'shoud be empty value') + const fieldNames = editor.findAll(figureCustomMetadataFieldNameSelector) + const fieldValues = editor.findAll(figureCustomMetadataFieldValuesSelector) + t.equal(fieldNames.length, 1, 'there should be one input for a field name') + t.equal(fieldNames.length, fieldValues.length, 'there should be the same number of custom metadata field name and values') + t.equal(fieldNames[0].getTextContent(), 'Field I', 'shoud be replicated keyword label inside custom field name') + t.equal(fieldValues[0].getTextContent(), 'Click to add keywords', 'shoud be empty value') t.end() }) diff --git a/test/FigureMetadata.test.js b/test/FigureMetadata.test.js index cf37c0e39..38e82cad9 100644 --- a/test/FigureMetadata.test.js +++ b/test/FigureMetadata.test.js @@ -11,8 +11,8 @@ const moveUpCustomMetadataFieldToolSelector = '.sm-move-up-metadata-field' const removeCustomMetadataFieldToolSelector = '.sm-remove-metadata-field' const figureMetadataSelector = '.sc-custom-metadata-field' -const figureCustomMetadataFieldInputSelector = '.sc-custom-metadata-field .sc-string' -const figureCustomMetadataFieldNameSelector = '.sc-custom-metadata-field .se-field-name .se-input' +const figureCustomMetadataFieldNameSelector = '.sc-custom-metadata-field .sc-string' +const figureCustomMetadataFieldValuesSelector = '.sc-custom-metadata-field .sc-keyword-input' const FIXTURE = ` @@ -35,10 +35,12 @@ test('Figure Metadata: open figure with custom fields in manuscript and metadata let editor = openManuscriptEditor(app) loadBodyFixture(editor, FIXTURE) t.notNil(editor.find(figureMetadataSelector), 'there should be a figure with metadata in manuscript') - const fields = editor.findAll(figureCustomMetadataFieldInputSelector) - t.equal(fields.length, 2, 'there should be two inputs') - t.equal(fields[0].getTextContent(), 'Field I', 'shoud be keyword label inside first') - t.equal(fields[1].getTextContent(), 'Value A, Value B', 'shoud be values joined with comma inside second') + const fieldNames = editor.findAll(figureCustomMetadataFieldNameSelector) + const fieldValues = editor.findAll(figureCustomMetadataFieldValuesSelector) + t.equal(fieldNames.length, 1, 'there should be one input for a field name') + t.equal(fieldNames.length, fieldValues.length, 'there should be the same number of custom metadata field name and values') + t.equal(fieldNames[0].getTextContent(), 'Field I', 'shoud be keyword label inside first') + t.equal(fieldValues[0].getTextContent(), 'Value A, Value B', 'shoud be values joined with comma inside second') editor = openMetadataEditor(app) t.notNil(editor.find(figureMetadataSelector), 'there should be a figure with metadata in manuscript') t.end() @@ -54,8 +56,9 @@ test('Figure Metadata: add a new custom field', t => { t.ok(addCustomMetadataFieldTool.click(), 'clicking on add custom field tool should not throw error') t.equal(editor.findAll(figureMetadataSelector).length, 2, 'there should be two custom fields now') const selectedNodePath = getSelection(editor).path - const secondCustomFieldInputPath = editor.findAll(figureCustomMetadataFieldNameSelector)[1].getPath() - t.deepEqual(selectedNodePath, secondCustomFieldInputPath, 'selection path and second custom field path should match') + const secondCustomFieldInput = editor.findAll(figureCustomMetadataFieldNameSelector)[1] + const secondCustomFieldInputModel = secondCustomFieldInput.props.model + t.deepEqual(selectedNodePath, secondCustomFieldInputModel.getPath(), 'selection path and second custom field path should match') t.end() }) @@ -70,8 +73,9 @@ test('Figure Metadata: add a new custom field when figure is selected', t => { t.ok(addCustomMetadataFieldTool.click(), 'clicking on add custom field tool should not throw error') t.equal(editor.findAll(figureMetadataSelector).length, 2, 'there should be two custom fields now') const selectedNodePath = getSelection(editor).path - const secondCustomFieldInputPath = editor.findAll(figureCustomMetadataFieldNameSelector)[1].getPath() - t.deepEqual(selectedNodePath, secondCustomFieldInputPath, 'selection path and second custom field path should match') + const secondCustomFieldInput = editor.findAll(figureCustomMetadataFieldNameSelector)[1] + const secondCustomFieldInputModel = secondCustomFieldInput.props.model + t.deepEqual(selectedNodePath, secondCustomFieldInputModel.getPath(), 'selection path and second custom field path should match') t.end() }) From ea0c296e806ad9ac77cbdd0b221c248a7f476a8d Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 13 May 2019 21:16:46 +0300 Subject: [PATCH 48/56] Do not move popup down when new values added. --- src/kit/styles/_popup.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/kit/styles/_popup.css b/src/kit/styles/_popup.css index 29eafc346..bd6df2bdd 100644 --- a/src/kit/styles/_popup.css +++ b/src/kit/styles/_popup.css @@ -1,8 +1,7 @@ .sc-popup { position: absolute; width: 100%; - left: 50%; - transform: translate(-50%, 10%); + top: 45px; background: var(--t-background-color); border: var(--t-default-border); border-radius: var(--t-border-radius); From 065a32161c657626e604719db97d258d4037d1e3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 13 May 2019 21:18:41 +0300 Subject: [PATCH 49/56] Set old popup margin. --- src/kit/styles/_popup.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kit/styles/_popup.css b/src/kit/styles/_popup.css index bd6df2bdd..789010b0d 100644 --- a/src/kit/styles/_popup.css +++ b/src/kit/styles/_popup.css @@ -1,7 +1,7 @@ .sc-popup { position: absolute; width: 100%; - top: 45px; + top: 55px; background: var(--t-background-color); border: var(--t-default-border); border-radius: var(--t-border-radius); From 5d525838ad6a86dab8b1b4afaec22734de8c3781 Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 14 May 2019 16:11:00 +0300 Subject: [PATCH 50/56] Use css variable. --- src/kit/styles/_text-input.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/kit/styles/_text-input.css b/src/kit/styles/_text-input.css index 9efd88944..e90946a7f 100644 --- a/src/kit/styles/_text-input.css +++ b/src/kit/styles/_text-input.css @@ -5,7 +5,7 @@ .sc-text-input .se-input { width: 100%; height: var(--t-input-height); - border-radius: 5px; + border-radius: var(--t-border-radius); padding: var(--t-input-padding); border: var(--t-input-default-border); margin: var(--t-negative-input-padding); From 9e87aa2c90ed38da225b7bd15fa1950ae14d653c Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 14 May 2019 16:11:02 +0300 Subject: [PATCH 51/56] Improve look of keyword popup. --- src/article/manuscript/ManuscriptPackage.js | 1 + src/article/metadata/MetadataPackage.js | 1 + .../shared/CustomMetadataFieldComponent.js | 1 + src/article/shared/KeywordInput.js | 42 ++++++---------- src/kit/styles/_keyword-input.css | 50 ++++++++----------- 5 files changed, 41 insertions(+), 54 deletions(-) diff --git a/src/article/manuscript/ManuscriptPackage.js b/src/article/manuscript/ManuscriptPackage.js index 576726d41..abdae5ce5 100644 --- a/src/article/manuscript/ManuscriptPackage.js +++ b/src/article/manuscript/ManuscriptPackage.js @@ -337,6 +337,7 @@ export default { config.addLabel('enter-url-placeholder', 'Enter url') config.addLabel('enter-keyword', 'Enter keyword') config.addLabel('enter-keywords', 'Click to add keywords') + config.addLabel('edit-keywords', 'Edit keywords') // Icons config.addIcon('create-unordered-list', { 'fontawesome': 'fa-list-ul' }) diff --git a/src/article/metadata/MetadataPackage.js b/src/article/metadata/MetadataPackage.js index cb1f58fa3..717920d8a 100644 --- a/src/article/metadata/MetadataPackage.js +++ b/src/article/metadata/MetadataPackage.js @@ -361,6 +361,7 @@ export default { config.addLabel('open-link', 'Open Link') config.addLabel('enter-keyword', 'Enter keyword') config.addLabel('enter-keywords', 'Click to add keywords') + config.addLabel('edit-keywords', 'Edit keywords') // Icons config.addIcon('input-error', { 'fontawesome': 'fa-exclamation-circle' }) diff --git a/src/article/shared/CustomMetadataFieldComponent.js b/src/article/shared/CustomMetadataFieldComponent.js index 6f40b166c..f3908a698 100644 --- a/src/article/shared/CustomMetadataFieldComponent.js +++ b/src/article/shared/CustomMetadataFieldComponent.js @@ -19,6 +19,7 @@ export default class CustomMetadataFieldComponent extends NodeComponent { $$(KeywordInput, { model: valuesModel, placeholder: this.getLabel('enter-keywords'), + label: this.getLabel('edit-keywords'), overlayId: valuesModel.id }).addClass('se-field-values') ) diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js index 097ae4b85..e19514473 100644 --- a/src/article/shared/KeywordInput.js +++ b/src/article/shared/KeywordInput.js @@ -74,6 +74,7 @@ export default class KeywordInput extends CustomSurface { _renderEditor ($$) { const model = this.props.model const metadataValues = model.getItems() + const label = this.props.label const placeholder = this.getLabel('enter-keyword') const Button = this.getComponent('button') @@ -95,30 +96,26 @@ export default class KeywordInput extends CustomSurface { }).ref(value.id) editorEl.append( $$('div').addClass('se-keyword').append( - $$('div').addClass('se-keyword-input').append( - input - ), - this._renderIcon($$, 'trash').on('click', this._removeKeyword.bind(this, value.id)) + input ) ) }) editorEl.append( - $$('div').addClass('se-keyword').append( - $$('div').addClass('se-keyword-input').append( - $$(Input, { - placeholder, - handleEscape: false - }).ref('newKeywordInput') - .attr({ tabindex: '2' }) - .on('keydown', this._onNewKeywordKeydown) - ), + $$('div').addClass('se-new-keyword-input').append( + $$(Input, { + placeholder, + handleEscape: false + }).ref('newKeywordInput') + .attr({ tabindex: '2' }) + .on('keydown', this._onNewKeywordKeydown) + .on('click', this._onNewKeywordClick), $$(Button).append( this.getLabel('create') ).addClass('se-create-value') .on('click', this._addKeyword) ) ) - return $$(Popup).append(editorEl) + return $$(Popup, { label }).append(editorEl) } _renderIcon ($$, iconName) { @@ -149,6 +146,11 @@ export default class KeywordInput extends CustomSurface { } } + _onNewKeywordClick (event) { + domHelpers.stopAndPrevent(event) + this._select(true) + } + _onNewKeywordKeydown (event) { // console.log('### _onNewKeywordKeydown()', event) if (event.keyCode === keys.ENTER) { @@ -234,18 +236,6 @@ export default class KeywordInput extends CustomSurface { } } - _removeKeyword (nodeId) { - const model = this.props.model - const path = model.getPath() - const values = model.getValue() - const valueIndex = values.indexOf(nodeId) - const isLast = valueIndex === values.length - 1 - // NOTE: current strartegy it to put selection to a next keyword. - // To do it we need to pass next input surfaceId or new keyword input surfaceId - const surfaceId = isLast ? this._surfaceId : this.refs[values[valueIndex + 1]].getSurfaceId() - this.send('executeCommand', 'remove-keyword', { path, nodeId, surfaceId }) - } - _focusNewKeyworkInput () { if (this.state.isExpanded) { this.refs['newKeywordInput'].focus() diff --git a/src/kit/styles/_keyword-input.css b/src/kit/styles/_keyword-input.css index 90dd7dc6f..934f2c767 100644 --- a/src/kit/styles/_keyword-input.css +++ b/src/kit/styles/_keyword-input.css @@ -1,6 +1,5 @@ .sc-keyword-input { position: relative; - width: 100%; line-height: var(--t-input-line-height); cursor: pointer; padding: var(--t-input-padding); @@ -9,55 +8,50 @@ border: var(--t-input-default-border); } +.sc-keyword-input .se-keyword-editor { + padding: var(--t-half-spacing) var(--t-default-spacing); +} + .sc-keyword-input.sm-active { border: var(--t-input-focus-border); background: var(--t-focus-background-color); } -.sc-keyword-input .se-keyword { - display: flex; - align-items: center; - border-bottom: var(--t-default-border); -} - -.sc-keyword-input .se-keyword:last-child { - border-bottom: none; -} - -.sc-keyword-input .se-keyword > * { - margin-right: var(--t-half-spacing); +.sc-keyword-input .sc-text-input .se-input { + width: auto; } -.sc-keyword-input .se-keyword .se-keyword-input { - flex: 1 1 auto; +.sc-keyword-input .se-keyword-editor .se-keyword { + margin: var(--t-half-spacing) 0; } -.sc-keyword-input .sc-text-input .se-input { - border-radius: 0; - margin: 0; +.sc-keyword-input .se-new-keyword-input { + display: flex; + margin: 0 var(--t-negative-input-padding); } -.sc-keyword-input .sc-text-input:focus .se-input { +.sc-keyword-input .se-new-keyword-input > .sc-input { border: var(--t-input-default-border); -} - -.sc-keyword-input .se-keyword-input > .sc-input { - width: 100%; - height: var(--t-input-height); + border-radius: var(--t-border-radius); padding: var(--t-input-padding); + margin-right: var(--t-half-spacing); font-size: var(--t-small-font-size); line-height: var(--t-input-line-height); - border: var(--t-input-default-border); - border-radius: var(--t-border-radius); + flex-grow: 1; } -.sc-keyword-input .se-keyword-input > .sc-input::placeholder { +.sc-keyword-input .se-new-keyword-input > .sc-input:focus { + border: var(--t-input-focus-border); +} + +.sc-keyword-input .se-new-keyword-input > .sc-input::placeholder { color: var(--t-placeholder-text-color); font-weight: var(--t-normal-font-weight); } + .sc-keyword-input .se-create-value { background: var(--t-action-background-color); color: var(--t-inverted-text-color); border-radius: var(--t-border-radius); - padding: 2px 6px; + padding: var(--t-input-padding) var(--t-half-spacing); } \ No newline at end of file From d16bbc94d50ad10913ea5499c5a6863a7a136ed3 Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 May 2019 00:40:35 +0300 Subject: [PATCH 52/56] Remove kwywords through context menu. --- src/article/manuscript/ManuscriptPackage.js | 4 ++- src/article/metadata/MetadataPackage.js | 4 +++ src/article/shared/ArticleToolbarPackage.js | 1 + src/article/shared/RemoveKeywordCommand.js | 39 ++++++++------------- 4 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/article/manuscript/ManuscriptPackage.js b/src/article/manuscript/ManuscriptPackage.js index abdae5ce5..3da90d5d6 100644 --- a/src/article/manuscript/ManuscriptPackage.js +++ b/src/article/manuscript/ManuscriptPackage.js @@ -242,7 +242,9 @@ export default { nodeType: 'footnote', commandGroup: 'footnote' }) - config.addCommand('remove-keyword', RemoveKeywordCommand) + config.addCommand('remove-metadata-keyword', RemoveKeywordCommand, { + commandGroup: 'custom-metadata-fields' + }) config.addCommand('replace-figure-panel-image', ReplaceFigurePanelImageCommand, { commandGroup: 'figure-panel' }) diff --git a/src/article/metadata/MetadataPackage.js b/src/article/metadata/MetadataPackage.js index 717920d8a..e9da5ce3d 100644 --- a/src/article/metadata/MetadataPackage.js +++ b/src/article/metadata/MetadataPackage.js @@ -36,6 +36,7 @@ import { TableSelectAllCommand, ToggleCellHeadingCommand, ToggleCellMergeCommand } from '../manuscript/TableCommands' import OpenFigurePanelImageTool from '../shared/OpenFigurePanelImageTool' +import RemoveKeywordCommand from '../shared/RemoveKeywordCommand' import ReplaceFigurePanelTool from '../shared/ReplaceFigurePanelTool' import TableFigureComponent from '../shared/TableFigureComponent' import TranslatableEntryEditor from './TranslatableEntryEditor' @@ -243,6 +244,9 @@ export default { config.addCommand('remove-figure-panel', RemoveFigurePanelCommand, { commandGroup: 'figure-panel' }) + config.addCommand('remove-metadata-keyword', RemoveKeywordCommand, { + commandGroup: 'custom-metadata-fields' + }) config.addCommand('remove-reference', RemoveReferenceCommand, { commandGroup: 'reference' }) diff --git a/src/article/shared/ArticleToolbarPackage.js b/src/article/shared/ArticleToolbarPackage.js index acd5b1995..692c8a036 100644 --- a/src/article/shared/ArticleToolbarPackage.js +++ b/src/article/shared/ArticleToolbarPackage.js @@ -373,6 +373,7 @@ export default { config.addLabel('move-down-metadata-field', 'Move Down Metadata Field') config.addLabel('move-up-metadata-field', 'Move Up Metadata Field') config.addLabel('remove-metadata-field', 'Remove Metadata Field') + config.addLabel('remove-metadata-keyword', 'Remove Keyword') // Author tools config.addLabel('edit-author', 'Edit Author') // Reference tools diff --git a/src/article/shared/RemoveKeywordCommand.js b/src/article/shared/RemoveKeywordCommand.js index a18a2d559..4d19c3386 100644 --- a/src/article/shared/RemoveKeywordCommand.js +++ b/src/article/shared/RemoveKeywordCommand.js @@ -2,38 +2,29 @@ import { Command, documentHelpers } from 'substance' export default class RemoveKeywordCommand extends Command { getCommandState (params, context) { - return { disabled: false } + const xpath = params.selectionState.xpath + if (xpath.length > 0) { + const selectedType = xpath[xpath.length - 1].type + if (selectedType === 'custom-metadata-value') { + return { disabled: false } + } + } + return { disabled: true } } execute (params, context) { - const { path, nodeId, surfaceId } = params + const selectionState = params.selectionState + const node = selectionState.node + const nodeId = node.id + const parentNode = node.getParent() + const path = [parentNode.id, 'values'] const editorSession = context.editorSession + editorSession.transaction(tx => { const index = tx.get(path).indexOf(nodeId) - const size = tx.get(path).length - if (index === -1) return false - documentHelpers.removeAt(tx, path, index) documentHelpers.deepDeleteNode(nodeId) - // NOTE: After removing keyword selection should be moved to - // the next reasonable place: next keyword value or new keyword input - if (index < size - 1) { - const nextNodeId = tx.get(path)[index] - tx.setSelection({ - type: 'property', - path: [nextNodeId, 'content'], - surfaceId, - startOffset: 0 - }) - } else { - tx.setSelection({ - type: 'custom', - customType: 'keywordInput', - nodeId, - data: { isExpanded: true }, - surfaceId - }) - } + tx.setSelection(null) }) } } From f1e5edf874330e2bbabbd89d2390630303eed29d Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 May 2019 00:40:51 +0300 Subject: [PATCH 53/56] Set new keyword input width. --- src/kit/styles/_keyword-input.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/kit/styles/_keyword-input.css b/src/kit/styles/_keyword-input.css index 934f2c767..c6f0ac796 100644 --- a/src/kit/styles/_keyword-input.css +++ b/src/kit/styles/_keyword-input.css @@ -31,6 +31,7 @@ } .sc-keyword-input .se-new-keyword-input > .sc-input { + width: 100%; border: var(--t-input-default-border); border-radius: var(--t-border-radius); padding: var(--t-input-padding); From 31373a501496a3008f89e298c7f46340685ceefe Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 15 May 2019 09:22:00 +0300 Subject: [PATCH 54/56] Add keyboard shortcuts for keyword removing. --- src/article/manuscript/ManuscriptPackage.js | 1 + src/article/metadata/MetadataPackage.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/article/manuscript/ManuscriptPackage.js b/src/article/manuscript/ManuscriptPackage.js index 3da90d5d6..184ef759f 100644 --- a/src/article/manuscript/ManuscriptPackage.js +++ b/src/article/manuscript/ManuscriptPackage.js @@ -455,6 +455,7 @@ export default { // KeyboardShortcuts config.addKeyboardShortcut('CommandOrControl+a', { command: 'table:select-all' }) + config.addKeyboardShortcut('CommandOrControl+Delete', { command: 'remove-metadata-keyword' }) // Register commands and keyboard shortcuts for collections registerCollectionCommand(config, 'author', ['metadata', 'authors'], { keyboardShortcut: 'CommandOrControl+Alt+A', nodeType: 'person' }) diff --git a/src/article/metadata/MetadataPackage.js b/src/article/metadata/MetadataPackage.js index e9da5ce3d..ff8a2f0db 100644 --- a/src/article/metadata/MetadataPackage.js +++ b/src/article/metadata/MetadataPackage.js @@ -322,6 +322,7 @@ export default { config.addKeyboardShortcut('CommandOrControl+Alt+Up', { command: 'move-up-col-item' }) config.addKeyboardShortcut('CommandOrControl+Alt+Down', { command: 'move-down-col-item' }) config.addKeyboardShortcut('CommandOrControl+Alt+Delete', { command: 'remove-col-item' }) + config.addKeyboardShortcut('CommandOrControl+Delete', { command: 'remove-metadata-keyword' }) // Labels config.addLabel('abstracts', 'Abstracts') From fcff54905fa468aa69fe79ff22643b35d4ddfc7b Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Thu, 16 May 2019 02:35:56 +0200 Subject: [PATCH 55/56] Fix rebase regression. --- src/article/shared/styles/_index.css | 1 - 1 file changed, 1 deletion(-) diff --git a/src/article/shared/styles/_index.css b/src/article/shared/styles/_index.css index 2833b0440..db5fc29f0 100644 --- a/src/article/shared/styles/_index.css +++ b/src/article/shared/styles/_index.css @@ -9,7 +9,6 @@ @import './_inline-formula.css'; @import './_manuscript-view.css'; @import './_manuscript.css'; -@import './_multi-select-input.css'; @import './_query.css'; @import './_ref-contrib-editor.css'; @import './_section-label.css'; From 0e7268c9ec4d4f87227977d067d291c2ea25490a Mon Sep 17 00:00:00 2001 From: Oliver Buchtala Date: Thu, 16 May 2019 16:52:55 +0200 Subject: [PATCH 56/56] Fix demo/index.html Regression after changing directory layout. --- builds/demo/index.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/builds/demo/index.html b/builds/demo/index.html index bad861a90..80f447cb8 100644 --- a/builds/demo/index.html +++ b/builds/demo/index.html @@ -5,11 +5,11 @@ Texture Editor