Rich Text Editor (WIP) x3

This commit is contained in:
mgabdev 2020-06-17 13:25:10 -04:00
parent 861ae55aec
commit 1a506327db
13 changed files with 113 additions and 110 deletions

@ -52,9 +52,10 @@ class Api::V1::StatusesController < Api::BaseController
end
def create
markdown = status_params[:markdown] unless status_params[:markdown] === status_params[:status]
@status = PostStatusService.new.call(current_user.account,
text: status_params[:status],
markdown: status_params[:markdown],
markdown: markdown,
thread: status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
@ -72,9 +73,10 @@ class Api::V1::StatusesController < Api::BaseController
def update
authorize @status, :update?
markdown = status_params[:markdown] unless status_params[:markdown] === status_params[:status]
@status = EditStatusService.new.call(@status,
text: status_params[:status],
markdown: markdown,
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],

@ -268,7 +268,7 @@ export function submitCompose(group, replyToId = null, router, isStandalone) {
// : hack :
//Prepend http:// to urls in status that don't have protocol
status = status.replace(urlRegex, (match, a, b, c) =>{
status = `${status}`.replace(urlRegex, (match, a, b, c) =>{
const hasProtocol = match.startsWith('https://') || match.startsWith('http://')
//Make sure not a remote mention like @someone@somewhere.com
if (!hasProtocol) {
@ -276,18 +276,20 @@ export function submitCompose(group, replyToId = null, router, isStandalone) {
}
return hasProtocol ? match : `http://${match}`
})
markdown = markdown.replace(urlRegex, (match) =>{
markdown = !!markdown ? markdown.replace(urlRegex, (match) =>{
const hasProtocol = match.startsWith('https://') || match.startsWith('http://')
if (!hasProtocol) {
if (status.indexOf(`@${match}`) > -1) return match
}
return hasProtocol ? match : `http://${match}`
})
}) : undefined
if (status === markdown) {
markdown = undefined
}
const inReplyToId = getState().getIn(['compose', 'in_reply_to'], null) || replyToId
// console.log("markdown:", markdown)
dispatch(submitComposeRequest());
dispatch(closeModal());
@ -709,9 +711,9 @@ export function changeScheduledAt(date) {
};
};
export function changeRichTextEditorControlsVisibility(status) {
export function changeRichTextEditorControlsVisibility(open) {
return {
type: COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY,
status: status,
open,
}
}

@ -109,7 +109,6 @@ export function fetchStatus(id) {
}).then(() => {
dispatch(fetchStatusSuccess(skipLoading));
}, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => {
console.log("response.data:", response.data)
dispatch(importFetchedStatus(response.data));
dispatch(fetchStatusSuccess(skipLoading));
})).catch(error => {

@ -201,7 +201,7 @@ export default class AutosuggestTextbox extends ImmutablePureComponent {
}
setTextbox = (c) => {
this.textbox = c;
this.textbox = c
}
render() {

@ -4,6 +4,7 @@ import {
CompositeDecorator,
RichUtils,
convertToRaw,
ContentState,
} from 'draft-js'
import draftToMarkdown from '../features/ui/util/draft-to-markdown'
import { urlRegex } from '../features/ui/util/url_regex'
@ -27,11 +28,11 @@ function handleStrategy(contentBlock, callback, contentState) {
findWithRegex(HANDLE_REGEX, contentBlock, callback)
}
function hashtagStrategy (contentBlock, callback, contentState) {
function hashtagStrategy(contentBlock, callback, contentState) {
findWithRegex(HASHTAG_REGEX, contentBlock, callback)
}
function urlStrategy (contentBlock, callback, contentState) {
function urlStrategy(contentBlock, callback, contentState) {
findWithRegex(urlRegex, contentBlock, callback)
}
@ -70,22 +71,15 @@ const compositeDecorator = new CompositeDecorator([
}
])
const HANDLE_REGEX = /\@[\w]+/g;
const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g;
const HANDLE_REGEX = /\@[\w]+/g
const HASHTAG_REGEX = /\#[\w\u0590-\u05ff]+/g
const mapDispatchToProps = (dispatch) => ({
})
export default
@connect(null, mapDispatchToProps)
class Composer extends PureComponent {
export default class Composer extends PureComponent {
static propTypes = {
inputRef: PropTypes.func,
disabled: PropTypes.bool,
placeholder: PropTypes.string,
autoFocus: PropTypes.bool,
value: PropTypes.string,
onChange: PropTypes.func,
onKeyDown: PropTypes.func,
@ -97,40 +91,35 @@ class Composer extends PureComponent {
}
state = {
markdownText: '',
plainText: '',
editorState: EditorState.createEmpty(compositeDecorator),
}
static getDerivedStateFromProps(nextProps, prevState) {
// if (!nextProps.isHidden && nextProps.isIntersecting && !prevState.fetched) {
// return {
// fetched: true
// }
// }
return null
plainText: this.props.value,
}
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) {
// const editorState = EditorState.push(this.state.editorState, ContentState.createFromText(this.props.value));
// this.setState({ editorState })
componentDidUpdate() {
if (this.state.plainText !== this.props.value) {
let editorState
if (!this.props.value) {
editorState = EditorState.createEmpty(compositeDecorator)
} else {
editorState = EditorState.push(this.state.editorState, ContentState.createFromText(this.props.value))
}
this.setState({
editorState,
plainText: this.props.value,
})
}
}
// EditorState.createWithContent(ContentState.createFromText('Hello'))
onChange = (editorState) => {
const content = editorState.getCurrentContent()
const plainText = content.getPlainText('\u0001')
onChange = (editorState, b, c, d) => {
this.setState({ editorState })
this.setState({ editorState, plainText })
const content = editorState.getCurrentContent();
const text = content.getPlainText('\u0001')
const selectionState = editorState.getSelection()
const selectionStart = selectionState.getStartOffset()
const rawObject = convertToRaw(content);
const rawObject = convertToRaw(content)
const markdownString = draftToMarkdown(rawObject, {
escapeMarkdownCharacters: false,
preserveNewlines: false,
@ -143,24 +132,11 @@ class Composer extends PureComponent {
inline: ['del', 'ins'],
}
}
});
})
console.log("text:", markdownString)
// console.log("html:", html)
this.props.onChange(null, text, markdownString, selectionStart)
this.props.onChange(null, plainText, markdownString, selectionStart)
}
// **bold**
// *italic*
// __underline__
// ~~strike~~
// # header
// > quote
// ```
// code
// ```
focus = () => {
this.textbox.editor.focus()
}
@ -177,27 +153,24 @@ class Composer extends PureComponent {
return false
}
handleOnTogglePopoutEditor = () => {
//
}
onTab = (e) => {
const maxDepth = 4
this.onChange(RichUtils.onTab(e, this.state.editorState, maxDepth))
}
setRef = (n) => {
this.textbox = n
try {
this.textbox = n
this.props.inputRef(n)
} catch (error) {
//
}
}
render() {
const {
inputRef,
disabled,
placeholder,
autoFocus,
value,
onChange,
onKeyDown,
onKeyUp,
onFocus,
@ -217,8 +190,6 @@ class Composer extends PureComponent {
pt15: !small,
px15: !small,
px10: small,
pt5: small,
pb5: small,
pb10: !small,
})

@ -42,6 +42,7 @@ class ProUpgradeModal extends ImmutablePureComponent {
<Text> Larger Video and Image Uploads</Text>
<Text> Receive the PRO Badge</Text>
<Text> Remove in-feed promotions</Text>
<Text> Compose Rich Text posts (Bold, Italic, Underline and more)</Text>
</div>
<Button

@ -38,35 +38,35 @@ const RTE_ITEMS = [
// icon: 'circle',
// },
{
label: 'H1',
label: 'Title',
style: 'header-one',
type: 'block',
icon: 'text-size',
},
{
label: 'Blockquote',
style: 'blockquote',
type: 'block',
icon: 'blockquote',
},
{
label: 'Code Block',
style: 'code-block',
type: 'block',
icon: 'code',
},
{
label: 'UL',
style: 'unordered-list-item',
type: 'block',
icon: 'ul-list',
},
{
label: 'OL',
style: 'ordered-list-item',
type: 'block',
icon: 'ol-list',
},
// {
// label: 'Blockquote',
// style: 'blockquote',
// type: 'block',
// icon: 'blockquote',
// },
// {
// label: 'Code Block',
// style: 'code-block',
// type: 'block',
// icon: 'code',
// },
// {
// label: 'UL',
// style: 'unordered-list-item',
// type: 'block',
// icon: 'ul-list',
// },
// {
// label: 'OL',
// style: 'ordered-list-item',
// type: 'block',
// icon: 'ol-list',
// },
]
const mapStateToProps = (state) => {

@ -46,7 +46,7 @@ class TimelineComposeBlock extends ImmutablePureComponent {
return (
<section className={_s.default}>
<div className={[_s.default, _s.flexRow].join(' ')}>
<ComposeFormContainer {...rest} />
<ComposeFormContainer {...rest} modal={modal} />
</div>
</section>
)

@ -370,7 +370,7 @@ class ComposeForm extends ImmutablePureComponent {
>
{
!!reduxReplyToId && isModalOpen &&
!!reduxReplyToId && isModalOpen && isMatch &&
<div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer
contextType='compose'
@ -443,7 +443,7 @@ class ComposeForm extends ImmutablePureComponent {
}
{
!!quoteOfId && isModalOpen &&
!!quoteOfId && isModalOpen && isMatch &&
<div className={[_s.default, _s.px15, _s.py10, _s.mt5].join(' ')}>
<StatusContainer
contextType='compose'

@ -1,5 +1,7 @@
import { injectIntl, defineMessages } from 'react-intl'
import { changeRichTextEditorControlsVisibility } from '../../../actions/compose'
import { openModal } from '../../../actions/modal'
import { me } from '../../../initial_state'
import ComposeExtraButton from './compose_extra_button'
const messages = defineMessages({
@ -10,14 +12,18 @@ const messages = defineMessages({
const mapStateToProps = (state) => ({
active: state.getIn(['compose', 'rte_controls_visible']),
isPro: state.getIn(['accounts', me, 'is_pro']),
})
const mapDispatchToProps = (dispatch) => ({
onClick (status) {
dispatch(changeRichTextEditorControlsVisibility(status))
onChangeRichTextEditorControlsVisibility() {
dispatch(changeRichTextEditorControlsVisibility())
},
onOpenProUpgradeModal() {
dispatch(openModal('PRO_UPGRADE'))
},
})
export default
@ -29,14 +35,21 @@ class RichTextEditorButton extends PureComponent {
active: PropTypes.bool,
intl: PropTypes.object.isRequired,
small: PropTypes.bool,
isPro: PropTypes.bool,
onOpenProUpgradeModal: PropTypes.func.isRequired,
onChangeRichTextEditorControlsVisibility: PropTypes.func.isRequired,
}
handleClick = (e) => {
e.preventDefault()
this.props.onClick()
if (!this.props.isPro) {
this.props.onOpenProUpgradeModal()
} else {
this.props.onChangeRichTextEditorControlsVisibility()
}
}
render () {
render() {
const { active, intl, small } = this.props
return (

@ -12,7 +12,13 @@ import {
} from '../../../actions/compose'
import { me } from '../../../initial_state'
const mapStateToProps = (state, { replyToId, isStandalone }) => {
const mapStateToProps = (state, props) => {
const {
replyToId,
isStandalone,
shouldCondense,
modal,
} = props
const reduxReplyToId = state.getIn(['compose', 'in_reply_to'])
const isModalOpen = state.getIn(['modal', 'modalType']) === 'COMPOSE' || isStandalone
@ -27,7 +33,9 @@ const mapStateToProps = (state, { replyToId, isStandalone }) => {
}
if (isModalOpen) isMatch = true
if (isModalOpen && shouldCondense) isMatch = false
if (isModalOpen && !modal) isMatch = false
// console.log("isMatch:", isMatch, reduxReplyToId, replyToId, state.getIn(['compose', 'text']))
// console.log("reduxReplyToId:", reduxReplyToId, isModalOpen, isStandalone)

@ -100,6 +100,7 @@ function clearAll(state) {
return state.withMutations(map => {
map.set('id', null);
map.set('text', '');
map.set('markdown', null);
map.set('spoiler', false);
map.set('spoiler_text', '');
map.set('is_submitting', false);
@ -112,6 +113,8 @@ function clearAll(state) {
map.set('poll', null);
map.set('idempotencyKey', uuid());
map.set('scheduled_at', null);
map.set('rte_controls_visible', false);
map.set('gif', false);
});
};
@ -271,6 +274,7 @@ export default function compose(state = initialState, action) {
map.set('idempotencyKey', uuid());
map.set('spoiler', false);
map.set('spoiler_text', '');
map.set('rte_controls_visible', false);
if (action.text) {
map.set('text', `${statusToTextMentions(state, action.status)}${action.text}`);
} else {
@ -289,6 +293,7 @@ export default function compose(state = initialState, action) {
map.set('idempotencyKey', uuid());
map.set('spoiler', false);
map.set('spoiler_text', '');
map.set('rte_controls_visible', '');
});
case COMPOSE_REPLY_CANCEL:
case COMPOSE_RESET:
@ -371,6 +376,7 @@ export default function compose(state = initialState, action) {
map.set('focusDate', new Date());
map.set('caretPosition', null);
map.set('idempotencyKey', uuid());
map.set('rte_controls_visible', false);
if (action.status.get('spoiler_text').length > 0) {
map.set('spoiler', true);
@ -396,7 +402,7 @@ export default function compose(state = initialState, action) {
return state.set('scheduled_at', action.date);
case COMPOSE_RICH_TEXT_EDITOR_CONTROLS_VISIBILITY:
return state.withMutations(map => {
map.set('rte_controls_visible', !state.get('rte_controls_visible'));
map.set('rte_controls_visible', action.open || !state.get('rte_controls_visible'));
});
default:
return state;

@ -188,9 +188,10 @@ pre {
}
.statusContent code {
background-color: rgba(0,0,0,.05);
padding-left: 0.5em;
padding-right: 0.5em;
background-color: var(--border_color_secondary);
color: var(--text_color_secondary) !important;
font-size: var(--fs_n) !important;
padding: 0.25em 0.5em;
}
.dangerousContent,