docbrown/app/javascript/article-form/articleForm.jsx
Mac Siri 0f3342bcec [WIP] Implement non front-matter editor (#415)
* Create alternate editor

* Install linkstate (?)

Not sure how it disappear in the first place

* Run yarn install

* Modulize ArticleForm component

* Refactor

* Isolating css WIP

* Implement simplified frontmatter-less editor

* Modulize individual form element

* Ajust props names

* Transform json params to snakecase

* Remove codes

* Update /new, almost there for release

* Fix editor resize issues

* Change defaultvalue to value in article form tag element

* Modify html buttons in article form
2018-07-27 20:12:33 -07:00

343 lines
11 KiB
JavaScript

import 'preact/devtools';
import { h, Component } from 'preact';
import linkState from 'linkstate';
import { submitArticle, previewArticle } from './actions';
import BodyMarkdown from './elements/bodyMarkdown';
import BodyPreview from './elements/bodyPreview';
import Description from './elements/description';
import PublishToggle from './elements/publishToggle';
import Notice from './elements/notice';
import Tags from './elements/tags';
import Title from './elements/title';
import MainImage from './elements/mainImage';
import ImageManagement from './elements/imageManagement';
import OrgSettings from './elements/orgSettings';
import Errors from './elements/errors';
import ImageUploadIcon from 'images/image-upload.svg';
import CodeMirror from 'codemirror';
import 'codemirror/mode/markdown/markdown';
const algoliaId = document.querySelector("meta[name='algolia-public-id']").content
const algoliaKey = document.querySelector("meta[name='algolia-public-key']").content
const env = document.querySelector("meta[name='environment']").content
const client = algoliasearch(algoliaId, algoliaKey);
const index = client.initIndex('Tag_'+env);
export default class ArticleForm extends Component {
constructor(props) {
super(props);
const article = JSON.parse(this.props.article);
const organization = this.props.organization ? JSON.parse(this.props.organization) : null;
this.state = {
id: article.id || null,
title: article.title || '',
tagList: article.cached_tag_list || '',
description: '',
bodyMarkdown: article.body_markdown || '',
published: article.published || false,
tagOptions: [],
selectedTags: [],
tagInputListIndex: -1,
previewShowing: false,
helpShowing: false,
previewHTML: '',
helpHTML: document.getElementById('editor-help-guide').innerHTML,
submitting: false,
editing: article.id != null,
imageManagementShowing: false,
mainImageUrl: article.main_image || null,
organization: organization,
postUnderOrg: false,
errors: null,
};
}
componentDidMount() {
initEditorResize();
console.log('codemirror-ify')
const editor = document.getElementById("article_body_markdown");
const myCodeMirror = CodeMirror(editor, {
mode: "markdown",
theme: "material",
highlightFormatting: true,
});
myCodeMirror.setSize("100%", "100%");
}
handleTagKeyUp = e => {
const component = this;
const inputArray = e.target.value.split(',')
component.setState({
tagList: e.target.value,
})
const query = inputArray[inputArray.length -1].replace(/ /g,'')
if (query === '' && e.target.value != '') {
component.setState({
tagOptions: [],
})
return
} else if (e.target.value === '') {
component.setState({
tagOptions: [],
tagList: '',
selectedTags: []
})
return
}
index.search(query, {
hitsPerPage: 10,
filters: 'supported:true',
})
.then(function(content) {
component.setState({
tagOptions: content.hits.filter(hit => !component.state.selectedTags.includes(hit.name)),
})
});
}
handleTagKeyDown = e => {
const component = this;
let keyCode = e.keyCode;
if (component.state.selectedTags.length === 4 && e.keyCode === 188) {
e.preventDefault()
return
}
if((e.keyCode === 40 || e.keyCode === 9) &&
(component.state.tagInputListIndex < component.state.tagOptions.length -1) &&
(component.state.tagList != '')) {
// down key or tab key
e.preventDefault()
this.setState({tagInputListIndex: component.state.tagInputListIndex + 1})
} else if (e.keyCode === 38 && component.state.tagInputListIndex > -1) {
// up key
e.preventDefault()
this.setState({tagInputListIndex: component.state.tagInputListIndex - 1})
} else if (e.keyCode === 13 && component.state.tagInputListIndex > -1 ) {
// return key
e.preventDefault();
const newInput = component.state.selectedTags + component.state.tagOptions[component.state.tagInputListIndex].name + ',';
document.getElementById('tag-input').value = newInput;
component.setState({
tagOptions: [],
tagList: newInput,
tagInputListIndex: -1,
selectedTags: newInput.split(',')
})
setTimeout(function(){
document.getElementById('tag-input').focus();
}, 10)
} else if (e.keyCode === 188 && component.state.tagInputListIndex === -1){
// comma key
e.preventDefault();
const newInput = component.state.tagList + ',';
document.getElementById('tag-input').value = newInput;
component.setState({
tagOptions: [],
tagList: newInput,
tagInputListIndex: -1,
selectedTags: newInput.split(',')
})
} else if (e.keyCode === 8 ) {
// Delete key
if (component.state.tagList[component.state.tagList.length -1] === ',') {
let selectedTags = component.state.selectedTags
component.setState({
tagInputListIndex: -1,
selectedTags: selectedTags.slice(0, selectedTags.length -2)
})
}
} else if ((e.keyCode < 65 || e.keyCode > 90) &&
e.keyCode != 188 && e.keyCode != 8 &&
e.keyCode != 37 && e.keyCode != 39 &&
e.keyCode != 9) {
// not letter or comma or delete
e.preventDefault()
}
}
handleTagClick = e => {
document.getElementById('tag-input').focus();
const newInput = this.state.selectedTags + e.target.dataset.content + ',';
document.getElementById('tag-input').value = newInput;
console.log('CLICK')
this.setState({
tagOptions: [],
tagList: newInput,
tagInputListIndex: -1,
selectedTags: newInput.split(',')
})
}
handleFocusChange = e => {
const component = this;
setTimeout(function(){
if (document.activeElement.id === 'tag-input'){ return }
component.forceUpdate();
}, 100)
}
toggleHelp = e => {
e.preventDefault();
window.scrollTo(0, 0);
this.setState({
helpShowing: !this.state.helpShowing,
previewShowing: false,
})
}
fetchPreview = e => {
e.preventDefault();
if (this.state.previewShowing) {
this.setState({
previewShowing: false,
helpShowing: false,
})
} else {
previewArticle(this.state.bodyMarkdown, this.showPreview, this.failedPreview)
}
}
toggleImageManagement = e => {
e.preventDefault();
this.setState({imageManagementShowing: !this.state.imageManagementShowing})
}
showPreview = response => {
this.setState({
previewShowing: true,
helpShowing: false,
previewHTML: response.processed_html
})
}
toggleOrgPosting = e => {
e.preventDefault();
this.setState({postUnderOrg: !this.state.postUnderOrg})
}
failedPreview = response => {
console.log(response)
}
handleMainImageUrlChange = payload => {
this.setState({mainImageUrl: payload.link, imageManagementShowing: false})
}
onPublish = e => {
e.preventDefault();
this.setState({submitting: true, published: true})
let state = this.state;
state['published'] = true;
submitArticle(state, this.handleArticleError);
};
onSaveDraft = e => {
e.preventDefault();
this.setState({submitting: true, published: false})
let state = this.state;
state['published'] = false;
submitArticle(state, this.handleArticleError);
}
handleArticleError = response => {
window.scrollTo(0, 0);
this.setState({
errors: response,
submitting: false,
})
}
render() {
// cover image url should asking for url OR providing option to upload an image
const { title,
tagList,
description,
bodyMarkdown,
published,
tagOptions,
tagInputListIndex,
previewShowing,
helpShowing,
previewHTML,
helpHTML,
submitting,
imageManagementShowing,
organization,
postUnderOrg,
mainImageUrl,
errors,
} = this.state;
// <input type="image" name="cover-image" />
let tagOptionsHTML = ''
const component = this;
const tagOptionRows = tagOptions.map((tag, index) => {
return <div tabIndex="-1" className={'articleform__tagoptionrow articleform__tagoptionrow--'+(tagInputListIndex === index ? 'active' : 'inactive')} onClick={component.handleTagClick} data-content={tag.name}>
{tag.name}
</div>
})
if (tagOptions.length > 0 && document.activeElement.id === 'tag-input') {
tagOptionsHTML = <div className="articleform__tagsoptions">
{tagOptionRows}
</div>
}
let bodyArea = ''
if (previewShowing) {
bodyArea = <BodyPreview previewHTML={previewHTML} />
} else if (helpShowing) {
bodyArea = <BodyPreview previewHTML={helpHTML} />
} else {
bodyArea = <BodyMarkdown
defaultValue={bodyMarkdown}
onChange={linkState(this, 'bodyMarkdown')}
/>;
}
const notice = submitting ? <Notice published={published} /> : '';
const imageArea = mainImageUrl ? <MainImage mainImage={mainImageUrl} onEdit={this.toggleImageManagement} /> : ''
const imageManagement = imageManagementShowing ? <ImageManagement
onExit={this.toggleImageManagement}
mainImageUrl={mainImageUrl}
onMainImageUrlChange={this.handleMainImageUrlChange}
/> : '';
const orgArea = organization ? <OrgSettings
organization={organization}
postUnderOrg={postUnderOrg}
onToggle={this.toggleOrgPosting} /> : '';
const errorsArea = errors ? <Errors errorsList={errors} /> : '';
return (
<form className="articleform__form" onSubmit={this.onSubmit}>
{errorsArea}
{orgArea}
{imageArea}
<Title defaultValue={title} onChange={linkState(this, 'title')} />
<div className="articleform__detailfields">
<Tags
defaultValue={tagList}
onKeyDown={this.handleTagKeyDown}
onKeyUp={this.handleTagKeyUp}
options={tagOptionsHTML}
onFocusChange={this.handleFocusChange} />
<button className='articleform__imageButton' onClick={this.toggleImageManagement}>
<img src={ImageUploadIcon} /> IMAGES
</button>
</div>
{bodyArea}
<PublishToggle
published={published}
previewShowing={previewShowing}
helpShowing={helpShowing}
onPreview={this.fetchPreview}
onPublish={this.onPublish}
onHelp={this.toggleHelp}
onSaveDraft={this.onSaveDraft}
onChange={linkState(this, 'published')}
/>
{notice}
{imageManagement}
</form>
);
}
}