Chore: Update Slate to 0.47.8 (#19197)

* Chore: Update Slate to 0.47.8
Closes #17430
This commit is contained in:
kay delaney
2019-09-23 12:26:05 +01:00
committed by GitHub
parent 918cb78092
commit 68d6da77da
56 changed files with 1760 additions and 1412 deletions
@@ -149,7 +149,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => {
'emotion',
'prismjs',
'slate-plain-serializer',
'slate-react',
'@grafana/slate-react',
'react',
'react-dom',
'react-redux',
+5
View File
@@ -26,10 +26,12 @@
},
"dependencies": {
"@grafana/data": "^6.4.0-alpha",
"@grafana/slate-react": "0.22.9-grafana",
"@torkelo/react-select": "2.1.1",
"@types/react-color": "2.17.0",
"classnames": "2.2.6",
"d3": "5.9.1",
"immutable": "3.8.2",
"jquery": "3.4.1",
"lodash": "4.17.15",
"moment": "2.24.0",
@@ -45,6 +47,7 @@
"react-storybook-addon-props-combinations": "1.1.0",
"react-transition-group": "2.6.1",
"react-virtualized": "9.21.0",
"slate": "0.47.8",
"tinycolor2": "1.4.1"
},
"devDependencies": {
@@ -65,6 +68,8 @@
"@types/react-custom-scrollbars": "4.0.5",
"@types/react-test-renderer": "16.8.1",
"@types/react-transition-group": "2.0.16",
"@types/slate": "0.47.1",
"@types/slate-react": "0.22.5",
"@types/storybook__addon-actions": "3.4.2",
"@types/storybook__addon-info": "4.1.1",
"@types/storybook__addon-knobs": "4.0.4",
+4 -3
View File
@@ -1,6 +1,6 @@
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import sourceMaps from 'rollup-plugin-sourcemaps';
// import sourceMaps from 'rollup-plugin-sourcemaps';
import { terser } from 'rollup-plugin-terser';
const pkg = require('./package.json');
@@ -47,19 +47,20 @@ const buildCjsPackage = ({ env }) => {
],
'../../node_modules/react-color/lib/components/common': ['Saturation', 'Hue', 'Alpha'],
'../../node_modules/immutable/dist/immutable.js': [
'Record',
'Set',
'Map',
'List',
'OrderedSet',
'is',
'Stack',
'Record',
],
'node_modules/immutable/dist/immutable.js': ['Record', 'Set', 'Map', 'List', 'OrderedSet', 'is', 'Stack'],
'../../node_modules/esrever/esrever.js': ['reverse'],
},
}),
resolve(),
sourceMaps(),
// sourceMaps(),
env === 'production' && terser(),
],
};
@@ -11,7 +11,7 @@ interface DataLinkEditorProps {
isLast: boolean;
value: DataLink;
suggestions: VariableSuggestion[];
onChange: (index: number, link: DataLink) => void;
onChange: (index: number, link: DataLink, callback?: () => void) => void;
onRemove: (link: DataLink) => void;
}
@@ -20,8 +20,8 @@ export const DataLinkEditor: React.FC<DataLinkEditorProps> = React.memo(
const theme = useContext(ThemeContext);
const [title, setTitle] = useState(value.title);
const onUrlChange = (url: string) => {
onChange(index, { ...value, url });
const onUrlChange = (url: string, callback?: () => void) => {
onChange(index, { ...value, url }, callback);
};
const onTitleChange = (event: ChangeEvent<HTMLInputElement>) => {
setTitle(event.target.value);
@@ -1,46 +1,45 @@
import React, { useState, useMemo, useCallback, useContext } from 'react';
import React, { useState, useMemo, useCallback, useContext, useRef, RefObject } from 'react';
import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions';
import { makeValue, ThemeContext, DataLinkBuiltInVars } from '../../index';
import { ThemeContext, DataLinkBuiltInVars, makeValue } from '../../index';
import { SelectionReference } from './SelectionReference';
import { Portal } from '../index';
// @ts-ignore
import { Editor } from 'slate-react';
// @ts-ignore
import { Value, Change, Document } from 'slate';
// @ts-ignore
import { Editor } from '@grafana/slate-react';
import { Value, Editor as CoreEditor } from 'slate';
import Plain from 'slate-plain-serializer';
import { Popper as ReactPopper } from 'react-popper';
import useDebounce from 'react-use/lib/useDebounce';
import { css, cx } from 'emotion';
// @ts-ignore
import PluginPrism from 'slate-prism';
import { SlatePrism } from '../../slate-plugins';
import { SCHEMA } from '../../utils/slate';
const modulo = (a: number, n: number) => a - n * Math.floor(a / n);
interface DataLinkInputProps {
value: string;
onChange: (url: string) => void;
onChange: (url: string, callback?: () => void) => void;
suggestions: VariableSuggestion[];
}
const plugins = [
PluginPrism({
SlatePrism({
onlyIn: (node: any) => node.type === 'code_block',
getSyntax: () => 'links',
}),
];
export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, suggestions }) => {
const editorRef = useRef<Editor>() as RefObject<Editor>;
const theme = useContext(ThemeContext);
const [showingSuggestions, setShowingSuggestions] = useState(false);
const [suggestionsIndex, setSuggestionsIndex] = useState(0);
const [usedSuggestions, setUsedSuggestions] = useState(
suggestions.filter(suggestion => {
return value.indexOf(suggestion.value) > -1;
})
suggestions.filter(suggestion => value.includes(suggestion.value))
);
// Using any here as TS has problem pickung up `change` method existance on Value
// According to code and documentation `change` is an instance method on Value in slate 0.33.8 that we use
// https://github.com/ianstormtaylor/slate/blob/slate%400.33.8/docs/reference/slate/value.md#change
const [linkUrl, setLinkUrl] = useState<any>(makeValue(value));
const [linkUrl, setLinkUrl] = useState<Value>(makeValue(value));
const getStyles = useCallback(() => {
return {
@@ -56,22 +55,21 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
}, [theme]);
const currentSuggestions = useMemo(
() =>
suggestions.filter(suggestion => {
return usedSuggestions.map(s => s.value).indexOf(suggestion.value) === -1;
}),
() => suggestions.filter(suggestion => !usedSuggestions.map(s => s.value).includes(suggestion.value)),
[usedSuggestions, suggestions]
);
// Workaround for https://github.com/ianstormtaylor/slate/issues/2927
const stateRef = useRef({ showingSuggestions, currentSuggestions, suggestionsIndex, linkUrl, onChange });
stateRef.current = { showingSuggestions, currentSuggestions, suggestionsIndex, linkUrl, onChange };
// SelectionReference is used to position the variables suggestion relatively to current DOM selection
const selectionRef = useMemo(() => new SelectionReference(), [setShowingSuggestions]);
// Keep track of variables that has been used already
const updateUsedSuggestions = () => {
const currentLink = Plain.serialize(linkUrl);
const next = usedSuggestions.filter(suggestion => {
return currentLink.indexOf(suggestion.value) > -1;
});
const next = usedSuggestions.filter(suggestion => currentLink.includes(suggestion.value));
if (next.length !== usedSuggestions.length) {
setUsedSuggestions(next);
}
@@ -79,75 +77,64 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
useDebounce(updateUsedSuggestions, 250, [linkUrl]);
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Backspace' || event.key === 'Escape') {
setShowingSuggestions(false);
setSuggestionsIndex(0);
}
if (event.key === 'Enter') {
if (showingSuggestions) {
onVariableSelect(currentSuggestions[suggestionsIndex]);
const onKeyDown = React.useCallback((event: KeyboardEvent, next: () => any) => {
if (!stateRef.current.showingSuggestions) {
if (event.key === '?' || event.key === '&' || event.key === '$' || (event.keyCode === 32 && event.ctrlKey)) {
return setShowingSuggestions(true);
}
return next();
}
if (showingSuggestions) {
if (event.key === 'ArrowDown') {
switch (event.key) {
case 'Backspace':
case 'Escape':
setShowingSuggestions(false);
return setSuggestionsIndex(0);
case 'Enter':
event.preventDefault();
setSuggestionsIndex(index => {
return (index + 1) % currentSuggestions.length;
});
}
if (event.key === 'ArrowUp') {
return onVariableSelect(stateRef.current.currentSuggestions[stateRef.current.suggestionsIndex]);
case 'ArrowDown':
case 'ArrowUp':
event.preventDefault();
setSuggestionsIndex(index => {
const nextIndex = index - 1 < 0 ? currentSuggestions.length - 1 : (index - 1) % currentSuggestions.length;
return nextIndex;
});
}
}
const direction = event.key === 'ArrowDown' ? 1 : -1;
return setSuggestionsIndex(index => modulo(index + direction, stateRef.current.currentSuggestions.length));
if (event.key === '?' || event.key === '&' || event.key === '$' || (event.keyCode === 32 && event.ctrlKey)) {
setShowingSuggestions(true);
default:
return next();
}
}, []);
if (event.key === 'Enter' && showingSuggestions) {
// Preventing entering a new line
// As of https://github.com/ianstormtaylor/slate/issues/1345#issuecomment-340508289
return false;
} else {
// @ts-ignore
return;
}
};
const onUrlChange = ({ value }: Change) => {
const onUrlChange = React.useCallback(({ value }: { value: Value }) => {
setLinkUrl(value);
};
}, []);
const onUrlBlur = () => {
onChange(Plain.serialize(linkUrl));
};
const onUrlBlur = React.useCallback((event: Event, editor: CoreEditor, next: () => any) => {
// Callback needed for blur to work correctly
stateRef.current.onChange(Plain.serialize(stateRef.current.linkUrl), () => {
editorRef.current!.blur();
});
}, []);
const onVariableSelect = (item: VariableSuggestion) => {
const onVariableSelect = (item: VariableSuggestion, editor = editorRef.current!) => {
const includeDollarSign = Plain.serialize(linkUrl).slice(-1) !== '$';
const change = linkUrl.change();
if (item.origin !== VariableOrigin.Template || item.value === DataLinkBuiltInVars.includeVars) {
change.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
editor.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
} else {
change.insertText(`var-${item.value}=$\{${item.value}}`);
editor.insertText(`var-${item.value}=$\{${item.value}}`);
}
setLinkUrl(change.value);
setLinkUrl(editor.value);
setShowingSuggestions(false);
setUsedSuggestions((previous: VariableSuggestion[]) => {
return [...previous, item];
});
setSuggestionsIndex(0);
onChange(Plain.serialize(change.value));
onChange(Plain.serialize(editor.value));
};
return (
<div
className={cx(
@@ -186,11 +173,13 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = ({ value, onChange, s
</Portal>
)}
<Editor
schema={SCHEMA}
ref={editorRef}
placeholder="http://your-grafana.com/d/000000010/annotations"
value={linkUrl}
value={stateRef.current.linkUrl}
onChange={onUrlChange}
onBlur={onUrlBlur}
onKeyDown={onKeyDown}
onKeyDown={(event, _editor, next) => onKeyDown(event as KeyboardEvent, next)}
plugins={plugins}
className={getStyles().editor}
/>
@@ -12,7 +12,7 @@ import { VariableSuggestion } from './DataLinkSuggestions';
interface DataLinksEditorProps {
value: DataLink[];
onChange: (links: DataLink[]) => void;
onChange: (links: DataLink[], callback?: () => void) => void;
suggestions: VariableSuggestion[];
maxLinks?: number;
}
@@ -30,14 +30,15 @@ export const DataLinksEditor: FC<DataLinksEditorProps> = React.memo(({ value, on
onChange(value ? [...value, { url: '', title: '' }] : [{ url: '', title: '' }]);
};
const onLinkChanged = (linkIndex: number, newLink: DataLink) => {
const onLinkChanged = (linkIndex: number, newLink: DataLink, callback?: () => void) => {
onChange(
value.map((item, listIndex) => {
if (linkIndex === listIndex) {
return newLink;
}
return item;
})
}),
callback
);
};
+1
View File
@@ -2,3 +2,4 @@ export * from './components';
export * from './types';
export * from './utils';
export * from './themes';
export * from './slate-plugins';
@@ -0,0 +1 @@
export { SlatePrism } from './slate-prism';
@@ -0,0 +1,3 @@
const TOKEN_MARK = 'prism-token';
export default TOKEN_MARK;
@@ -0,0 +1,160 @@
import Prism from 'prismjs';
import { Block, Text, Decoration } from 'slate';
import { Plugin } from '@grafana/slate-react';
import Options, { OptionsFormat } from './options';
import TOKEN_MARK from './TOKEN_MARK';
/**
* A Slate plugin to highlight code syntax.
*/
export function SlatePrism(optsParam: OptionsFormat = {}): Plugin {
const opts: Options = new Options(optsParam);
return {
decorateNode: (node, editor, next) => {
if (!opts.onlyIn(node)) {
return next();
}
return decorateNode(opts, Block.create(node as Block));
},
renderDecoration: (props, editor, next) =>
opts.renderDecoration(
{
children: props.children,
decoration: props.decoration,
},
editor as any,
next
),
};
}
/**
* Returns the decoration for a node
*/
function decorateNode(opts: Options, block: Block) {
const grammarName = opts.getSyntax(block);
const grammar = Prism.languages[grammarName];
if (!grammar) {
// Grammar not loaded
return [];
}
// Tokenize the whole block text
const texts = block.getTexts();
const blockText = texts.map(text => text && text.getText()).join('\n');
const tokens = Prism.tokenize(blockText, grammar);
// The list of decorations to return
const decorations: Decoration[] = [];
let textStart = 0;
let textEnd = 0;
texts.forEach(text => {
textEnd = textStart + text!.getText().length;
let offset = 0;
function processToken(token: string | Prism.Token, accu?: string | number) {
if (typeof token === 'string') {
if (accu) {
const decoration = createDecoration({
text: text!,
textStart,
textEnd,
start: offset,
end: offset + token.length,
className: `prism-token token ${accu}`,
block,
});
if (decoration) {
decorations.push(decoration);
}
}
offset += token.length;
} else {
accu = `${accu} ${token.type} ${token.alias || ''}`;
if (typeof token.content === 'string') {
const decoration = createDecoration({
text: text!,
textStart,
textEnd,
start: offset,
end: offset + token.content.length,
className: `prism-token token ${accu}`,
block,
});
if (decoration) {
decorations.push(decoration);
}
offset += token.content.length;
} else {
// When using token.content instead of token.matchedStr, token can be deep
for (let i = 0; i < token.content.length; i += 1) {
// @ts-ignore
processToken(token.content[i], accu);
}
}
}
}
tokens.forEach(processToken);
textStart = textEnd + 1; // account for added `\n`
});
return decorations;
}
/**
* Return a decoration range for the given text.
*/
function createDecoration({
text,
textStart,
textEnd,
start,
end,
className,
block,
}: {
text: Text; // The text being decorated
textStart: number; // Its start position in the whole text
textEnd: number; // Its end position in the whole text
start: number; // The position in the whole text where the token starts
end: number; // The position in the whole text where the token ends
className: string; // The prism token classname
block: Block;
}): Decoration | null {
if (start >= textEnd || end <= textStart) {
// Ignore, the token is not in the text
return null;
}
// Shrink to this text boundaries
start = Math.max(start, textStart);
end = Math.min(end, textEnd);
// Now shift offsets to be relative to this text
start -= textStart;
end -= textStart;
const myDec = block.createDecoration({
object: 'decoration',
anchor: {
key: text.key,
offset: start,
object: 'point',
},
focus: {
key: text.key,
offset: end,
object: 'point',
},
type: TOKEN_MARK,
data: { className },
});
return myDec;
}
@@ -0,0 +1,77 @@
import React from 'react';
import { Mark, Node, Decoration } from 'slate';
import { Editor } from '@grafana/slate-react';
import { Record } from 'immutable';
import TOKEN_MARK from './TOKEN_MARK';
export interface OptionsFormat {
// Determine which node should be highlighted
onlyIn?: (node: Node) => boolean;
// Returns the syntax for a node that should be highlighted
getSyntax?: (node: Node) => string;
// Render a highlighting mark in a highlighted node
renderMark?: ({ mark, children }: { mark: Mark; children: React.ReactNode }) => void | React.ReactNode;
}
/**
* Default filter for code blocks
*/
function defaultOnlyIn(node: Node): boolean {
return node.object === 'block' && node.type === 'code_block';
}
/**
* Default getter for syntax
*/
function defaultGetSyntax(node: Node): string {
return 'javascript';
}
/**
* Default rendering for decorations
*/
function defaultRenderDecoration(
props: { children: React.ReactNode; decoration: Decoration },
editor: Editor,
next: () => any
): void | React.ReactNode {
const { decoration } = props;
if (decoration.type !== TOKEN_MARK) {
return next();
}
const className = decoration.data.get('className');
return <span className={className}>{props.children}</span>;
}
/**
* The plugin options
*/
class Options
extends Record({
onlyIn: defaultOnlyIn,
getSyntax: defaultGetSyntax,
renderDecoration: defaultRenderDecoration,
})
implements OptionsFormat {
readonly onlyIn!: (node: Node) => boolean;
readonly getSyntax!: (node: Node) => string;
readonly renderDecoration!: (
{
decoration,
children,
}: {
decoration: Decoration;
children: React.ReactNode;
},
editor: Editor,
next: () => any
) => void | React.ReactNode;
constructor(props: OptionsFormat) {
super(props);
}
}
export default Options;
+13 -14
View File
@@ -1,22 +1,22 @@
// @ts-ignore
import { Block, Document, Text, Value } from 'slate';
import { Block, Document, Text, Value, SchemaProperties } from 'slate';
const SCHEMA = {
blocks: {
paragraph: 'paragraph',
codeblock: 'code_block',
codeline: 'code_line',
export const SCHEMA: SchemaProperties = {
document: {
nodes: [
{
match: [{ type: 'paragraph' }, { type: 'code_block' }, { type: 'code_line' }],
},
],
},
inlines: {},
marks: {},
};
export const makeFragment = (text: string, syntax?: string) => {
export const makeFragment = (text: string, syntax?: string): Document => {
const lines = text.split('\n').map(line =>
Block.create({
type: 'code_line',
nodes: [Text.create(line)],
} as any)
})
);
const block = Block.create({
@@ -25,18 +25,17 @@ export const makeFragment = (text: string, syntax?: string) => {
},
type: 'code_block',
nodes: lines,
} as any);
});
return Document.create({
nodes: [block],
});
};
export const makeValue = (text: string, syntax?: string) => {
export const makeValue = (text: string, syntax?: string): Value => {
const fragment = makeFragment(text, syntax);
return Value.create({
document: fragment,
SCHEMA,
} as any);
});
};
+4
View File
@@ -5,6 +5,10 @@
"compilerOptions": {
"rootDirs": [".", "stories"],
"typeRoots": ["./node_modules/@types", "types"],
"baseUrl": "./node_modules/@types",
"paths": {
"@grafana/slate-react": ["slate-react"]
},
"declarationDir": "dist",
"outDir": "compiled"
}