Align title correctly when first character is emoji
This commit is contained in:
parent
cfcdae8aa0
commit
297bf850c2
|
@ -1,5 +1,7 @@
|
||||||
[include]
|
[include]
|
||||||
.*/frontend/.*
|
.*/frontend/.*
|
||||||
|
.*/server/.*
|
||||||
|
.*/shared/.*
|
||||||
|
|
||||||
[ignore]
|
[ignore]
|
||||||
.*/node_modules/styled-components/.*
|
.*/node_modules/styled-components/.*
|
||||||
|
|
|
@ -29,6 +29,7 @@ type Props = {
|
||||||
onImageUploadStart: Function,
|
onImageUploadStart: Function,
|
||||||
onImageUploadStop: Function,
|
onImageUploadStop: Function,
|
||||||
starred: boolean,
|
starred: boolean,
|
||||||
|
emoji: string,
|
||||||
readOnly: boolean,
|
readOnly: boolean,
|
||||||
heading?: ?React.Element<*>,
|
heading?: ?React.Element<*>,
|
||||||
};
|
};
|
||||||
|
@ -213,6 +214,7 @@ type KeyData = {
|
||||||
className={cx(styles.editor, { readOnly: this.props.readOnly })}
|
className={cx(styles.editor, { readOnly: this.props.readOnly })}
|
||||||
schema={this.schema}
|
schema={this.schema}
|
||||||
plugins={this.plugins}
|
plugins={this.plugins}
|
||||||
|
emoji={this.props.emoji}
|
||||||
state={this.state.state}
|
state={this.state.state}
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
|
|
|
@ -25,6 +25,10 @@ type Context = {
|
||||||
starred?: boolean,
|
starred?: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Wrapper = styled.div`
|
||||||
|
margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)}
|
||||||
|
`;
|
||||||
|
|
||||||
const StyledStar = styled(StarIcon)`
|
const StyledStar = styled(StarIcon)`
|
||||||
top: 3px;
|
top: 3px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -61,10 +65,14 @@ function Heading(props: Props, { starred }: Context) {
|
||||||
const showStar = readOnly && !!onStar;
|
const showStar = readOnly && !!onStar;
|
||||||
const showHash = readOnly && !!slugish && !showStar;
|
const showHash = readOnly && !!slugish && !showStar;
|
||||||
const Component = component;
|
const Component = component;
|
||||||
|
const emoji = editor.props.emoji || '';
|
||||||
|
const title = node.text.trim();
|
||||||
|
const startsWithEmojiAndSpace =
|
||||||
|
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Component className={styles.title}>
|
<Component className={styles.title}>
|
||||||
{children}
|
<Wrapper hasEmoji={startsWithEmojiAndSpace}>{children}</Wrapper>
|
||||||
{showPlaceholder &&
|
{showPlaceholder &&
|
||||||
<span className={styles.placeholder} contentEditable={false}>
|
<span className={styles.placeholder} contentEditable={false}>
|
||||||
{editor.props.placeholder}
|
{editor.props.placeholder}
|
||||||
|
|
|
@ -5,15 +5,11 @@ import invariant from 'invariant';
|
||||||
import { client } from 'utils/ApiClient';
|
import { client } from 'utils/ApiClient';
|
||||||
import stores from 'stores';
|
import stores from 'stores';
|
||||||
import ErrorsStore from 'stores/ErrorsStore';
|
import ErrorsStore from 'stores/ErrorsStore';
|
||||||
|
import parseTitle from '../../shared/parseTitle';
|
||||||
|
|
||||||
import type { User } from 'types';
|
import type { User } from 'types';
|
||||||
import Collection from './Collection';
|
import Collection from './Collection';
|
||||||
|
|
||||||
const parseHeader = text => {
|
|
||||||
const firstLine = text.trim().split(/\r?\n/)[0];
|
|
||||||
return firstLine.replace(/^#/, '').trim();
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_TITLE = 'Untitled document';
|
const DEFAULT_TITLE = 'Untitled document';
|
||||||
|
|
||||||
class Document {
|
class Document {
|
||||||
|
@ -31,6 +27,7 @@ class Document {
|
||||||
html: string;
|
html: string;
|
||||||
id: string;
|
id: string;
|
||||||
team: string;
|
team: string;
|
||||||
|
emoji: string;
|
||||||
private: boolean = false;
|
private: boolean = false;
|
||||||
starred: boolean = false;
|
starred: boolean = false;
|
||||||
text: string = '';
|
text: string = '';
|
||||||
|
@ -181,7 +178,11 @@ class Document {
|
||||||
};
|
};
|
||||||
|
|
||||||
updateData(data: Object = {}, dirty: boolean = false) {
|
updateData(data: Object = {}, dirty: boolean = false) {
|
||||||
if (data.text) data.title = parseHeader(data.text);
|
if (data.text) {
|
||||||
|
const { title, emoji } = parseTitle(data.text);
|
||||||
|
data.title = title;
|
||||||
|
data.emoji = emoji;
|
||||||
|
}
|
||||||
if (dirty) this.hasPendingChanges = true;
|
if (dirty) this.hasPendingChanges = true;
|
||||||
this.data = data;
|
this.data = data;
|
||||||
extendObservable(this, data);
|
extendObservable(this, data);
|
||||||
|
|
|
@ -203,6 +203,7 @@ type Props = {
|
||||||
<Editor
|
<Editor
|
||||||
key={document.id}
|
key={document.id}
|
||||||
text={document.text}
|
text={document.text}
|
||||||
|
emoji={document.emoji}
|
||||||
onImageUploadStart={this.onImageUploadStart}
|
onImageUploadStart={this.onImageUploadStart}
|
||||||
onImageUploadStop={this.onImageUploadStop}
|
onImageUploadStop={this.onImageUploadStop}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import isUUID from 'validator/lib/isUUID';
|
||||||
import { DataTypes, sequelize } from '../sequelize';
|
import { DataTypes, sequelize } from '../sequelize';
|
||||||
import { convertToMarkdown } from '../../frontend/utils/markdown';
|
import { convertToMarkdown } from '../../frontend/utils/markdown';
|
||||||
import { truncateMarkdown } from '../utils/truncate';
|
import { truncateMarkdown } from '../utils/truncate';
|
||||||
|
import parseTitle from '../../shared/parseTitle';
|
||||||
import Revision from './Revision';
|
import Revision from './Revision';
|
||||||
|
|
||||||
const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{10,15})$/;
|
const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{10,15})$/;
|
||||||
|
@ -35,15 +36,10 @@ const createUrlId = doc => {
|
||||||
return (doc.urlId = doc.urlId || randomstring.generate(10));
|
return (doc.urlId = doc.urlId || randomstring.generate(10));
|
||||||
};
|
};
|
||||||
|
|
||||||
const extractEmoji = doc => {
|
|
||||||
const regex = emojiRegex();
|
|
||||||
const match = regex.exec(doc.title);
|
|
||||||
|
|
||||||
if (match.length) return match[0];
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const beforeSave = async doc => {
|
const beforeSave = async doc => {
|
||||||
|
const { emoji } = parseTitle(doc.text);
|
||||||
|
|
||||||
|
doc.emoji = emoji;
|
||||||
doc.html = convertToMarkdown(doc.text);
|
doc.html = convertToMarkdown(doc.text);
|
||||||
doc.preview = truncateMarkdown(doc.text, 160);
|
doc.preview = truncateMarkdown(doc.text, 160);
|
||||||
doc.revisionCount += 1;
|
doc.revisionCount += 1;
|
||||||
|
@ -62,7 +58,6 @@ const beforeSave = async doc => {
|
||||||
// We'll add the current user as revision hasn't been generated yet
|
// We'll add the current user as revision hasn't been generated yet
|
||||||
ids.push(doc.lastModifiedById);
|
ids.push(doc.lastModifiedById);
|
||||||
doc.collaboratorIds = _.uniq(ids);
|
doc.collaboratorIds = _.uniq(ids);
|
||||||
doc.emoji = extractEmoji(doc);
|
|
||||||
|
|
||||||
return doc;
|
return doc;
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
// @flow
|
||||||
|
import emojiRegex from 'emoji-regex';
|
||||||
|
|
||||||
|
export default function parseTitle(text: string = '') {
|
||||||
|
const regex = emojiRegex();
|
||||||
|
|
||||||
|
// find and extract title
|
||||||
|
const firstLine = text.trim().split(/\r?\n/)[0];
|
||||||
|
const title = firstLine.replace(/^#/, '').trim();
|
||||||
|
|
||||||
|
// find and extract first emoji
|
||||||
|
const matches = regex.exec(title);
|
||||||
|
const firstEmoji = matches ? matches[0] : null;
|
||||||
|
const startsWithEmoji = firstEmoji && title.startsWith(firstEmoji);
|
||||||
|
const emoji = startsWithEmoji ? firstEmoji : undefined;
|
||||||
|
|
||||||
|
return { title, emoji };
|
||||||
|
}
|
|
@ -26,7 +26,10 @@ module.exports = {
|
||||||
{
|
{
|
||||||
test: /\.js$/,
|
test: /\.js$/,
|
||||||
loader: 'babel',
|
loader: 'babel',
|
||||||
include: path.join(__dirname, 'frontend'),
|
include: [
|
||||||
|
path.join(__dirname, 'frontend'),
|
||||||
|
path.join(__dirname, 'shared'),
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{ test: /\.json$/, loader: 'json-loader' },
|
{ test: /\.json$/, loader: 'json-loader' },
|
||||||
// inline base64 URLs for <=8k images, direct URLs for the rest
|
// inline base64 URLs for <=8k images, direct URLs for the rest
|
||||||
|
|
|
@ -2499,6 +2499,10 @@ emoji-regex@^6.1.0:
|
||||||
version "6.4.2"
|
version "6.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.4.2.tgz#a30b6fee353d406d96cfb9fa765bdc82897eff6e"
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.4.2.tgz#a30b6fee353d406d96cfb9fa765bdc82897eff6e"
|
||||||
|
|
||||||
|
emoji-regex@^6.5.1:
|
||||||
|
version "6.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.5.1.tgz#9baea929b155565c11ea41c6626eaa65cef992c2"
|
||||||
|
|
||||||
emojilib@^2.0.2:
|
emojilib@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.0.2.tgz#df91c45ede69f2d0ffd3d80acf8c72771b2a5ea1"
|
resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.0.2.tgz#df91c45ede69f2d0ffd3d80acf8c72771b2a5ea1"
|
||||||
|
|
Reference in New Issue