@@ -1,5 +1,7 @@
|
||||
[include]
|
||||
.*/frontend/.*
|
||||
.*/server/.*
|
||||
.*/shared/.*
|
||||
|
||||
[ignore]
|
||||
.*/node_modules/styled-components/.*
|
||||
|
||||
@@ -29,6 +29,7 @@ type Props = {
|
||||
onImageUploadStart: Function,
|
||||
onImageUploadStop: Function,
|
||||
starred: boolean,
|
||||
emoji: string,
|
||||
readOnly: boolean,
|
||||
heading?: ?React.Element<*>,
|
||||
};
|
||||
@@ -213,6 +214,7 @@ type KeyData = {
|
||||
className={cx(styles.editor, { readOnly: this.props.readOnly })}
|
||||
schema={this.schema}
|
||||
plugins={this.plugins}
|
||||
emoji={this.props.emoji}
|
||||
state={this.state.state}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
|
||||
@@ -25,6 +25,10 @@ type Context = {
|
||||
starred?: boolean,
|
||||
};
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-left: ${props => (props.hasEmoji ? '-1.2em' : 0)}
|
||||
`;
|
||||
|
||||
const StyledStar = styled(StarIcon)`
|
||||
top: 3px;
|
||||
position: relative;
|
||||
@@ -61,10 +65,14 @@ function Heading(props: Props, { starred }: Context) {
|
||||
const showStar = readOnly && !!onStar;
|
||||
const showHash = readOnly && !!slugish && !showStar;
|
||||
const Component = component;
|
||||
const emoji = editor.props.emoji || '';
|
||||
const title = node.text.trim();
|
||||
const startsWithEmojiAndSpace =
|
||||
emoji && title.match(new RegExp(`^${emoji}\\s`));
|
||||
|
||||
return (
|
||||
<Component className={styles.title}>
|
||||
{children}
|
||||
<Wrapper hasEmoji={startsWithEmojiAndSpace}>{children}</Wrapper>
|
||||
{showPlaceholder &&
|
||||
<span className={styles.placeholder} contentEditable={false}>
|
||||
{editor.props.placeholder}
|
||||
|
||||
@@ -5,15 +5,11 @@ import invariant from 'invariant';
|
||||
import { client } from 'utils/ApiClient';
|
||||
import stores from 'stores';
|
||||
import ErrorsStore from 'stores/ErrorsStore';
|
||||
import parseTitle from '../../shared/parseTitle';
|
||||
|
||||
import type { User } from 'types';
|
||||
import Collection from './Collection';
|
||||
|
||||
const parseHeader = text => {
|
||||
const firstLine = text.trim().split(/\r?\n/)[0];
|
||||
return firstLine.replace(/^#/, '').trim();
|
||||
};
|
||||
|
||||
const DEFAULT_TITLE = 'Untitled document';
|
||||
|
||||
class Document {
|
||||
@@ -31,6 +27,7 @@ class Document {
|
||||
html: string;
|
||||
id: string;
|
||||
team: string;
|
||||
emoji: string;
|
||||
private: boolean = false;
|
||||
starred: boolean = false;
|
||||
text: string = '';
|
||||
@@ -181,7 +178,11 @@ class Document {
|
||||
};
|
||||
|
||||
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;
|
||||
this.data = data;
|
||||
extendObservable(this, data);
|
||||
|
||||
@@ -203,6 +203,7 @@ type Props = {
|
||||
<Editor
|
||||
key={document.id}
|
||||
text={document.text}
|
||||
emoji={document.emoji}
|
||||
onImageUploadStart={this.onImageUploadStart}
|
||||
onImageUploadStop={this.onImageUploadStop}
|
||||
onChange={this.onChange}
|
||||
|
||||
29
package.json
29
package.json
@@ -22,19 +22,35 @@
|
||||
"precommit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": ["eslint --fix", "git add"]
|
||||
"*.js": [
|
||||
"eslint --fix",
|
||||
"git add"
|
||||
]
|
||||
},
|
||||
"jest": {
|
||||
"verbose": false,
|
||||
"roots": ["frontend"],
|
||||
"roots": [
|
||||
"frontend"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^.*[.](s?css|css)$": "<rootDir>/__mocks__/styleMock.js",
|
||||
"^.*[.](gif|ttf|eot|svg)$": "<rootDir>/__test__/fileMock.js"
|
||||
},
|
||||
"moduleFileExtensions": ["js", "jsx", "json"],
|
||||
"moduleDirectories": ["node_modules"],
|
||||
"modulePaths": ["frontend"],
|
||||
"setupFiles": ["<rootDir>/setupJest.js", "<rootDir>/__mocks__/window.js"]
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"jsx",
|
||||
"json"
|
||||
],
|
||||
"moduleDirectories": [
|
||||
"node_modules"
|
||||
],
|
||||
"modulePaths": [
|
||||
"frontend"
|
||||
],
|
||||
"setupFiles": [
|
||||
"<rootDir>/setupJest.js",
|
||||
"<rootDir>/__mocks__/window.js"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 7.6"
|
||||
@@ -69,6 +85,7 @@
|
||||
"debug": "2.2.0",
|
||||
"dotenv": "^4.0.0",
|
||||
"emoji-name-map": "1.1.2",
|
||||
"emoji-regex": "^6.5.1",
|
||||
"eslint": "^3.19.0",
|
||||
"eslint-config-react-app": "^0.6.2",
|
||||
"eslint-import-resolver-webpack": "^0.3.1",
|
||||
|
||||
12
server/migrations/20170729215619-emoji.js
Normal file
12
server/migrations/20170729215619-emoji.js
Normal file
@@ -0,0 +1,12 @@
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
queryInterface.addColumn('documents', 'emoji', {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface, _Sequelize) => {
|
||||
queryInterface.removeColumn('documents', 'emoji');
|
||||
},
|
||||
};
|
||||
@@ -2,11 +2,13 @@
|
||||
import slug from 'slug';
|
||||
import _ from 'lodash';
|
||||
import randomstring from 'randomstring';
|
||||
import emojiRegex from 'emoji-regex';
|
||||
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { convertToMarkdown } from '../../frontend/utils/markdown';
|
||||
import { truncateMarkdown } from '../utils/truncate';
|
||||
import parseTitle from '../../shared/parseTitle';
|
||||
import Revision from './Revision';
|
||||
|
||||
const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{10,15})$/;
|
||||
@@ -35,6 +37,9 @@ const createUrlId = doc => {
|
||||
};
|
||||
|
||||
const beforeSave = async doc => {
|
||||
const { emoji } = parseTitle(doc.text);
|
||||
|
||||
doc.emoji = emoji;
|
||||
doc.html = convertToMarkdown(doc.text);
|
||||
doc.preview = truncateMarkdown(doc.text, 160);
|
||||
doc.revisionCount += 1;
|
||||
|
||||
@@ -22,6 +22,7 @@ async function present(ctx: Object, document: Document, options: ?Options) {
|
||||
text: document.text,
|
||||
html: document.html,
|
||||
preview: document.preview,
|
||||
emoji: document.emoji,
|
||||
createdAt: document.createdAt,
|
||||
createdBy: presentUser(ctx, document.createdBy),
|
||||
updatedAt: document.updatedAt,
|
||||
|
||||
18
shared/parseTitle.js
Normal file
18
shared/parseTitle.js
Normal file
@@ -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$/,
|
||||
loader: 'babel',
|
||||
include: path.join(__dirname, 'frontend'),
|
||||
include: [
|
||||
path.join(__dirname, 'frontend'),
|
||||
path.join(__dirname, 'shared'),
|
||||
]
|
||||
},
|
||||
{ test: /\.json$/, loader: 'json-loader' },
|
||||
// inline base64 URLs for <=8k images, direct URLs for the rest
|
||||
|
||||
@@ -2499,6 +2499,10 @@ emoji-regex@^6.1.0:
|
||||
version "6.4.2"
|
||||
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:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/emojilib/-/emojilib-2.0.2.tgz#df91c45ede69f2d0ffd3d80acf8c72771b2a5ea1"
|
||||
|
||||
Reference in New Issue
Block a user