-
-
Notifications
You must be signed in to change notification settings - Fork 191
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(plugins): add support for custom menu item
- Loading branch information
Showing
19 changed files
with
408 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import { exitCode } from 'prosemirror-commands'; | ||
import { undo, redo } from 'prosemirror-history'; | ||
import { TextSelection, Selection } from 'prosemirror-state'; | ||
import { Node as ProsemirrorNode } from 'prosemirror-model'; | ||
import { EditorView } from 'prosemirror-view'; | ||
|
||
import CodeMirror from 'codemirror'; | ||
import 'codemirror/mode/javascript/javascript'; | ||
|
||
import schema from '../schema'; | ||
|
||
function computeChange(oldVal: string, newVal: string) { | ||
if (oldVal === newVal) { return null; } | ||
let start = 0; | ||
let oldEnd = oldVal.length; | ||
let newEnd = newVal.length; | ||
while (start < oldEnd && oldVal.charCodeAt(start) === newVal.charCodeAt(start)) { | ||
++start; | ||
} | ||
while (oldEnd > start && newEnd > start && oldVal.charCodeAt(oldEnd - 1) === newVal.charCodeAt(newEnd - 1)) { | ||
oldEnd--; newEnd--; | ||
} | ||
return { from: start, to: oldEnd, text: newVal.slice(start, newEnd) }; | ||
} | ||
|
||
type GetPos = () => number; | ||
|
||
class CodeMirrorView { | ||
node: ProsemirrorNode; | ||
getPos: GetPos; | ||
incomingChanges: boolean; | ||
|
||
cm: CodeMirror; | ||
|
||
view: EditorView; | ||
dom: HTMLElement; | ||
updating: boolean; | ||
|
||
constructor(node: ProsemirrorNode, view: EditorView, getPos: GetPos) { | ||
// Store for later | ||
this.node = node; | ||
this.view = view; | ||
this.getPos = getPos; | ||
this.incomingChanges = false; | ||
|
||
// Create a CodeMirror instance | ||
this.cm = new CodeMirror(null, { | ||
value: this.node.textContent, | ||
lineNumbers: true, | ||
extraKeys: this.codeMirrorKeymap() | ||
}); | ||
|
||
// The editor's outer node is our DOM representation | ||
this.dom = this.cm.getWrapperElement(); | ||
// CodeMirror needs to be in the DOM to properly initialize, so | ||
// schedule it to update itself | ||
setTimeout(() => this.cm.refresh(), 20); | ||
|
||
// This flag is used to avoid an update loop between the outer and | ||
// inner editor | ||
this.updating = false; | ||
// Track whether changes are have been made but not yet propagated | ||
this.cm.on('beforeChange', () => this.incomingChanges = true); | ||
// Propagate updates from the code editor to ProseMirror | ||
this.cm.on('cursorActivity', () => { | ||
if (!this.updating && !this.incomingChanges) { this.forwardSelection(); } | ||
}); | ||
|
||
this.cm.on('changes', () => { | ||
if (!this.updating) { | ||
this.valueChanged(); | ||
this.forwardSelection(); | ||
} | ||
this.incomingChanges = false; | ||
}); | ||
|
||
this.cm.on('focus', () => this.forwardSelection()); | ||
} | ||
|
||
forwardSelection() { | ||
if (!this.cm.hasFocus()) { | ||
return; | ||
} | ||
|
||
const state = this.view.state; | ||
const selection = this.asProseMirrorSelection(state.doc); | ||
|
||
if (!selection.eq(state.selection)) { | ||
this.view.dispatch(state.tr.setSelection(selection)); | ||
} | ||
} | ||
|
||
asProseMirrorSelection(doc: ProsemirrorNode) { | ||
const offset = this.getPos() + 1; | ||
const anchor = this.cm.indexFromPos(this.cm.getCursor('anchor')) + offset; | ||
const head = this.cm.indexFromPos(this.cm.getCursor('head')) + offset; | ||
return TextSelection.create(doc, anchor, head); | ||
} | ||
|
||
setSelection(anchor: number, head: number) { | ||
this.cm.focus(); | ||
this.updating = true; | ||
this.cm.setSelection(this.cm.posFromIndex(anchor), this.cm.posFromIndex(head)); | ||
this.updating = false; | ||
} | ||
|
||
valueChanged() { | ||
const change = computeChange(this.node.textContent, this.cm.getValue()); | ||
if (change) { | ||
const start = this.getPos() + 1; | ||
const tr = this.view.state.tr.replaceWith( | ||
start + change.from, start + change.to, | ||
change.text ? schema.text(change.text) : null); | ||
this.view.dispatch(tr); | ||
} | ||
} | ||
codeMirrorKeymap() { | ||
const view = this.view; | ||
const mod = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'; | ||
return CodeMirror.normalizeKeyMap({ | ||
Up: () => this.maybeEscape('line', -1), | ||
Left: () => this.maybeEscape('char', -1), | ||
Down: () => this.maybeEscape('line', 1), | ||
Right: () => this.maybeEscape('char', 1), | ||
[`${mod}-Z`]: () => undo(view.state, view.dispatch), | ||
[`Shift-${mod}-Z`]: () => redo(view.state, view.dispatch), | ||
[`${mod}-Y`]: () => redo(view.state, view.dispatch), | ||
'Ctrl-Enter': () => { | ||
if (exitCode(view.state, view.dispatch)) { view.focus(); } | ||
} | ||
}); | ||
} | ||
|
||
maybeEscape(unit: string, dir: number) { | ||
const pos = this.cm.getCursor(); | ||
if (this.cm.somethingSelected() || | ||
pos.line !== (dir < 0 ? this.cm.firstLine() : this.cm.lastLine()) || | ||
(unit === 'char' && | ||
pos.ch !== (dir < 0 ? 0 : this.cm.getLine(pos.line).length))) { | ||
return CodeMirror.Pass; | ||
} | ||
|
||
this.view.focus(); | ||
const targetPos = this.getPos() + (dir < 0 ? 0 : this.node.nodeSize); | ||
const selection = Selection.near(this.view.state.doc.resolve(targetPos), dir); | ||
this.view.dispatch(this.view.state.tr.setSelection(selection).scrollIntoView()); | ||
this.view.focus(); | ||
} | ||
|
||
update(node: ProsemirrorNode) { | ||
if (node.type !== this.node.type) { return false; } | ||
this.node = node; | ||
const change = computeChange(this.cm.getValue(), node.textContent); | ||
if (change) { | ||
this.updating = true; | ||
this.cm.replaceRange(change.text, this.cm.posFromIndex(change.from), | ||
this.cm.posFromIndex(change.to)); | ||
this.updating = false; | ||
} | ||
return true; | ||
} | ||
} | ||
|
||
export default CodeMirrorView; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import CodeBlockView from './CodeMirror'; | ||
import { Node as ProsemirrorNode } from 'prosemirror-model'; | ||
import { EditorView } from 'prosemirror-view'; | ||
|
||
const nodeViews = { | ||
code_block: (node: ProsemirrorNode, view: EditorView, getPos: () => number) => { | ||
return new CodeBlockView(node, view, getPos); | ||
} | ||
}; | ||
|
||
export default nodeViews; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { EditorState } from 'prosemirror-state'; | ||
import { isNodeActive, toggleBlockType, ToolbarCustomMenuItem } from 'ngx-editor'; | ||
|
||
import schema from '../../schema'; | ||
|
||
const codeMirror: ToolbarCustomMenuItem = (editorView) => { | ||
const dom: HTMLElement = document.createElement('div'); | ||
dom.innerHTML = 'CodeMirror'; | ||
|
||
dom.classList.add('NgxEditor-MenuItem'); | ||
dom.classList.add('CustomMenuItem'); | ||
|
||
const type = schema.nodes.code_block; | ||
|
||
let command; | ||
|
||
dom.addEventListener('mousedown', (e: MouseEvent) => { | ||
e.preventDefault(); | ||
|
||
// don't execute if not left click | ||
if (e.buttons !== 1) { | ||
return; | ||
} | ||
|
||
command = toggleBlockType(type, schema.nodes.paragraph); | ||
command(editorView.state, editorView.dispatch); | ||
}); | ||
|
||
|
||
const update = (state: EditorState): void => { | ||
const isActive = isNodeActive(state, type); | ||
let canExecute = true; | ||
|
||
if (command) { | ||
canExecute = command(state, null); | ||
} | ||
|
||
dom.classList.toggle(`NgxEditor-MenuItem__Active`, isActive); | ||
dom.classList.toggle(`disabled`, !canExecute); | ||
}; | ||
|
||
return { | ||
dom, | ||
update | ||
}; | ||
}; | ||
|
||
export default codeMirror; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { nodes as basicNodes, marks as basicMarks } from 'ngx-editor'; | ||
import { Schema, Node as ProsemirrorNode, NodeSpec } from 'prosemirror-model'; | ||
|
||
const codeBlock: NodeSpec = { | ||
group: 'block', | ||
attrs: { | ||
text: { default: '' }, | ||
language: { default: 'text/javascript' } | ||
}, | ||
parseDOM: [{ | ||
tag: 'pre', | ||
getAttrs: (dom: HTMLElement) => { | ||
return { | ||
text: dom.textContent, | ||
language: dom.getAttribute('data-language') || 'text/plain' | ||
}; | ||
} | ||
} | ||
], | ||
toDOM(node: ProsemirrorNode) { | ||
return ['pre', { 'data-language': node.attrs.language }, node.attrs.text]; | ||
} | ||
}; | ||
|
||
const nodes = Object.assign( | ||
{}, | ||
basicNodes, | ||
{ | ||
code_mirror: codeBlock | ||
} | ||
); | ||
|
||
const schema = new Schema({ | ||
nodes, | ||
marks: basicMarks | ||
}); | ||
|
||
export default schema; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
@import "~codemirror/lib/codemirror.css"; |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.