Completing functionality for tags

This commit is contained in:
Alex Hunt 2018-11-23 14:23:08 -07:00
parent a2d8b92d22
commit 49eb07102d
30 changed files with 396 additions and 75 deletions

View File

@ -11,13 +11,13 @@
"author": "Alex Hunt",
"license": "ISC",
"dependencies": {
"bootstrap": "^4.1.0",
"bootstrap": "^4.1.3",
"draft-js": "^0.10.4",
"draft-js-export-html": "^1.2.0",
"draft-js-import-html": "^1.2.1",
"font-awesome": "^4.7.0",
"history": "^4.7.2",
"popper.js": "^1.12.6",
"popper.js": "^1.14.5",
"prop-types": "^15.6.1",
"react": "^16.0.0",
"react-dom": "^16.0.0",
@ -25,7 +25,10 @@
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",
"react-router-redux": "^5.0.0-alpha.9",
"redux": "^3.7.2"
"redux": "^3.7.2",
"webpack": "^4.25.1",
"webpack-bundle-analyzer": "^2.13.1",
"webpack-cli": "^3.1.2"
},
"devDependencies": {
"babel-core": "^6.26.0",

View File

@ -5,11 +5,16 @@
const inputActions = {
// Handle changes to the document title input box
updateDocumentTitleChange: documentTitleInput =>
updateDocumentTitleInput: documentTitleInput =>
({
type: 'UPDATE_DOCUMENT_TITLE',
data: documentTitleInput.target.value
}),
updateColorPickerInput: colorPickerInput =>
({
type: 'UPDATE_COLOR_PICKER',
data: colorPickerInput.target.value
}),
// Handle changes to the search input box
updateSearchInput: searchInput =>
({

View File

@ -18,12 +18,13 @@ const userActions = {
docType: docType
}),
// Click 'Submit' to save a document edit
submitDocumentEdit: (currentDocument, editorState, documentTitleInput, docType) =>
submitDocumentEdit: (currentDocument, editorState, documentTitleInput, colorPickerInput, docType) =>
({
type: 'SUBMIT_DOCUMENT_EDIT',
currentDocument: currentDocument,
editorState: editorState,
documentTitleInput: documentTitleInput,
colorPickerInput: colorPickerInput,
docType: docType
}),
// Click 'Cancel' to discard document changes
@ -43,6 +44,12 @@ const userActions = {
docType: docType
}),
// Select a color swatch to assign to a tag
selectColorSwatch: color =>
({
type: 'SELECT_COLOR_SWATCH',
data: color
}),
// Toggle the button to hide or display note highlights
toggleHighlight: () =>
({
@ -78,10 +85,11 @@ const userActions = {
data: data
}),
// Click 'Submit' to save annotation
submitAnnotation: (annotationNote, selectionState, editorState) =>
submitAnnotation: (annotationNote, annotationTag, selectionState, editorState) =>
({
type: 'SUBMIT_ANNOTATION',
annotationNote: annotationNote,
annotationTag: annotationTag,
selectionState: selectionState,
editorState: editorState
}),
@ -98,6 +106,16 @@ const userActions = {
type: 'SELECT_ANNOTATION_NOTE',
id: id
}),
// Select a tag from the drop down to go with your annotation
selectAnnotationTag: tag =>
({
type: 'SET_ANNOTATION_TAG',
data: tag
}),
clearAnnotationTag: () =>
({
type: 'CLEAR_ANNOTATION_TAG'
}),
}
export default userActions

View File

@ -2,6 +2,7 @@ import React from 'react'
import PropTypes from 'prop-types'
import { Link } from 'react-router-dom'
import TagColorPreview from './tagColorPreview'
import romanize from '../modules/romanize'
import helpers from '../modules/helpers'
@ -38,6 +39,7 @@ export const NoteButton = ({note, currentNote, onClick}) =>
export const TagButton = ({tag, currentTag, onClick}) =>
<div className ='tag_button'>
<button onClick={onClick} className={currentTag.id === tag.id ? 'btn btn-info' : 'btn btn-outline-info inactive_button'}>
<TagColorPreview color={tag.color}/>
{tag.title}
</button>
</div>

View File

@ -2,10 +2,11 @@ import React from 'react'
import PropTypes from 'prop-types'
import { EditorSubmitButton, EditorCancelButton } from './button'
import TagColorPreview from './tagColorPreview'
import { DocumentList } from './list'
import helpers from '../modules/helpers'
const ChooseAnnotationModal = ({notes, annotationNote, onSubmitClick, selectAnnotationNote}) =>
const ChooseAnnotationModal = ({notes, tags, annotationNote, annotationTag, onSubmitClick, selectAnnotationNote, selectAnnotationTag, clearAnnotationTag}) =>
<div className='modal fade' id='annotate_modal' tabIndex='-1' role='dialog'>
<div className='modal-dialog modal-lg' role='document'>
<div className='modal-content'>
@ -18,9 +19,33 @@ const ChooseAnnotationModal = ({notes, annotationNote, onSubmitClick, selectAnno
<DocumentList docs={notes} currentDocument={annotationNote} onDocumentClick={selectAnnotationNote} docType={'notes'}/>
</div>
<div className='col-md-8 offset-md-1'>
<div dangerouslySetInnerHTML={{__html: annotationNote.text}} />
<div dangerouslySetInnerHTML={{__html: annotationNote.html_source}} />
</div>
</div>
<div className='row'>
<div id='annotation_tag_dropdown' className='col-md-2'>
<button className={annotationNote.id ? 'btn btn-primary dropdown-toggle caret-off' : 'btn btn-primary dropdown-toggle caret-off disabled'} data-toggle='dropdown' type='button'><i className='fa fa-chevron-down'></i>Tags</button>
<div className='dropdown-menu'>
{tags.map(tag =>
<div key={tag.id} className='dropdown-item'>
<TagColorPreview color={tag.color}/>
<a className='select_annotation_tag' href='#' onClick={()=>selectAnnotationTag(tag)}>{tag.title}</a>
</div>
)}
</div>
</div>
<div id='annotation_tag_preview' className='col-md-8 offset-md-1'>
{annotationTag.id &&
<div id='selected_annotation_tag'>
<TagColorPreview color={annotationTag.color}/>
{annotationTag.title}
<a id='clear_anntation_tag' href='#' onClick={clearAnnotationTag}>
<i className='fa fa-times'></i>
</a>
</div>
}
</div>
</div>
</div>
<div className='modal-footer'>
<EditorCancelButton />
@ -33,8 +58,10 @@ const ChooseAnnotationModal = ({notes, annotationNote, onSubmitClick, selectAnno
ChooseAnnotationModal.propTypes = {
notes: PropTypes.arrayOf(PropTypes.object),
annotationNote: PropTypes.object,
annotationTag: PropTypes.object,
onSubmitClick: PropTypes.func,
selectAnnotationNote: PropTypes.func,
selectAnnotationTag: PropTypes.func,
}
export default ChooseAnnotationModal

View File

@ -3,12 +3,14 @@ import PropTypes from 'prop-types'
import { ReaderAnnotateButton, ReaderEditButton, EditorToolButton, EditorDeleteToolButton, AnnotatorNewButton, AnnotatorRemoveButton } from './button'
export const ReadModeTopBar = ({setMode}) =>
export const ReadModeTopBar = ({docType, setMode}) =>
<div className='row'>
<div className='col-sm-5'>
<ReaderAnnotateButton onClick={()=>setMode('ANNOTATE_MODE')}/>
</div>
<div className='col-sm-5 offset-sm-2'>
{['chapters', 'notes'].indexOf(docType) >= 0 &&
<div className='col-sm-5'>
<ReaderAnnotateButton onClick={()=>setMode('ANNOTATE_MODE')}/>
</div>
}
<div className='col-sm-5 ml-auto'>
<ReaderEditButton onClick={()=>setMode('EDIT_MODE')} />
</div>
</div>

View File

@ -1,3 +1,7 @@
import React from 'react'
import PropTypes from 'prop-types'
import { Editor } from 'draft-js'
import { ReadModeTopBar, EditModeTopBar, AnnotateModeTopBar } from '../components/contentTopBar'
import { EditModeBottomBar } from '../components/contentBottomBar'
import actions from '../actions'
@ -15,7 +19,6 @@ const EditorAnnotateMode = ({
onRemoveAnnotationClick,
cancelEdit,
onSubmitClick,
documentTitleInput
}) =>
<div>
<div id='editor_metadata'>
@ -32,7 +35,7 @@ const EditorAnnotateMode = ({
removeDisabled={(editorState.getSelection().isCollapsed() ) ? true : false}
/>
</div>
<div id='editor_content'>
<div id='editor_content' className={docType === 'tags' ? 'short_editor' : 'tall_editor'}>
<Editor editorState={editorState} onChange={onChangeEditorState} keyBindingFn={annotateKeyBindings} />
</div>
<div id='editor_bottombar'>

View File

@ -4,6 +4,7 @@ import { Editor } from 'draft-js'
import { EditModeTopBar } from '../components/contentTopBar'
import { EditModeBottomBar } from '../components/contentBottomBar'
import TagColorPicker from '../components/tagColorPicker'
import actions from '../actions'
import DocumentTitle from '../components/documentTitle'
import LoadingSpinner from '../components/loadingSpinner'
@ -19,8 +20,12 @@ const EditorEditMode = ({
setMode,
cancelEdit,
onSubmitClick,
colorPickerInput,
documentTitleInput,
onDocumentTitleChange
onDocumentTitleChange,
onColorPickerInputChange,
onColorSwatchClick,
userErrors,
}) =>
<div>
<div id='editor_metadata'>
@ -36,16 +41,26 @@ const EditorEditMode = ({
disabled={!currentDocument.id ? true : false}
/>
</div>
<div id='editor_content'>
<div id='editor_content' className={docType === 'tags' ? 'short_editor' : 'tall_editor'}>
<Editor
editorState={editorState}
handleKeyCommand={handleKeyCommand}
handleKeyCommand={handleKeyCommand}
onChange={onChangeEditorState}
/>
</div>
<div id='editor_attributes'>
{docType === 'tags' &&
<TagColorPicker colorPickerInput={colorPickerInput} onChange={onColorPickerInputChange} onColorSwatchClick={onColorSwatchClick}/>
}
</div>
<div id='editor_bottombar'>
<EditModeBottomBar cancelEdit={cancelEdit} onSubmitClick={onSubmitClick} />
</div>
<div id='user_errors'>
{userErrors.map(error =>
<div key={error} className='user_error_message'>{error}</div>
)}
</div>
</div>
export default EditorEditMode

View File

@ -16,16 +16,16 @@ const EditorReadMode = ({
}) =>
<div id='editor_read_mode'>
<div id='editor_metadata'>
{loadingToggle === true &&
<LoadingSpinner />
}
<DocumentTitle docType={docType} currentDocument={currentDocument} />
{loadingToggle === true &&
<LoadingSpinner />
}
<DocumentTitle docType={docType} currentDocument={currentDocument} />
</div>
<div id='editor_topbar'>
<ReadModeTopBar setMode={setMode} />
<ReadModeTopBar docType={docType} setMode={setMode} />
</div>
<div id='editor_content'>
<Editor editorState={editorState} readOnly={true} />
<div id='editor_content' className={docType === 'tags' ? 'short_editor' : 'tall_editor'}>
<Editor editorState={editorState} readOnly={true}/>
</div>
</div>

View File

@ -0,0 +1,32 @@
import React from 'react'
// import defaultTagColors from '../modules/editorSettings'
const defaultTagColors = [
'307EE3',
'CF2929',
'AB59C2',
'9C632A',
'F59627',
'40b324'
]
const TagColorPicker = ({colorPickerInput, onChange, onColorSwatchClick}) =>
<div className='input-group'>
<div className='input-group-prepend'>
<span className='input-group-text' id='basic-addon1'>#</span>
</div>
<input type='text' className='form-control' placeholder='Color' value={colorPickerInput} onChange={onChange}/>
<div className='input-group-append'>
<button className='btn btn-primary dropdown-toggle caret-off' data-toggle='dropdown' type='button'><i className='fa fa-chevron-down'></i></button>
<div className='dropdown-menu'>
{defaultTagColors.map(color =>
<div key={color} className='dropdown-item'>
<a className='color_preview' href='#' style={{backgroundColor: '#' + color }} onClick={()=>onColorSwatchClick(color)}></a>
</div>
)}
</div>
</div>
</div>
export default TagColorPicker

View File

@ -0,0 +1,12 @@
import React from 'react'
import PropTypes from 'prop-types'
const TagColorPreview = ({color}) =>
<div className='tag_button_color_preview' style={{backgroundColor: '#' + color }}>
</div>
export default TagColorPreview
TagColorPreview.propTypes = {
color: PropTypes.string,
}

View File

@ -19,11 +19,15 @@ const EditorContent = ({
setMode,
cancelEdit,
onSubmitClick,
onColorSwatchClick,
documentTitleInput,
colorPickerInput,
onDocumentTitleChange,
onColorPickerInputChange,
onNewAnnotationClick,
annotateKeyBindings,
onRemoveAnnotationClick,
userErrors,
}) =>
<div id='editor_container'>
{mode === 'READ_MODE' &&
@ -42,13 +46,14 @@ const EditorContent = ({
docType={docType}
loadingToggle={loadingToggle}
handleKeyCommand={handleKeyCommand}
onNewAnnotationClick={()=>onNewAnnotationClick(editorState.getSelection())}
onRemoveAnnotationClick={()=>onRemoveAnnotationClick(editorState)}
annotateKeyBindings={annotateKeyBindings}
onChangeEditorState={onChangeEditorState}
onToolButtonClick={onToolButtonClick}
setMode={setMode}
setMode={setMode}
cancelEdit={cancelEdit}
onSubmitClick={()=>onSubmitClick(currentDocument, editorState, documentTitleInput, docType)}
documentTitleInput={documentTitleInput}
onDocumentTitleChange={onDocumentTitleChange}
onSubmitClick={()=>onSubmitClick(currentDocument, editorState, documentTitleInput, colorPickerInput, docType)}
/>
}
{mode === 'EDIT_MODE' &&
@ -58,12 +63,14 @@ const EditorContent = ({
docType={docType}
loadingToggle={loadingToggle}
onChangeEditorState={onChangeEditorState}
onNewAnnotationClick={()=>onNewAnnotationClick(editorState.getSelection())}
annotateKeyBindings={annotateKeyBindings}
onRemoveAnnotationClick={()=>onRemoveAnnotationClick(editorState)}
cancelEdit={cancelEdit}
onSubmitClick={()=>onSubmitClick(currentDocument, editorState, documentTitleInput, docType)}
onSubmitClick={()=>onSubmitClick(currentDocument, editorState, documentTitleInput, colorPickerInput, docType)}
documentTitleInput={documentTitleInput}
colorPickerInput={colorPickerInput}
onColorSwatchClick={onColorSwatchClick}
onDocumentTitleChange={onDocumentTitleChange}
onColorPickerInputChange={onColorPickerInputChange}
userErrors={userErrors}
/>
}
</div>
@ -75,7 +82,9 @@ const mapStateToProps = (state, props) => {
docType: state.docType,
editorState: state.editorState,
documentTitleInput: state.documentTitleInput,
loadingToggle: state.loadingToggle
colorPickerInput: state.colorPickerInput,
loadingToggle: state.loadingToggle,
userErrors: state.userErrors,
}
}
@ -85,8 +94,11 @@ const mapDispatchToProps = dispatch => {
dispatch(actions.updateEditorState(editorState))
},
onDocumentTitleChange: documentTitleInput => {
dispatch(actions.updateDocumentTitleChange(documentTitleInput))
},
dispatch(actions.updateDocumentTitleInput(documentTitleInput))
},
onColorPickerInputChange: colorPickerInput => {
dispatch(actions.updateColorPickerInput(colorPickerInput))
},
handleKeyCommand: (command, editorState) => {
dispatch(actions.handleEditorKeyCommand(editorState, command))
},
@ -100,6 +112,9 @@ const mapDispatchToProps = dispatch => {
cancelEdit: () => {
dispatch(actions.cancelEdit())
},
onColorSwatchClick: (color) => {
dispatch(actions.selectColorSwatch(color))
},
onNewAnnotationClick: (selectionState) => {
dispatch(actions.addAnnotation(selectionState))
},
@ -109,8 +124,8 @@ const mapDispatchToProps = dispatch => {
onToolButtonClick: (editorState, style) => {
dispatch(actions.applyInlineStyles(editorState, style))
},
onSubmitClick: (currentDocument, editorState, documentTitleInput, docType) => {
dispatch(actions.submitDocumentEdit(currentDocument, editorState, documentTitleInput, docType))
onSubmitClick: (currentDocument, editorState, documentTitleInput, colorPickerInput, docType) => {
dispatch(actions.submitDocumentEdit(currentDocument, editorState, documentTitleInput, colorPickerInput, docType))
}
}
}

View File

@ -17,12 +17,16 @@ const EditorPage = ({
notes,
currentDocument,
annotationNote,
annotationTag,
editorState,
docType,
tags,
loadingToggle,
onDeleteConfirm,
onSubmitAnnotationClick,
selectAnnotationNote,
selectAnnotationTag,
clearAnnotationTag,
selectionState,
}) =>
<div id='joyce_reader' className='container-fluid'>
@ -41,15 +45,26 @@ const EditorPage = ({
</Content>
</div>
<DeleteConfirmModal onDeleteConfirm={()=>onDeleteConfirm(currentDocument.id, docType)}/>
<AnnotationModal annotationNote={annotationNote} />
<ChooseAnnotationModal notes={notes} annotationNote={annotationNote} onSubmitClick={()=>onSubmitAnnotationClick(annotationNote, selectionState, editorState)} selectAnnotationNote={selectAnnotationNote} />
<AnnotationModal annotationNote={annotationNote}/>
<ChooseAnnotationModal
notes={notes}
tags={tags}
annotationNote={annotationNote}
annotationTag={annotationTag}
onSubmitClick={()=>onSubmitAnnotationClick(annotationNote, annotationTag, selectionState, editorState)}
selectAnnotationNote={selectAnnotationNote}
selectAnnotationTag={selectAnnotationTag}
clearAnnotationTag={clearAnnotationTag}
/>
</div>
const mapStateToProps = state => {
return {
notes: state.notes,
tags: state.tags,
currentDocument: state.currentDocument,
annotationNote: state.annotationNote,
annotationTag: state.annotationTag,
editorState: state.editorState,
selectionState: state.selectionState,
docType: state.docType,
@ -65,16 +80,24 @@ const mapDispatchToProps = dispatch => {
selectAnnotationNote: id => {
dispatch(actions.selectAnnotationNote(id))
},
onSubmitAnnotationClick: (annotationNote, selectionState, editorState) => {
dispatch(actions.submitAnnotation(annotationNote, selectionState, editorState))
clearAnnotationTag: () => {
dispatch(actions.clearAnnotationTag())
},
selectAnnotationTag: tag => {
dispatch(actions.selectAnnotationTag(tag))
},
onSubmitAnnotationClick: (annotationNote, annotationTag, selectionState, editorState) => {
dispatch(actions.submitAnnotation(annotationNote, annotationTag, selectionState, editorState))
}
}
}
EditorPage.propTypes = {
notes: PropTypes.arrayOf(PropTypes.object),
tags: PropTypes.arrayOf(PropTypes.object),
currentDocument: PropTypes.object,
annotationNote: PropTypes.object,
annotationTag: PropTypes.object,
editorState: PropTypes.object,
selectionState: PropTypes.object,
docType: PropTypes.string,
@ -82,6 +105,7 @@ EditorPage.propTypes = {
onDeleteConfirm: PropTypes.func,
onSubmitAnnotationClick: PropTypes.func,
selectAnnotationNote: PropTypes.func,
selectAnnotationTag: PropTypes.func,
}
const EditorPageContainer = connect(mapStateToProps, mapDispatchToProps)(EditorPage)

View File

@ -7,16 +7,21 @@ import actions from '../actions'
const Link = (props) => {
const data = props.contentState.getEntity(props.entityKey).getData()
return (
<a href='#' onClick={()=>props.onAnnotationClick(data.url)} data-toggle='modal' data-target='#annotation_modal' data-url={data.url}>
<a href='#'
onClick={()=>props.onAnnotationClick(data['url'])}
style={{color: '#' + data['data-color']}}
data-toggle='modal'
data-target='#annotation_modal'
data-color={data['data-color']}
data-url={data['url']}
>
{props.children}
</a>
)
}
const mapStateToProps = state => {
return {
annotationNote: state.annotationNote
}
return {}
}
const mapDispatchToProps = dispatch => {
@ -28,7 +33,6 @@ const mapDispatchToProps = dispatch => {
}
Link.propTypes = {
annotationNote: PropTypes.object,
onAnnotationClick: PropTypes.func,
}

View File

@ -3,6 +3,7 @@ import { stateToHTML } from 'draft-js-export-html'
import actions from '../actions'
import helpers from '../modules/helpers'
import { validateSubmittedDocument } from '../modules/validation'
import { html_export_options, convertToSearchText } from '../modules/editorSettings.js'
@ -13,20 +14,26 @@ const joyceInterface = store => next => action => {
store.dispatch(actions.getDocumentText({id: action.id, docType: action.docType, state: 'currentDocument'}))
break
case 'SUBMIT_DOCUMENT_EDIT':
const textContent = action.editorState.getCurrentContent()
const data = { title: action.documentTitleInput, html_source: stateToHTML(textContent, html_export_options), search_text: convertToSearchText(textContent) }
if (action.currentDocument.id) {
data.id = action.currentDocument.id
const errors = validateSubmittedDocument(action.docType, action.documentTitleInput, action.colorPickerInput)
if (errors.length < 1) {
const textContent = action.editorState.getCurrentContent()
const data = { title: action.documentTitleInput, html_source: stateToHTML(textContent, html_export_options), search_text: convertToSearchText(textContent) }
if (action.docType === 'tags') {
data.color = action.colorPickerInput
}
if (action.currentDocument.id) {
data.id = action.currentDocument.id
}
if (action.docType === 'chapters') {
if (!action.currentDocument.id) {
const nextNumber = store.getState().chapters.length + 1
data.number = nextNumber
} else {
data.number = action.currentDocument.number
}
}
store.dispatch(actions.saveDocument({id: data.id ? data.id : null, docType: action.docType, data: data}))
}
if (action.docType === 'chapters') {
if (!action.currentDocument.id) {
const nextNumber = store.getState().chapters.length + 1
data.number = nextNumber
} else {
data.number = action.currentDocument.number
}
}
store.dispatch(actions.saveDocument({id: data.id ? data.id : null, docType: action.docType, data: data}))
break
case 'DELETE_CURRENT_DOCUMENT':
store.dispatch(actions.deleteDocument({id: action.id, docType: action.docType}))

View File

@ -81,9 +81,14 @@ const joyceRouter = store => next => action => {
}
break
case 'SAVE_DOCUMENT':
// If successfully saving a new document, load it by pulling the id from the last document in the list
if (action.status === 'success' && !action.id) {
store.dispatch(actions.setCurrentDocument(action.data.slice(-1)[0].id, action.docType))
}
// If successfully saving an existing document, reload the current document
if (action.status ==='success' && action.id) {
store.dispatch(actions.setCurrentDocument(currentDocument.id, action.docType))
}
break
case 'DELETE_DOCUMENT':
if (action.status === 'success' && action.data[0]) {

View File

@ -13,12 +13,15 @@ export const html_export_options = {
const entityType = entity.get('type').toUpperCase()
if (entityType === 'LINK') {
const data = entity.getData()
console.log('data when saving is:', data['data-color'])
return {
element: 'a',
attributes: {
'href': data.url,
'href': data['url'],
'data-target': '#annotation_modal',
'data-toggle': 'modal'
'data-toggle': 'modal',
'data-color': data['data-color'],
'data-tag': data['data-tag']
}
}
}
@ -31,6 +34,14 @@ export const convertToSearchText = contentState => {
(searchText, block) => ([...searchText, {key: block.key, text: block.text}]),
[]
)
console.log('Search text result:', searchText)
return searchText
}
}
export const defaultTagColors = [
'307EE3',
'CF2929',
'AB59C2',
'9C632A',
'F59627',
'40b324'
]

View File

@ -17,6 +17,7 @@ const regexParseBaseFunction = (path, pattern) => {
}
const regex = {
// Route Checks
checkIfRedirectPath: path => {
return regexCheckBaseFunction(path, /\/(\:id)$/)
},
@ -49,13 +50,18 @@ const regex = {
},
checkPathForID: path => {
return regexCheckBaseFunction(path, /\/([0-9A-Za-z0-9\-\_]{18,})$/)
},
},
// Route Parsers
parseNumberFromPath: path => {
return Number(regexParseBaseFunction(path, /\/([0-9]{1,3})$/))
},
parseIDFromPath: path => {
return regexParseBaseFunction(path, /\/([0-9A-Za-z0-9\-\_]{18,})$/)
}
},
// Validation Checks
checkColorPickerHexValue: input => {
return regexCheckBaseFunction(input, /(^[0-9A-F]{6})$|(^[0-9A-F]{3}$)/)
}
}
export default regex

16
src/modules/validation.js Normal file
View File

@ -0,0 +1,16 @@
import regex from './regex'
export const validateSubmittedDocument = (docType, documentTitleInput, colorPickerInput) => {
const errors = []
if (docType === 'tags') {
if (colorPickerInput.length < 1) {
errors.push('Please select a tag color.')
} else if (!regex.checkColorPickerHexValue(colorPickerInput)) {
errors.push('Please select a valid hex code color.')
}
}
if (documentTitleInput.length < 1) {
errors.push('Please enter a title.')
}
return errors
}

View File

@ -4,6 +4,8 @@ const annotationNote = (state={}, action) => {
if (action.status === 'success' && action.docType === 'notes' && action.state === 'annotationNote') {
return action.data
} else { return state }
case 'ADD_ANNOTATION':
return {}
case 'SUBMIT_ANNOTATION':
return {}
default:

View File

@ -0,0 +1,14 @@
const annotationTag = (state={}, action) => {
switch(action.type) {
case 'SET_ANNOTATION_TAG':
return action.data
case 'SUBMIT_ANNOTATION':
return {}
case 'CLEAR_ANNOTATION_TAG':
return {}
default:
return state
}
}
export default annotationTag

View File

@ -0,0 +1,18 @@
const colorPickerInput = (state='', action) => {
switch(action.type) {
case 'GET_DOCUMENT_TEXT':
if (action.status === 'success' && action.state === 'currentDocument' && action.docType === 'tags') {
return action.data.color
} else { return state }
case 'CREATE_DOCUMENT':
return ''
case 'SELECT_COLOR_SWATCH':
return action.data
case 'UPDATE_COLOR_PICKER':
return action.data.toUpperCase()
default:
return state
}
}
export default colorPickerInput

View File

@ -58,7 +58,7 @@ const editorState = (state=blankEditor, action) => {
const contentStateWithEntity = contentState.createEntity(
'LINK',
'MUTABLE',
{url: action.annotationNote.id}
{'url': action.annotationNote.id, 'data-color': action.annotationTag.color, 'data-tag': action.annotationTag.title}
)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey()
const contentStateWithLink = Modifier.applyEntity(

View File

@ -8,12 +8,14 @@ import notes from './notes'
import tags from './tags'
import currentDocument from './currentDocument'
import annotationNote from './annotationNote'
import annotationTag from './annotationTag'
import searchResults from './searchResults'
// User Inputs
import editorState from './editorState'
import selectionState from './selectionState'
import documentTitleInput from './documentTitleInput'
import colorPickerInput from './colorPickerInput'
import searchInput from './searchInput'
// Toggles
@ -22,25 +24,32 @@ import mode from './mode'
import highlightToggle from './highlightToggle'
import loadingToggle from './loadingToggle'
// Validation
import userErrors from './userErrors'
const reduceJoyce = combineReducers({
routerReducer,
//
// Documents
chapters,
notes,
tags,
currentDocument,
annotationNote,
annotationTag,
searchResults,
//
// User Inputs
editorState,
selectionState,
colorPickerInput,
documentTitleInput,
searchInput,
//
// Toggles
docType,
mode,
highlightToggle,
loadingToggle
loadingToggle,
// Validation
userErrors,
})
export default reduceJoyce

View File

@ -0,0 +1,16 @@
import { validateSubmittedDocument } from '../modules/validation'
const userErrors = (state=[], action) => {
switch(action.type) {
case 'SUBMIT_DOCUMENT_EDIT':
return validateSubmittedDocument(action.docType, action.documentTitleInput, action.colorPickerInput)
case 'GET_DOCUMENT_TEXT':
if (action.status === 'success' && action.state === 'currentDocument') {
return []
}
default:
return state
}
}
export default userErrors

View File

@ -0,0 +1,16 @@
@import "variables";
.color_preview {
display: block;
height: 2em;
width: 80%;
border: 1px solid black;
}
.tag_button_color_preview {
height: 10px;
width: 10px;
display: inline-flex;
margin-right: 0.5em;
border: 1px solid black;
}

View File

@ -0,0 +1,19 @@
@import "variables";
#annotation_tag_preview {
border: 1px solid rgba(120, 120, 120, 0.7);
border-radius: 5px;
background-color: rgba(200, 200, 200, 0.8);
padding: .1em;
}
#selected_annotation_tag {
border: 1px solid rgba(120, 120, 120, 0.7);
border-radius: 0.3rem;
background-color: #fafafa;
padding: 0.15em 1em;
}
#clear_anntation_tag {
float: right;
}

View File

@ -17,7 +17,7 @@
animation-fill-mode: both;
}
&.hidden_annotations a {
color: black;
color: black !important;
text-decoration: none;
cursor: default;
pointer-events: none;
@ -41,8 +41,8 @@
text-decoration: none;
}
to {
color: map-get($theme_colors, "primary");
background-color: #f2e9de;
// color: map-get($theme_colors, "primary");
// background-color: #f2e9de;
border-radius: 3px;
}
}

View File

@ -1,7 +1,6 @@
@import "variables";
#editor_content {
height: 43em;
overflow-y: auto;
background-color: rgba(256,256,256,.8);
box-shadow: 2px 5px 10px 1px rgba(0, 0, 0, 0.3);
@ -22,6 +21,14 @@
}
}
.tall_editor {
height: 43em;
}
.short_editor {
height: 30em;
}
#editor_metadata {
h4 {
font-size: 1.75rem
@ -49,4 +56,15 @@
button {
width: 100%;
}
}
button.caret-off::before {
display: none;
}
button.caret-off::after {
display: none;
}
.user_error_message {
color: map-get($theme_colors, "danger");
}

View File

@ -4,6 +4,8 @@
@import "sidebar";
@import "page";
@import "texteditor";
@import "button";
@import "modal";
@import "node_modules/bootstrap/scss/bootstrap";
$fa-font-path: "../../node_modules/font-awesome/fonts";