Bringing in emotes, stickers, and refactors from ody #7435
17 changed files with 1458 additions and 323 deletions
|
@ -2239,5 +2239,85 @@
|
|||
"A channel is required to repost on LBRY": "A channel is required to repost on LBRY",
|
||||
"Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.": "Failed to view lbry://@gatogalactico#9/gato-galactico-e-as-estrelas-ninja-dos#1, please try again. If this problem persists, visit https://lbry.com/faq/support for support.",
|
||||
"Admin": "Admin",
|
||||
":ALIEN:": ":ALIEN:",
|
||||
":ANGRY_2:": ":ANGRY_2:",
|
||||
":ANGRY_3:": ":ANGRY_3:",
|
||||
":ANGRY_4:": ":ANGRY_4:",
|
||||
":ANGRY_1:": ":ANGRY_1:",
|
||||
":BLIND:": ":BLIND:",
|
||||
":BLOCK:": ":BLOCK:",
|
||||
":BOMB:": ":BOMB:",
|
||||
":BRAIN_CHIP:": ":BRAIN_CHIP:",
|
||||
":CONFIRM:": ":CONFIRM:",
|
||||
":CONFUSED_1:": ":CONFUSED_1:",
|
||||
":CONFUSED_2:": ":CONFUSED_2:",
|
||||
":COOKING_SOMETHING_NICE:": ":COOKING_SOMETHING_NICE:",
|
||||
":CRY_2:": ":CRY_2:",
|
||||
":CRY_3:": ":CRY_3:",
|
||||
":CRY_4:": ":CRY_4:",
|
||||
":CRY_5:": ":CRY_5:",
|
||||
":CRY_1:": ":CRY_1:",
|
||||
":SPACE_DOGE:": ":SPACE_DOGE:",
|
||||
":DONUT:": ":DONUT:",
|
||||
":EGGPLANT_WITH_CONDOM:": ":EGGPLANT_WITH_CONDOM:",
|
||||
":EGGPLANT:": ":EGGPLANT:",
|
||||
":FIRE_UP:": ":FIRE_UP:",
|
||||
":FLAT_EARTH:": ":FLAT_EARTH:",
|
||||
":FLYING_SAUCER:": ":FLYING_SAUCER:",
|
||||
":HEART_CHOPPER:": ":HEART_CHOPPER:",
|
||||
":HYPER_TROLL:": ":HYPER_TROLL:",
|
||||
":ICE_CREAM:": ":ICE_CREAM:",
|
||||
":IDK:": ":IDK:",
|
||||
":ILLUMINATI_1:": ":ILLUMINATI_1:",
|
||||
":ILLUMINATI_2:": ":ILLUMINATI_2:",
|
||||
":KISS_2:": ":KISS_2:",
|
||||
":KISS_1:": ":KISS_1:",
|
||||
":LASER_GUN:": ":LASER_GUN:",
|
||||
":LAUGHING_2:": ":LAUGHING_2:",
|
||||
":LAUGHING_1:": ":LAUGHING_1:",
|
||||
":LOLLIPOP:": ":LOLLIPOP:",
|
||||
":LOVE_2:": ":LOVE_2:",
|
||||
":LOVE_1:": ":LOVE_1:",
|
||||
":MONSTER:": ":MONSTER:",
|
||||
":MUSHROOM:": ":MUSHROOM:",
|
||||
":NAIL_IT:": ":NAIL_IT:",
|
||||
":NO:": ":NO:",
|
||||
":OUCH:": ":OUCH:",
|
||||
":PREACE:": ":PREACE:",
|
||||
":PIZZA:": ":PIZZA:",
|
||||
":RABBIT_HOLE:": ":RABBIT_HOLE:",
|
||||
":RAINBOW_PUKE_1:": ":RAINBOW_PUKE_1:",
|
||||
":RAINBOW_PUKE_2:": ":RAINBOW_PUKE_2:",
|
||||
":SPACE_RESITAS:": ":SPACE_RESITAS:",
|
||||
":ROCK:": ":ROCK:",
|
||||
":SAD:": ":SAD:",
|
||||
":SALTY:": ":SALTY:",
|
||||
":SCARY:": ":SCARY:",
|
||||
":SLEEP:": ":SLEEP:",
|
||||
":SLIME_DOWN:": ":SLIME_DOWN:",
|
||||
":SMELLY_SOCKS:": ":SMELLY_SOCKS:",
|
||||
":SMILE_2:": ":SMILE_2:",
|
||||
":SMILE_1:": ":SMILE_1:",
|
||||
":SPACE_CHAD:": ":SPACE_CHAD:",
|
||||
":SPACE_JULIAN:": ":SPACE_JULIAN:",
|
||||
":SPACE_TOM:": ":SPACE_TOM:",
|
||||
":SPACE_WOJAK_1:": ":SPACE_WOJAK_1:",
|
||||
":SPOCK:": ":SPOCK:",
|
||||
":STAR:": ":STAR:",
|
||||
":SUNNY_DAY:": ":SUNNY_DAY:",
|
||||
":SUPRISED:": ":SUPRISED:",
|
||||
":SWEET:": ":SWEET:",
|
||||
":THINKING_1:": ":THINKING_1:",
|
||||
":THINKING_2:": ":THINKING_2:",
|
||||
":THUMB_DOWN:": ":THUMB_DOWN:",
|
||||
":THUMB_UP_1:": ":THUMB_UP_1:",
|
||||
":THUMB_UP_2:": ":THUMB_UP_2:",
|
||||
":TINFOIL_HAT:": ":TINFOIL_HAT:",
|
||||
":TROLL_KING:": ":TROLL_KING:",
|
||||
":UFO:": ":UFO:",
|
||||
":WAITING:": ":WAITING:",
|
||||
":WHAT:": ":WHAT:",
|
||||
":WOODOO_DOLL:": ":WOODOO_DOLL:",
|
||||
"Global Emotes": "Global Emotes",
|
||||
"--end--": "--end--"
|
||||
}
|
||||
|
|
68
ui/component/commentCreate/emote-selector.jsx
Normal file
68
ui/component/commentCreate/emote-selector.jsx
Normal file
|
@ -0,0 +1,68 @@
|
|||
// @flow
|
||||
import 'scss/component/_emote-selector.scss';
|
||||
import { EMOTES_24px as EMOTES } from 'constants/emotes';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import EMOJIS from 'emoji-dictionary';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
import React from 'react';
|
||||
|
||||
const OLD_QUICK_EMOJIS = [
|
||||
EMOJIS.getUnicode('rocket'),
|
||||
EMOJIS.getUnicode('jeans'),
|
||||
EMOJIS.getUnicode('fire'),
|
||||
EMOJIS.getUnicode('heart'),
|
||||
EMOJIS.getUnicode('open_mouth'),
|
||||
];
|
||||
|
||||
type Props = { commentValue: string, setCommentValue: (string) => void, closeSelector: () => void };
|
||||
|
||||
export default function EmoteSelector(props: Props) {
|
||||
const { commentValue, setCommentValue, closeSelector } = props;
|
||||
|
||||
function addEmoteToComment(emote: string) {
|
||||
setCommentValue(
|
||||
commentValue + (commentValue && commentValue.charAt(commentValue.length - 1) !== ' ' ? ` ${emote} ` : `${emote} `)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="emoteSelector">
|
||||
<Button button="close" icon={ICONS.REMOVE} onClick={closeSelector} />
|
||||
|
||||
<div className="emoteSelector__list">
|
||||
<div className="emoteSelector__listRow">
|
||||
<div className="emoteSelector__listRowTitle">{__('Global Emotes')}</div>
|
||||
|
||||
<div className="emoteSelector__listRowItems">
|
||||
{OLD_QUICK_EMOJIS.map((emoji) => (
|
||||
<Button
|
||||
key={emoji}
|
||||
label={emoji}
|
||||
title={`:${EMOJIS.getName(emoji)}:`}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => addEmoteToComment(emoji)}
|
||||
/>
|
||||
))}
|
||||
{EMOTES.map((emote) => {
|
||||
const emoteName = emote.name.toLowerCase();
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={String(emote)}
|
||||
title={emoteName}
|
||||
button="alt"
|
||||
className="button--file-action"
|
||||
onClick={() => addEmoteToComment(emoteName)}
|
||||
>
|
||||
<OptimizedImage src={emote.url} waitLoad />
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
// @flow
|
||||
import { FF_MAX_CHARS_IN_COMMENT } from 'constants/form-field';
|
||||
import 'scss/component/_comment-create.scss';
|
||||
import { FormField, Form } from 'component/common/form';
|
||||
import { getChannelIdFromClaim } from 'util/claim';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
|
@ -12,6 +13,7 @@ import ChannelMentionSuggestions from 'component/channelMentionSuggestions';
|
|||
import ChannelThumbnail from 'component/channelThumbnail';
|
||||
import classnames from 'classnames';
|
||||
import CreditAmount from 'component/common/credit-amount';
|
||||
import EmoteSelector from './emote-selector';
|
||||
import Empty from 'component/common/empty';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import Icon from 'component/common/icon';
|
||||
|
@ -102,6 +104,7 @@ export function CommentCreate(props: Props) {
|
|||
const [deletedComment, setDeletedComment] = React.useState(false);
|
||||
const [pauseQuickSend, setPauseQuickSend] = React.useState(false);
|
||||
const [shouldDisableReviewButton, setShouldDisableReviewButton] = React.useState();
|
||||
const [showEmotes, setShowEmotes] = React.useState(false);
|
||||
|
||||
const selectedMentionIndex =
|
||||
commentValue.indexOf('@', selectionIndex) === selectionIndex
|
||||
|
@ -199,12 +202,6 @@ export function CommentCreate(props: Props) {
|
|||
window.removeEventListener('keydown', altEnterListener);
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
if (activeChannelClaim && commentValue.length) {
|
||||
handleCreateComment();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSupportComment() {
|
||||
if (!activeChannelClaim) {
|
||||
return;
|
||||
|
@ -362,6 +359,7 @@ export function CommentCreate(props: Props) {
|
|||
* @param {string} [environment] Optional environment for Stripe (test|live)
|
||||
*/
|
||||
function handleCreateComment(txid, payment_intent_id, environment) {
|
||||
setShowEmotes(false);
|
||||
setIsSubmitting(true);
|
||||
|
||||
createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment)
|
||||
|
@ -494,13 +492,20 @@ export function CommentCreate(props: Props) {
|
|||
|
||||
return (
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
className={classnames('comment__create', {
|
||||
'comment__create--reply': isReply,
|
||||
'comment__create--nested-reply': isNested,
|
||||
'comment__create--bottom': bottom,
|
||||
})}
|
||||
>
|
||||
{showEmotes && (
|
||||
<EmoteSelector
|
||||
commentValue={commentValue}
|
||||
setCommentValue={setCommentValue}
|
||||
closeSelector={() => setShowEmotes(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!advancedEditor && (
|
||||
<ChannelMentionSuggestions
|
||||
uri={uri}
|
||||
|
@ -524,6 +529,7 @@ export function CommentCreate(props: Props) {
|
|||
}
|
||||
quickActionLabel={isReply ? undefined : advancedEditor ? __('Simple Editor') : __('Advanced Editor')}
|
||||
quickActionHandler={() => setAdvancedEditor(!advancedEditor)}
|
||||
openEmoteMenu={() => setShowEmotes(!showEmotes)}
|
||||
onFocus={onTextareaFocus}
|
||||
onBlur={onTextareaBlur}
|
||||
placeholder={__('Say something about this...')}
|
||||
|
@ -579,6 +585,7 @@ export function CommentCreate(props: Props) {
|
|||
? __('Commenting...')
|
||||
: __('Comment --[button to submit something]--')
|
||||
}
|
||||
onClick={() => activeChannelClaim && commentValue.length && handleCreateComment()}
|
||||
/>
|
||||
)}
|
||||
{!supportDisabled && !claimIsMine && (
|
||||
|
|
|
@ -1,33 +1,23 @@
|
|||
// @flow
|
||||
import type { ElementRef, Node } from 'react';
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import Button from 'component/button';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import SimpleMDE from 'react-simplemde-editor';
|
||||
import MarkdownPreview from 'component/common/markdown-preview';
|
||||
import { openEditorMenu, stopContextMenu } from 'util/context-menu';
|
||||
import { FF_MAX_CHARS_DEFAULT } from 'constants/form-field';
|
||||
import 'easymde/dist/easymde.min.css';
|
||||
import Button from 'component/button';
|
||||
import emoji from 'emoji-dictionary';
|
||||
|
||||
const QUICK_EMOJIS = [
|
||||
emoji.getUnicode('rocket'),
|
||||
emoji.getUnicode('jeans'),
|
||||
emoji.getUnicode('fire'),
|
||||
emoji.getUnicode('heart'),
|
||||
emoji.getUnicode('open_mouth'),
|
||||
];
|
||||
import type { ElementRef, Node } from 'react';
|
||||
|
||||
type Props = {
|
||||
name: string,
|
||||
label?: string | Node,
|
||||
render?: () => React$Node,
|
||||
prefix?: string,
|
||||
postfix?: string,
|
||||
error?: string | boolean,
|
||||
helper?: string | React$Node,
|
||||
type?: string,
|
||||
onChange?: (any) => any,
|
||||
defaultValue?: string | number,
|
||||
placeholder?: string | number,
|
||||
children?: React$Node,
|
||||
|
@ -43,18 +33,17 @@ type Props = {
|
|||
min?: number,
|
||||
max?: number,
|
||||
quickActionLabel?: string,
|
||||
quickActionHandler?: (any) => any,
|
||||
disabled?: boolean,
|
||||
onChange: (any) => void,
|
||||
value?: string | number,
|
||||
noEmojis?: boolean,
|
||||
render?: () => React$Node,
|
||||
onChange?: (any) => any,
|
||||
quickActionHandler?: (any) => any,
|
||||
openEmoteMenu?: () => void,
|
||||
};
|
||||
|
||||
export class FormField extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
labelOnLeft: false,
|
||||
blockWrap: true,
|
||||
};
|
||||
static defaultProps = { labelOnLeft: false, blockWrap: true };
|
||||
|
||||
input: { current: ElementRef<any> };
|
||||
|
||||
|
@ -67,14 +56,11 @@ export class FormField extends React.PureComponent<Props> {
|
|||
const { autoFocus } = this.props;
|
||||
const input = this.input.current;
|
||||
|
||||
if (input && autoFocus) {
|
||||
input.focus();
|
||||
}
|
||||
if (input && autoFocus) input.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
render,
|
||||
label,
|
||||
prefix,
|
||||
postfix,
|
||||
|
@ -92,11 +78,24 @@ export class FormField extends React.PureComponent<Props> {
|
|||
charCount,
|
||||
textAreaMaxLength,
|
||||
quickActionLabel,
|
||||
quickActionHandler,
|
||||
noEmojis,
|
||||
render,
|
||||
quickActionHandler,
|
||||
openEmoteMenu,
|
||||
...inputProps
|
||||
} = this.props;
|
||||
|
||||
const errorMessage = typeof error === 'object' ? error.message : error;
|
||||
|
||||
// Ideally, the character count should (and can) be appended to the
|
||||
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
||||
// to pass the current value to it's callback, nor query the current
|
||||
// text length from the callback. So, we'll use our own widget.
|
||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||
);
|
||||
|
||||
const Wrapper = blockWrap
|
||||
? ({ children: innerChildren }) => <fieldset-section class="radio">{innerChildren}</fieldset-section>
|
||||
: ({ children: innerChildren }) => <span className="radio">{innerChildren}</span>;
|
||||
|
@ -108,207 +107,164 @@ export class FormField extends React.PureComponent<Props> {
|
|||
</div>
|
||||
) : null;
|
||||
|
||||
let input;
|
||||
if (type) {
|
||||
if (type === 'radio') {
|
||||
input = (
|
||||
<Wrapper>
|
||||
<input id={name} type="radio" {...inputProps} />
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</Wrapper>
|
||||
);
|
||||
} else if (type === 'checkbox') {
|
||||
input = (
|
||||
<div className="checkbox">
|
||||
<input id={name} type="checkbox" {...inputProps} />
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'range') {
|
||||
input = (
|
||||
<div>
|
||||
<input id={name} type="range" {...inputProps} />
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'select') {
|
||||
input = (
|
||||
<fieldset-section>
|
||||
{(label || errorMessage) && (
|
||||
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
||||
)}
|
||||
<select id={name} {...inputProps}>
|
||||
{children}
|
||||
</select>
|
||||
</fieldset-section>
|
||||
);
|
||||
} else if (type === 'select-tiny') {
|
||||
input = (
|
||||
<fieldset-section class="select--slim">
|
||||
{(label || errorMessage) && (
|
||||
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
||||
)}
|
||||
<select id={name} {...inputProps}>
|
||||
{children}
|
||||
</select>
|
||||
</fieldset-section>
|
||||
);
|
||||
} else if (type === 'markdown') {
|
||||
const handleEvents = {
|
||||
contextmenu: openEditorMenu,
|
||||
};
|
||||
const inputSimple = (type: string) => (
|
||||
<>
|
||||
<input id={name} type={type} {...inputProps} />
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</>
|
||||
);
|
||||
|
||||
const getInstance = (editor) => {
|
||||
// SimpleMDE max char check
|
||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||
if (textAreaMaxLength && changes.update) {
|
||||
var str = changes.text.join('\n');
|
||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||
if (delta <= 0) {
|
||||
return;
|
||||
}
|
||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||
if (delta > 0) {
|
||||
str = str.substr(0, str.length - delta);
|
||||
changes.update(changes.from, changes.to, str.split('\n'));
|
||||
}
|
||||
}
|
||||
});
|
||||
const inputSelect = (selectClass: string) => (
|
||||
<fieldset-section class={selectClass}>
|
||||
{(label || errorMessage) && (
|
||||
<label htmlFor={name}>{errorMessage ? <span className="error__text">{errorMessage}</span> : label}</label>
|
||||
)}
|
||||
<select id={name} {...inputProps}>
|
||||
{children}
|
||||
</select>
|
||||
</fieldset-section>
|
||||
);
|
||||
|
||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||
editor.codemirror.on('changes', (instance, changes) => {
|
||||
try {
|
||||
// Grab the last change from the buffered list. I assume the
|
||||
// buffered one ('changes', instead of 'change') is more efficient,
|
||||
// and that "Create Link" will always end up last in the list.
|
||||
const lastChange = changes[changes.length - 1];
|
||||
if (lastChange.origin === '+input') {
|
||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||
const input = () => {
|
||||
switch (type) {
|
||||
case 'radio':
|
||||
return <Wrapper>{inputSimple('radio')}</Wrapper>;
|
||||
case 'checkbox':
|
||||
return <div className="checkbox">{inputSimple('checkbox')}</div>;
|
||||
case 'range':
|
||||
return <div>{inputSimple('range')}</div>;
|
||||
case 'select':
|
||||
return inputSelect('');
|
||||
case 'select-tiny':
|
||||
return inputSelect('select--slim');
|
||||
case 'markdown':
|
||||
const handleEvents = { contextmenu: openEditorMenu };
|
||||
|
||||
// The URL placeholder is always placed last, so just look at the
|
||||
// last text in the array to also cover the multi-line case:
|
||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||
const getInstance = (editor) => {
|
||||
// SimpleMDE max char check
|
||||
editor.codemirror.on('beforeChange', (instance, changes) => {
|
||||
if (textAreaMaxLength && changes.update) {
|
||||
var str = changes.text.join('\n');
|
||||
var delta = str.length - (instance.indexFromPos(changes.to) - instance.indexFromPos(changes.from));
|
||||
|
||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||
const from = lastChange.from;
|
||||
const to = lastChange.to;
|
||||
const isSelectionMultiline = lastChange.text.length > 1;
|
||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||
if (delta <= 0) return;
|
||||
|
||||
// Everything works fine for the [Ctrl-K] case, but for the
|
||||
// [Button] case, this handler happens before the original
|
||||
// code, thus our change got wiped out.
|
||||
// Add a small delay to handle that case.
|
||||
setTimeout(() => {
|
||||
instance.setSelection(
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||
);
|
||||
}, 25);
|
||||
delta = instance.getValue().length + delta - textAreaMaxLength;
|
||||
if (delta > 0) {
|
||||
str = str.substr(0, str.length - delta);
|
||||
changes.update(changes.from, changes.to, str.split('\n'));
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// Do nothing (revert to original behavior)
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
// Ideally, the character count should (and can) be appended to the
|
||||
// SimpleMDE's "options::status" bar. However, I couldn't figure out how
|
||||
// to pass the current value to it's callback, nor query the current
|
||||
// text length from the callback. So, we'll use our own widget.
|
||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||
<span className="comment__char-count-mde">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||
);
|
||||
// "Create Link (Ctrl-K)": highlight URL instead of label:
|
||||
editor.codemirror.on('changes', (instance, changes) => {
|
||||
try {
|
||||
// Grab the last change from the buffered list. I assume the
|
||||
// buffered one ('changes', instead of 'change') is more efficient,
|
||||
// and that "Create Link" will always end up last in the list.
|
||||
const lastChange = changes[changes.length - 1];
|
||||
if (lastChange.origin === '+input') {
|
||||
// https://github.com/Ionaru/easy-markdown-editor/blob/8fa54c496f98621d5f45f57577ce630bee8c41ee/src/js/easymde.js#L765
|
||||
const EASYMDE_URL_PLACEHOLDER = '(https://)';
|
||||
|
||||
input = (
|
||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||
// The URL placeholder is always placed last, so just look at the
|
||||
// last text in the array to also cover the multi-line case:
|
||||
const urlLineText = lastChange.text[lastChange.text.length - 1];
|
||||
|
||||
if (urlLineText.endsWith(EASYMDE_URL_PLACEHOLDER) && urlLineText !== '[]' + EASYMDE_URL_PLACEHOLDER) {
|
||||
const from = lastChange.from;
|
||||
const to = lastChange.to;
|
||||
const isSelectionMultiline = lastChange.text.length > 1;
|
||||
const baseIndex = isSelectionMultiline ? 0 : from.ch;
|
||||
|
||||
// Everything works fine for the [Ctrl-K] case, but for the
|
||||
// [Button] case, this handler happens before the original
|
||||
// code, thus our change got wiped out.
|
||||
// Add a small delay to handle that case.
|
||||
setTimeout(() => {
|
||||
instance.setSelection(
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf('(') + 1 },
|
||||
{ line: to.line, ch: baseIndex + urlLineText.lastIndexOf(')') }
|
||||
);
|
||||
}, 25);
|
||||
}
|
||||
}
|
||||
} catch (e) {} // Do nothing (revert to original behavior)
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="form-field--SimpleMDE" onContextMenu={stopContextMenu}>
|
||||
<fieldset-section>
|
||||
<div className="form-field__two-column">
|
||||
<div>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
{quickAction}
|
||||
</div>
|
||||
<SimpleMDE
|
||||
{...inputProps}
|
||||
id={name}
|
||||
type="textarea"
|
||||
events={handleEvents}
|
||||
getMdeInstance={getInstance}
|
||||
options={{
|
||||
spellChecker: true,
|
||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||
previewRender(plainText) {
|
||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||
return ReactDOMServer.renderToString(preview);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{countInfo}
|
||||
</fieldset-section>
|
||||
</div>
|
||||
);
|
||||
case 'textarea':
|
||||
return (
|
||||
<fieldset-section>
|
||||
<div className="form-field__two-column">
|
||||
<div>
|
||||
{(label || quickAction) && (
|
||||
<div className="form-field__two-column">
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
{quickAction}
|
||||
</div>
|
||||
<SimpleMDE
|
||||
{...inputProps}
|
||||
id={name}
|
||||
type="textarea"
|
||||
events={handleEvents}
|
||||
getMdeInstance={getInstance}
|
||||
options={{
|
||||
spellChecker: true,
|
||||
hideIcons: ['heading', 'image', 'fullscreen', 'side-by-side'],
|
||||
previewRender(plainText) {
|
||||
const preview = <MarkdownPreview content={plainText} noDataStore />;
|
||||
return ReactDOMServer.renderToString(preview);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{countInfo}
|
||||
</fieldset-section>
|
||||
</div>
|
||||
);
|
||||
} else if (type === 'textarea') {
|
||||
const hasCharCount = charCount !== undefined && charCount >= 0;
|
||||
const countInfo = hasCharCount && textAreaMaxLength !== undefined && (
|
||||
<span className="comment__char-count">{`${charCount || '0'}/${textAreaMaxLength}`}</span>
|
||||
);
|
||||
input = (
|
||||
<fieldset-section>
|
||||
{(label || quickAction) && (
|
||||
<div className="form-field__two-column">
|
||||
<div>
|
||||
<label htmlFor={name}>{label}</label>
|
||||
</div>
|
||||
{quickAction}
|
||||
</div>
|
||||
)}
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
<div className="form-field__textarea-info">
|
||||
{!noEmojis && (
|
||||
<div className="form-field__quick-emojis">
|
||||
{QUICK_EMOJIS.map((emoji) => (
|
||||
<Button
|
||||
key={emoji}
|
||||
disabled={inputProps.disabled}
|
||||
type="button"
|
||||
className="button--emoji"
|
||||
label={emoji}
|
||||
onClick={() => {
|
||||
inputProps.onChange({
|
||||
target: { value: inputProps.value ? `${inputProps.value} ${emoji}` : emoji },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{quickAction}
|
||||
</div>
|
||||
)}
|
||||
{countInfo}
|
||||
</div>
|
||||
</fieldset-section>
|
||||
);
|
||||
} else {
|
||||
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
|
||||
const inner = inputButton ? (
|
||||
<input-submit>
|
||||
{inputElement}
|
||||
{inputButton}
|
||||
</input-submit>
|
||||
) : (
|
||||
inputElement
|
||||
);
|
||||
<textarea
|
||||
type={type}
|
||||
id={name}
|
||||
maxLength={textAreaMaxLength || FF_MAX_CHARS_DEFAULT}
|
||||
ref={this.input}
|
||||
{...inputProps}
|
||||
/>
|
||||
<div className="form-field__textarea-info">
|
||||
{!noEmojis && (
|
||||
<Button
|
||||
type="alt"
|
||||
className="button--file-action"
|
||||
title="Emotes"
|
||||
onClick={openEmoteMenu}
|
||||
icon={ICONS.EMOJI}
|
||||
iconSize={20}
|
||||
/>
|
||||
)}
|
||||
{countInfo}
|
||||
</div>
|
||||
</fieldset-section>
|
||||
);
|
||||
default:
|
||||
const inputElement = <input type={type} id={name} {...inputProps} ref={this.input} />;
|
||||
const inner = inputButton ? (
|
||||
<input-submit>
|
||||
{inputElement}
|
||||
{inputButton}
|
||||
</input-submit>
|
||||
) : (
|
||||
inputElement
|
||||
);
|
||||
|
||||
input = (
|
||||
<React.Fragment>
|
||||
return (
|
||||
<fieldset-section>
|
||||
{(label || errorMessage) && (
|
||||
<label htmlFor={name}>
|
||||
|
@ -318,17 +274,15 @@ export class FormField extends React.PureComponent<Props> {
|
|||
{prefix && <label htmlFor={name}>{prefix}</label>}
|
||||
{inner}
|
||||
</fieldset-section>
|
||||
</React.Fragment>
|
||||
);
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
{input}
|
||||
|
||||
<>
|
||||
{type && input()}
|
||||
{helper && <div className="form-field__help">{helper}</div>}
|
||||
</React.Fragment>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2014,4 +2014,12 @@ export const icons = {
|
|||
<line x1="19" y1="20.5" x2="20" y2="20.5" />
|
||||
</g>
|
||||
),
|
||||
[ICONS.EMOJI]: buildIcon(
|
||||
<g>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M8 14s1.5 2 4 2 4-2 4-2" />
|
||||
<line x1="9" y1="9" x2="9.01" y2="9" />
|
||||
<line x1="15" y1="9" x2="15.01" y2="9" />
|
||||
</g>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -10,13 +10,18 @@ import remarkFrontMatter from 'remark-frontmatter';
|
|||
import reactRenderer from 'remark-react';
|
||||
import MarkdownLink from 'component/markdownLink';
|
||||
import defaultSchema from 'hast-util-sanitize/lib/github.json';
|
||||
import { formatedLinks, inlineLinks } from 'util/remark-lbry';
|
||||
import { formattedLinks, inlineLinks } from 'util/remark-lbry';
|
||||
import { formattedTimestamp, inlineTimestamp } from 'util/remark-timestamp';
|
||||
import { formattedEmote, inlineEmote } from 'util/remark-emote';
|
||||
import ZoomableImage from 'component/zoomableImage';
|
||||
import { CHANNEL_STAKED_LEVEL_VIDEO_COMMENTS } from 'config';
|
||||
import Button from 'component/button';
|
||||
import * as ICONS from 'constants/icons';
|
||||
import { parse } from 'node-html-parser';
|
||||
import OptimizedImage from 'component/optimizedImage';
|
||||
|
||||
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
|
||||
|
||||
type SimpleTextProps = {
|
||||
children?: React.Node,
|
||||
};
|
||||
|
@ -42,6 +47,7 @@ type MarkdownProps = {
|
|||
className?: string,
|
||||
parentCommentId?: string,
|
||||
isMarkdownPost?: boolean,
|
||||
disableTimestamps?: boolean,
|
||||
stakedLevel?: number,
|
||||
};
|
||||
|
||||
|
@ -93,10 +99,15 @@ const SimpleLink = (props: SimpleLinkProps) => {
|
|||
|
||||
const SimpleImageLink = (props: ImageLinkProps) => {
|
||||
const { src, title, alt, helpText } = props;
|
||||
|
||||
if (!src) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (title && RE_EMOTE.test(title) && src.includes('static.odycdn.com/emoticons')) {
|
||||
return <OptimizedImage title={title} src={src} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
button="link"
|
||||
|
@ -132,9 +143,20 @@ function isStakeEnoughForPreview(stakedLevel) {
|
|||
// ****************************************************************************
|
||||
|
||||
const MarkdownPreview = (props: MarkdownProps) => {
|
||||
const { content, strip, simpleLinks, noDataStore, className, parentCommentId, isMarkdownPost, stakedLevel } = props;
|
||||
const {
|
||||
content,
|
||||
strip,
|
||||
simpleLinks,
|
||||
noDataStore,
|
||||
className,
|
||||
parentCommentId,
|
||||
isMarkdownPost,
|
||||
disableTimestamps,
|
||||
stakedLevel,
|
||||
} = props;
|
||||
|
||||
const strippedContent = content
|
||||
? content.replace(REPLACE_REGEX, (iframeHtml, y, iframeSrc) => {
|
||||
? content.replace(REPLACE_REGEX, (iframeHtml) => {
|
||||
// Let the browser try to create an iframe to see if the markup is valid
|
||||
let lbrySrc;
|
||||
try {
|
||||
|
@ -152,6 +174,10 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
|||
})
|
||||
: '';
|
||||
|
||||
const initialQuote = strippedContent.split(' ').find((word) => word.length > 0 || word.charAt(0) === '>');
|
||||
let stripQuote;
|
||||
if (initialQuote && initialQuote.charAt(0) === '>') stripQuote = true;
|
||||
|
||||
const remarkOptions: Object = {
|
||||
sanitize: schema,
|
||||
fragment: React.Fragment,
|
||||
|
@ -183,10 +209,22 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
|||
};
|
||||
|
||||
// Strip all content and just render text
|
||||
if (strip) {
|
||||
if (strip || stripQuote) {
|
||||
// Remove new lines and extra space
|
||||
remarkOptions.remarkReactComponents.p = SimpleText;
|
||||
return (
|
||||
return stripQuote ? (
|
||||
<span dir="auto" className="markdown-preview">
|
||||
<blockquote>
|
||||
{
|
||||
remark()
|
||||
.use(remarkStrip)
|
||||
.use(remarkFrontMatter, ['yaml'])
|
||||
.use(reactRenderer, remarkOptions)
|
||||
.processSync(content).contents
|
||||
}
|
||||
</blockquote>
|
||||
</span>
|
||||
) : (
|
||||
<span dir="auto" className="markdown-preview">
|
||||
{
|
||||
remark()
|
||||
|
@ -206,11 +244,13 @@ const MarkdownPreview = (props: MarkdownProps) => {
|
|||
.use(remarkAttr, remarkAttrOpts)
|
||||
// Remark plugins for lbry urls
|
||||
// Note: The order is important
|
||||
.use(formatedLinks)
|
||||
.use(formattedLinks)
|
||||
.use(inlineLinks)
|
||||
.use(isMarkdownPost ? null : inlineTimestamp)
|
||||
.use(isMarkdownPost ? null : formattedTimestamp)
|
||||
.use(disableTimestamps || isMarkdownPost ? null : inlineTimestamp)
|
||||
.use(disableTimestamps || isMarkdownPost ? null : formattedTimestamp)
|
||||
// Emojis
|
||||
.use(inlineEmote)
|
||||
.use(formattedEmote)
|
||||
.use(remarkEmoji)
|
||||
// Render new lines without needing spaces.
|
||||
.use(remarkBreaks)
|
||||
|
|
|
@ -10,10 +10,11 @@ function scaleToDevicePixelRatio(value: number, window: any) {
|
|||
type Props = {
|
||||
src: string,
|
||||
objectFit?: string,
|
||||
waitLoad?: boolean,
|
||||
};
|
||||
|
||||
function OptimizedImage(props: Props) {
|
||||
const { objectFit, src, ...imgProps } = props;
|
||||
const { objectFit, src, waitLoad, ...imgProps } = props;
|
||||
const [optimizedSrc, setOptimizedSrc] = React.useState('');
|
||||
const ref = React.useRef<any>();
|
||||
|
||||
|
@ -101,8 +102,12 @@ function OptimizedImage(props: Props) {
|
|||
<img
|
||||
ref={ref}
|
||||
{...imgProps}
|
||||
style={{ visibility: waitLoad ? 'hidden' : 'visible' }}
|
||||
src={optimizedSrc}
|
||||
onLoad={() => adjustOptimizationIfNeeded(ref.current, objectFit, src)}
|
||||
onLoad={() => {
|
||||
if (waitLoad) ref.current.style.visibility = 'visible';
|
||||
adjustOptimizationIfNeeded(ref.current, objectFit, src);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
96
ui/constants/emotes.js
Normal file
96
ui/constants/emotes.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
// @flow
|
||||
|
||||
const buildCDNUrl = (path: string) => `https://static.odycdn.com/emoticons/${path}`;
|
||||
|
||||
const buildEmote = (name: string, path: string) => ({
|
||||
name: __(`:${name}:`),
|
||||
url: buildCDNUrl(path),
|
||||
});
|
||||
|
||||
const getEmotes = (px: string, multiplier: string) => [
|
||||
buildEmote('ALIEN', `${px}/Alien${multiplier}.png`),
|
||||
buildEmote('ANGRY_2', `${px}/angry%202${multiplier}.png`),
|
||||
buildEmote('ANGRY_3', `${px}/angry%203${multiplier}.png`),
|
||||
buildEmote('ANGRY_4', `${px}/angry%204${multiplier}.png`),
|
||||
buildEmote('ANGRY_1', `${px}/angry${multiplier}.png`),
|
||||
buildEmote('BLIND', `${px}/blind${multiplier}.png`),
|
||||
buildEmote('BLOCK', `${px}/block${multiplier}.png`),
|
||||
buildEmote('BOMB', `${px}/bomb${multiplier}.png`),
|
||||
buildEmote('BRAIN_CHIP', `${px}/Brain%20chip${multiplier}.png`),
|
||||
buildEmote('CONFIRM', `${px}/CONFIRM${multiplier}.png`),
|
||||
buildEmote('CONFUSED_1', `${px}/confused-1${multiplier}.png`),
|
||||
buildEmote('CONFUSED_2', `${px}/confused${multiplier}.png`),
|
||||
buildEmote('COOKING_SOMETHING_NICE', `${px}/cooking%20something%20nice${multiplier}.png`),
|
||||
buildEmote('CRY_2', `${px}/cry%202${multiplier}.png`),
|
||||
buildEmote('CRY_3', `${px}/cry%203${multiplier}.png`),
|
||||
buildEmote('CRY_4', `${px}/cry%204${multiplier}.png`),
|
||||
buildEmote('CRY_5', `${px}/cry%205${multiplier}.png`),
|
||||
buildEmote('CRY_1', `${px}/cry${multiplier}.png`),
|
||||
buildEmote('SPACE_DOGE', `${px}/doge${multiplier}.png`),
|
||||
buildEmote('DONUT', `${px}/donut${multiplier}.png`),
|
||||
buildEmote('EGGPLANT_WITH_CONDOM', `${px}/eggplant%20with%20condom${multiplier}.png`),
|
||||
buildEmote('EGGPLANT', `${px}/eggplant${multiplier}.png`),
|
||||
buildEmote('FIRE_UP', `${px}/fire%20up${multiplier}.png`),
|
||||
buildEmote('FLAT_EARTH', `${px}/Flat%20earth${multiplier}.png`),
|
||||
buildEmote('FLYING_SAUCER', `${px}/Flying%20saucer${multiplier}.png`),
|
||||
buildEmote('HEART_CHOPPER', `${px}/heart%20chopper${multiplier}.png`),
|
||||
buildEmote('HYPER_TROLL', `${px}/HyperTroll${multiplier}.png`),
|
||||
buildEmote('ICE_CREAM', `${px}/ice%20cream${multiplier}.png`),
|
||||
buildEmote('IDK', `${px}/IDK${multiplier}.png`),
|
||||
buildEmote('ILLUMINATI_1', `${px}/Illuminati-1${multiplier}.png`),
|
||||
buildEmote('ILLUMINATI_2', `${px}/Illuminati${multiplier}.png`),
|
||||
buildEmote('KISS_2', `${px}/kiss%202${multiplier}.png`),
|
||||
buildEmote('KISS_1', `${px}/kiss${multiplier}.png`),
|
||||
buildEmote('LASER_GUN', `${px}/laser%20gun${multiplier}.png`),
|
||||
buildEmote('LAUGHING_2', `${px}/Laughing 2${multiplier}.png`),
|
||||
buildEmote('LAUGHING_1', `${px}/Laughing${multiplier}.png`),
|
||||
buildEmote('LOLLIPOP', `${px}/Lollipop${multiplier}.png`),
|
||||
buildEmote('LOVE_2', `${px}/Love%202${multiplier}.png`),
|
||||
buildEmote('LOVE_1', `${px}/Love${multiplier}.png`),
|
||||
buildEmote('MONSTER', `${px}/Monster${multiplier}.png`),
|
||||
buildEmote('MUSHROOM', `${px}/mushroom${multiplier}.png`),
|
||||
buildEmote('NAIL_IT', `${px}/Nail%20It${multiplier}.png`),
|
||||
buildEmote('NO', `${px}/NO${multiplier}.png`),
|
||||
buildEmote('OUCH', `${px}/ouch${multiplier}.png`),
|
||||
buildEmote('PREACE', `${px}/peace${multiplier}.png`),
|
||||
buildEmote('PIZZA', `${px}/pizza${multiplier}.png`),
|
||||
buildEmote('RABBIT_HOLE', `${px}/rabbit%20hole${multiplier}.png`),
|
||||
buildEmote('RAINBOW_PUKE_1', `${px}/rainbow%20puke-1${multiplier}.png`),
|
||||
buildEmote('RAINBOW_PUKE_2', `${px}/rainbow%20puke${multiplier}.png`),
|
||||
buildEmote('SPACE_RESITAS', `${px}/resitas${multiplier}.png`),
|
||||
buildEmote('ROCK', `${px}/ROCK${multiplier}.png`),
|
||||
buildEmote('SAD', `${px}/sad${multiplier}.png`),
|
||||
buildEmote('SALTY', `${px}/salty${multiplier}.png`),
|
||||
buildEmote('SCARY', `${px}/scary${multiplier}.png`),
|
||||
buildEmote('SLEEP', `${px}/Sleep${multiplier}.png`),
|
||||
buildEmote('SLIME_DOWN', `${px}/slime%20down${multiplier}.png`),
|
||||
buildEmote('SMELLY_SOCKS', `${px}/smelly%20socks${multiplier}.png`),
|
||||
buildEmote('SMILE_2', `${px}/smile%202${multiplier}.png`),
|
||||
buildEmote('SMILE_1', `${px}/smile${multiplier}.png`),
|
||||
buildEmote('SPACE_CHAD', `${px}/space%20chad${multiplier}.png`),
|
||||
buildEmote('SPACE_JULIAN', `${px}/Space%20Julian${multiplier}.png`),
|
||||
buildEmote('SPACE_TOM', `${px}/space%20Tom${multiplier}.png`),
|
||||
buildEmote('SPACE_WOJAK_1', `${px}/space%20wojak-1${multiplier}.png`),
|
||||
buildEmote('ANGRY_3', `${px}/space%20wojak${multiplier}.png`),
|
||||
buildEmote('SPOCK', `${px}/SPOCK${multiplier}.png`),
|
||||
buildEmote('STAR', `${px}/Star${multiplier}.png`),
|
||||
buildEmote('SUNNY_DAY', `${px}/sunny%20day${multiplier}.png`),
|
||||
buildEmote('SUPRISED', `${px}/surprised${multiplier}.png`),
|
||||
buildEmote('SWEET', `${px}/sweet${multiplier}.png`),
|
||||
buildEmote('THINKING_1', `${px}/thinking-1${multiplier}.png`),
|
||||
buildEmote('THINKING_2', `${px}/thinking${multiplier}.png`),
|
||||
buildEmote('THUMB_DOWN', `${px}/thumb%20down${multiplier}.png`),
|
||||
buildEmote('THUMB_UP_1', `${px}/thumb%20up-1${multiplier}.png`),
|
||||
buildEmote('THUMB_UP_2', `${px}/thumb%20up${multiplier}.png`),
|
||||
buildEmote('TINFOIL_HAT', `${px}/tin%20hat${multiplier}.png`),
|
||||
buildEmote('TROLL_KING', `${px}/Troll%20king${multiplier}.png`),
|
||||
buildEmote('UFO', `${px}/ufo${multiplier}.png`),
|
||||
buildEmote('WAITING', `${px}/waiting${multiplier}.png`),
|
||||
buildEmote('WHAT', `${px}/what_${multiplier}.png`),
|
||||
buildEmote('WOODOO_DOLL', `${px}/woodo%20doll${multiplier}.png`),
|
||||
];
|
||||
|
||||
export const EMOTES_24px = getEmotes('24%20px', '');
|
||||
export const EMOTES_36px = getEmotes('36px', '@1.5x');
|
||||
export const EMOTES_48px = getEmotes('48%20px', '@2x');
|
||||
export const EMOTES_72px = getEmotes('72%20px', '@3x');
|
|
@ -178,3 +178,4 @@ export const LIFE = 'Life';
|
|||
export const ARTISTS = 'Artists';
|
||||
export const MYSTERIES = 'Mysteries';
|
||||
export const TECHNOLOGY = 'Technology';
|
||||
export const EMOJI = 'Emoji';
|
||||
|
|
|
@ -549,12 +549,6 @@
|
|||
.button--highlighted {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.button--emoji {
|
||||
font-size: 1.1rem;
|
||||
border-radius: 3rem;
|
||||
}
|
||||
|
||||
.button__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
82
ui/scss/component/_comment-create.scss
Normal file
82
ui/scss/component/_comment-create.scss
Normal file
|
@ -0,0 +1,82 @@
|
|||
@import '../init/vars';
|
||||
|
||||
$thumbnailWidth: 1.5rem;
|
||||
$thumbnailWidthSmall: 1rem;
|
||||
|
||||
.comment__create {
|
||||
font-size: var(--font-small);
|
||||
position: relative;
|
||||
|
||||
fieldset-section,
|
||||
.form-field--SimpleMDE {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-field__two-column {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__create--reply {
|
||||
margin-top: var(--spacing-m);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content_comment {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment__create--nested-reply {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
margin-left: calc((#{$thumbnailWidth} + var(--spacing-m)) * 2 + var(--spacing-m) + 4px);
|
||||
}
|
||||
}
|
||||
|
||||
.comment__create--bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment-new__label-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
fieldset-section {
|
||||
max-width: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-new__label {
|
||||
white-space: nowrap;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.comment__sc-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-s);
|
||||
margin: var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.comment__sc-preview-amount {
|
||||
margin-right: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
||||
.comment--min-amount-notice {
|
||||
.icon {
|
||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
||||
}
|
||||
}
|
|
@ -32,29 +32,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment__create {
|
||||
font-size: var(--font-small);
|
||||
position: relative;
|
||||
|
||||
fieldset-section,
|
||||
.form-field--SimpleMDE {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-field__two-column {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.comment__create--reply {
|
||||
margin-top: var(--spacing-m);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment__create--bottom {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.comment {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
@ -90,10 +67,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.content_comment {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.comment__thumbnail-wrapper {
|
||||
flex: 0;
|
||||
margin-top: var(--spacing-xxs);
|
||||
|
@ -136,24 +109,10 @@ $thumbnailWidthSmall: 1rem;
|
|||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.comment__sc-preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
padding: var(--spacing-s);
|
||||
margin: var(--spacing-s) 0;
|
||||
}
|
||||
|
||||
.comment__edit-input {
|
||||
margin-top: var(--spacing-xxs);
|
||||
}
|
||||
|
||||
.comment__sc-preview-amount {
|
||||
margin-right: var(--spacing-m);
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
|
||||
.comment__threadline {
|
||||
@extend .button--alt;
|
||||
height: auto;
|
||||
|
@ -173,26 +132,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
}
|
||||
}
|
||||
|
||||
.comment-new__label-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: baseline;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
fieldset-section {
|
||||
max-width: 10rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment-new__label {
|
||||
white-space: nowrap;
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.comment--highlighted {
|
||||
background: var(--color-comment-highlighted);
|
||||
box-shadow: 0 0 0 5px var(--color-comment-highlighted);
|
||||
|
@ -429,8 +368,7 @@ $thumbnailWidthSmall: 1rem;
|
|||
@extend .comment__action;
|
||||
}
|
||||
|
||||
.comment__action--nested,
|
||||
.comment__create--nested-reply {
|
||||
.comment__action--nested {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: calc((#{$thumbnailWidthSmall} + var(--spacing-xs)) * 2 + var(--spacing-m) + 4px);
|
||||
|
||||
|
@ -485,12 +423,6 @@ $thumbnailWidthSmall: 1rem;
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.comment--min-amount-notice {
|
||||
.icon {
|
||||
margin-bottom: -3px; // TODO fix few instances of these (find "-2px")
|
||||
}
|
||||
}
|
||||
|
||||
.comments-own {
|
||||
.section__actions {
|
||||
align-items: flex-start;
|
||||
|
|
40
ui/scss/component/_emote-selector.scss
Normal file
40
ui/scss/component/_emote-selector.scss
Normal file
|
@ -0,0 +1,40 @@
|
|||
@import '../init/vars';
|
||||
|
||||
.emoteSelector {
|
||||
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: var(--spacing-m);
|
||||
}
|
||||
|
||||
.emoteSelector__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
max-height: 25vh;
|
||||
padding: var(--spacing-s);
|
||||
|
||||
.emoteSelector__listRowItems {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.button--file-action {
|
||||
margin: var(--spacing-xxs);
|
||||
padding: var(--spacing-xs);
|
||||
|
||||
.button__content {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
|
||||
span {
|
||||
margin: auto;
|
||||
font-size: var(--font-large);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -448,9 +448,7 @@ fieldset-group {
|
|||
}
|
||||
|
||||
.form-field__quick-action {
|
||||
float: right;
|
||||
font-size: var(--font-xsmall);
|
||||
margin-top: 2.5%;
|
||||
}
|
||||
|
||||
.form-field__textarea-info {
|
||||
|
@ -462,12 +460,6 @@ fieldset-group {
|
|||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.form-field__quick-emojis {
|
||||
> *:not(:last-child) {
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section {
|
||||
.form-field__internal-option {
|
||||
margin-top: var(--spacing-s);
|
||||
|
|
125
ui/util/remark-emote.js
Normal file
125
ui/util/remark-emote.js
Normal file
|
@ -0,0 +1,125 @@
|
|||
import { EMOTES_24px as EMOTES } from 'constants/emotes';
|
||||
import visit from 'unist-util-visit';
|
||||
|
||||
const EMOTE_NODE_TYPE = 'emote';
|
||||
const RE_EMOTE = /:\+1:|:-1:|:[\w-]+:/;
|
||||
|
||||
// ***************************************************************************
|
||||
// Tokenize emote
|
||||
// ***************************************************************************
|
||||
|
||||
function findNextEmote(value, fromIndex, strictlyFromIndex) {
|
||||
let begin = 0;
|
||||
|
||||
while (begin < value.length) {
|
||||
const match = value.substring(begin).match(RE_EMOTE);
|
||||
|
||||
if (!match) return null;
|
||||
|
||||
match.index += begin;
|
||||
|
||||
if (strictlyFromIndex && match.index !== fromIndex) {
|
||||
if (match.index > fromIndex) {
|
||||
// Already gone past desired index. Skip the rest.
|
||||
return null;
|
||||
} else {
|
||||
// Next match might fit 'fromIndex'.
|
||||
begin = match.index + match[0].length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (fromIndex > 0 && fromIndex > match.index && fromIndex < match.index + match[0].length) {
|
||||
// Skip previously-rejected word
|
||||
// This assumes that a non-zero 'fromIndex' means that a previous lookup has failed.
|
||||
begin = match.index + match[0].length;
|
||||
continue;
|
||||
}
|
||||
|
||||
const str = match[0];
|
||||
|
||||
if (EMOTES.some(({ name }) => str.toUpperCase() === name)) {
|
||||
// Profit!
|
||||
return { text: str, index: match.index };
|
||||
}
|
||||
|
||||
if (strictlyFromIndex && match.index >= fromIndex) {
|
||||
return null; // Since it failed and we've gone past the desired index, skip the rest.
|
||||
}
|
||||
|
||||
begin = match.index + match[0].length;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function locateEmote(value, fromIndex) {
|
||||
const emote = findNextEmote(value, fromIndex, false);
|
||||
return emote ? emote.index : -1;
|
||||
}
|
||||
|
||||
// Generate 'emote' markdown node
|
||||
const createEmoteNode = (text) => ({
|
||||
type: EMOTE_NODE_TYPE,
|
||||
value: text,
|
||||
children: [{ type: 'text', value: text }],
|
||||
});
|
||||
|
||||
// Generate a markdown image from emote
|
||||
function tokenizeEmote(eat, value, silent) {
|
||||
if (silent) return true;
|
||||
|
||||
const emote = findNextEmote(value, 0, true);
|
||||
if (emote) {
|
||||
try {
|
||||
const text = emote.text;
|
||||
return eat(text)(createEmoteNode(text));
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
tokenizeEmote.locator = locateEmote;
|
||||
|
||||
export function inlineEmote() {
|
||||
const Parser = this.Parser;
|
||||
const tokenizers = Parser.prototype.inlineTokenizers;
|
||||
const methods = Parser.prototype.inlineMethods;
|
||||
|
||||
// Add an inline tokenizer (defined in the following example).
|
||||
tokenizers.emote = tokenizeEmote;
|
||||
|
||||
// Run it just before `text`.
|
||||
methods.splice(methods.indexOf('text'), 0, 'emote');
|
||||
}
|
||||
|
||||
// ***************************************************************************
|
||||
// Format emote
|
||||
// ***************************************************************************
|
||||
|
||||
const transformer = (node, index, parent) => {
|
||||
if (node.type === EMOTE_NODE_TYPE && parent && parent.type === 'paragraph') {
|
||||
const emoteStr = node.value;
|
||||
const emote = EMOTES.find(({ name }) => emoteStr.toUpperCase() === name);
|
||||
|
||||
node.type = 'image';
|
||||
node.url = emote.url;
|
||||
node.title = emoteStr;
|
||||
node.children = [{ type: 'text', value: emoteStr }];
|
||||
if (!node.data || !node.data.hProperties) {
|
||||
// Create new node data
|
||||
node.data = {
|
||||
hProperties: { emote: true },
|
||||
};
|
||||
} else if (node.data.hProperties) {
|
||||
// Don't overwrite current attributes
|
||||
node.data.hProperties = {
|
||||
emote: true,
|
||||
...node.data.hProperties,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const transform = (tree) => visit(tree, [EMOTE_NODE_TYPE], transformer);
|
||||
|
||||
export const formattedEmote = () => transform;
|
|
@ -149,7 +149,7 @@ const transform = (tree) => {
|
|||
visit(tree, ['link'], visitor);
|
||||
};
|
||||
|
||||
export const formatedLinks = () => transform;
|
||||
export const formattedLinks = () => transform;
|
||||
|
||||
// Main module
|
||||
export function inlineLinks() {
|
||||
|
|
711
web/scss/themes/odysee/component/_form-field.scss
Normal file
711
web/scss/themes/odysee/component/_form-field.scss
Normal file
|
@ -0,0 +1,711 @@
|
|||
@import '../init/mixins';
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
.date-picker-input {
|
||||
height: var(--height-input);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid;
|
||||
color: var(--color-input);
|
||||
border-color: var(--color-input-border);
|
||||
background-color: var(--color-input-bg);
|
||||
padding-right: var(--spacing-s);
|
||||
padding-left: var(--spacing-s);
|
||||
|
||||
&:focus {
|
||||
@include focus;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: var(--color-input-placeholder);
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
|
||||
& + label {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
&[type='range'] {
|
||||
height: auto;
|
||||
height: 0.5rem;
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
checkbox-element,
|
||||
radio-element,
|
||||
select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
background-image: var(--select-toggle-background);
|
||||
background-position: 99% center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 1rem;
|
||||
padding-right: var(--spacing-l);
|
||||
padding-left: var(--spacing-s);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
fieldset-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
&.fieldset-group--smushed {
|
||||
fieldset-section + fieldset-section {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section,
|
||||
fieldset-group,
|
||||
form,
|
||||
.checkbox,
|
||||
.radio,
|
||||
.form-field--SimpleMDE,
|
||||
.form-field__help {
|
||||
+ fieldset-section,
|
||||
+ fieldset-group,
|
||||
+ form,
|
||||
+ .checkbox,
|
||||
+ .radio,
|
||||
+ .form-field--SimpleMDE {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
+ .form-field__help {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section,
|
||||
.checkbox,
|
||||
.radio {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: var(--font-small);
|
||||
color: var(--color-input-label);
|
||||
display: inline-block;
|
||||
margin-bottom: 0.1rem;
|
||||
|
||||
.icon__lbc {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
input-submit {
|
||||
display: flex;
|
||||
|
||||
& > *:first-child,
|
||||
& > *:nth-child(2) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& > *:first-child {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
& > *:nth-child(2) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox,
|
||||
.radio {
|
||||
position: relative;
|
||||
|
||||
input[type='checkbox'],
|
||||
input[type='radio'] {
|
||||
height: var(--height-checkbox);
|
||||
width: var(--height-checkbox);
|
||||
position: absolute;
|
||||
border: none;
|
||||
left: 0;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
|
||||
&:disabled + label {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
font-size: var(--font-base);
|
||||
padding-left: calc(var(--height-checkbox) + var(--spacing-s));
|
||||
min-height: var(--height-checkbox);
|
||||
|
||||
&::before {
|
||||
background-color: var(--color-input-toggle-bg);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&::before {
|
||||
background-color: var(--color-input-toggle-bg-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
label::before,
|
||||
label::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
}
|
||||
|
||||
// Hide the checkmark by default
|
||||
input[type='checkbox'] + label::after,
|
||||
input[type='radio'] + label::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
// Unhide on the checked state
|
||||
input[type='checkbox']:checked + label::after,
|
||||
input[type='radio']:checked + label::after {
|
||||
content: '';
|
||||
}
|
||||
|
||||
input[type='checkbox']:focus + label::before,
|
||||
input[type='radio']:focus + label::before {
|
||||
@include focus;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
// Outer box of the fake checkbox
|
||||
label::before {
|
||||
height: var(--height-checkbox);
|
||||
width: var(--height-checkbox);
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: var(--border-radius);
|
||||
left: 0px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
// Checkmark of the fake checkbox
|
||||
label::after {
|
||||
height: 6px;
|
||||
width: 12px;
|
||||
border-left: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
border-color: var(--color-input-toggle);
|
||||
border-left-color: var(--color-input-toggle);
|
||||
transform: rotate(-45deg);
|
||||
left: 6px;
|
||||
top: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.radio {
|
||||
input[type='radio'] {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
// Outer box of the fake radio
|
||||
label::before {
|
||||
height: var(--height-radio);
|
||||
width: var(--height-radio);
|
||||
border: 1px solid var(--color-input-border);
|
||||
border-radius: calc(var(--height-radio) * 0.5);
|
||||
left: 0px;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
// Checkmark of the fake radio
|
||||
label::after {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-primary);
|
||||
left: 6px;
|
||||
top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.range__label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-bottom: var(--spacing-m);
|
||||
|
||||
> * {
|
||||
width: 33%;
|
||||
text-align: center;
|
||||
|
||||
&:first-of-type {
|
||||
text-align: left;
|
||||
}
|
||||
&:last-of-type {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.fieldset-group {
|
||||
@extend fieldset-group;
|
||||
}
|
||||
|
||||
.fieldset-section {
|
||||
@extend fieldset-section;
|
||||
}
|
||||
|
||||
.input-submit {
|
||||
@extend input-submit;
|
||||
}
|
||||
|
||||
input-submit {
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
|
||||
input[type='number'] {
|
||||
width: 8rem;
|
||||
}
|
||||
|
||||
fieldset-group {
|
||||
+ fieldset-group {
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
&.fieldset-group--smushed {
|
||||
justify-content: flex-start;
|
||||
|
||||
fieldset-section {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
|
||||
&:first-child {
|
||||
input,
|
||||
select {
|
||||
border-right: none;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-of-type(2) {
|
||||
input,
|
||||
select {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
margin-left: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.fieldgroup--paginate {
|
||||
padding-bottom: var(--spacing-l);
|
||||
margin-top: var(--spacing-l);
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// This is a special case where the prefix appears "inside" the input
|
||||
// It would be way simpler to just use position: absolute and give it a width
|
||||
// but the width can change when we use it for the name prefix
|
||||
// lbry:// {input}, lbry://@short {input}, @lbry://longername {input}
|
||||
// The spacing/alignment isn't very robust and will probably need to be changed
|
||||
// if we use this in more places
|
||||
&.fieldset-group--disabled-prefix {
|
||||
align-items: flex-end;
|
||||
|
||||
label {
|
||||
min-height: 18px;
|
||||
white-space: nowrap;
|
||||
// Set width 0 and overflow visible so the label can act as if it's the input label and not a random text node in a side by side div
|
||||
overflow: visible;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
fieldset-section:first-child {
|
||||
max-width: 40%;
|
||||
|
||||
.form-field__prefix {
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding: 0.5rem;
|
||||
height: var(--height-input);
|
||||
border: 1px solid;
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
border-color: var(--color-input-border);
|
||||
border-right-color: var(--color-input-prefix-border);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-input-prefix-bg);
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section:last-child {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
// Overwrite the input's label to wrap instead. This is usually
|
||||
// an error message, which could be long in other languages.
|
||||
width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
input {
|
||||
border-left: 0;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-color: var(--color-input-border);
|
||||
padding-left: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-field--copyable {
|
||||
padding: 0.2rem 0.75rem;
|
||||
text-overflow: ellipsis;
|
||||
user-select: text;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.form-field--short {
|
||||
width: 100%;
|
||||
@media (min-width: $breakpoint-small) {
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field--price-amount {
|
||||
max-width: 6em;
|
||||
}
|
||||
|
||||
.form-field--price-amount--auto {
|
||||
width: auto;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.form-field--address {
|
||||
min-width: 18em;
|
||||
@media (max-width: $breakpoint-xxsmall) {
|
||||
min-width: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__help {
|
||||
@extend .help;
|
||||
}
|
||||
|
||||
.form-field__help + .checkbox,
|
||||
.form-field__help + .radio {
|
||||
margin-top: var(--spacing-l);
|
||||
}
|
||||
|
||||
.form-field__conjuction {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.form-field__two-column {
|
||||
@media (min-width: $breakpoint-small) {
|
||||
column-count: 2;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field__quick-action {
|
||||
font-size: var(--font-xsmall);
|
||||
}
|
||||
|
||||
.form-field__textarea-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-xxs);
|
||||
margin-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.form-field__quick-emojis {
|
||||
> *:not(:last-child) {
|
||||
margin-right: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
fieldset-section {
|
||||
.form-field__internal-option {
|
||||
margin-top: var(--spacing-s);
|
||||
margin-left: 2.2rem;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: var(--spacing-s); // Extra specificity needed here since _section.scss is applied after this file
|
||||
}
|
||||
}
|
||||
|
||||
.select--slim {
|
||||
margin-bottom: var(--spacing-xxs);
|
||||
|
||||
@media (min-width: $breakpoint-small) {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
select {
|
||||
max-height: 1.5rem !important;
|
||||
padding: 0 var(--spacing-xs);
|
||||
padding-right: var(--spacing-l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#automatic_dark_mode_range_start,
|
||||
#automatic_dark_mode_range_end {
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.date-picker-input {
|
||||
font-weight: bold;
|
||||
|
||||
.react-datetime-picker__wrapper {
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-date-picker {
|
||||
margin-bottom: var(--spacing-l);
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
|
||||
.date-picker-input,
|
||||
.button--link {
|
||||
margin-right: var(--spacing-m);
|
||||
}
|
||||
}
|
||||
|
||||
.react-datetime-picker__button {
|
||||
svg {
|
||||
stroke: var(--color-text);
|
||||
}
|
||||
}
|
||||
|
||||
.react-datetime-picker__button:enabled:hover .react-datetime-picker__button__icon,
|
||||
.react-datetime-picker__button:enabled:focus .react-datetime-picker__button__icon {
|
||||
stroke: var(--color-primary);
|
||||
}
|
||||
|
||||
.react-date-picker__calendar {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.react-calendar {
|
||||
width: 350px;
|
||||
max-width: 100%;
|
||||
background: var(--color-card-background);
|
||||
border: 1px solid #a0a096;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView {
|
||||
width: 700px;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView .react-calendar__viewContainer {
|
||||
display: flex;
|
||||
margin: -0.5em;
|
||||
}
|
||||
|
||||
.react-calendar--doubleView .react-calendar__viewContainer > * {
|
||||
width: 50%;
|
||||
margin: 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar,
|
||||
.react-calendar *,
|
||||
.react-calendar *:before,
|
||||
.react-calendar *:after {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
padding: 2px 1px;
|
||||
}
|
||||
|
||||
.react-calendar button {
|
||||
margin: 0;
|
||||
border: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.react-calendar button:enabled:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.react-calendar__navigation {
|
||||
height: 44px;
|
||||
margin-bottom: 1em;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__navigation__label {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__navigation button {
|
||||
min-width: 44px;
|
||||
background: none;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__navigation button:enabled:hover,
|
||||
.react-calendar__navigation button:enabled:focus {
|
||||
background: var(--color-button-alt-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__navigation button[disabled] {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays {
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
font-size: 0.75em;
|
||||
color: var(--color-text-alt);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekdays__weekday {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekNumbers {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__weekNumbers .react-calendar__tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.75em;
|
||||
padding: calc(0.75em / 0.75) calc(0.5em / 0.75);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day,
|
||||
.react-calendar__month-view__days__day--weekend {
|
||||
color: var(--color-text);
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
color: var(--color-gray-5);
|
||||
}
|
||||
|
||||
.react-calendar__year-view .react-calendar__tile,
|
||||
.react-calendar__decade-view .react-calendar__tile,
|
||||
.react-calendar__century-view .react-calendar__tile {
|
||||
padding: 2em 0.5em;
|
||||
}
|
||||
|
||||
.react-calendar__tile {
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
padding: 0.75em 0.5em;
|
||||
background: none;
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.react-calendar__tile:enabled:hover,
|
||||
.react-calendar__tile:enabled:focus {
|
||||
background: var(--color-button-alt-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__tile--now {
|
||||
background: var(--color-button-secondary-bg);
|
||||
}
|
||||
|
||||
.react-calendar__tile--now:enabled:hover,
|
||||
.react-calendar__tile--now:enabled:focus {
|
||||
background: var(--color-button-secondary-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive {
|
||||
color: var(--color-button-primary-text);
|
||||
background: var(--color-button-primary-bg);
|
||||
}
|
||||
|
||||
.react-calendar__tile--hasActive:enabled:hover,
|
||||
.react-calendar__tile--hasActive:enabled:focus {
|
||||
background: var(--color-button-primary-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar__tile--active {
|
||||
color: var(--color-button-primary-text);
|
||||
background: var(--color-button-primary-bg);
|
||||
}
|
||||
|
||||
.react-calendar__tile--active:enabled:hover,
|
||||
.react-calendar__tile--active:enabled:focus {
|
||||
background: var(--color-button-primary-bg-hover);
|
||||
}
|
||||
|
||||
.react-calendar--selectRange .react-calendar__tile--hover {
|
||||
background-color: #e6e6e6;
|
||||
}
|
||||
|
||||
.react-datetime-picker__inputGroup__amPm {
|
||||
background: var(--color-input-bg);
|
||||
}
|
||||
|
||||
.react-datetime-picker__inputGroup__leadingZero {
|
||||
// Not perfect, but good enough for our standard zoom levels.
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.react-datetime-picker__inputGroup__input--hasLeadingZero {
|
||||
margin-left: -0.54em;
|
||||
padding-left: calc(1px + 0.54em);
|
||||
}
|
||||
|
||||
.react-calendar__month-view__days__day--neighboringMonth {
|
||||
color: var(--color-gray-5);
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-calendar {
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--color-border);
|
||||
margin-left: calc(var(--spacing-xs) * -1);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
animation: menu-animate-in var(--animation-duration) var(--animation-style);
|
||||
box-shadow: 3px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
Loading…
Reference in a new issue