Update master
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import invariant from 'invariant';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
@@ -10,14 +10,14 @@ import { color } from 'styles/constants';
|
||||
import { fadeAndScaleIn } from 'styles/animations';
|
||||
|
||||
type Props = {
|
||||
label: React.Element<any>,
|
||||
onShow?: () => void,
|
||||
label: React.Element<*>,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
children?: React.Element<any>,
|
||||
children?: React.Element<*>,
|
||||
style?: Object,
|
||||
};
|
||||
|
||||
@observer class DropdownMenu extends React.Component {
|
||||
@observer class DropdownMenu extends Component {
|
||||
props: Props;
|
||||
actionRef: Object;
|
||||
@observable open: boolean = false;
|
||||
@@ -37,7 +37,7 @@ type Props = {
|
||||
this.open = true;
|
||||
this.top = targetRect.bottom - bodyRect.top;
|
||||
this.right = bodyRect.width - targetRect.left - targetRect.width;
|
||||
if (this.props.onShow) this.props.onShow();
|
||||
if (this.props.onOpen) this.props.onOpen();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ const DropdownMenuItem = ({
|
||||
onClick,
|
||||
children,
|
||||
}: {
|
||||
onClick?: () => void,
|
||||
onClick?: SyntheticEvent => void,
|
||||
children?: React.Element<any>,
|
||||
}) => {
|
||||
return (
|
||||
@@ -24,11 +24,15 @@ const MenuItem = styled.div`
|
||||
|
||||
color: ${color.slateDark};
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
|
||||
svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
width: 100%;
|
||||
|
||||
@@ -9,6 +9,7 @@ import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||
import Flex from 'components/Flex';
|
||||
import ClickablePadding from './components/ClickablePadding';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import BlockInsert from './components/BlockInsert';
|
||||
import Placeholder from './components/Placeholder';
|
||||
import Contents from './components/Contents';
|
||||
import Markdown from './serializer';
|
||||
@@ -24,7 +25,7 @@ type Props = {
|
||||
onCancel: Function,
|
||||
onImageUploadStart: Function,
|
||||
onImageUploadStop: Function,
|
||||
emoji: string,
|
||||
emoji?: string,
|
||||
readOnly: boolean,
|
||||
};
|
||||
|
||||
@@ -172,6 +173,8 @@ type KeyData = {
|
||||
};
|
||||
|
||||
render = () => {
|
||||
const { readOnly, emoji, onSave } = this.props;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
onDrop={this.handleDrop}
|
||||
@@ -182,25 +185,32 @@ type KeyData = {
|
||||
auto
|
||||
>
|
||||
<MaxWidth column auto>
|
||||
<Header onClick={this.focusAtStart} readOnly={this.props.readOnly} />
|
||||
<Toolbar state={this.editorState} onChange={this.onChange} />
|
||||
<Header onClick={this.focusAtStart} readOnly={readOnly} />
|
||||
<Contents state={this.editorState} />
|
||||
{!readOnly &&
|
||||
<Toolbar state={this.editorState} onChange={this.onChange} />}
|
||||
{!readOnly &&
|
||||
<BlockInsert
|
||||
state={this.editorState}
|
||||
onChange={this.onChange}
|
||||
onInsertImage={this.insertImageFile}
|
||||
/>}
|
||||
<StyledEditor
|
||||
innerRef={ref => (this.editor = ref)}
|
||||
placeholder="Start with a title…"
|
||||
bodyPlaceholder="…the rest is your canvas"
|
||||
schema={this.schema}
|
||||
plugins={this.plugins}
|
||||
emoji={this.props.emoji}
|
||||
emoji={emoji}
|
||||
state={this.editorState}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onChange={this.onChange}
|
||||
onDocumentChange={this.onDocumentChange}
|
||||
onSave={this.props.onSave}
|
||||
readOnly={this.props.readOnly}
|
||||
onSave={onSave}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
<ClickablePadding
|
||||
onClick={!this.props.readOnly ? this.focusAtEnd : undefined}
|
||||
onClick={!readOnly ? this.focusAtEnd : undefined}
|
||||
grow
|
||||
/>
|
||||
</MaxWidth>
|
||||
@@ -281,6 +291,10 @@ const StyledEditor = styled(Editor)`
|
||||
position: relative;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: ${({ readOnly }) => (readOnly ? 'underline' : 'none')};
|
||||
}
|
||||
|
||||
li p {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
@@ -322,6 +336,10 @@ const StyledEditor = styled(Editor)`
|
||||
td {
|
||||
padding: 5px 20px 5px 0;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
`;
|
||||
|
||||
export default MarkdownEditor;
|
||||
|
||||
197
frontend/components/Editor/components/BlockInsert.js
Normal file
197
frontend/components/Editor/components/BlockInsert.js
Normal file
@@ -0,0 +1,197 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import EditList from '../plugins/EditList';
|
||||
import getDataTransferFiles from 'utils/getDataTransferFiles';
|
||||
import Portal from 'react-portal';
|
||||
import { observable } from 'mobx';
|
||||
import { observer } from 'mobx-react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
import Icon from 'components/Icon';
|
||||
import BlockMenu from 'menus/BlockMenu';
|
||||
import type { State } from '../types';
|
||||
|
||||
const { transforms } = EditList;
|
||||
|
||||
type Props = {
|
||||
state: State,
|
||||
onChange: Function,
|
||||
onInsertImage: File => Promise<*>,
|
||||
};
|
||||
|
||||
@observer
|
||||
export default class BlockInsert extends Component {
|
||||
props: Props;
|
||||
mouseMoveTimeout: number;
|
||||
file: HTMLInputElement;
|
||||
|
||||
@observable active: boolean = false;
|
||||
@observable menuOpen: boolean = false;
|
||||
@observable top: number;
|
||||
@observable left: number;
|
||||
@observable mouseX: number;
|
||||
|
||||
componentDidMount = () => {
|
||||
this.update();
|
||||
window.addEventListener('mousemove', this.handleMouseMove);
|
||||
};
|
||||
|
||||
componentWillUpdate = (nextProps: Props) => {
|
||||
this.update(nextProps);
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
window.removeEventListener('mousemove', this.handleMouseMove);
|
||||
};
|
||||
|
||||
setInactive = () => {
|
||||
if (this.menuOpen) return;
|
||||
this.active = false;
|
||||
};
|
||||
|
||||
handleMouseMove = (ev: SyntheticMouseEvent) => {
|
||||
const windowWidth = window.innerWidth / 3;
|
||||
let active = ev.clientX < windowWidth;
|
||||
|
||||
if (active !== this.active) {
|
||||
this.active = active || this.menuOpen;
|
||||
}
|
||||
if (active) {
|
||||
clearTimeout(this.mouseMoveTimeout);
|
||||
this.mouseMoveTimeout = setTimeout(this.setInactive, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
handleMenuOpen = () => {
|
||||
this.menuOpen = true;
|
||||
};
|
||||
|
||||
handleMenuClose = () => {
|
||||
this.menuOpen = false;
|
||||
};
|
||||
|
||||
update = (props?: Props) => {
|
||||
if (!document.activeElement) return;
|
||||
const { state } = props || this.props;
|
||||
const boxRect = document.activeElement.getBoundingClientRect();
|
||||
const selection = window.getSelection();
|
||||
if (!selection.focusNode) return;
|
||||
|
||||
const range = selection.getRangeAt(0);
|
||||
const rect = range.getBoundingClientRect();
|
||||
|
||||
if (rect.top <= 0 || boxRect.left <= 0) return;
|
||||
|
||||
if (state.startBlock.type === 'heading1') {
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
this.top = Math.round(rect.top + window.scrollY);
|
||||
this.left = Math.round(boxRect.left + window.scrollX - 20);
|
||||
};
|
||||
|
||||
insertBlock = (
|
||||
ev: SyntheticEvent,
|
||||
options: {
|
||||
type: string | Object,
|
||||
wrapper?: string | Object,
|
||||
append?: string | Object,
|
||||
}
|
||||
) => {
|
||||
ev.preventDefault();
|
||||
const { type, wrapper, append } = options;
|
||||
let { state } = this.props;
|
||||
let transform = state.transform();
|
||||
const { document } = state;
|
||||
const parent = document.getParent(state.startBlock.key);
|
||||
|
||||
// lists get some special treatment
|
||||
if (parent && parent.type === 'list-item') {
|
||||
transform = transforms.unwrapList(
|
||||
transforms
|
||||
.splitListItem(transform.collapseToStart())
|
||||
.collapseToEndOfPreviousBlock()
|
||||
);
|
||||
}
|
||||
|
||||
transform = transform.insertBlock(type);
|
||||
|
||||
if (wrapper) transform = transform.wrapBlock(wrapper);
|
||||
if (append) transform = transform.insertBlock(append);
|
||||
|
||||
state = transform.focus().apply();
|
||||
this.props.onChange(state);
|
||||
this.active = false;
|
||||
};
|
||||
|
||||
onPickImage = (ev: SyntheticEvent) => {
|
||||
// simulate a click on the file upload input element
|
||||
this.file.click();
|
||||
};
|
||||
|
||||
onChooseImage = async (ev: SyntheticEvent) => {
|
||||
const files = getDataTransferFiles(ev);
|
||||
for (const file of files) {
|
||||
await this.props.onInsertImage(file);
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const style = { top: `${this.top}px`, left: `${this.left}px` };
|
||||
const todo = { type: 'list-item', data: { checked: false } };
|
||||
const rule = { type: 'horizontal-rule', isVoid: true };
|
||||
|
||||
return (
|
||||
<Portal isOpened>
|
||||
<Trigger active={this.active} style={style}>
|
||||
<HiddenInput
|
||||
type="file"
|
||||
innerRef={ref => (this.file = ref)}
|
||||
onChange={this.onChooseImage}
|
||||
accept="image/*"
|
||||
/>
|
||||
<BlockMenu
|
||||
label={<Icon type="PlusCircle" />}
|
||||
onPickImage={this.onPickImage}
|
||||
onInsertList={ev =>
|
||||
this.insertBlock(ev, {
|
||||
type: 'list-item',
|
||||
wrapper: 'bulleted-list',
|
||||
})}
|
||||
onInsertTodoList={ev =>
|
||||
this.insertBlock(ev, { type: todo, wrapper: 'todo-list' })}
|
||||
onInsertBreak={ev =>
|
||||
this.insertBlock(ev, { type: rule, append: 'paragraph' })}
|
||||
onOpen={this.handleMenuOpen}
|
||||
onClose={this.handleMenuClose}
|
||||
/>
|
||||
</Trigger>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const HiddenInput = styled.input`
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: -100px;
|
||||
visibility: hidden;
|
||||
`;
|
||||
|
||||
const Trigger = styled.div`
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
background-color: ${color.white};
|
||||
border-radius: 4px;
|
||||
transition: opacity 250ms ease-in-out, transform 250ms ease-in-out;
|
||||
line-height: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
transform: scale(.9);
|
||||
|
||||
${({ active }) => active && `
|
||||
transform: scale(1);
|
||||
opacity: .9;
|
||||
`}
|
||||
`;
|
||||
17
frontend/components/Editor/components/HorizontalRule.js
Normal file
17
frontend/components/Editor/components/HorizontalRule.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import type { Props } from '../types';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
function HorizontalRule(props: Props) {
|
||||
const { state, node } = props;
|
||||
const active = state.isFocused && state.selection.hasEdgeIn(node);
|
||||
return <StyledHr active={active} />;
|
||||
}
|
||||
|
||||
const StyledHr = styled.hr`
|
||||
border-bottom: 1px solid ${props => (props.active ? color.slate : color.slateLight)};
|
||||
`;
|
||||
|
||||
export default HorizontalRule;
|
||||
@@ -9,7 +9,6 @@ import Heading2Icon from 'components/Icon/Heading2Icon';
|
||||
import ItalicIcon from 'components/Icon/ItalicIcon';
|
||||
import LinkIcon from 'components/Icon/LinkIcon';
|
||||
import StrikethroughIcon from 'components/Icon/StrikethroughIcon';
|
||||
import BulletedListIcon from 'components/Icon/BulletedListIcon';
|
||||
|
||||
export default class FormattingToolbar extends Component {
|
||||
props: {
|
||||
@@ -95,7 +94,6 @@ export default class FormattingToolbar extends Component {
|
||||
{this.renderMarkButton('deleted', StrikethroughIcon)}
|
||||
{this.renderBlockButton('heading1', Heading1Icon)}
|
||||
{this.renderBlockButton('heading2', Heading2Icon)}
|
||||
{this.renderBlockButton('bulleted-list', BulletedListIcon)}
|
||||
{this.renderMarkButton('code', CodeIcon)}
|
||||
<ToolbarButton onMouseDown={this.onCreateLink}>
|
||||
<LinkIcon light />
|
||||
|
||||
@@ -15,7 +15,6 @@ export default async function insertImageFile(
|
||||
try {
|
||||
// load the file as a data URL
|
||||
const id = uuid.v4();
|
||||
const alt = file.name;
|
||||
const reader = new FileReader();
|
||||
reader.addEventListener('load', () => {
|
||||
const src = reader.result;
|
||||
@@ -25,7 +24,7 @@ export default async function insertImageFile(
|
||||
.insertBlock({
|
||||
type: 'image',
|
||||
isVoid: true,
|
||||
data: { src, alt, id, loading: true },
|
||||
data: { src, id, loading: true },
|
||||
})
|
||||
.apply();
|
||||
editor.onChange(state);
|
||||
@@ -46,7 +45,7 @@ export default async function insertImageFile(
|
||||
);
|
||||
|
||||
return finalTransform.setNodeByKey(placeholder.key, {
|
||||
data: { src, alt, loading: false },
|
||||
data: { src, loading: false },
|
||||
});
|
||||
} catch (err) {
|
||||
throw err;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// @flow
|
||||
import DropOrPasteImages from '@tommoor/slate-drop-or-paste-images';
|
||||
import PasteLinkify from 'slate-paste-linkify';
|
||||
import EditList from 'slate-edit-list';
|
||||
import CollapseOnEscape from 'slate-collapse-on-escape';
|
||||
import TrailingBlock from 'slate-trailing-block';
|
||||
import EditCode from 'slate-edit-code';
|
||||
import Prism from 'slate-prism';
|
||||
import EditList from './plugins/EditList';
|
||||
import KeyboardShortcuts from './plugins/KeyboardShortcuts';
|
||||
import MarkdownShortcuts from './plugins/MarkdownShortcuts';
|
||||
import insertImage from './insertImage';
|
||||
@@ -35,10 +35,7 @@ const createPlugins = ({ onImageUploadStart, onImageUploadStop }: Options) => {
|
||||
);
|
||||
},
|
||||
}),
|
||||
EditList({
|
||||
types: ['ordered-list', 'bulleted-list', 'todo-list'],
|
||||
typeItem: 'list-item',
|
||||
}),
|
||||
EditList,
|
||||
EditCode({
|
||||
onlyIn: onlyInCode,
|
||||
containerType: 'code',
|
||||
|
||||
7
frontend/components/Editor/plugins/EditList.js
Normal file
7
frontend/components/Editor/plugins/EditList.js
Normal file
@@ -0,0 +1,7 @@
|
||||
// @flow
|
||||
import EditList from 'slate-edit-list';
|
||||
|
||||
export default EditList({
|
||||
types: ['ordered-list', 'bulleted-list', 'todo-list'],
|
||||
typeItem: 'list-item',
|
||||
});
|
||||
@@ -112,19 +112,17 @@ export default function MarkdownShortcuts() {
|
||||
|
||||
if (chars === '--') {
|
||||
ev.preventDefault();
|
||||
const transform = state
|
||||
return state
|
||||
.transform()
|
||||
.extendToStartOf(startBlock)
|
||||
.delete()
|
||||
.setBlock({
|
||||
type: 'horizontal-rule',
|
||||
isVoid: true,
|
||||
});
|
||||
state = transform
|
||||
})
|
||||
.collapseToStartOfNextBlock()
|
||||
.insertBlock('paragraph')
|
||||
.apply();
|
||||
return state;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Code from './components/Code';
|
||||
import HorizontalRule from './components/HorizontalRule';
|
||||
import InlineCode from './components/InlineCode';
|
||||
import Image from './components/Image';
|
||||
import Link from './components/Link';
|
||||
@@ -33,7 +34,7 @@ const createSchema = () => {
|
||||
'block-quote': (props: Props) => (
|
||||
<blockquote>{props.children}</blockquote>
|
||||
),
|
||||
'horizontal-rule': (props: Props) => <hr />,
|
||||
'horizontal-rule': HorizontalRule,
|
||||
'bulleted-list': (props: Props) => <ul>{props.children}</ul>,
|
||||
'ordered-list': (props: Props) => <ol>{props.children}</ol>,
|
||||
'todo-list': (props: Props) => <TodoList>{props.children}</TodoList>,
|
||||
|
||||
21
frontend/components/Empty/Empty.js
Normal file
21
frontend/components/Empty/Empty.js
Normal file
@@ -0,0 +1,21 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { color } from 'styles/constants';
|
||||
|
||||
type Props = {
|
||||
children: string,
|
||||
};
|
||||
|
||||
const Empty = (props: Props) => {
|
||||
const { children, ...rest } = props;
|
||||
return <Container {...rest}>{children}</Container>;
|
||||
};
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
color: ${color.slate};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export default Empty;
|
||||
3
frontend/components/Empty/index.js
Normal file
3
frontend/components/Empty/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
// @flow
|
||||
import Empty from './Empty';
|
||||
export default Empty;
|
||||
@@ -1,84 +0,0 @@
|
||||
// @flow
|
||||
import styled from 'styled-components';
|
||||
|
||||
const HtmlContent = styled.div`
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
:global {
|
||||
.anchor {
|
||||
visibility: hidden;
|
||||
color: ;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
:global {
|
||||
.anchor {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.5em;
|
||||
|
||||
ul {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
border-left: 2px solid $lightGray;
|
||||
padding-left: 0.8em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
display: block;
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
|
||||
thead, tbody {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
thead {
|
||||
tr {
|
||||
border-bottom: 2px solid $lightGray;
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid $lightGray;
|
||||
}
|
||||
}
|
||||
|
||||
tr {
|
||||
background-color: #fff;
|
||||
|
||||
// &:nth-child(2n) {
|
||||
// background-color: #f8f8f8;
|
||||
// }
|
||||
}
|
||||
|
||||
th, td {
|
||||
text-align: left;
|
||||
border: 1px 0 solid $lightGray;
|
||||
padding: 5px 20px 5px 0;
|
||||
|
||||
&:last-child {
|
||||
padding-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default HtmlContent;
|
||||
@@ -1,3 +0,0 @@
|
||||
// @flow
|
||||
import HtmlContent from './HtmlContent';
|
||||
export default HtmlContent;
|
||||
@@ -95,7 +95,7 @@ type Props = {
|
||||
<CollectionMenu
|
||||
history={history}
|
||||
collection={collection}
|
||||
onShow={() => (this.menuOpen = true)}
|
||||
onOpen={() => (this.menuOpen = true)}
|
||||
onClose={() => (this.menuOpen = false)}
|
||||
onImport={this.handleImport}
|
||||
open={this.menuOpen}
|
||||
|
||||
@@ -16,7 +16,7 @@ const activeStyle = {
|
||||
const StyleableDiv = props => <div {...props} />;
|
||||
|
||||
const styleComponent = component => styled(component)`
|
||||
display: block;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -42,7 +42,7 @@ function SidebarLink(props: Object) {
|
||||
<Flex>
|
||||
<Component exact activeStyle={activeStyle} {...props}>
|
||||
{props.hasChildren && <StyledChevron expanded={props.expanded} />}
|
||||
{props.children}
|
||||
<Content>{props.children}</Content>
|
||||
</Component>
|
||||
</Flex>
|
||||
);
|
||||
@@ -62,4 +62,8 @@ const StyledChevron = styled(ChevronIcon)`
|
||||
}
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export default SidebarLink;
|
||||
|
||||
@@ -73,6 +73,15 @@ const Auth = ({ children }: AuthProps) => {
|
||||
}),
|
||||
};
|
||||
|
||||
if (window.Bugsnag) {
|
||||
Bugsnag.user = {
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
teamId: team.id,
|
||||
team: team.name,
|
||||
};
|
||||
}
|
||||
|
||||
authenticatedStores.collections.fetchAll();
|
||||
}
|
||||
|
||||
|
||||
49
frontend/menus/BlockMenu.js
Normal file
49
frontend/menus/BlockMenu.js
Normal file
@@ -0,0 +1,49 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'components/Icon';
|
||||
import { observer } from 'mobx-react';
|
||||
import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
|
||||
@observer class BlockMenu extends Component {
|
||||
props: {
|
||||
label?: React$Element<*>,
|
||||
onPickImage: SyntheticEvent => void,
|
||||
onInsertList: SyntheticEvent => void,
|
||||
onInsertTodoList: SyntheticEvent => void,
|
||||
onInsertBreak: SyntheticEvent => void,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
label,
|
||||
onPickImage,
|
||||
onInsertList,
|
||||
onInsertTodoList,
|
||||
onInsertBreak,
|
||||
...rest
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
style={{ marginRight: -70, marginTop: 5 }}
|
||||
label={label}
|
||||
{...rest}
|
||||
>
|
||||
<DropdownMenuItem onClick={onPickImage}>
|
||||
<Icon type="Image" /> Add images
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onInsertList}>
|
||||
<Icon type="List" /> Start list
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onInsertTodoList}>
|
||||
<Icon type="CheckSquare" /> Start checklist
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={onInsertBreak}>
|
||||
<Icon type="Minus" /> Add break
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default BlockMenu;
|
||||
@@ -12,7 +12,7 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
@observer class CollectionMenu extends Component {
|
||||
props: {
|
||||
label?: React$Element<any>,
|
||||
onShow?: () => void,
|
||||
onOpen?: () => void,
|
||||
onClose?: () => void,
|
||||
onImport?: () => void,
|
||||
history: Object,
|
||||
@@ -36,13 +36,13 @@ import { DropdownMenu, DropdownMenuItem } from 'components/DropdownMenu';
|
||||
};
|
||||
|
||||
render() {
|
||||
const { collection, label, onShow, onClose, onImport } = this.props;
|
||||
const { collection, label, onOpen, onClose, onImport } = this.props;
|
||||
const { allowDelete } = collection;
|
||||
|
||||
return (
|
||||
<DropdownMenu
|
||||
label={label || <MoreIcon type="MoreHorizontal" />}
|
||||
onShow={onShow}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
>
|
||||
{collection &&
|
||||
|
||||
@@ -2,30 +2,31 @@
|
||||
import React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import HtmlContent from 'components/HtmlContent';
|
||||
import Editor from 'components/Editor';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
|
||||
import { convertToMarkdown } from 'utils/markdown';
|
||||
|
||||
type Props = {
|
||||
title: string,
|
||||
content: string,
|
||||
};
|
||||
|
||||
@observer class Flatpage extends React.Component {
|
||||
props: Props;
|
||||
const Flatpage = observer((props: Props) => {
|
||||
const { title, content } = props;
|
||||
|
||||
render() {
|
||||
const { title, content } = this.props;
|
||||
const htmlContent = convertToMarkdown(content);
|
||||
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title={title} />
|
||||
<HtmlContent dangerouslySetInnerHTML={{ __html: htmlContent }} />
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<CenteredContent>
|
||||
<PageTitle title={title} />
|
||||
<Editor
|
||||
text={content}
|
||||
onChange={() => {}}
|
||||
onSave={() => {}}
|
||||
onCancel={() => {}}
|
||||
onImageUploadStart={() => {}}
|
||||
onImageUploadStop={() => {}}
|
||||
readOnly
|
||||
/>
|
||||
</CenteredContent>
|
||||
);
|
||||
});
|
||||
|
||||
export default Flatpage;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { searchUrl } from 'utils/routeHelpers';
|
||||
import styled from 'styled-components';
|
||||
import ArrowKeyNavigation from 'boundless-arrow-key-navigation';
|
||||
|
||||
import Empty from 'components/Empty';
|
||||
import Flex from 'components/Flex';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import LoadingIndicator from 'components/LoadingIndicator';
|
||||
@@ -57,7 +58,7 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
|
||||
firstDocument: HTMLElement;
|
||||
props: Props;
|
||||
|
||||
@observable resultIds: Array<string> = []; // Document IDs
|
||||
@observable resultIds: string[] = []; // Document IDs
|
||||
@observable searchTerm: ?string = null;
|
||||
@observable isFetching = false;
|
||||
|
||||
@@ -131,18 +132,19 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
|
||||
}
|
||||
|
||||
render() {
|
||||
const { documents } = this.props;
|
||||
const { documents, notFound } = this.props;
|
||||
const query = this.props.match.params.query;
|
||||
const hasResults = this.resultIds.length > 0;
|
||||
const showEmpty = !this.isFetching && this.searchTerm && !hasResults;
|
||||
|
||||
return (
|
||||
<Container auto>
|
||||
<PageTitle title={this.title} />
|
||||
{this.isFetching && <LoadingIndicator />}
|
||||
{this.props.notFound &&
|
||||
{notFound &&
|
||||
<div>
|
||||
<h1>Not Found</h1>
|
||||
<p>We're unable to find the page you're accessing.</p>
|
||||
<p>We’re unable to find the page you’re accessing.</p>
|
||||
</div>}
|
||||
<ResultsWrapper pinToTop={hasResults} column auto>
|
||||
<SearchField
|
||||
@@ -151,6 +153,7 @@ const StyledArrowKeyNavigation = styled(ArrowKeyNavigation)`
|
||||
onChange={this.updateQuery}
|
||||
value={query || ''}
|
||||
/>
|
||||
{showEmpty && <Empty>Oop, no matching documents.</Empty>}
|
||||
<ResultList visible={hasResults}>
|
||||
<StyledArrowKeyNavigation
|
||||
mode={ArrowKeyNavigation.mode.VERTICAL}
|
||||
|
||||
@@ -5,22 +5,8 @@ import Flex from 'components/Flex';
|
||||
import { color } from 'styles/constants';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Field = styled.input`
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 48px;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: 0;
|
||||
|
||||
::-webkit-input-placeholder { color: ${color.slate}; }
|
||||
:-moz-placeholder { color: ${color.slate}; }
|
||||
::-moz-placeholder { color: ${color.slate}; }
|
||||
:-ms-input-placeholder { color: ${color.slate}; }
|
||||
`;
|
||||
|
||||
class SearchField extends Component {
|
||||
input: HTMLElement;
|
||||
input: HTMLInputElement;
|
||||
props: {
|
||||
onChange: Function,
|
||||
};
|
||||
@@ -33,23 +19,24 @@ class SearchField extends Component {
|
||||
this.input.focus();
|
||||
};
|
||||
|
||||
setRef = (ref: HTMLElement) => {
|
||||
setRef = (ref: HTMLInputElement) => {
|
||||
this.input = ref;
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Flex align="center">
|
||||
<Icon
|
||||
<StyledIcon
|
||||
type="Search"
|
||||
size={48}
|
||||
color="#C9CFD6"
|
||||
size={46}
|
||||
color={color.slateLight}
|
||||
onClick={this.focusInput}
|
||||
/>
|
||||
<Field
|
||||
<StyledInput
|
||||
{...this.props}
|
||||
innerRef={this.setRef}
|
||||
onChange={this.handleChange}
|
||||
spellCheck="false"
|
||||
placeholder="Search…"
|
||||
autoFocus
|
||||
/>
|
||||
@@ -58,4 +45,22 @@ class SearchField extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
const StyledInput = styled.input`
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
font-size: 48px;
|
||||
font-weight: 400;
|
||||
outline: none;
|
||||
border: 0;
|
||||
|
||||
::-webkit-input-placeholder { color: ${color.slateLight}; }
|
||||
:-moz-placeholder { color: ${color.slateLight}; }
|
||||
::-moz-placeholder { color: ${color.slateLight}; }
|
||||
:-ms-input-placeholder { color: ${color.slateLight}; }
|
||||
`;
|
||||
|
||||
const StyledIcon = styled(Icon)`
|
||||
top: 3px;
|
||||
`;
|
||||
|
||||
export default SearchField;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router';
|
||||
import queryString from 'query-string';
|
||||
import { observable } from 'mobx';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import { client } from 'utils/ApiClient';
|
||||
|
||||
@@ -12,17 +13,15 @@ type Props = {
|
||||
location: Object,
|
||||
};
|
||||
|
||||
type State = {
|
||||
redirectTo: string,
|
||||
};
|
||||
|
||||
@observer class SlackAuth extends React.Component {
|
||||
props: Props;
|
||||
state: State;
|
||||
state = {};
|
||||
@observable redirectTo: string;
|
||||
|
||||
// $FlowIssue Flow doesn't like async lifecycle components https://github.com/facebook/flow/issues/1803
|
||||
async componentDidMount(): void {
|
||||
componentDidMount() {
|
||||
this.redirect();
|
||||
}
|
||||
|
||||
async redirect() {
|
||||
const { error, code, state } = queryString.parse(
|
||||
this.props.location.search
|
||||
);
|
||||
@@ -30,18 +29,18 @@ type State = {
|
||||
if (error) {
|
||||
if (error === 'access_denied') {
|
||||
// User selected "Deny" access on Slack OAuth
|
||||
this.setState({ redirectTo: '/dashboard' });
|
||||
this.redirectTo = '/dashboard';
|
||||
} else {
|
||||
this.setState({ redirectTo: '/auth/error' });
|
||||
this.redirectTo = '/auth/error';
|
||||
}
|
||||
} else {
|
||||
if (this.props.location.pathname === '/auth/slack/commands') {
|
||||
// User adding webhook integrations
|
||||
try {
|
||||
await client.post('/auth.slackCommands', { code });
|
||||
this.setState({ redirectTo: '/dashboard' });
|
||||
this.redirectTo = '/dashboard';
|
||||
} catch (e) {
|
||||
this.setState({ redirectTo: '/auth/error' });
|
||||
this.redirectTo = '/auth/error';
|
||||
}
|
||||
} else {
|
||||
// Regular Slack authentication
|
||||
@@ -50,18 +49,15 @@ type State = {
|
||||
|
||||
const { success } = await this.props.auth.authWithSlack(code, state);
|
||||
success
|
||||
? this.setState({ redirectTo: redirectTo || '/dashboard' })
|
||||
: this.setState({ redirectTo: '/auth/error' });
|
||||
? (this.redirectTo = redirectTo || '/dashboard')
|
||||
: (this.redirectTo = '/auth/error');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
{this.state.redirectTo && <Redirect to={this.state.redirectTo} />}
|
||||
</div>
|
||||
);
|
||||
if (this.redirectTo) return <Redirect to={this.redirectTo} />;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { Component } from 'react';
|
||||
import { observer, inject } from 'mobx-react';
|
||||
import CenteredContent from 'components/CenteredContent';
|
||||
import { ListPlaceholder } from 'components/LoadingPlaceholder';
|
||||
import Empty from 'components/Empty';
|
||||
import PageTitle from 'components/PageTitle';
|
||||
import DocumentList from 'components/DocumentList';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
@@ -17,14 +18,17 @@ import DocumentsStore from 'stores/DocumentsStore';
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoaded, isFetching } = this.props.documents;
|
||||
const { isLoaded, isFetching, starred } = this.props.documents;
|
||||
const showLoading = !isLoaded && isFetching;
|
||||
const showEmpty = isLoaded && !starred.length;
|
||||
|
||||
return (
|
||||
<CenteredContent column auto>
|
||||
<PageTitle title="Starred" />
|
||||
<h1>Starred</h1>
|
||||
{!isLoaded && isFetching && <ListPlaceholder />}
|
||||
<DocumentList documents={this.props.documents.starred} />
|
||||
{showLoading && <ListPlaceholder />}
|
||||
{showEmpty && <Empty>No starred documents yet.</Empty>}
|
||||
<DocumentList documents={starred} />
|
||||
</CenteredContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
// @flow
|
||||
import { keyframes } from 'styled-components';
|
||||
|
||||
export const fadeIn = keyframes`
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
`;
|
||||
|
||||
export const fadeAndScaleIn = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,16 +0,0 @@
|
||||
// @flow
|
||||
import emojiMapping from './emoji-mapping.json';
|
||||
|
||||
const EMOJI_REGEX = /:([A-Za-z0-9_\-+]+?):/gm;
|
||||
|
||||
const emojify = (text: string = '') => {
|
||||
let emojifiedText = text;
|
||||
|
||||
emojifiedText = text.replace(EMOJI_REGEX, (match, p1, offset, string) => {
|
||||
return emojiMapping[p1] || match;
|
||||
});
|
||||
|
||||
return emojifiedText;
|
||||
};
|
||||
|
||||
export default emojify;
|
||||
@@ -1,53 +0,0 @@
|
||||
// @flow
|
||||
import slug from 'slug';
|
||||
import marked from 'marked';
|
||||
import sanitizedRenderer from 'marked-sanitized';
|
||||
import highlight from 'highlight.js';
|
||||
import _ from 'lodash';
|
||||
import emojify from './emojify';
|
||||
import toc from './toc';
|
||||
|
||||
// $FlowIssue invalid flow-typed
|
||||
slug.defaults.mode = 'rfc3986';
|
||||
|
||||
const Renderer = sanitizedRenderer(marked.Renderer);
|
||||
const renderer = new Renderer();
|
||||
renderer.code = (code, language) => {
|
||||
const validLang = !!(language && highlight.getLanguage(language));
|
||||
const highlighted = validLang
|
||||
? highlight.highlight(language, code).value
|
||||
: _.escape(code);
|
||||
return `<pre><code class="hljs ${_.escape(language)}">${highlighted}</code></pre>`;
|
||||
};
|
||||
renderer.heading = (text, level) => {
|
||||
const headingSlug = _.escape(slug(text));
|
||||
return `
|
||||
<h${level}>
|
||||
${text}
|
||||
<a name="${headingSlug}" class="anchor" href="#${headingSlug}">#</a>
|
||||
</h${level}>
|
||||
`;
|
||||
};
|
||||
|
||||
const convertToMarkdown = (text: string) => {
|
||||
// Add TOC
|
||||
text = toc.insert(text || '', {
|
||||
slugify: heading => {
|
||||
// FIXME: E.g. `&` gets messed up
|
||||
const headingSlug = _.escape(slug(heading));
|
||||
return headingSlug;
|
||||
},
|
||||
});
|
||||
|
||||
return marked.parse(emojify(text), {
|
||||
renderer,
|
||||
gfm: true,
|
||||
tables: true,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
smartLists: true,
|
||||
smartypants: true,
|
||||
});
|
||||
};
|
||||
|
||||
export { convertToMarkdown };
|
||||
@@ -1,148 +0,0 @@
|
||||
// @flow
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* marked-toc <https://github.com/jonschlinkert/marked-toc>
|
||||
*
|
||||
* Copyright (c) 2014 Jon Schlinkert, contributors.
|
||||
* Licensed under the MIT license.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var marked = require('marked');
|
||||
var _ = require('lodash');
|
||||
var utils = require('./utils');
|
||||
|
||||
/**
|
||||
* Expose `toc`
|
||||
*/
|
||||
|
||||
module.exports = toc;
|
||||
|
||||
/**
|
||||
* Default template to use for generating
|
||||
* a table of contents.
|
||||
*/
|
||||
|
||||
var defaultTemplate =
|
||||
'<%= depth %><%= bullet %>[<%= heading %>](#<%= url %>)\n';
|
||||
|
||||
/**
|
||||
* Create the table of contents object that
|
||||
* will be used as context for the template.
|
||||
*
|
||||
* @param {String} `str`
|
||||
* @param {Object} `options`
|
||||
* @return {Object}
|
||||
*/
|
||||
|
||||
function generate(str, options) {
|
||||
var opts = _.extend(
|
||||
{
|
||||
firsth1: false,
|
||||
blacklist: true,
|
||||
omit: [],
|
||||
maxDepth: 3,
|
||||
slugify: function(text) {
|
||||
return text; // Override this!
|
||||
},
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
var toc = '';
|
||||
// $FlowIssue invalid flow-typed
|
||||
var tokens = marked.lexer(str);
|
||||
var tocArray = [];
|
||||
|
||||
// Remove the very first h1, true by default
|
||||
if (opts.firsth1 === false) {
|
||||
tokens.shift();
|
||||
}
|
||||
|
||||
// Do any h1's still exist?
|
||||
var h1 = _.some(tokens, { depth: 1 });
|
||||
|
||||
tokens
|
||||
.filter(function(token) {
|
||||
// Filter out everything but headings
|
||||
if (token.type !== 'heading' || token.type === 'code') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Since we removed the first h1, we'll check to see if other h1's
|
||||
// exist. If none exist, then we unindent the rest of the TOC
|
||||
if (!h1) {
|
||||
token.depth = token.depth - 1;
|
||||
}
|
||||
|
||||
// Store original text and create an id for linking
|
||||
token.heading = opts.strip ? utils.strip(token.text, opts) : token.text;
|
||||
|
||||
// Create a "slugified" id for linking
|
||||
token.id = opts.slugify(token.text);
|
||||
|
||||
// Omit headings with these strings
|
||||
var omissions = ['Table of Contents', 'TOC', 'TABLE OF CONTENTS'];
|
||||
var omit = _.union([], opts.omit, omissions);
|
||||
|
||||
if (utils.isMatch(omit, token.heading)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.forEach(function(h) {
|
||||
if (h.depth > opts.maxDepth) {
|
||||
return;
|
||||
}
|
||||
|
||||
var bullet = Array.isArray(opts.bullet)
|
||||
? opts.bullet[(h.depth - 1) % opts.bullet.length]
|
||||
: opts.bullet;
|
||||
|
||||
var data = _.extend({}, opts.data, {
|
||||
depth: new Array((h.depth - 1) * 2 + 1).join(' '),
|
||||
bullet: bullet ? bullet : '* ',
|
||||
heading: h.heading,
|
||||
url: h.id,
|
||||
});
|
||||
|
||||
tocArray.push(data);
|
||||
toc += _.template(opts.template || defaultTemplate)(data);
|
||||
});
|
||||
|
||||
return {
|
||||
data: tocArray,
|
||||
toc: opts.strip ? utils.strip(toc, opts) : toc,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* toc
|
||||
*/
|
||||
|
||||
function toc(str: string, options: Object) {
|
||||
return generate(str, options).toc;
|
||||
}
|
||||
|
||||
toc.raw = function(str, options) {
|
||||
return generate(str, options);
|
||||
};
|
||||
|
||||
toc.insert = function(content, options) {
|
||||
var start = '<!-- toc -->';
|
||||
var stop = '<!-- tocstop -->';
|
||||
var re = /<!-- toc -->([\s\S]+?)<!-- tocstop -->/;
|
||||
|
||||
// remove the existing TOC
|
||||
content = content.replace(re, start);
|
||||
|
||||
// generate new TOC
|
||||
var newtoc =
|
||||
'\n\n' + start + '\n\n' + toc(content, options) + '\n' + stop + '\n';
|
||||
|
||||
// If front-matter existed, put it back
|
||||
return content.replace(start, newtoc);
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
/* eslint-disable */
|
||||
|
||||
/*!
|
||||
* marked-toc <https://github.com/jonschlinkert/marked-toc>
|
||||
*
|
||||
* Copyright (c) 2014 Jon Schlinkert, contributors.
|
||||
* Licensed under the MIT license.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var _ = require('lodash');
|
||||
var utils = (module.exports = {});
|
||||
|
||||
utils.arrayify = function(arr) {
|
||||
return !Array.isArray(arr) ? [arr] : arr;
|
||||
};
|
||||
|
||||
utils.escapeRegex = function(re) {
|
||||
return re.replace(/(\[|\]|\(|\)|\/|\.|\^|\$|\*|\+|\?)/g, '\\$1');
|
||||
};
|
||||
|
||||
utils.isDest = function(dest) {
|
||||
return !dest || dest === 'undefined' || typeof dest === 'object';
|
||||
};
|
||||
|
||||
utils.isMatch = function(keys, str) {
|
||||
keys = utils.arrayify(keys);
|
||||
keys = keys.length > 0 ? keys.join('|') : '.*';
|
||||
|
||||
// Escape certain characters, like '[', '('
|
||||
var k = utils.escapeRegex(String(keys));
|
||||
|
||||
// Build up the regex to use for replacement patterns
|
||||
var re = new RegExp('(?:' + k + ')', 'g');
|
||||
if (String(str).match(re)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
utils.sanitize = function(src) {
|
||||
src = src.replace(/(\s*\[!|(?:\[.+ →\]\()).+/g, '');
|
||||
src = src.replace(/\s*\*\s*\[\].+/g, '');
|
||||
return src;
|
||||
};
|
||||
|
||||
utils.slugify = function(str) {
|
||||
str = str.replace(/\/\//g, '-');
|
||||
str = str.replace(/\//g, '-');
|
||||
str = str.replace(/\./g, '-');
|
||||
str = _.str.slugify(str);
|
||||
str = str.replace(/^-/, '');
|
||||
str = str.replace(/-$/, '');
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Strip certain words from headings. These can be
|
||||
* overridden. Might seem strange but it makes
|
||||
* sense in context.
|
||||
*/
|
||||
|
||||
var omit = [
|
||||
'grunt',
|
||||
'helper',
|
||||
'handlebars-helper',
|
||||
'mixin',
|
||||
'filter',
|
||||
'assemble-contrib',
|
||||
'assemble',
|
||||
];
|
||||
|
||||
utils.strip = function(name, options) {
|
||||
var opts = _.extend({}, options);
|
||||
if (opts.omit === false) {
|
||||
omit = [];
|
||||
}
|
||||
var exclusions = _.union(omit, utils.arrayify(opts.strip || []));
|
||||
var re = new RegExp('^(?:' + exclusions.join('|') + ')[-_]?', 'g');
|
||||
return name.replace(re, '');
|
||||
};
|
||||
Reference in New Issue
Block a user