diff --git a/builds/demo/index.html b/builds/demo/index.html
index 27404acae..80f447cb8 100644
--- a/builds/demo/index.html
+++ b/builds/demo/index.html
@@ -5,21 +5,21 @@
Texture Editor
-
+
-
+
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')
-```
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')
diff --git a/src/article/converter/jats/FigurePanelConverter.js b/src/article/converter/jats/FigurePanelConverter.js
index 8095be804..c91f5e1cc 100644
--- a/src/article/converter/jats/FigurePanelConverter.js
+++ b/src/article/converter/jats/FigurePanelConverter.js
@@ -59,11 +59,16 @@ 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 => {
+ return doc.create({
+ type: 'custom-metadata-value',
+ content: kwdEl.textContent
+ }).id
+ })
return doc.create({
type: 'custom-metadata-field',
name,
- value
+ values
}).id
})
}
@@ -109,8 +114,8 @@ export default class FigurePanelConverter {
let kwdGroupEl = $$('kwd-group').append(
$$('label').text(field.name)
)
- let kwdEls = field.value.split(',').map(str => {
- return $$('kwd').text(str.trim())
+ let kwdEls = field.resolve('values').map(keyword => {
+ return $$('kwd').text(keyword.content)
})
kwdGroupEl.append(kwdEls)
return kwdGroupEl
diff --git a/src/article/manuscript/ManuscriptPackage.js b/src/article/manuscript/ManuscriptPackage.js
index 2780e58a7..184ef759f 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,9 @@ export default {
nodeType: 'footnote',
commandGroup: 'footnote'
})
+ config.addCommand('remove-metadata-keyword', RemoveKeywordCommand, {
+ commandGroup: 'custom-metadata-fields'
+ })
config.addCommand('replace-figure-panel-image', ReplaceFigurePanelImageCommand, {
commandGroup: 'figure-panel'
})
@@ -306,6 +310,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')
@@ -332,6 +337,9 @@ 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')
+ config.addLabel('enter-keywords', 'Click to add keywords')
+ config.addLabel('edit-keywords', 'Edit keywords')
// Icons
config.addIcon('create-unordered-list', { 'fontawesome': 'fa-list-ul' })
@@ -447,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 7ef504105..ff8a2f0db 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'
})
@@ -318,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')
@@ -359,11 +364,16 @@ 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')
+ config.addLabel('enter-keywords', 'Click to add keywords')
+ config.addLabel('edit-keywords', 'Edit keywords')
+
// 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' })
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/CustomMetadataField.js b/src/article/models/CustomMetadataField.js
index 534a0e83f..7232222e8 100644
--- a/src/article/models/CustomMetadataField.js
+++ b/src/article/models/CustomMetadataField.js
@@ -1,4 +1,4 @@
-import { DocumentNode, STRING } from 'substance'
+import { CHILDREN, DocumentNode, STRING } 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: 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
+}
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'
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/CustomMetadataFieldComponent.js b/src/article/shared/CustomMetadataFieldComponent.js
index 6e4da3215..f3908a698 100644
--- a/src/article/shared/CustomMetadataFieldComponent.js
+++ b/src/article/shared/CustomMetadataFieldComponent.js
@@ -1,12 +1,44 @@
-import { NodeComponent } from '../../kit'
+import { createValueModel, NodeComponent } from '../../kit'
+import KeywordInput from './KeywordInput'
export default class CustomMetadataFieldComponent extends NodeComponent {
+ getActionHandlers () {
+ return {
+ addValue: this._addValue
+ }
+ }
+
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()
+ 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, {
+ model: valuesModel,
+ placeholder: this.getLabel('enter-keywords'),
+ label: this.getLabel('edit-keywords'),
+ overlayId: valuesModel.id
+ }).addClass('se-field-values')
+ )
+ } else {
+ el.append(
+ $$('div').addClass('se-field-name').append(node.name),
+ $$('div').addClass('se-field-values').append(valuesModel.getValue())
+ )
+ }
return el
}
+
+ _addValue (value) {
+ const node = this._getNode()
+ this.context.api._appendChild([node.id, 'values'], { type: 'custom-metadata-value', content: value })
+ }
+
+ _getValuesModel () {
+ const node = this._getNode()
+ return createValueModel(this.context.api, [node.id, 'values'])
+ }
}
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)
}
}
diff --git a/src/article/shared/KeywordInput.js b/src/article/shared/KeywordInput.js
new file mode 100644
index 000000000..e19514473
--- /dev/null
+++ b/src/article/shared/KeywordInput.js
@@ -0,0 +1,318 @@
+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 CustomSurface {
+ // HACK: keyboard handling between TextInputs and the
+ // native input is pretty hacky, and should be approached differently
+ getActionHandlers () {
+ return {
+ select: this._select
+ }
+ }
+
+ getInitialState () {
+ return {
+ isSelected: false,
+ isExpanded: false
+ }
+ }
+
+ didMount () {
+ super.didMount()
+
+ let appState = this.context.appState
+ appState.addObserver(['selection'], this._onSelectionHasChanged, this, { stage: 'render' })
+ }
+
+ dispose () {
+ super.dispose()
+
+ this.context.appState.removeObserver(this)
+ }
+
+ render ($$) {
+ 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 => {
+ return 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.append(
+ this._renderEditor($$)
+ )
+ }
+ 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.getItems()
+ const label = this.props.label
+ 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 = metadataValues.length - 1
+ metadataValues.forEach((value, idx) => {
+ 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(
+ input
+ )
+ )
+ })
+ editorEl.append(
+ $$('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, { label }).append(editorEl)
+ }
+
+ _renderIcon ($$, iconName) {
+ return $$('div').addClass('se-icon').append(
+ this.context.iconProvider.renderIcon($$, iconName)
+ )
+ }
+
+ _getCustomResourceId () {
+ return this.props.model.id
+ }
+
+ _onClick (event) {
+ domHelpers.stopAndPrevent(event)
+ 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()
+ }
+ }
+
+ _onNewKeywordClick (event) {
+ domHelpers.stopAndPrevent(event)
+ this._select(true)
+ }
+
+ _onNewKeywordKeydown (event) {
+ // console.log('### _onNewKeywordKeydown()', event)
+ 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('')
+ }
+ }
+
+ _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 {
+ this._ensureIsExpanded()
+ this.context.parentSurface.rerenderDOMSelection()
+ }
+ }
+
+ _ensureIsExpanded () {
+ if (!this.state.isExpanded) {
+ this.extendState({
+ isExpanded: true
+ })
+ }
+ }
+
+ _ensureIsCollapsed () {
+ if (this.state.isExpanded) {
+ this.extendState({
+ isExpanded: false
+ })
+ }
+ }
+
+ _addKeyword () {
+ const keyword = this.refs.newKeywordInput.val()
+ this.refs.newKeywordInput.val('')
+ if (keyword) {
+ this.send('addValue', keyword)
+ }
+ }
+
+ _focusNewKeyworkInput () {
+ if (this.state.isExpanded) {
+ 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 (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)
+ }
+}
diff --git a/src/article/shared/RemoveKeywordCommand.js b/src/article/shared/RemoveKeywordCommand.js
new file mode 100644
index 000000000..4d19c3386
--- /dev/null
+++ b/src/article/shared/RemoveKeywordCommand.js
@@ -0,0 +1,30 @@
+import { Command, documentHelpers } from 'substance'
+
+export default class RemoveKeywordCommand extends Command {
+ getCommandState (params, context) {
+ 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 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)
+ documentHelpers.removeAt(tx, path, index)
+ documentHelpers.deepDeleteNode(nodeId)
+ tx.setSelection(null)
+ })
+ }
+}
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/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';
diff --git a/src/kit/app/EditorSession.js b/src/kit/app/EditorSession.js
index d5052f074..3dbb70aa9 100644
--- a/src/kit/app/EditorSession.js
+++ b/src/kit/app/EditorSession.js
@@ -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)
+ }
}
}
}
diff --git a/src/kit/styles/_index.css b/src/kit/styles/_index.css
index 7e1157376..e81400d9b 100644
--- a/src/kit/styles/_index.css
+++ b/src/kit/styles/_index.css
@@ -3,9 +3,12 @@
@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';
@import './_placeholder.css';
+@import './_popup.css';
@import './_scroll-pane.css';
@import './_surface.css';
@import './_text-input.css';
diff --git a/src/kit/styles/_keyword-input.css b/src/kit/styles/_keyword-input.css
new file mode 100644
index 000000000..c6f0ac796
--- /dev/null
+++ b/src/kit/styles/_keyword-input.css
@@ -0,0 +1,58 @@
+.sc-keyword-input {
+ position: relative;
+ 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 .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 .sc-text-input .se-input {
+ width: auto;
+}
+
+.sc-keyword-input .se-keyword-editor .se-keyword {
+ margin: var(--t-half-spacing) 0;
+}
+
+.sc-keyword-input .se-new-keyword-input {
+ display: flex;
+ margin: 0 var(--t-negative-input-padding);
+}
+
+.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);
+ margin-right: var(--t-half-spacing);
+ font-size: var(--t-small-font-size);
+ line-height: var(--t-input-line-height);
+ flex-grow: 1;
+}
+
+.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: var(--t-input-padding) var(--t-half-spacing);
+}
\ No newline at end of file
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..82ac6a80e 100644
--- a/src/article/shared/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;
@@ -10,7 +11,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 {
@@ -19,22 +20,7 @@
}
.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;
+ padding: var(--t-half-spacing) var(--t-default-spacing);
}
.sc-multi-select-input .se-select-editor .se-select-item {
@@ -48,27 +34,5 @@
}
.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;
+ margin-left: var(--t-half-spacing);
}
\ 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..789010b0d
--- /dev/null
+++ b/src/kit/styles/_popup.css
@@ -0,0 +1,41 @@
+.sc-popup {
+ position: absolute;
+ width: 100%;
+ top: 55px;
+ background: var(--t-background-color);
+ border: var(--t-default-border);
+ border-radius: var(--t-border-radius);
+ max-width: 100%;
+ box-shadow: var(--t-popup-box-shadow);
+ z-index: 10;
+}
+
+.sc-popup .se-popup-label {
+ text-align: center;
+ font-size: var(--t-tiny-font-size);
+ color: var(--t-light-text-color);
+ margin-top: var(--t-half-spacing);
+}
+
+.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: var(--t-default-border);
+ background: var(--t-background-color);
+ box-shadow: var(--t-popup-box-shadow);
+}
\ No newline at end of file
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);
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:
diff --git a/src/kit/ui/MultiSelectInput.js b/src/kit/ui/MultiSelectInput.js
index 0a1424e07..9cae0faee 100644
--- a/src/kit/ui/MultiSelectInput.js
+++ b/src/kit/ui/MultiSelectInput.js
@@ -1,5 +1,6 @@
import { Component } from 'substance'
import OverlayMixin from './OverlayMixin'
+import Popup from './Popup'
export default class MultiSelectInput extends OverlayMixin(Component) {
getInitialState () {
@@ -43,11 +44,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,11 +57,7 @@ export default class MultiSelectInput extends OverlayMixin(Component) {
).on('click', this._onToggleItem.bind(this, option))
)
})
- return editorEl
- }
-
- _getOverlayId () {
- return this.props.overlayId || this.getId()
+ return $$(Popup, { label }).append(editorEl)
}
_getOptions () {
diff --git a/src/kit/ui/OverlayMixin.js b/src/kit/ui/OverlayMixin.js
index 5da1e3e24..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()
@@ -15,7 +19,7 @@ export default function (Component) {
}
_getOverlayId () {
- return this.getId()
+ return this.props.overlayId || this.getId()
}
_canShowOverlay () {
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 d298e8053..642746f9a 100644
--- a/src/kit/ui/index.js
+++ b/src/kit/ui/index.js
@@ -30,6 +30,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'
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 */
/* ----------------------------------------------------------------------*/
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()
})