Rich Text Editor (WIP) x3
This commit is contained in:
parent
861ae55aec
commit
1a506327db
@ -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'
|
||||
@ -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),
|
||||
plainText: this.props.value,
|
||||
}
|
||||
|
||||
static getDerivedStateFromProps(nextProps, prevState) {
|
||||
// if (!nextProps.isHidden && nextProps.isIntersecting && !prevState.fetched) {
|
||||
// return {
|
||||
// fetched: true
|
||||
// }
|
||||
// }
|
||||
|
||||
return null
|
||||
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))
|
||||
}
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.value !== this.props.value) {
|
||||
// const editorState = EditorState.push(this.state.editorState, ContentState.createFromText(this.props.value));
|
||||
// this.setState({ editorState })
|
||||
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 })
|
||||
|
||||
const content = editorState.getCurrentContent();
|
||||
const text = content.getPlainText('\u0001')
|
||||
this.setState({ editorState, plainText })
|
||||
|
||||
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) => {
|
||||
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,11 +35,18 @@ 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() {
|
||||
|
@ -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,6 +33,8 @@ 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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user