@atlaskit/editor-plugin-code-block 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -0
- package/LICENSE.md +13 -0
- package/README.md +30 -0
- package/dist/cjs/actions.js +201 -0
- package/dist/cjs/ide-ux/bracket-handling.js +35 -0
- package/dist/cjs/ide-ux/commands.js +115 -0
- package/dist/cjs/ide-ux/line-handling.js +81 -0
- package/dist/cjs/ide-ux/paired-character-handling.js +23 -0
- package/dist/cjs/ide-ux/quote-handling.js +40 -0
- package/dist/cjs/index.js +13 -0
- package/dist/cjs/language-list.js +62 -0
- package/dist/cjs/nodeviews/code-block.js +153 -0
- package/dist/cjs/plugin-key.js +8 -0
- package/dist/cjs/plugin.js +120 -0
- package/dist/cjs/pm-plugins/actions.js +10 -0
- package/dist/cjs/pm-plugins/codeBlockCopySelectionPlugin.js +113 -0
- package/dist/cjs/pm-plugins/ide-ux.js +132 -0
- package/dist/cjs/pm-plugins/input-rule.js +68 -0
- package/dist/cjs/pm-plugins/keymaps.js +66 -0
- package/dist/cjs/pm-plugins/main-state.js +10 -0
- package/dist/cjs/pm-plugins/main.js +114 -0
- package/dist/cjs/refresh-browser-selection.js +29 -0
- package/dist/cjs/toolbar.js +131 -0
- package/dist/cjs/transform-to-code-block.js +84 -0
- package/dist/cjs/types.js +5 -0
- package/dist/cjs/ui/class-names.js +15 -0
- package/dist/cjs/utils.js +28 -0
- package/dist/es2019/actions.js +211 -0
- package/dist/es2019/ide-ux/bracket-handling.js +27 -0
- package/dist/es2019/ide-ux/commands.js +115 -0
- package/dist/es2019/ide-ux/line-handling.js +75 -0
- package/dist/es2019/ide-ux/paired-character-handling.js +12 -0
- package/dist/es2019/ide-ux/quote-handling.js +32 -0
- package/dist/es2019/index.js +1 -0
- package/dist/es2019/language-list.js +51 -0
- package/dist/es2019/nodeviews/code-block.js +126 -0
- package/dist/es2019/plugin-key.js +2 -0
- package/dist/es2019/plugin.js +104 -0
- package/dist/es2019/pm-plugins/actions.js +4 -0
- package/dist/es2019/pm-plugins/codeBlockCopySelectionPlugin.js +101 -0
- package/dist/es2019/pm-plugins/ide-ux.js +135 -0
- package/dist/es2019/pm-plugins/input-rule.js +60 -0
- package/dist/es2019/pm-plugins/keymaps.js +58 -0
- package/dist/es2019/pm-plugins/main-state.js +2 -0
- package/dist/es2019/pm-plugins/main.js +102 -0
- package/dist/es2019/refresh-browser-selection.js +25 -0
- package/dist/es2019/toolbar.js +108 -0
- package/dist/es2019/transform-to-code-block.js +79 -0
- package/dist/es2019/types.js +1 -0
- package/dist/es2019/ui/class-names.js +9 -0
- package/dist/es2019/utils.js +4 -0
- package/dist/esm/actions.js +191 -0
- package/dist/esm/ide-ux/bracket-handling.js +29 -0
- package/dist/esm/ide-ux/commands.js +107 -0
- package/dist/esm/ide-ux/line-handling.js +73 -0
- package/dist/esm/ide-ux/paired-character-handling.js +16 -0
- package/dist/esm/ide-ux/quote-handling.js +34 -0
- package/dist/esm/index.js +1 -0
- package/dist/esm/language-list.js +52 -0
- package/dist/esm/nodeviews/code-block.js +146 -0
- package/dist/esm/plugin-key.js +2 -0
- package/dist/esm/plugin.js +113 -0
- package/dist/esm/pm-plugins/actions.js +4 -0
- package/dist/esm/pm-plugins/codeBlockCopySelectionPlugin.js +103 -0
- package/dist/esm/pm-plugins/ide-ux.js +126 -0
- package/dist/esm/pm-plugins/input-rule.js +62 -0
- package/dist/esm/pm-plugins/keymaps.js +59 -0
- package/dist/esm/pm-plugins/main-state.js +4 -0
- package/dist/esm/pm-plugins/main.js +107 -0
- package/dist/esm/refresh-browser-selection.js +25 -0
- package/dist/esm/toolbar.js +121 -0
- package/dist/esm/transform-to-code-block.js +77 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/ui/class-names.js +9 -0
- package/dist/esm/utils.js +4 -0
- package/dist/types/actions.d.ts +18 -0
- package/dist/types/ide-ux/bracket-handling.d.ts +12 -0
- package/dist/types/ide-ux/commands.d.ts +7 -0
- package/dist/types/ide-ux/line-handling.d.ts +25 -0
- package/dist/types/ide-ux/paired-character-handling.d.ts +2 -0
- package/dist/types/ide-ux/quote-handling.d.ts +12 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/language-list.d.ts +942 -0
- package/dist/types/nodeviews/code-block.d.ts +21 -0
- package/dist/types/plugin-key.d.ts +2 -0
- package/dist/types/plugin.d.ts +19 -0
- package/dist/types/pm-plugins/actions.d.ts +4 -0
- package/dist/types/pm-plugins/codeBlockCopySelectionPlugin.d.ts +11 -0
- package/dist/types/pm-plugins/ide-ux.d.ts +5 -0
- package/dist/types/pm-plugins/input-rule.d.ts +3 -0
- package/dist/types/pm-plugins/keymaps.d.ts +4 -0
- package/dist/types/pm-plugins/main-state.d.ts +8 -0
- package/dist/types/pm-plugins/main.d.ts +10 -0
- package/dist/types/refresh-browser-selection.d.ts +5 -0
- package/dist/types/toolbar.d.ts +14 -0
- package/dist/types/transform-to-code-block.d.ts +3 -0
- package/dist/types/types.d.ts +6 -0
- package/dist/types/ui/class-names.d.ts +8 -0
- package/dist/types/utils.d.ts +4 -0
- package/dist/types-ts4.5/actions.d.ts +18 -0
- package/dist/types-ts4.5/ide-ux/bracket-handling.d.ts +12 -0
- package/dist/types-ts4.5/ide-ux/commands.d.ts +7 -0
- package/dist/types-ts4.5/ide-ux/line-handling.d.ts +25 -0
- package/dist/types-ts4.5/ide-ux/paired-character-handling.d.ts +2 -0
- package/dist/types-ts4.5/ide-ux/quote-handling.d.ts +12 -0
- package/dist/types-ts4.5/index.d.ts +3 -0
- package/dist/types-ts4.5/language-list.d.ts +1641 -0
- package/dist/types-ts4.5/nodeviews/code-block.d.ts +21 -0
- package/dist/types-ts4.5/plugin-key.d.ts +2 -0
- package/dist/types-ts4.5/plugin.d.ts +19 -0
- package/dist/types-ts4.5/pm-plugins/actions.d.ts +4 -0
- package/dist/types-ts4.5/pm-plugins/codeBlockCopySelectionPlugin.d.ts +14 -0
- package/dist/types-ts4.5/pm-plugins/ide-ux.d.ts +5 -0
- package/dist/types-ts4.5/pm-plugins/input-rule.d.ts +3 -0
- package/dist/types-ts4.5/pm-plugins/keymaps.d.ts +4 -0
- package/dist/types-ts4.5/pm-plugins/main-state.d.ts +8 -0
- package/dist/types-ts4.5/pm-plugins/main.d.ts +10 -0
- package/dist/types-ts4.5/refresh-browser-selection.d.ts +5 -0
- package/dist/types-ts4.5/toolbar.d.ts +14 -0
- package/dist/types-ts4.5/transform-to-code-block.d.ts +3 -0
- package/dist/types-ts4.5/types.d.ts +6 -0
- package/dist/types-ts4.5/ui/class-names.d.ts +8 -0
- package/dist/types-ts4.5/utils.d.ts +4 -0
- package/package.json +99 -0
- package/report.api.md +73 -0
- package/tmp/api-report-tmp.d.ts +45 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
2
|
+
import { filterCommand as filter } from '@atlaskit/editor-common/utils';
|
|
3
|
+
import { keydownHandler } from '@atlaskit/editor-prosemirror/keymap';
|
|
4
|
+
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
5
|
+
import { setTextSelection } from '@atlaskit/editor-prosemirror/utils';
|
|
6
|
+
import { getAutoClosingBracketInfo, shouldAutoCloseBracket } from '../ide-ux/bracket-handling';
|
|
7
|
+
import { indent, insertIndent, insertNewlineWithIndent, outdent } from '../ide-ux/commands';
|
|
8
|
+
import { getEndOfCurrentLine, getLineInfo, getStartOfCurrentLine, isCursorInsideCodeBlock, isSelectionEntirelyInsideCodeBlock } from '../ide-ux/line-handling';
|
|
9
|
+
import { isClosingCharacter, isCursorBeforeClosingCharacter } from '../ide-ux/paired-character-handling';
|
|
10
|
+
import { getAutoClosingQuoteInfo, shouldAutoCloseQuote } from '../ide-ux/quote-handling';
|
|
11
|
+
import { getCursor } from '../utils';
|
|
12
|
+
const ideUX = pluginInjectionApi => {
|
|
13
|
+
var _pluginInjectionApi$a;
|
|
14
|
+
const editorAnalyticsAPI = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions;
|
|
15
|
+
return new SafePlugin({
|
|
16
|
+
props: {
|
|
17
|
+
handleTextInput(view, from, to, text) {
|
|
18
|
+
var _pluginInjectionApi$c;
|
|
19
|
+
const {
|
|
20
|
+
state,
|
|
21
|
+
dispatch
|
|
22
|
+
} = view;
|
|
23
|
+
const compositionPluginState = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c = pluginInjectionApi.composition) === null || _pluginInjectionApi$c === void 0 ? void 0 : _pluginInjectionApi$c.sharedState.currentState();
|
|
24
|
+
if (isCursorInsideCodeBlock(state) && !(compositionPluginState !== null && compositionPluginState !== void 0 && compositionPluginState.isComposing)) {
|
|
25
|
+
const beforeText = getStartOfCurrentLine(state).text;
|
|
26
|
+
const afterText = getEndOfCurrentLine(state).text;
|
|
27
|
+
|
|
28
|
+
// If text is a closing bracket/quote and we've already inserted it, move the selection after
|
|
29
|
+
if (isCursorBeforeClosingCharacter(afterText) && isClosingCharacter(text) && afterText.startsWith(text)) {
|
|
30
|
+
dispatch(setTextSelection(to + text.length)(state.tr));
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Automatically add right-hand side bracket when user types the left bracket
|
|
35
|
+
if (shouldAutoCloseBracket(beforeText, afterText)) {
|
|
36
|
+
const {
|
|
37
|
+
left,
|
|
38
|
+
right
|
|
39
|
+
} = getAutoClosingBracketInfo(beforeText + text, afterText);
|
|
40
|
+
if (left && right) {
|
|
41
|
+
const bracketPair = state.schema.text(text + right);
|
|
42
|
+
let tr = state.tr.replaceWith(from, to, bracketPair);
|
|
43
|
+
dispatch(setTextSelection(from + text.length)(tr));
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Automatically add closing quote when user types a starting quote
|
|
49
|
+
if (shouldAutoCloseQuote(beforeText, afterText)) {
|
|
50
|
+
const {
|
|
51
|
+
left: leftQuote,
|
|
52
|
+
right: rightQuote
|
|
53
|
+
} = getAutoClosingQuoteInfo(beforeText + text, afterText);
|
|
54
|
+
if (leftQuote && rightQuote) {
|
|
55
|
+
const quotePair = state.schema.text(text + rightQuote);
|
|
56
|
+
let tr = state.tr.replaceWith(from, to, quotePair);
|
|
57
|
+
dispatch(setTextSelection(from + text.length)(tr));
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
},
|
|
64
|
+
handleKeyDown: keydownHandler({
|
|
65
|
+
Backspace: (state, dispatch) => {
|
|
66
|
+
if (isCursorInsideCodeBlock(state)) {
|
|
67
|
+
const $cursor = getCursor(state.selection);
|
|
68
|
+
const beforeText = getStartOfCurrentLine(state).text;
|
|
69
|
+
const afterText = getEndOfCurrentLine(state).text;
|
|
70
|
+
const {
|
|
71
|
+
left: leftBracket,
|
|
72
|
+
right: rightBracket,
|
|
73
|
+
hasTrailingMatchingBracket
|
|
74
|
+
} = getAutoClosingBracketInfo(beforeText, afterText);
|
|
75
|
+
if (leftBracket && rightBracket && hasTrailingMatchingBracket && dispatch) {
|
|
76
|
+
dispatch(state.tr.delete($cursor.pos - leftBracket.length, $cursor.pos + rightBracket.length));
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
const {
|
|
80
|
+
left: leftQuote,
|
|
81
|
+
right: rightQuote,
|
|
82
|
+
hasTrailingMatchingQuote
|
|
83
|
+
} = getAutoClosingQuoteInfo(beforeText, afterText);
|
|
84
|
+
if (leftQuote && rightQuote && hasTrailingMatchingQuote && dispatch) {
|
|
85
|
+
dispatch(state.tr.delete($cursor.pos - leftQuote.length, $cursor.pos + rightQuote.length));
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
const {
|
|
89
|
+
indentToken: {
|
|
90
|
+
size,
|
|
91
|
+
token
|
|
92
|
+
},
|
|
93
|
+
indentText
|
|
94
|
+
} = getLineInfo(beforeText);
|
|
95
|
+
if (beforeText === indentText) {
|
|
96
|
+
if (indentText.endsWith(token.repeat(size)) && dispatch) {
|
|
97
|
+
dispatch(state.tr.delete($cursor.pos - (size - indentText.length % size || size), $cursor.pos));
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
},
|
|
104
|
+
Enter: filter(isSelectionEntirelyInsideCodeBlock, insertNewlineWithIndent),
|
|
105
|
+
'Mod-]': filter(isSelectionEntirelyInsideCodeBlock, indent(editorAnalyticsAPI)),
|
|
106
|
+
'Mod-[': filter(isSelectionEntirelyInsideCodeBlock, outdent(editorAnalyticsAPI)),
|
|
107
|
+
Tab: filter(isSelectionEntirelyInsideCodeBlock, (state, dispatch) => {
|
|
108
|
+
if (!dispatch) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (isCursorInsideCodeBlock(state)) {
|
|
112
|
+
return insertIndent(state, dispatch);
|
|
113
|
+
}
|
|
114
|
+
return indent(editorAnalyticsAPI)(state, dispatch);
|
|
115
|
+
}),
|
|
116
|
+
'Shift-Tab': filter(isSelectionEntirelyInsideCodeBlock, outdent(editorAnalyticsAPI)),
|
|
117
|
+
'Mod-a': (state, dispatch) => {
|
|
118
|
+
if (isSelectionEntirelyInsideCodeBlock(state)) {
|
|
119
|
+
const {
|
|
120
|
+
$from,
|
|
121
|
+
$to
|
|
122
|
+
} = state.selection;
|
|
123
|
+
const isFullCodeBlockSelection = $from.parentOffset === 0 && $to.parentOffset === $to.parent.nodeSize - 2;
|
|
124
|
+
if (!isFullCodeBlockSelection && dispatch) {
|
|
125
|
+
dispatch(state.tr.setSelection(TextSelection.create(state.doc, $from.start(), $to.end())));
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
export default ideUX;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
|
|
2
|
+
import { insertBlock } from '@atlaskit/editor-common/commands';
|
|
3
|
+
import { inputRuleWithAnalytics } from '@atlaskit/editor-common/utils';
|
|
4
|
+
import { safeInsert } from '@atlaskit/editor-prosemirror/utils';
|
|
5
|
+
import { createPlugin, createRule, leafNodeReplacementCharacter } from '@atlaskit/prosemirror-input-rules';
|
|
6
|
+
import { isConvertableToCodeBlock, transformToCodeBlockAction } from '../transform-to-code-block';
|
|
7
|
+
export function createCodeBlockInputRule(schema, editorAnalyticsAPI) {
|
|
8
|
+
const rules = getCodeBlockRules(editorAnalyticsAPI, schema);
|
|
9
|
+
return createPlugin('code-block-input-rule', rules, {
|
|
10
|
+
isBlockNodeRule: true
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get all code block input rules
|
|
16
|
+
*
|
|
17
|
+
* @param {Schema} schema
|
|
18
|
+
* @returns {InputRuleWithHandler[]}
|
|
19
|
+
*/
|
|
20
|
+
function getCodeBlockRules(editorAnalyticsAPI, schema) {
|
|
21
|
+
const ruleAnalytics = inputRuleWithAnalytics({
|
|
22
|
+
action: ACTION.INSERTED,
|
|
23
|
+
actionSubject: ACTION_SUBJECT.DOCUMENT,
|
|
24
|
+
actionSubjectId: ACTION_SUBJECT_ID.CODE_BLOCK,
|
|
25
|
+
attributes: {
|
|
26
|
+
inputMethod: INPUT_METHOD.FORMATTING
|
|
27
|
+
},
|
|
28
|
+
eventType: EVENT_TYPE.TRACK
|
|
29
|
+
}, editorAnalyticsAPI);
|
|
30
|
+
const validMatchLength = match => match.length > 0 && match[0].length === 3;
|
|
31
|
+
const threeTildeRule = createRule(/(?!\s)(`{3,})$/, (state, match, start, end) => {
|
|
32
|
+
if (!validMatchLength(match)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const attributes = {};
|
|
36
|
+
if (match[4]) {
|
|
37
|
+
attributes.language = match[4];
|
|
38
|
+
}
|
|
39
|
+
if (isConvertableToCodeBlock(state)) {
|
|
40
|
+
return transformToCodeBlockAction(state, start, attributes);
|
|
41
|
+
}
|
|
42
|
+
const tr = state.tr;
|
|
43
|
+
tr.delete(start, end);
|
|
44
|
+
const codeBlock = tr.doc.type.schema.nodes.codeBlock.createChecked();
|
|
45
|
+
safeInsert(codeBlock)(tr);
|
|
46
|
+
return tr;
|
|
47
|
+
});
|
|
48
|
+
const leftNodeReplacementThreeTildeRule = createRule(new RegExp(`((${leafNodeReplacementCharacter}\`{3,})|^\\s(\`{3,}))(\\S*)$`), (state, match, start, end) => {
|
|
49
|
+
if (!validMatchLength(match)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
const attributes = {};
|
|
53
|
+
if (match[4]) {
|
|
54
|
+
attributes.language = match[4];
|
|
55
|
+
}
|
|
56
|
+
const inlineStart = Math.max(match.index + state.selection.$from.start(), 1);
|
|
57
|
+
return insertBlock(state, schema.nodes.codeBlock, inlineStart, end, attributes);
|
|
58
|
+
});
|
|
59
|
+
return [ruleAnalytics(threeTildeRule), ruleAnalytics(leftNodeReplacementThreeTildeRule)];
|
|
60
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { isEmptyNode } from '@atlaskit/editor-common/utils';
|
|
2
|
+
import { keymap } from '@atlaskit/editor-prosemirror/keymap';
|
|
3
|
+
import { Selection } from '@atlaskit/editor-prosemirror/state';
|
|
4
|
+
import { findParentNodeOfTypeClosestToPos, hasParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
|
|
5
|
+
import { getCursor } from '../utils';
|
|
6
|
+
const deleteCurrentItem = $from => tr => {
|
|
7
|
+
return tr.delete($from.before($from.depth), $from.after($from.depth));
|
|
8
|
+
};
|
|
9
|
+
const setTextSelection = pos => tr => {
|
|
10
|
+
const newSelection = Selection.findFrom(tr.doc.resolve(pos), -1, true);
|
|
11
|
+
if (newSelection) {
|
|
12
|
+
tr.setSelection(newSelection);
|
|
13
|
+
}
|
|
14
|
+
return tr;
|
|
15
|
+
};
|
|
16
|
+
export function keymapPlugin(schema) {
|
|
17
|
+
return keymap({
|
|
18
|
+
Backspace: (state, dispatch) => {
|
|
19
|
+
const $cursor = getCursor(state.selection);
|
|
20
|
+
const {
|
|
21
|
+
paragraph,
|
|
22
|
+
codeBlock,
|
|
23
|
+
listItem,
|
|
24
|
+
table,
|
|
25
|
+
layoutColumn
|
|
26
|
+
} = state.schema.nodes;
|
|
27
|
+
if (!$cursor || $cursor.parent.type !== codeBlock || !dispatch) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if ($cursor.pos === 1 || hasParentNodeOfType(listItem)(state.selection) && $cursor.parentOffset === 0) {
|
|
31
|
+
const node = findParentNodeOfTypeClosestToPos($cursor, codeBlock);
|
|
32
|
+
if (!node) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
dispatch(state.tr.setNodeMarkup(node.pos, node.node.type, node.node.attrs, []).setBlockType($cursor.pos, $cursor.pos, paragraph));
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if ($cursor.node && isEmptyNode(schema)($cursor.node()) && (hasParentNodeOfType(layoutColumn)(state.selection) || hasParentNodeOfType(table)(state.selection))) {
|
|
39
|
+
const {
|
|
40
|
+
tr
|
|
41
|
+
} = state;
|
|
42
|
+
const insertPos = $cursor.pos;
|
|
43
|
+
deleteCurrentItem($cursor)(tr);
|
|
44
|
+
setTextSelection(insertPos)(tr);
|
|
45
|
+
dispatch(tr.scrollIntoView());
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle not nested empty code block
|
|
50
|
+
if (isEmptyNode(schema)($cursor.node())) {
|
|
51
|
+
dispatch(deleteCurrentItem($cursor)(state === null || state === void 0 ? void 0 : state.tr));
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export default keymapPlugin;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
|
|
2
|
+
import { createSelectionClickHandler } from '@atlaskit/editor-common/selection';
|
|
3
|
+
import { browser } from '@atlaskit/editor-common/utils';
|
|
4
|
+
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
|
|
5
|
+
import { ignoreFollowingMutations, resetShouldIgnoreFollowingMutations } from '../actions';
|
|
6
|
+
import { codeBlockNodeView } from '../nodeviews/code-block';
|
|
7
|
+
import { pluginKey } from '../plugin-key';
|
|
8
|
+
import { codeBlockClassNames } from '../ui/class-names';
|
|
9
|
+
import { findCodeBlock } from '../utils';
|
|
10
|
+
import { ACTIONS } from './actions';
|
|
11
|
+
export const createPlugin = ({
|
|
12
|
+
useLongPressSelection = false,
|
|
13
|
+
getIntl,
|
|
14
|
+
appearance,
|
|
15
|
+
allowCompositionInputOverride = false
|
|
16
|
+
}) => {
|
|
17
|
+
const handleDOMEvents = {};
|
|
18
|
+
|
|
19
|
+
// ME-1599: Composition on mobile was causing the DOM observer to mutate the code block
|
|
20
|
+
// incorrecly and lose content when pressing enter in the middle of a code block line.
|
|
21
|
+
if (allowCompositionInputOverride) {
|
|
22
|
+
handleDOMEvents.beforeinput = (view, event) => {
|
|
23
|
+
const keyEvent = event;
|
|
24
|
+
const eventInputType = keyEvent.inputType;
|
|
25
|
+
const eventText = keyEvent.data;
|
|
26
|
+
if (browser.ios && event.composed &&
|
|
27
|
+
// insertParagraph will be the input type when the enter key is pressed.
|
|
28
|
+
eventInputType === 'insertParagraph' && findCodeBlock(view.state, view.state.selection)) {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
return true;
|
|
31
|
+
} else if (browser.android && event.composed && eventInputType === 'insertCompositionText' && eventText[(eventText === null || eventText === void 0 ? void 0 : eventText.length) - 1] === '\n' && findCodeBlock(view.state, view.state.selection)) {
|
|
32
|
+
const resultingText = event.target.outerText + '\n';
|
|
33
|
+
if (resultingText.endsWith(eventText)) {
|
|
34
|
+
// End of paragraph
|
|
35
|
+
setTimeout(() => {
|
|
36
|
+
view.someProp('handleKeyDown', f => f(view, new KeyboardEvent('keydown', {
|
|
37
|
+
bubbles: true,
|
|
38
|
+
cancelable: true,
|
|
39
|
+
key: 'Enter',
|
|
40
|
+
code: 'Enter'
|
|
41
|
+
})));
|
|
42
|
+
}, 0);
|
|
43
|
+
} else {
|
|
44
|
+
// Middle of paragraph, end of line
|
|
45
|
+
ignoreFollowingMutations(view.state, view.dispatch);
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (browser.android) {
|
|
50
|
+
resetShouldIgnoreFollowingMutations(view.state, view.dispatch);
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return new SafePlugin({
|
|
56
|
+
state: {
|
|
57
|
+
init(_, state) {
|
|
58
|
+
const node = findCodeBlock(state, state.selection);
|
|
59
|
+
return {
|
|
60
|
+
pos: node ? node.pos : null,
|
|
61
|
+
contentCopied: false,
|
|
62
|
+
isNodeSelected: false,
|
|
63
|
+
shouldIgnoreFollowingMutations: false
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
apply(tr, pluginState, _oldState, newState) {
|
|
67
|
+
if (tr.docChanged || tr.selectionSet) {
|
|
68
|
+
const node = findCodeBlock(newState, tr.selection);
|
|
69
|
+
const newPluginState = {
|
|
70
|
+
...pluginState,
|
|
71
|
+
pos: node ? node.pos : null,
|
|
72
|
+
isNodeSelected: tr.selection instanceof NodeSelection
|
|
73
|
+
};
|
|
74
|
+
return newPluginState;
|
|
75
|
+
}
|
|
76
|
+
const meta = tr.getMeta(pluginKey);
|
|
77
|
+
if ((meta === null || meta === void 0 ? void 0 : meta.type) === ACTIONS.SET_COPIED_TO_CLIPBOARD) {
|
|
78
|
+
return {
|
|
79
|
+
...pluginState,
|
|
80
|
+
contentCopied: meta.data
|
|
81
|
+
};
|
|
82
|
+
} else if ((meta === null || meta === void 0 ? void 0 : meta.type) === ACTIONS.SET_SHOULD_IGNORE_FOLLOWING_MUTATIONS) {
|
|
83
|
+
return {
|
|
84
|
+
...pluginState,
|
|
85
|
+
shouldIgnoreFollowingMutations: meta.data
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
return pluginState;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
key: pluginKey,
|
|
92
|
+
props: {
|
|
93
|
+
nodeViews: {
|
|
94
|
+
codeBlock: codeBlockNodeView
|
|
95
|
+
},
|
|
96
|
+
handleClickOn: createSelectionClickHandler(['codeBlock'], target => !!(target.closest(`.${codeBlockClassNames.gutter}`) || target.classList.contains(codeBlockClassNames.content)), {
|
|
97
|
+
useLongPressSelection
|
|
98
|
+
}),
|
|
99
|
+
handleDOMEvents
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { browser } from '@atlaskit/editor-common/utils';
|
|
2
|
+
import { pluginKey } from './plugin-key';
|
|
3
|
+
|
|
4
|
+
// Workaround for a firefox issue where dom selection is off sync
|
|
5
|
+
// https://product-fabric.atlassian.net/browse/ED-12442
|
|
6
|
+
const refreshBrowserSelection = () => {
|
|
7
|
+
const domSelection = window.getSelection();
|
|
8
|
+
if (domSelection) {
|
|
9
|
+
const domRange = domSelection && domSelection.rangeCount === 1 && domSelection.getRangeAt(0).cloneRange();
|
|
10
|
+
if (domRange) {
|
|
11
|
+
domSelection.removeAllRanges();
|
|
12
|
+
domSelection.addRange(domRange);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
const refreshBrowserSelectionOnChange = (transaction, editorState) => {
|
|
17
|
+
var _pluginKey$getState;
|
|
18
|
+
if (browser.gecko && transaction.docChanged &&
|
|
19
|
+
// codeblockState.pos should be set if current selection is in a codeblock.
|
|
20
|
+
typeof ((_pluginKey$getState = pluginKey.getState(editorState)) === null || _pluginKey$getState === void 0 ? void 0 : _pluginKey$getState.pos) === 'number') {
|
|
21
|
+
refreshBrowserSelection();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
export default refreshBrowserSelectionOnChange;
|
|
25
|
+
export { refreshBrowserSelection };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { defineMessages } from 'react-intl-next';
|
|
2
|
+
import commonMessages, { codeBlockButtonMessages } from '@atlaskit/editor-common/messages';
|
|
3
|
+
import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils';
|
|
4
|
+
import CopyIcon from '@atlaskit/icon/glyph/copy';
|
|
5
|
+
import RemoveIcon from '@atlaskit/icon/glyph/editor/remove';
|
|
6
|
+
import { changeLanguage, copyContentToClipboard, removeCodeBlock, resetCopiedState } from './actions';
|
|
7
|
+
import { createLanguageList, DEFAULT_LANGUAGES, getLanguageIdentifier } from './language-list';
|
|
8
|
+
import { pluginKey } from './plugin-key';
|
|
9
|
+
import { provideVisualFeedbackForCopyButton, removeVisualFeedbackForCopyButton } from './pm-plugins/codeBlockCopySelectionPlugin';
|
|
10
|
+
export const messages = defineMessages({
|
|
11
|
+
selectLanguage: {
|
|
12
|
+
id: 'fabric.editor.selectLanguage',
|
|
13
|
+
defaultMessage: 'Select language',
|
|
14
|
+
description: 'Code blocks display software code. A prompt to select the software language the code is written in.'
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
const languageList = createLanguageList(DEFAULT_LANGUAGES);
|
|
18
|
+
export const getToolbarConfig = (allowCopyToClipboard = false, api) => (state, {
|
|
19
|
+
formatMessage
|
|
20
|
+
}) => {
|
|
21
|
+
var _api$decorations$acti, _api$analytics, _codeBlockState$pos, _node$attrs;
|
|
22
|
+
const {
|
|
23
|
+
hoverDecoration
|
|
24
|
+
} = (_api$decorations$acti = api === null || api === void 0 ? void 0 : api.decorations.actions) !== null && _api$decorations$acti !== void 0 ? _api$decorations$acti : {};
|
|
25
|
+
const editorAnalyticsAPI = api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions;
|
|
26
|
+
const codeBlockState = pluginKey.getState(state);
|
|
27
|
+
const pos = (_codeBlockState$pos = codeBlockState === null || codeBlockState === void 0 ? void 0 : codeBlockState.pos) !== null && _codeBlockState$pos !== void 0 ? _codeBlockState$pos : null;
|
|
28
|
+
if (!codeBlockState || pos === null) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const node = state.doc.nodeAt(pos);
|
|
32
|
+
const nodeType = state.schema.nodes.codeBlock;
|
|
33
|
+
if ((node === null || node === void 0 ? void 0 : node.type) !== nodeType) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const language = node === null || node === void 0 ? void 0 : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.language;
|
|
37
|
+
const options = languageList.map(lang => ({
|
|
38
|
+
label: lang.name,
|
|
39
|
+
value: getLanguageIdentifier(lang),
|
|
40
|
+
alias: lang.alias
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
// If language is not undefined search for it in the value and then search in the aliases
|
|
44
|
+
const defaultValue = language ? options.find(option => option.value === language) || options.find(option => option.alias.includes(language)) : null;
|
|
45
|
+
const languageSelect = {
|
|
46
|
+
id: 'editor.codeBlock.languageOptions',
|
|
47
|
+
type: 'select',
|
|
48
|
+
selectType: 'list',
|
|
49
|
+
onChange: option => changeLanguage(editorAnalyticsAPI)(option.value),
|
|
50
|
+
defaultValue,
|
|
51
|
+
placeholder: formatMessage(messages.selectLanguage),
|
|
52
|
+
options,
|
|
53
|
+
filterOption: languageListFilter
|
|
54
|
+
};
|
|
55
|
+
const separator = {
|
|
56
|
+
type: 'separator'
|
|
57
|
+
};
|
|
58
|
+
const copyToClipboardItems = !allowCopyToClipboard ? [] : [{
|
|
59
|
+
id: 'editor.codeBlock.copy',
|
|
60
|
+
type: 'button',
|
|
61
|
+
appearance: 'subtle',
|
|
62
|
+
icon: CopyIcon,
|
|
63
|
+
// note: copyContentToClipboard contains logic that also removes the
|
|
64
|
+
// visual feedback for the copy button
|
|
65
|
+
onClick: copyContentToClipboard,
|
|
66
|
+
title: formatMessage(codeBlockState.contentCopied ? codeBlockButtonMessages.copiedCodeToClipboard : codeBlockButtonMessages.copyCodeToClipboard),
|
|
67
|
+
onMouseEnter: provideVisualFeedbackForCopyButton,
|
|
68
|
+
// note: resetCopiedState contains logic that also removes the
|
|
69
|
+
// visual feedback for the copy button
|
|
70
|
+
onMouseLeave: resetCopiedState,
|
|
71
|
+
onFocus: provideVisualFeedbackForCopyButton,
|
|
72
|
+
onBlur: removeVisualFeedbackForCopyButton,
|
|
73
|
+
hideTooltipOnClick: false,
|
|
74
|
+
disabled: codeBlockState.isNodeSelected,
|
|
75
|
+
tabIndex: null
|
|
76
|
+
}, separator];
|
|
77
|
+
const deleteButton = {
|
|
78
|
+
id: 'editor.codeBlock.delete',
|
|
79
|
+
type: 'button',
|
|
80
|
+
appearance: 'danger',
|
|
81
|
+
icon: RemoveIcon,
|
|
82
|
+
onMouseEnter: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
|
|
83
|
+
onMouseLeave: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
|
|
84
|
+
onFocus: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, true),
|
|
85
|
+
onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(nodeType, false),
|
|
86
|
+
onClick: removeCodeBlock,
|
|
87
|
+
title: formatMessage(commonMessages.remove),
|
|
88
|
+
tabIndex: null
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
title: 'CodeBlock floating controls',
|
|
92
|
+
getDomRef: view => findDomRefAtPos(pos, view.domAtPos.bind(view)),
|
|
93
|
+
nodeType,
|
|
94
|
+
items: [languageSelect, separator, ...copyToClipboardItems, deleteButton],
|
|
95
|
+
scrollable: true
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Filters language list based on both name and alias properties.
|
|
101
|
+
*/
|
|
102
|
+
export const languageListFilter = (option, rawInput) => {
|
|
103
|
+
const {
|
|
104
|
+
data
|
|
105
|
+
} = option;
|
|
106
|
+
const searchString = rawInput.toLowerCase();
|
|
107
|
+
return data.label.toLowerCase().includes(searchString) || data.alias.some(alias => alias.toLowerCase() === searchString);
|
|
108
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { GapCursorSelection } from '@atlaskit/editor-common/selection';
|
|
2
|
+
import { mapSlice, timestampToString } from '@atlaskit/editor-common/utils';
|
|
3
|
+
import { Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
4
|
+
import { TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
5
|
+
export function transformToCodeBlockAction(state, start, attrs) {
|
|
6
|
+
const startOfCodeBlockText = state.selection.$from;
|
|
7
|
+
const endPosition = state.selection.empty && !(state.selection instanceof GapCursorSelection) ? startOfCodeBlockText.end() : state.selection.$to.pos;
|
|
8
|
+
const startLinePosition = startOfCodeBlockText.start();
|
|
9
|
+
//when cmd+A is used to select the content. start position should be 0.
|
|
10
|
+
const parentStartPosition = startOfCodeBlockText.depth === 0 ? 0 : startOfCodeBlockText.before();
|
|
11
|
+
const contentSlice = state.doc.slice(startOfCodeBlockText.pos, endPosition);
|
|
12
|
+
const codeBlockSlice = mapSlice(contentSlice, (node, parent, index) => {
|
|
13
|
+
if (node.type === state.schema.nodes.hardBreak) {
|
|
14
|
+
return state.schema.text('\n');
|
|
15
|
+
}
|
|
16
|
+
if (node.isText) {
|
|
17
|
+
return node.mark([]);
|
|
18
|
+
}
|
|
19
|
+
if (node.isInline) {
|
|
20
|
+
// Convert dates
|
|
21
|
+
if (node.attrs.timestamp) {
|
|
22
|
+
return state.schema.text(timestampToString(node.attrs.timestamp, null));
|
|
23
|
+
}
|
|
24
|
+
// Convert links
|
|
25
|
+
if (node.attrs.url) {
|
|
26
|
+
return state.schema.text(node.attrs.url);
|
|
27
|
+
}
|
|
28
|
+
return node.attrs.text ? state.schema.text(node.attrs.text) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// if the current node is the last child of the Slice exit early to prevent
|
|
32
|
+
// adding additional line breaks
|
|
33
|
+
if (contentSlice.content.childCount - 1 === index) {
|
|
34
|
+
return node.content;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//useful to decide whether to append line breaks when the content has list items.
|
|
38
|
+
const isParentLastChild = parent && contentSlice.content.childCount - 1 === index;
|
|
39
|
+
|
|
40
|
+
// add line breaks at the end of each paragraph to mimic layout of selected content
|
|
41
|
+
// do not add line breaks when the 'paragraph' parent is last child.
|
|
42
|
+
if (node.content.childCount && node.type === state.schema.nodes.paragraph && !isParentLastChild) {
|
|
43
|
+
return node.content.append(Fragment.from(state.schema.text('\n\n')));
|
|
44
|
+
}
|
|
45
|
+
return node.content.childCount ? node.content : null;
|
|
46
|
+
});
|
|
47
|
+
const tr = state.tr;
|
|
48
|
+
|
|
49
|
+
// Replace current block node
|
|
50
|
+
const startMapped = startLinePosition === start ? parentStartPosition : start;
|
|
51
|
+
const codeBlock = state.schema.nodes.codeBlock;
|
|
52
|
+
const codeBlockNode = codeBlock.createChecked(attrs, codeBlockSlice.content);
|
|
53
|
+
tr.replaceWith(startMapped, Math.min(endPosition, tr.doc.content.size), codeBlockNode);
|
|
54
|
+
|
|
55
|
+
// Reposition cursor when inserting into layouts or table headers
|
|
56
|
+
const mapped = tr.doc.resolve(tr.mapping.map(startMapped) + 1);
|
|
57
|
+
const selection = TextSelection.findFrom(mapped, state.selection instanceof GapCursorSelection ? -1 : 1, true);
|
|
58
|
+
if (selection) {
|
|
59
|
+
return tr.setSelection(selection);
|
|
60
|
+
}
|
|
61
|
+
return tr.setSelection(TextSelection.create(tr.doc, Math.min(start + startOfCodeBlockText.node().nodeSize - 1, tr.doc.content.size)));
|
|
62
|
+
}
|
|
63
|
+
export function isConvertableToCodeBlock(state) {
|
|
64
|
+
// Before a document is loaded, there is no selection.
|
|
65
|
+
if (!state.selection) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
const {
|
|
69
|
+
$from
|
|
70
|
+
} = state.selection;
|
|
71
|
+
const node = $from.parent;
|
|
72
|
+
if (!node.isTextblock || node.type === state.schema.nodes.codeBlock) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const parentDepth = $from.depth - 1;
|
|
76
|
+
const parentNode = $from.node(parentDepth);
|
|
77
|
+
const index = $from.index(parentDepth);
|
|
78
|
+
return parentNode.canReplaceWith(index, index + 1, state.schema.nodes.codeBlock);
|
|
79
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { CodeBlockSharedCssClassName } from '@atlaskit/editor-common/styles';
|
|
2
|
+
export const codeBlockClassNames = {
|
|
3
|
+
container: CodeBlockSharedCssClassName.CODEBLOCK_CONTAINER,
|
|
4
|
+
start: CodeBlockSharedCssClassName.CODEBLOCK_START,
|
|
5
|
+
end: CodeBlockSharedCssClassName.CODEBLOCK_END,
|
|
6
|
+
contentWrapper: CodeBlockSharedCssClassName.CODEBLOCK_CONTENT_WRAPPER,
|
|
7
|
+
gutter: CodeBlockSharedCssClassName.CODEBLOCK_LINE_NUMBER_GUTTER,
|
|
8
|
+
content: CodeBlockSharedCssClassName.CODEBLOCK_CONTENT
|
|
9
|
+
};
|