mirror of https://github.com/fantasticit/think.git
Merge pull request #176 from fantasticit/fix/tiptap
This commit is contained in:
commit
b318923fa3
|
@ -24,15 +24,25 @@ export function renderMind(options: Options) {
|
||||||
const Editor = window.kityminder.Editor;
|
const Editor = window.kityminder.Editor;
|
||||||
const editor = new Editor(options.container);
|
const editor = new Editor(options.container);
|
||||||
const mind = editor.minder;
|
const mind = editor.minder;
|
||||||
|
mind.editor = editor;
|
||||||
|
|
||||||
options.data && mind.importJson(options.data);
|
options.data && mind.importJson(options.data);
|
||||||
|
|
||||||
if (!options.isEditable) {
|
if (!options.isEditable) {
|
||||||
mind.disable();
|
mind.disable();
|
||||||
|
mind.setStatus('readonly');
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mind.execCommand('camera');
|
mind.execCommand('camera');
|
||||||
|
|
||||||
|
if (!options.isEditable) {
|
||||||
|
const selectedNodes = mind.getSelectedNodes();
|
||||||
|
|
||||||
|
if (selectedNodes.length) {
|
||||||
|
mind.removeSelectedNodes(selectedNodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return mind;
|
return mind;
|
||||||
|
|
|
@ -138,6 +138,9 @@ define(function (require, exports, module) {
|
||||||
},
|
},
|
||||||
|
|
||||||
_bindEvents: function () {
|
_bindEvents: function () {
|
||||||
|
if (this.getStatus() === 'readonly') {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
/* jscs:disable maximumLineLength */
|
/* jscs:disable maximumLineLength */
|
||||||
this._paper.on(
|
this._paper.on(
|
||||||
'click dblclick mousedown contextmenu mouseup mousemove mouseover mousewheel DOMMouseScroll touchstart touchmove touchend dragenter dragleave drop',
|
'click dblclick mousedown contextmenu mouseup mousemove mouseover mousewheel DOMMouseScroll touchstart touchmove touchend dragenter dragleave drop',
|
||||||
|
|
|
@ -17,6 +17,10 @@ define(function (require, exports, module) {
|
||||||
|
|
||||||
kity.extendClass(Minder, {
|
kity.extendClass(Minder, {
|
||||||
focus: function () {
|
focus: function () {
|
||||||
|
if (this.getStatus() === 'readonly') {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isFocused()) {
|
if (!this.isFocused()) {
|
||||||
var renderTarget = this._renderTarget;
|
var renderTarget = this._renderTarget;
|
||||||
renderTarget.classList.add('focus');
|
renderTarget.classList.add('focus');
|
||||||
|
|
|
@ -23,6 +23,10 @@ define(function (require, exports, module) {
|
||||||
|
|
||||||
kity.extendClass(Minder, {
|
kity.extendClass(Minder, {
|
||||||
_initKeyReceiver: function () {
|
_initKeyReceiver: function () {
|
||||||
|
if (this.getStatus() === 'readonly') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._keyReceiver) return;
|
if (this._keyReceiver) return;
|
||||||
|
|
||||||
var receiver = (this._keyReceiver = document.createElement('input'));
|
var receiver = (this._keyReceiver = document.createElement('input'));
|
||||||
|
@ -34,6 +38,10 @@ define(function (require, exports, module) {
|
||||||
var minder = this;
|
var minder = this;
|
||||||
|
|
||||||
listen(receiver, 'keydown keyup keypress copy paste blur focus input', function (e) {
|
listen(receiver, 'keydown keyup keypress copy paste blur focus input', function (e) {
|
||||||
|
if (minder.getStatus() === 'readonly') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case 'blur':
|
case 'blur':
|
||||||
minder.blur();
|
minder.blur();
|
||||||
|
@ -50,14 +58,24 @@ define(function (require, exports, module) {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.on('focus', function () {
|
this.on('focus', function () {
|
||||||
|
if (this.getStatus() === 'readonly') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
receiver.select();
|
receiver.select();
|
||||||
receiver.focus();
|
receiver.focus();
|
||||||
});
|
});
|
||||||
this.on('blur', function () {
|
this.on('blur', function () {
|
||||||
|
if (this.getStatus() === 'readonly') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
receiver.blur();
|
receiver.blur();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isFocused()) {
|
if (this.isFocused()) {
|
||||||
|
if (this.getStatus() === 'readonly') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
receiver.select();
|
receiver.select();
|
||||||
receiver.focus();
|
receiver.focus();
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,8 @@ define(function (require, exports, module) {
|
||||||
|
|
||||||
initEvent: function (node) {
|
initEvent: function (node) {
|
||||||
this.on('mousedown', function (e) {
|
this.on('mousedown', function (e) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
minder.select([node], true);
|
minder.select([node], true);
|
||||||
if (node.isExpanded()) {
|
if (node.isExpanded()) {
|
||||||
node.collapse();
|
node.collapse();
|
||||||
|
|
|
@ -82,7 +82,7 @@ define(function (require, exports, module) {
|
||||||
mask.fill(color[0]);
|
mask.fill(color[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
number.setContent(value);
|
number.setContent('P' + value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -21,9 +21,11 @@ define(function (require, exports, module) {
|
||||||
var receiverElement = receiver.element;
|
var receiverElement = receiver.element;
|
||||||
var isGecko = window.kity.Browser.gecko;
|
var isGecko = window.kity.Browser.gecko;
|
||||||
// setup everything to go
|
// setup everything to go
|
||||||
|
|
||||||
setupReciverElement();
|
setupReciverElement();
|
||||||
setupFsm();
|
setupFsm();
|
||||||
setupHotbox();
|
setupHotbox();
|
||||||
|
|
||||||
// expose editText()
|
// expose editText()
|
||||||
this.editText = editText;
|
this.editText = editText;
|
||||||
// listen the fsm changes, make action.
|
// listen the fsm changes, make action.
|
||||||
|
@ -32,6 +34,8 @@ define(function (require, exports, module) {
|
||||||
fsm.when('* -> input', enterInputMode);
|
fsm.when('* -> input', enterInputMode);
|
||||||
// when exited, commit or exit depends on the exit reason
|
// when exited, commit or exit depends on the exit reason
|
||||||
fsm.when('input -> *', function (exit, enter, reason) {
|
fsm.when('input -> *', function (exit, enter, reason) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
switch (reason) {
|
switch (reason) {
|
||||||
case 'input-cancel':
|
case 'input-cancel':
|
||||||
return exitInputMode();
|
return exitInputMode();
|
||||||
|
@ -43,11 +47,15 @@ define(function (require, exports, module) {
|
||||||
});
|
});
|
||||||
// lost focus to commit
|
// lost focus to commit
|
||||||
receiver.onblur(function (e) {
|
receiver.onblur(function (e) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
if (fsm.state() == 'input') {
|
if (fsm.state() == 'input') {
|
||||||
fsm.jump('normal', 'input-commit');
|
fsm.jump('normal', 'input-commit');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
minder.on('beforemousedown', function () {
|
minder.on('beforemousedown', function () {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
if (fsm.state() == 'input') {
|
if (fsm.state() == 'input') {
|
||||||
fsm.jump('normal', 'input-commit');
|
fsm.jump('normal', 'input-commit');
|
||||||
}
|
}
|
||||||
|
@ -69,10 +77,15 @@ define(function (require, exports, module) {
|
||||||
minder.on('layoutallfinish viewchange viewchanged selectionchange', function (e) {
|
minder.on('layoutallfinish viewchange viewchanged selectionchange', function (e) {
|
||||||
// viewchange event is too frequenced, lazy it
|
// viewchange event is too frequenced, lazy it
|
||||||
if (e.type == 'viewchange' && fsm.state() != 'input') return;
|
if (e.type == 'viewchange' && fsm.state() != 'input') return;
|
||||||
|
|
||||||
|
if (minder.getStatus() !== 'readonly') {
|
||||||
updatePosition();
|
updatePosition();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
if (minder.getStatus() !== 'readonly') {
|
||||||
updatePosition();
|
updatePosition();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// edit entrance in hotbox
|
// edit entrance in hotbox
|
||||||
function setupHotbox() {
|
function setupHotbox() {
|
||||||
hotbox.state('main').button({
|
hotbox.state('main').button({
|
||||||
|
@ -121,6 +134,8 @@ define(function (require, exports, module) {
|
||||||
* @Date 2015-12-2
|
* @Date 2015-12-2
|
||||||
*/
|
*/
|
||||||
function enterInputMode() {
|
function enterInputMode() {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
var node = minder.getSelectedNode();
|
var node = minder.getSelectedNode();
|
||||||
if (node) {
|
if (node) {
|
||||||
var fontSize = node.getData('font-size') || node.getStyle('font-size');
|
var fontSize = node.getData('font-size') || node.getStyle('font-size');
|
||||||
|
@ -141,6 +156,13 @@ define(function (require, exports, module) {
|
||||||
* @Date: 2015.9.16
|
* @Date: 2015.9.16
|
||||||
*/
|
*/
|
||||||
function commitInputText(textNodes) {
|
function commitInputText(textNodes) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
|
var node = minder.getSelectedNode();
|
||||||
|
if (!node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var text = '';
|
var text = '';
|
||||||
var TAB_CHAR = '\t',
|
var TAB_CHAR = '\t',
|
||||||
ENTER_CHAR = '\n',
|
ENTER_CHAR = '\n',
|
||||||
|
@ -282,6 +304,8 @@ define(function (require, exports, module) {
|
||||||
* @Date: 2015.9.16
|
* @Date: 2015.9.16
|
||||||
*/
|
*/
|
||||||
function commitInputNode(node, text) {
|
function commitInputNode(node, text) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
minder.decodeData('text', text).then(function (json) {
|
minder.decodeData('text', text).then(function (json) {
|
||||||
function importText(node, json, minder) {
|
function importText(node, json, minder) {
|
||||||
|
@ -309,6 +333,8 @@ define(function (require, exports, module) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function commitInputResult() {
|
function commitInputResult() {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Desc: 进行如下处理:
|
* @Desc: 进行如下处理:
|
||||||
* 根据用户的输入判断是否生成新的节点
|
* 根据用户的输入判断是否生成新的节点
|
||||||
|
@ -338,10 +364,14 @@ define(function (require, exports, module) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function exitInputMode() {
|
function exitInputMode() {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
receiverElement.classList.remove('input');
|
receiverElement.classList.remove('input');
|
||||||
receiver.selectAll();
|
receiver.selectAll();
|
||||||
}
|
}
|
||||||
function updatePosition() {
|
function updatePosition() {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
var planed = updatePosition;
|
var planed = updatePosition;
|
||||||
var focusNode = minder.getSelectedNode();
|
var focusNode = minder.getSelectedNode();
|
||||||
if (!focusNode) return;
|
if (!focusNode) return;
|
||||||
|
|
|
@ -48,18 +48,26 @@ define(function (require, exports, module) {
|
||||||
var receiverElement = receiver.element;
|
var receiverElement = receiver.element;
|
||||||
var hotbox = this.hotbox;
|
var hotbox = this.hotbox;
|
||||||
var compositionLock = false;
|
var compositionLock = false;
|
||||||
|
var that = this;
|
||||||
|
|
||||||
// normal -> *
|
// normal -> *
|
||||||
receiver.listen('normal', function (e) {
|
receiver.listen('normal', function (e) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
// 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable
|
// 为了防止处理进入edit模式而丢失处理的首字母,此时receiver必须为enable
|
||||||
receiver.enable();
|
receiver.enable();
|
||||||
// normal -> hotbox
|
// normal -> hotbox
|
||||||
if (e.is('Space')) {
|
if (e.is('Space')) {
|
||||||
|
// 非编辑模式
|
||||||
|
if (that.minder.getStatus() === 'readonly') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// safari下Space触发hotbox,然而这时Space已在receiver上留下作案痕迹,因此抹掉
|
// safari下Space触发hotbox,然而这时Space已在receiver上留下作案痕迹,因此抹掉
|
||||||
if (kity.Browser.safari) {
|
if (kity.Browser.safari) {
|
||||||
receiverElement.innerHTML = '';
|
receiverElement.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return fsm.jump('hotbox', 'space-trigger');
|
return fsm.jump('hotbox', 'space-trigger');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,6 +99,8 @@ define(function (require, exports, module) {
|
||||||
|
|
||||||
// hotbox -> normal
|
// hotbox -> normal
|
||||||
receiver.listen('hotbox', function (e) {
|
receiver.listen('hotbox', function (e) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
receiver.disable();
|
receiver.disable();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var handleResult = hotbox.dispatch(e);
|
var handleResult = hotbox.dispatch(e);
|
||||||
|
@ -101,6 +111,8 @@ define(function (require, exports, module) {
|
||||||
|
|
||||||
// input => normal
|
// input => normal
|
||||||
receiver.listen('input', function (e) {
|
receiver.listen('input', function (e) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
receiver.enable();
|
receiver.enable();
|
||||||
if (e.type == 'keydown') {
|
if (e.type == 'keydown') {
|
||||||
if (e.is('Enter')) {
|
if (e.is('Enter')) {
|
||||||
|
@ -138,6 +150,8 @@ define(function (require, exports, module) {
|
||||||
container.addEventListener(
|
container.addEventListener(
|
||||||
'mousedown',
|
'mousedown',
|
||||||
function (e) {
|
function (e) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
if (e.button == MOUSE_RB) {
|
if (e.button == MOUSE_RB) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
@ -155,6 +169,8 @@ define(function (require, exports, module) {
|
||||||
container.addEventListener(
|
container.addEventListener(
|
||||||
'mousewheel',
|
'mousewheel',
|
||||||
function (e) {
|
function (e) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
|
|
||||||
if (fsm.state() == 'hotbox') {
|
if (fsm.state() == 'hotbox') {
|
||||||
hotbox.active(Hotbox.STATE_IDLE);
|
hotbox.active(Hotbox.STATE_IDLE);
|
||||||
fsm.jump('normal', 'mousemove-blur');
|
fsm.jump('normal', 'mousemove-blur');
|
||||||
|
@ -164,12 +180,14 @@ define(function (require, exports, module) {
|
||||||
);
|
);
|
||||||
|
|
||||||
container.addEventListener('contextmenu', function (e) {
|
container.addEventListener('contextmenu', function (e) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
container.addEventListener(
|
container.addEventListener(
|
||||||
'mouseup',
|
'mouseup',
|
||||||
function (e) {
|
function (e) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
if (fsm.state() != 'normal') {
|
if (fsm.state() != 'normal') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -186,6 +204,7 @@ define(function (require, exports, module) {
|
||||||
|
|
||||||
// 阻止热盒事件冒泡,在热盒正确执行前导致热盒关闭
|
// 阻止热盒事件冒泡,在热盒正确执行前导致热盒关闭
|
||||||
hotbox.$element.addEventListener('mousedown', function (e) {
|
hotbox.$element.addEventListener('mousedown', function (e) {
|
||||||
|
if (minder._status === 'readonly') return;
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ export const Callout = Node.create({
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
wrappingInputRule({
|
wrappingInputRule({
|
||||||
find: /^\$callout $/,
|
find: /^\$callout\$$/,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
getAttributes: (match) => {
|
getAttributes: (match) => {
|
||||||
return { type: match[1] };
|
return { type: match[1] };
|
||||||
|
|
|
@ -268,7 +268,7 @@ export const Columns = Node.create({
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
nodeInputRule({
|
nodeInputRule({
|
||||||
find: /^\$columns $/,
|
find: /^\$columns\$$/,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
getAttributes: () => {
|
getAttributes: () => {
|
||||||
return { type: 'left-right', columns: 2 };
|
return { type: 'left-right', columns: 2 };
|
||||||
|
|
|
@ -78,7 +78,7 @@ export const Countdown = Node.create({
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
nodeInputRule({
|
nodeInputRule({
|
||||||
find: /^\$countdown $/,
|
find: /^\$countdown\$$/,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
getAttributes: () => {
|
getAttributes: () => {
|
||||||
return { width: '100%' };
|
return { width: '100%' };
|
||||||
|
|
|
@ -103,7 +103,7 @@ export const Excalidraw = Node.create({
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
nodeInputRule({
|
nodeInputRule({
|
||||||
find: /^\$excalidraw $/,
|
find: /^\$excalidraw\$$/,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
getAttributes: () => {
|
getAttributes: () => {
|
||||||
return { width: '100%' };
|
return { width: '100%' };
|
||||||
|
|
|
@ -12,6 +12,11 @@ export interface IFlowAttrs {
|
||||||
createUser?: IUser['id'];
|
createUser?: IUser['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IFlowOptions {
|
||||||
|
HTMLAttributes: Record<string, any>;
|
||||||
|
getCreateUserId: () => string | number;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
flow: {
|
flow: {
|
||||||
|
@ -20,7 +25,7 @@ declare module '@tiptap/core' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Flow = Node.create({
|
export const Flow = Node.create<IFlowOptions>({
|
||||||
name: 'flow',
|
name: 'flow',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
@ -55,6 +60,7 @@ export const Flow = Node.create({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: 'flow',
|
class: 'flow',
|
||||||
},
|
},
|
||||||
|
getCreateUserId: () => null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -102,10 +108,10 @@ export const Flow = Node.create({
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
nodeInputRule({
|
nodeInputRule({
|
||||||
find: /^\$flow $/,
|
find: /^\$flow\$$/,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
getAttributes: () => {
|
getAttributes: () => {
|
||||||
return { width: '100%' };
|
return { width: '100%', defaultShowPicker: true, createUser: this.options.getCreateUserId() };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
|
@ -98,7 +98,7 @@ export const Iframe = Node.create({
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
nodeInputRule({
|
nodeInputRule({
|
||||||
find: /^\$iframe $/,
|
find: /^\$iframe\$$/,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
getAttributes: () => {
|
getAttributes: () => {
|
||||||
return { width: '100%' };
|
return { width: '100%' };
|
||||||
|
|
|
@ -10,6 +10,11 @@ export type IKatexAttrs = {
|
||||||
createUser?: IUser['id'];
|
createUser?: IUser['id'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface IKatexOptions {
|
||||||
|
HTMLAttributes: Record<string, any>;
|
||||||
|
getCreateUserId: () => string | number;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
katex: {
|
katex: {
|
||||||
|
@ -18,7 +23,7 @@ declare module '@tiptap/core' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Katex = Node.create({
|
export const Katex = Node.create<IKatexOptions>({
|
||||||
name: 'katex',
|
name: 'katex',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
@ -30,6 +35,7 @@ export const Katex = Node.create({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: 'katex',
|
class: 'katex',
|
||||||
},
|
},
|
||||||
|
getCreateUserId: () => null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -72,8 +78,11 @@ export const Katex = Node.create({
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
nodeInputRule({
|
nodeInputRule({
|
||||||
find: /^\$katex $/,
|
find: /^\$katex\$$/,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
|
getAttributes: () => {
|
||||||
|
return { defaultShowPicker: true, createUser: this.options.getCreateUserId() };
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,6 +22,11 @@ export interface IMindAttrs {
|
||||||
zoom?: number;
|
zoom?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IMindOptions {
|
||||||
|
HTMLAttributes: Record<string, any>;
|
||||||
|
getCreateUserId: () => string | number;
|
||||||
|
}
|
||||||
|
|
||||||
declare module '@tiptap/core' {
|
declare module '@tiptap/core' {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
mind: {
|
mind: {
|
||||||
|
@ -30,7 +35,7 @@ declare module '@tiptap/core' {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Mind = Node.create({
|
export const Mind = Node.create<IMindOptions>({
|
||||||
name: 'mind',
|
name: 'mind',
|
||||||
group: 'block',
|
group: 'block',
|
||||||
selectable: true,
|
selectable: true,
|
||||||
|
@ -66,6 +71,7 @@ export const Mind = Node.create({
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: 'mind',
|
class: 'mind',
|
||||||
},
|
},
|
||||||
|
getCreateUserId: () => null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -113,10 +119,10 @@ export const Mind = Node.create({
|
||||||
addInputRules() {
|
addInputRules() {
|
||||||
return [
|
return [
|
||||||
nodeInputRule({
|
nodeInputRule({
|
||||||
find: /^\$mind $/,
|
find: /^\$mind\$$/,
|
||||||
type: this.type,
|
type: this.type,
|
||||||
getAttributes: () => {
|
getAttributes: () => {
|
||||||
return { width: '100%' };
|
return { width: '100%', defaultShowPicker: true, createUser: this.options.getCreateUserId() };
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,3 +1,109 @@
|
||||||
import BuiltInPlaceholder from '@tiptap/extension-placeholder';
|
import { Editor, Extension } from '@tiptap/core';
|
||||||
|
import { Node as ProsemirrorNode } from 'prosemirror-model';
|
||||||
|
import { Plugin } from 'prosemirror-state';
|
||||||
|
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||||
|
|
||||||
export const Placeholder = BuiltInPlaceholder;
|
export interface PlaceholderOptions {
|
||||||
|
emptyEditorClass: string;
|
||||||
|
emptyNodeClass: string;
|
||||||
|
placeholder: ((PlaceholderProps: { editor: Editor; node: ProsemirrorNode; pos: number }) => string) | string;
|
||||||
|
showOnlyWhenEditable: boolean;
|
||||||
|
showOnlyCurrent: boolean;
|
||||||
|
includeChildren: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Placeholder = Extension.create<PlaceholderOptions>({
|
||||||
|
name: 'placeholder',
|
||||||
|
|
||||||
|
addOptions() {
|
||||||
|
return {
|
||||||
|
emptyEditorClass: 'is-editor-empty',
|
||||||
|
emptyNodeClass: 'is-empty',
|
||||||
|
placeholder: 'Write something …',
|
||||||
|
showOnlyWhenEditable: true,
|
||||||
|
showOnlyCurrent: true,
|
||||||
|
includeChildren: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
addStorage() {
|
||||||
|
return new Map();
|
||||||
|
},
|
||||||
|
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
props: {
|
||||||
|
decorations: ({ doc, selection }) => {
|
||||||
|
const active = this.editor.isEditable || !this.options.showOnlyWhenEditable;
|
||||||
|
const { anchor } = selection;
|
||||||
|
const decorations: Decoration[] = [];
|
||||||
|
|
||||||
|
if (!active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.descendants((node, pos) => {
|
||||||
|
const hasAnchor = anchor >= pos && anchor <= pos + node.nodeSize;
|
||||||
|
const isEmpty = !node.isLeaf && !node.childCount;
|
||||||
|
|
||||||
|
if ((hasAnchor || !this.options.showOnlyCurrent) && isEmpty) {
|
||||||
|
const classes = [this.options.emptyNodeClass];
|
||||||
|
|
||||||
|
if (this.editor.isEmpty) {
|
||||||
|
classes.push(this.options.emptyEditorClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = pos;
|
||||||
|
const end = pos + node.nodeSize;
|
||||||
|
let placeholder = '';
|
||||||
|
|
||||||
|
if (this.editor.isEditable) {
|
||||||
|
const key = `${start}-${end}`;
|
||||||
|
|
||||||
|
if (!this.editor.storage[this.name].has(key)) {
|
||||||
|
this.editor.storage[this.name].set(
|
||||||
|
key,
|
||||||
|
typeof this.options.placeholder === 'function'
|
||||||
|
? this.options.placeholder({
|
||||||
|
editor: this.editor,
|
||||||
|
node,
|
||||||
|
pos,
|
||||||
|
})
|
||||||
|
: this.options.placeholder
|
||||||
|
);
|
||||||
|
}
|
||||||
|
placeholder = this.editor.storage[this.name].get(key);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.editor.storage[this.name].delete(key);
|
||||||
|
}, 500);
|
||||||
|
} else {
|
||||||
|
placeholder =
|
||||||
|
typeof this.options.placeholder === 'function'
|
||||||
|
? this.options.placeholder({
|
||||||
|
editor: this.editor,
|
||||||
|
node,
|
||||||
|
pos,
|
||||||
|
})
|
||||||
|
: this.options.placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoration = Decoration.node(start, end, {
|
||||||
|
'class': classes.join(' '),
|
||||||
|
'data-placeholder': placeholder,
|
||||||
|
});
|
||||||
|
|
||||||
|
decorations.push(decoration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.options.includeChildren;
|
||||||
|
});
|
||||||
|
|
||||||
|
return DecorationSet.create(doc, decorations);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { insertMenuLRUCache, QUICK_INSERT_COMMANDS, transformToCommands } from '
|
||||||
import { MenuList } from 'tiptap/core/wrappers/menu-list';
|
import { MenuList } from 'tiptap/core/wrappers/menu-list';
|
||||||
|
|
||||||
export const QuickInsertPluginKey = new PluginKey('quickInsert');
|
export const QuickInsertPluginKey = new PluginKey('quickInsert');
|
||||||
const extensionName = 'quickInsert'
|
const extensionName = 'quickInsert';
|
||||||
|
|
||||||
export const QuickInsert = Node.create({
|
export const QuickInsert = Node.create({
|
||||||
name: extensionName,
|
name: extensionName,
|
||||||
|
@ -63,8 +63,8 @@ export const QuickInsert = Node.create({
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
}).configure({
|
}).configure({
|
||||||
suggestion: {
|
suggestion: {
|
||||||
|
|
|
@ -32,7 +32,6 @@ export const ExcalidrawSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderExcalidraw = useCallback((app) => {
|
const renderExcalidraw = useCallback((app) => {
|
||||||
console.log('render', app);
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
app.refresh();
|
app.refresh();
|
||||||
});
|
});
|
||||||
|
@ -54,7 +53,14 @@ export const ExcalidrawSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: 绘图更新后会滚动 dom 到顶点,原因未知,在此 hack 修复下!
|
||||||
|
*/
|
||||||
|
const currentScrollTop = document.querySelector('main#js-tocs-container')?.scrollTop;
|
||||||
editor.chain().focus().setExcalidraw({ data }).run();
|
editor.chain().focus().setExcalidraw({ data }).run();
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelector('main#js-tocs-container').scrollTop = currentScrollTop;
|
||||||
|
});
|
||||||
toggleVisible(false);
|
toggleVisible(false);
|
||||||
}, [Excalidraw, editor, data, toggleVisible]);
|
}, [Excalidraw, editor, data, toggleVisible]);
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ export const KatexBubbleMenu: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
setTimeout(() => ref.current?.focus(), 100);
|
setTimeout(() => ref.current?.focus(), 200);
|
||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible]);
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,36 @@
|
||||||
|
const getProgressTitle = (index) => {
|
||||||
|
switch (index) {
|
||||||
|
case 0:
|
||||||
|
return '移除进度';
|
||||||
|
case 1:
|
||||||
|
return '未开始';
|
||||||
|
case 9:
|
||||||
|
return '全部完成';
|
||||||
|
default:
|
||||||
|
return '完成' + (index - 1) + '/8';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PROGRESSES = Array.from({ length: 10 }, (_, i) => {
|
||||||
|
return {
|
||||||
|
text: getProgressTitle(i),
|
||||||
|
value: i,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PRIORITIES = [
|
||||||
|
{
|
||||||
|
text: '移除优先级',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
...Array.from({ length: 9 }, (_, i) => {
|
||||||
|
return {
|
||||||
|
text: `P${i + 1}`,
|
||||||
|
value: i + 1,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
export const TEMPLATES = [
|
export const TEMPLATES = [
|
||||||
{
|
{
|
||||||
label: '经典',
|
label: '经典',
|
||||||
|
|
|
@ -46,7 +46,14 @@ export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = mind.exportJson();
|
const data = mind.exportJson();
|
||||||
|
/**
|
||||||
|
* FIXME: 百度脑图更新后会滚动 dom 到顶点,原因未知,在此 hack 修复下!
|
||||||
|
*/
|
||||||
|
const currentScrollTop = document.querySelector('main#js-tocs-container')?.scrollTop;
|
||||||
editor.chain().focus().setMind({ data }).run();
|
editor.chain().focus().setMind({ data }).run();
|
||||||
|
setTimeout(() => {
|
||||||
|
document.querySelector('main#js-tocs-container').scrollTop = currentScrollTop;
|
||||||
|
});
|
||||||
toggleVisible(false);
|
toggleVisible(false);
|
||||||
}, [editor, toggleVisible, mind]);
|
}, [editor, toggleVisible, mind]);
|
||||||
|
|
||||||
|
@ -65,6 +72,12 @@ export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
};
|
};
|
||||||
}, [editor, toggleVisible]);
|
}, [editor, toggleVisible]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible && mind) {
|
||||||
|
mind.destroy();
|
||||||
|
}
|
||||||
|
}, [visible, mind]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
centered
|
centered
|
||||||
|
@ -75,6 +88,7 @@ export const MindSettingModal: React.FC<IProps> = ({ editor }) => {
|
||||||
onOk={save}
|
onOk={save}
|
||||||
okText="保存"
|
okText="保存"
|
||||||
cancelText="退出"
|
cancelText="退出"
|
||||||
|
motion={false}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { IconMark } from '@douyinfe/semi-icons';
|
||||||
|
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
import { ColorPicker } from 'components/color-picker';
|
||||||
|
|
||||||
|
export const BgColor = ({ bgColor, selectedNode, setBackgroundColor }) => {
|
||||||
|
return (
|
||||||
|
<ColorPicker
|
||||||
|
onSetColor={(color) => {
|
||||||
|
setBackgroundColor(color);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip content="背景色" zIndex={10000}>
|
||||||
|
<Button
|
||||||
|
disabled={!selectedNode}
|
||||||
|
type="tertiary"
|
||||||
|
theme={bgColor ? 'light' : 'borderless'}
|
||||||
|
icon={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconMark />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 12,
|
||||||
|
height: 2,
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ColorPicker>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { IconFont } from '@douyinfe/semi-icons';
|
||||||
|
import { Button, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
import { ColorPicker } from 'components/color-picker';
|
||||||
|
|
||||||
|
export const FontColor = ({ selectedNode, setFontColor, textColor }) => {
|
||||||
|
return (
|
||||||
|
<ColorPicker
|
||||||
|
onSetColor={(color) => {
|
||||||
|
setFontColor(color);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tooltip content="文本色" zIndex={10000}>
|
||||||
|
<Button
|
||||||
|
disabled={!selectedNode}
|
||||||
|
type="tertiary"
|
||||||
|
theme={textColor ? 'light' : 'borderless'}
|
||||||
|
icon={
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconFont />
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
width: 12,
|
||||||
|
height: 2,
|
||||||
|
backgroundColor: textColor,
|
||||||
|
}}
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</ColorPicker>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { IconHelpCircle } from '@douyinfe/semi-icons';
|
||||||
|
import { Button, Descriptions, Popover } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const HELP_MESSAGE = [
|
||||||
|
{ key: '新增同级节点', value: 'Enter 键' },
|
||||||
|
{ key: '新增子节点', value: 'Tab 键' },
|
||||||
|
{ key: '编辑节点文字', value: '双击节点' },
|
||||||
|
{ key: '编辑节点菜单', value: '在节点右键' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const HELP_MESSAGE_STYLE = {
|
||||||
|
width: '200px',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Help = () => {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
zIndex={10000}
|
||||||
|
spacing={10}
|
||||||
|
style={{ padding: 12, overflow: 'hidden' }}
|
||||||
|
position="bottomLeft"
|
||||||
|
content={
|
||||||
|
<section className={styles.sectionWrap}>
|
||||||
|
<Descriptions data={HELP_MESSAGE} style={HELP_MESSAGE_STYLE} />
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button size="small" theme="borderless" type="tertiary" icon={<IconHelpCircle />} />
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import { IconFile } from '@douyinfe/semi-icons';
|
import { IconImage } from '@douyinfe/semi-icons';
|
||||||
import { Button, Dropdown, Form, Tooltip } from '@douyinfe/semi-ui';
|
import { Button, Dropdown, Form, Tooltip } from '@douyinfe/semi-ui';
|
||||||
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
import { FormApi } from '@douyinfe/semi-ui/lib/es/form';
|
||||||
import { Upload } from 'components/upload';
|
import { Upload } from 'components/upload';
|
||||||
|
@ -48,7 +48,7 @@ export const Image = ({ disabled, image, setImage }) => {
|
||||||
>
|
>
|
||||||
<span style={{ display: 'inline-block' }}>
|
<span style={{ display: 'inline-block' }}>
|
||||||
<Tooltip content="设置图片" zIndex={10000}>
|
<Tooltip content="设置图片" zIndex={10000}>
|
||||||
<Button disabled={disabled} type="tertiary" theme={image ? 'light' : 'borderless'} icon={<IconFile />} />
|
<Button disabled={disabled} type="tertiary" theme={image ? 'light' : 'borderless'} icon={<IconImage />} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</span>
|
</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
|
|
@ -1,108 +1,188 @@
|
||||||
import { IconBold, IconFont, IconHelpCircle, IconMark } from '@douyinfe/semi-icons';
|
import { IconBold, IconRedo, IconUndo } from '@douyinfe/semi-icons';
|
||||||
import { Button, Descriptions, Popover, Space, Tooltip, Typography } from '@douyinfe/semi-ui';
|
import { Button, Space, Tooltip } from '@douyinfe/semi-ui';
|
||||||
import cls from 'classnames';
|
import { Divider } from 'components/divider';
|
||||||
import { ColorPicker } from 'components/color-picker';
|
import { IconMindCenter } from 'components/icons';
|
||||||
import { IconDrawBoard, IconMindCenter, IconStructure } from 'components/icons';
|
|
||||||
import { IconZoomIn, IconZoomOut } from 'components/icons';
|
import { IconZoomIn, IconZoomOut } from 'components/icons';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { clamp } from 'tiptap/prose-utils';
|
|
||||||
|
|
||||||
import { MAX_ZOOM, MIN_ZOOM, TEMPLATES, THEMES, ZOOM_STEP } from '../constant';
|
import { BgColor } from './bgcolor';
|
||||||
|
import { FontColor } from './font-color';
|
||||||
|
import { Help } from './help';
|
||||||
import { Image } from './image';
|
import { Image } from './image';
|
||||||
import styles from './index.module.scss';
|
|
||||||
import { Link } from './link';
|
import { Link } from './link';
|
||||||
|
import { Priority } from './priority';
|
||||||
const { Text } = Typography;
|
import { Progress } from './progress';
|
||||||
|
import { Template } from './template';
|
||||||
const HELP_MESSAGE = [
|
import { Theme } from './theme';
|
||||||
{ key: '新增同级节点', value: 'Enter 键' },
|
|
||||||
{ key: '新增子节点', value: 'Tab 键' },
|
|
||||||
{ key: '编辑节点文字', value: '双击节点' },
|
|
||||||
{ key: '编辑节点菜单', value: '在节点右键' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const HELP_MESSAGE_STYLE = {
|
|
||||||
width: '200px',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Toolbar = ({ mind }) => {
|
export const Toolbar = ({ mind }) => {
|
||||||
const [template, setTemplateState] = useState('');
|
const [node, setNode] = useState(null); // 当前选择节点
|
||||||
const [theme, setThemeState] = useState('');
|
|
||||||
const [node, setNode] = useState(null);
|
const [hasUndo, toggleHasUndo] = useToggle(false);
|
||||||
|
const [hasRedo, toggleHasRedo] = useToggle(false);
|
||||||
|
|
||||||
const [isBold, toggleIsBold] = useToggle(false);
|
const [isBold, toggleIsBold] = useToggle(false);
|
||||||
const [textColor, setTextColor] = useState('');
|
const [textColor, setTextColor] = useState('');
|
||||||
const [bgColor, setBgColor] = useState('');
|
const [bgColor, setBgColor] = useState('');
|
||||||
const [link, setLink] = useState('');
|
const [link, setLink] = useState('');
|
||||||
const [image, setImage] = useState('');
|
const [image, setImage] = useState('');
|
||||||
|
|
||||||
const setTemplate = useCallback(
|
const [template, setTemplateState] = useState('');
|
||||||
(template) => {
|
const [theme, setThemeState] = useState('');
|
||||||
mind.execCommand('template', template);
|
|
||||||
},
|
|
||||||
[mind]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setTheme = useCallback(
|
/**
|
||||||
(theme) => {
|
* 撤销
|
||||||
mind.execCommand('theme', theme);
|
*/
|
||||||
},
|
const undo = useCallback(() => {
|
||||||
[mind]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setZoom = useCallback(
|
|
||||||
(type: 'minus' | 'plus') => {
|
|
||||||
return () => {
|
|
||||||
if (!mind) return;
|
if (!mind) return;
|
||||||
const currentZoom = mind.getZoomValue();
|
if (mind.editor.history.hasUndo()) {
|
||||||
const nextZoom = clamp(
|
mind.editor.history.undo();
|
||||||
type === 'minus' ? currentZoom - ZOOM_STEP : currentZoom + ZOOM_STEP,
|
}
|
||||||
MIN_ZOOM,
|
|
||||||
MAX_ZOOM
|
|
||||||
);
|
|
||||||
mind.zoom(nextZoom);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
[mind]
|
|
||||||
);
|
|
||||||
|
|
||||||
const setCenter = useCallback(() => {
|
|
||||||
if (!mind) return;
|
|
||||||
mind.execCommand('camera');
|
|
||||||
}, [mind]);
|
}, [mind]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重做
|
||||||
|
*/
|
||||||
|
const redo = useCallback(() => {
|
||||||
|
if (!mind) return;
|
||||||
|
if (mind.editor.history.hasRedo()) {
|
||||||
|
mind.editor.history.redo();
|
||||||
|
}
|
||||||
|
}, [mind]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加粗
|
||||||
|
*/
|
||||||
const toggleBold = useCallback(() => {
|
const toggleBold = useCallback(() => {
|
||||||
|
if (!mind) return;
|
||||||
|
|
||||||
mind.execCommand('Bold');
|
mind.execCommand('Bold');
|
||||||
}, [mind]);
|
}, [mind]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置文字颜色
|
||||||
|
*/
|
||||||
const setFontColor = useCallback(
|
const setFontColor = useCallback(
|
||||||
(color) => {
|
(color) => {
|
||||||
|
if (!mind) return;
|
||||||
|
|
||||||
mind.execCommand('ForeColor', color);
|
mind.execCommand('ForeColor', color);
|
||||||
},
|
},
|
||||||
[mind]
|
[mind]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置背景色
|
||||||
|
*/
|
||||||
const setBackgroundColor = useCallback(
|
const setBackgroundColor = useCallback(
|
||||||
(color) => {
|
(color) => {
|
||||||
|
if (!mind) return;
|
||||||
|
|
||||||
mind.execCommand('Background', color);
|
mind.execCommand('Background', color);
|
||||||
},
|
},
|
||||||
[mind]
|
[mind]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置链接
|
||||||
|
*/
|
||||||
const setHyperLink = useCallback(
|
const setHyperLink = useCallback(
|
||||||
(url) => {
|
(url) => {
|
||||||
|
if (!mind) return;
|
||||||
|
|
||||||
mind.execCommand('HyperLink', url);
|
mind.execCommand('HyperLink', url);
|
||||||
},
|
},
|
||||||
[mind]
|
[mind]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入图片
|
||||||
|
*/
|
||||||
const insertImage = useCallback(
|
const insertImage = useCallback(
|
||||||
(url) => {
|
(url) => {
|
||||||
|
if (!mind) return;
|
||||||
|
|
||||||
mind.execCommand('Image', url);
|
mind.execCommand('Image', url);
|
||||||
},
|
},
|
||||||
[mind]
|
[mind]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置进度
|
||||||
|
*/
|
||||||
|
const setProgress = useCallback(
|
||||||
|
(value) => () => {
|
||||||
|
if (!mind) return;
|
||||||
|
|
||||||
|
const node = mind.getSelectedNode();
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
mind.execCommand('progress', value);
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置优先级
|
||||||
|
*/
|
||||||
|
const setPriority = useCallback(
|
||||||
|
(value) => () => {
|
||||||
|
if (!mind) return;
|
||||||
|
|
||||||
|
const node = mind.getSelectedNode();
|
||||||
|
if (!node) return;
|
||||||
|
|
||||||
|
mind.execCommand('priority', value);
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板
|
||||||
|
*/
|
||||||
|
const setTemplate = useCallback(
|
||||||
|
(template) => {
|
||||||
|
if (!mind) return;
|
||||||
|
|
||||||
|
mind.execCommand('template', template);
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题
|
||||||
|
*/
|
||||||
|
const setTheme = useCallback(
|
||||||
|
(theme) => {
|
||||||
|
if (!mind) return;
|
||||||
|
|
||||||
|
mind.execCommand('theme', theme);
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缩放
|
||||||
|
*/
|
||||||
|
const setZoom = useCallback(
|
||||||
|
(type: 'minus' | 'plus') => {
|
||||||
|
return () => {
|
||||||
|
if (!mind) return;
|
||||||
|
mind.execCommand(type === 'minus' ? 'zoomOut' : 'zoomIn');
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[mind]
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定位到根节点
|
||||||
|
*/
|
||||||
|
const setCenter = useCallback(() => {
|
||||||
|
if (!mind) return;
|
||||||
|
mind.execCommand('camera', mind.getRoot(), 600);
|
||||||
|
}, [mind]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mind) return;
|
if (!mind) return;
|
||||||
|
|
||||||
|
@ -126,6 +206,9 @@ export const Toolbar = ({ mind }) => {
|
||||||
setNode(null);
|
setNode(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toggleHasUndo(mind.editor.history.hasUndo());
|
||||||
|
toggleHasRedo(mind.editor.history.hasRedo());
|
||||||
|
|
||||||
setTemplateState(mind.queryCommandValue('Template'));
|
setTemplateState(mind.queryCommandValue('Template'));
|
||||||
setThemeState(mind.queryCommandValue('Theme'));
|
setThemeState(mind.queryCommandValue('Theme'));
|
||||||
toggleIsBold(isBold);
|
toggleIsBold(isBold);
|
||||||
|
@ -140,68 +223,53 @@ export const Toolbar = ({ mind }) => {
|
||||||
return () => {
|
return () => {
|
||||||
mind.off('interactchange', handler);
|
mind.off('interactchange', handler);
|
||||||
};
|
};
|
||||||
}, [mind, toggleIsBold, setBackgroundColor]);
|
}, [mind, toggleHasUndo, toggleHasRedo, toggleIsBold, setBackgroundColor]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space>
|
<Space>
|
||||||
<Popover
|
<Tooltip content="撤销">
|
||||||
zIndex={10000}
|
<Button
|
||||||
spacing={10}
|
onClick={undo}
|
||||||
style={{ padding: '0 12px 12px', overflow: 'hidden' }}
|
icon={<IconUndo />}
|
||||||
position="bottomLeft"
|
disabled={!hasUndo}
|
||||||
content={
|
theme={hasUndo ? 'light' : 'borderless'}
|
||||||
<section className={styles.sectionWrap}>
|
type="tertiary"
|
||||||
<Text type="secondary">布局</Text>
|
/>
|
||||||
<div>
|
</Tooltip>
|
||||||
<ul>
|
|
||||||
{TEMPLATES.map((item) => {
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={item.label}
|
|
||||||
className={cls(template === item.value && styles.active)}
|
|
||||||
onClick={() => setTemplate(item.value)}
|
|
||||||
>
|
|
||||||
<Text>{item.label}</Text>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button icon={<IconStructure />} type="tertiary" theme="borderless" size="small" />
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<Popover
|
<Tooltip content="重做">
|
||||||
zIndex={10000}
|
<Button
|
||||||
spacing={10}
|
onClick={redo}
|
||||||
style={{ padding: '0 12px 12px', overflow: 'hidden' }}
|
icon={<IconRedo />}
|
||||||
position="bottomLeft"
|
disabled={!hasRedo}
|
||||||
content={
|
theme={hasRedo ? 'light' : 'borderless'}
|
||||||
<section className={styles.sectionWrap}>
|
type="tertiary"
|
||||||
<Text type="secondary">主题</Text>
|
/>
|
||||||
<div>
|
</Tooltip>
|
||||||
<ul>
|
|
||||||
{THEMES.map((item) => {
|
<Divider />
|
||||||
return (
|
|
||||||
<li
|
<Tooltip content="加粗" zIndex={10000}>
|
||||||
key={item.label}
|
<Button
|
||||||
className={cls(theme === item.value && styles.active)}
|
disabled={!node}
|
||||||
style={item.style || {}}
|
theme={isBold ? 'light' : 'borderless'}
|
||||||
onClick={() => setTheme(item.value)}
|
type="tertiary"
|
||||||
>
|
onClick={toggleBold}
|
||||||
{item.label}
|
icon={<IconBold />}
|
||||||
</li>
|
/>
|
||||||
);
|
</Tooltip>
|
||||||
})}
|
|
||||||
</ul>
|
<FontColor selectedNode={node} textColor={textColor} setFontColor={setFontColor} />
|
||||||
</div>
|
<BgColor selectedNode={node} bgColor={bgColor} setBackgroundColor={setBackgroundColor} />
|
||||||
</section>
|
<Link disabled={!node} link={link} setLink={setHyperLink} />
|
||||||
}
|
<Image disabled={!node} image={image} setImage={insertImage} />
|
||||||
>
|
|
||||||
<Button icon={<IconDrawBoard />} type="tertiary" theme="borderless" size="small" />
|
<Divider />
|
||||||
</Popover>
|
|
||||||
|
<Progress selectedNode={node} setProgress={setProgress} />
|
||||||
|
<Priority selectedNode={node} setPriority={setPriority} />
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
<Tooltip content="居中">
|
<Tooltip content="居中">
|
||||||
<Button
|
<Button
|
||||||
|
@ -233,97 +301,14 @@ export const Toolbar = ({ mind }) => {
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content="加粗" zIndex={10000}>
|
<Divider />
|
||||||
<Button
|
|
||||||
disabled={!node}
|
|
||||||
type="tertiary"
|
|
||||||
theme={isBold ? 'light' : 'borderless'}
|
|
||||||
onClick={toggleBold}
|
|
||||||
icon={<IconBold />}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
|
|
||||||
<ColorPicker
|
<Template template={template} setTemplate={setTemplate} />
|
||||||
onSetColor={(color) => {
|
<Theme theme={theme} setTheme={setTheme} />
|
||||||
setFontColor(color);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip content="文本色" zIndex={10000}>
|
|
||||||
<Button
|
|
||||||
disabled={!node}
|
|
||||||
type="tertiary"
|
|
||||||
theme={textColor ? 'light' : 'borderless'}
|
|
||||||
icon={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconFont />
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
width: 12,
|
|
||||||
height: 2,
|
|
||||||
backgroundColor: textColor,
|
|
||||||
}}
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</ColorPicker>
|
|
||||||
|
|
||||||
<ColorPicker
|
<Divider />
|
||||||
onSetColor={(color) => {
|
|
||||||
setBackgroundColor(color);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tooltip content="背景色" zIndex={10000}>
|
|
||||||
<Button
|
|
||||||
disabled={!node}
|
|
||||||
type="tertiary"
|
|
||||||
theme={bgColor ? 'light' : 'borderless'}
|
|
||||||
icon={
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconMark />
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
width: 12,
|
|
||||||
height: 2,
|
|
||||||
backgroundColor: bgColor,
|
|
||||||
}}
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</ColorPicker>
|
|
||||||
|
|
||||||
<Link disabled={!node} link={link} setLink={setHyperLink} />
|
<Help />
|
||||||
|
|
||||||
<Image disabled={!node} image={image} setImage={insertImage} />
|
|
||||||
|
|
||||||
<Popover
|
|
||||||
zIndex={10000}
|
|
||||||
spacing={10}
|
|
||||||
style={{ padding: 12, overflow: 'hidden' }}
|
|
||||||
position="bottomLeft"
|
|
||||||
content={
|
|
||||||
<section className={styles.sectionWrap}>
|
|
||||||
<Descriptions data={HELP_MESSAGE} style={HELP_MESSAGE_STYLE} />
|
|
||||||
</section>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button size="small" theme="borderless" type="tertiary" icon={<IconHelpCircle />} />
|
|
||||||
</Popover>
|
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { IconClock } from '@douyinfe/semi-icons';
|
||||||
|
import { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { PRIORITIES } from '../constant';
|
||||||
|
|
||||||
|
export const Priority = ({ selectedNode, setPriority }) => {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
trigger="click"
|
||||||
|
render={
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{PRIORITIES.map((item) => {
|
||||||
|
return (
|
||||||
|
<Dropdown.Item key={item.value} onClick={setPriority(item.value)}>
|
||||||
|
{item.text}
|
||||||
|
</Dropdown.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Tooltip content="优先级" zIndex={10000}>
|
||||||
|
<Button size="small" theme="borderless" type="tertiary" disabled={!selectedNode} icon={<IconClock />} />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { IconDuration } from '@douyinfe/semi-icons';
|
||||||
|
import { Button, Dropdown, Tooltip } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
import { PROGRESSES } from '../constant';
|
||||||
|
|
||||||
|
export const Progress = ({ selectedNode, setProgress }) => {
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
trigger="click"
|
||||||
|
render={
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{PROGRESSES.map((progress) => {
|
||||||
|
return (
|
||||||
|
<Dropdown.Item key={progress.value} onClick={setProgress(progress.value)}>
|
||||||
|
{progress.text}
|
||||||
|
</Dropdown.Item>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Dropdown.Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<Tooltip content="进度" zIndex={10000}>
|
||||||
|
<Button size="small" theme="borderless" type="tertiary" disabled={!selectedNode} icon={<IconDuration />} />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Button, Popover, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import cls from 'classnames';
|
||||||
|
import { IconStructure } from 'components/icons';
|
||||||
|
|
||||||
|
import { TEMPLATES } from '../constant';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
export const Template = ({ template, setTemplate }) => {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
zIndex={10000}
|
||||||
|
spacing={10}
|
||||||
|
style={{ padding: '0 12px 12px', overflow: 'hidden' }}
|
||||||
|
position="bottomLeft"
|
||||||
|
content={
|
||||||
|
<section className={styles.sectionWrap}>
|
||||||
|
<Text type="secondary">布局</Text>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{TEMPLATES.map((item) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={item.label}
|
||||||
|
className={cls(template === item.value && styles.active)}
|
||||||
|
onClick={() => setTemplate(item.value)}
|
||||||
|
>
|
||||||
|
<Text>{item.label}</Text>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button icon={<IconStructure />} type="tertiary" theme="borderless" size="small" />
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,42 @@
|
||||||
|
import { Button, Popover, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import cls from 'classnames';
|
||||||
|
import { IconDrawBoard } from 'components/icons';
|
||||||
|
|
||||||
|
import { THEMES } from '../constant';
|
||||||
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
export const Theme = ({ theme, setTheme }) => {
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
zIndex={10000}
|
||||||
|
spacing={10}
|
||||||
|
style={{ padding: '0 12px 12px', overflow: 'hidden' }}
|
||||||
|
position="bottomLeft"
|
||||||
|
content={
|
||||||
|
<section className={styles.sectionWrap}>
|
||||||
|
<Text type="secondary">主题</Text>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{THEMES.map((item) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={item.label}
|
||||||
|
className={cls(theme === item.value && styles.active)}
|
||||||
|
style={item.style || {}}
|
||||||
|
onClick={() => setTheme(item.value)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button icon={<IconDrawBoard />} type="tertiary" theme="borderless" size="small" />
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
|
@ -3,7 +3,9 @@ import { NodeViewWrapper } from '@tiptap/react';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { IconMind } from 'components/icons';
|
import { IconMind } from 'components/icons';
|
||||||
import { Resizeable } from 'components/resizeable';
|
import { Resizeable } from 'components/resizeable';
|
||||||
|
import deepEqual from 'deep-equal';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
import React from 'react';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import VisibilitySensor from 'react-visibility-sensor';
|
import VisibilitySensor from 'react-visibility-sensor';
|
||||||
import { Excalidraw } from 'tiptap/core/extensions/excalidraw';
|
import { Excalidraw } from 'tiptap/core/extensions/excalidraw';
|
||||||
|
@ -15,7 +17,7 @@ const { Text } = Typography;
|
||||||
|
|
||||||
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%', maxWidth: '100%' };
|
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%', maxWidth: '100%' };
|
||||||
|
|
||||||
export const ExcalidrawWrapper = ({ editor, node, updateAttributes }) => {
|
export const _ExcalidrawWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
const exportToSvgRef = useRef(null);
|
const exportToSvgRef = useRef(null);
|
||||||
const isEditable = editor.isEditable;
|
const isEditable = editor.isEditable;
|
||||||
const isActive = editor.isActive(Excalidraw.name);
|
const isActive = editor.isActive(Excalidraw.name);
|
||||||
|
@ -111,3 +113,11 @@ export const ExcalidrawWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ExcalidrawWrapper = React.memo(_ExcalidrawWrapper, (prevProps, nextProps) => {
|
||||||
|
if (deepEqual(prevProps.node.attrs, nextProps.node.attrs)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
|
@ -3,7 +3,9 @@ import { NodeViewWrapper } from '@tiptap/react';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { IconFlow, IconMindCenter, IconZoomIn, IconZoomOut } from 'components/icons';
|
import { IconFlow, IconMindCenter, IconZoomIn, IconZoomOut } from 'components/icons';
|
||||||
import { Resizeable } from 'components/resizeable';
|
import { Resizeable } from 'components/resizeable';
|
||||||
|
import deepEqual from 'deep-equal';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
|
import React from 'react';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import VisibilitySensor from 'react-visibility-sensor';
|
import VisibilitySensor from 'react-visibility-sensor';
|
||||||
import { load, renderXml } from 'thirtypart/diagram';
|
import { load, renderXml } from 'thirtypart/diagram';
|
||||||
|
@ -15,7 +17,7 @@ import styles from './index.module.scss';
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%', maxWidth: '100%' };
|
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%', maxWidth: '100%' };
|
||||||
|
|
||||||
export const FlowWrapper = ({ editor, node, updateAttributes }) => {
|
export const _FlowWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
const isEditable = editor.isEditable;
|
const isEditable = editor.isEditable;
|
||||||
const isActive = editor.isActive(Flow.name);
|
const isActive = editor.isActive(Flow.name);
|
||||||
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||||
|
@ -81,7 +83,7 @@ export const FlowWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
|
|
||||||
const onViewportChange = useCallback(
|
const onViewportChange = useCallback(
|
||||||
(visible) => {
|
(visible) => {
|
||||||
if (visible) {
|
if (visible && !$graph.current) {
|
||||||
toggleVisible(true);
|
toggleVisible(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -137,3 +139,11 @@ export const FlowWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const FlowWrapper = React.memo(_FlowWrapper, (prevProps, nextProps) => {
|
||||||
|
if (deepEqual(prevProps.node.attrs, nextProps.node.attrs)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
|
@ -6,10 +6,9 @@ import { Resizeable } from 'components/resizeable';
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import VisibilitySensor from 'react-visibility-sensor';
|
import VisibilitySensor from 'react-visibility-sensor';
|
||||||
import { load, renderMind } from 'thirtypart/kityminder';
|
import { load, renderMind } from 'thirtypart/kityminder';
|
||||||
import { Mind } from 'tiptap/core/extensions/mind';
|
|
||||||
import { MAX_ZOOM, MIN_ZOOM, ZOOM_STEP } from 'tiptap/core/menus/mind/constant';
|
import { MAX_ZOOM, MIN_ZOOM, ZOOM_STEP } from 'tiptap/core/menus/mind/constant';
|
||||||
import { clamp, getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
import { clamp, getEditorContainerDOMSize } from 'tiptap/prose-utils';
|
||||||
|
|
||||||
|
@ -19,10 +18,9 @@ const { Text } = Typography;
|
||||||
|
|
||||||
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%', maxWidth: '100%' };
|
const INHERIT_SIZE_STYLE = { width: '100%', height: '100%', maxWidth: '100%' };
|
||||||
|
|
||||||
export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
export const _MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
const $mind = useRef(null);
|
const $mind = useRef(null);
|
||||||
const isEditable = editor.isEditable;
|
const isEditable = editor.isEditable;
|
||||||
const isActive = editor.isActive(Mind.name);
|
|
||||||
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
const { width: maxWidth } = getEditorContainerDOMSize(editor);
|
||||||
const { data, width, height } = node.attrs;
|
const { data, width, height } = node.attrs;
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
@ -80,7 +78,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
|
|
||||||
const onViewportChange = useCallback(
|
const onViewportChange = useCallback(
|
||||||
(visible) => {
|
(visible) => {
|
||||||
if (visible) {
|
if (visible && !$mind.current) {
|
||||||
toggleVisible(true);
|
toggleVisible(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -108,7 +106,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
}, [width, height, setCenter]);
|
}, [width, height, setCenter]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className={cls(styles.wrap, isActive && styles.isActive)}>
|
<NodeViewWrapper className={cls(styles.wrap)}>
|
||||||
<VisibilitySensor onChange={onViewportChange}>
|
<VisibilitySensor onChange={onViewportChange}>
|
||||||
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
<Resizeable isEditable={isEditable} width={width} height={height} maxWidth={maxWidth} onChangeEnd={onResize}>
|
||||||
<div
|
<div
|
||||||
|
@ -123,7 +121,7 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
|
|
||||||
{loading && <Spin spinning style={INHERIT_SIZE_STYLE}></Spin>}
|
{loading && <Spin spinning style={INHERIT_SIZE_STYLE}></Spin>}
|
||||||
|
|
||||||
{!loading && !error && visible && (
|
{!loading && !error && (
|
||||||
<div style={{ height: '100%', maxHeight: '100%', overflow: 'hidden' }} ref={setMind}></div>
|
<div style={{ height: '100%', maxHeight: '100%', overflow: 'hidden' }} ref={setMind}></div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -165,3 +163,11 @@ export const MindWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const MindWrapper = React.memo(_MindWrapper, (prevProps, nextProps) => {
|
||||||
|
if (deepEqual(prevProps.node.attrs, nextProps.node.attrs)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { NodeViewWrapper } from '@tiptap/react';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { useUser } from 'data/user';
|
import { useUser } from 'data/user';
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
|
@ -24,14 +24,15 @@ export const StatusWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const ref = useRef<HTMLInputElement>();
|
const ref = useRef<HTMLInputElement>();
|
||||||
const [visible, toggleVisible] = useToggle(false);
|
const [visible, toggleVisible] = useToggle(false);
|
||||||
|
const [currentText, setCurrentText] = useState(text);
|
||||||
|
|
||||||
const content = useMemo(
|
const content = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<Tag className="render-wrapper" style={{ backgroundColor: bgcolor, border: `1px solid ${borderColor}` }}>
|
<Tag className="render-wrapper" style={{ backgroundColor: bgcolor, border: `1px solid ${borderColor}` }}>
|
||||||
<span style={{ color: currentTextColor }}>{text || '点击设置状态'}</span>
|
<span style={{ color: currentTextColor }}>{currentText || '点击设置状态'}</span>
|
||||||
</Tag>
|
</Tag>
|
||||||
),
|
),
|
||||||
[bgcolor, borderColor, currentTextColor, text]
|
[bgcolor, borderColor, currentTextColor, currentText]
|
||||||
);
|
);
|
||||||
|
|
||||||
const onVisibleChange = useCallback(
|
const onVisibleChange = useCallback(
|
||||||
|
@ -64,8 +65,10 @@ export const StatusWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
ref.current?.focus();
|
ref.current?.focus();
|
||||||
|
} else {
|
||||||
|
updateAttributes({ text: currentText });
|
||||||
}
|
}
|
||||||
}, [visible]);
|
}, [visible, updateAttributes, currentText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper as="span" className={cls(styles.wrap, 'status')}>
|
<NodeViewWrapper as="span" className={cls(styles.wrap, 'status')}>
|
||||||
|
@ -78,7 +81,7 @@ export const StatusWrapper = ({ editor, node, updateAttributes }) => {
|
||||||
content={
|
content={
|
||||||
<div style={{ width: 184, height: 65 }}>
|
<div style={{ width: 184, height: 65 }}>
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Input ref={ref} placeholder="输入状态" onChange={(v) => updateAttributes({ text: v })} />
|
<Input ref={ref} placeholder="输入状态" value={currentText} onChange={setCurrentText} />
|
||||||
</div>
|
</div>
|
||||||
<Space>
|
<Space>
|
||||||
{STATUS_COLORS.map((color) => {
|
{STATUS_COLORS.map((color) => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Toast } from '@douyinfe/semi-ui';
|
import { Toast } from '@douyinfe/semi-ui';
|
||||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
import { safeJSONParse } from 'helpers/json';
|
||||||
// 自定义节点扩展
|
// 自定义节点扩展
|
||||||
import { Attachment } from 'tiptap/core/extensions/attachment';
|
import { Attachment } from 'tiptap/core/extensions/attachment';
|
||||||
import { BackgroundColor } from 'tiptap/core/extensions/background-color';
|
import { BackgroundColor } from 'tiptap/core/extensions/background-color';
|
||||||
|
@ -78,6 +78,16 @@ const DocumentWithTitle = Document.extend({
|
||||||
|
|
||||||
export { Document };
|
export { Document };
|
||||||
|
|
||||||
|
const placeholders = [
|
||||||
|
'输入 / 唤起更多',
|
||||||
|
'使用 markdown 语法进行输入',
|
||||||
|
'输入 @ 来提及他人',
|
||||||
|
'输入 : 来插入表情',
|
||||||
|
'你知道吗?输入 $katex 然后在输入一个 $ 就可以快速插入数学公式,其他节点操作类似哦',
|
||||||
|
];
|
||||||
|
|
||||||
|
const getCreateUserId = () => safeJSONParse(window.localStorage.getItem('user')).id;
|
||||||
|
|
||||||
export const CollaborationKit = [
|
export const CollaborationKit = [
|
||||||
Paragraph,
|
Paragraph,
|
||||||
Placeholder.configure({
|
Placeholder.configure({
|
||||||
|
@ -88,7 +98,7 @@ export const CollaborationKit = [
|
||||||
|
|
||||||
if (!editor.isEditable) return;
|
if (!editor.isEditable) return;
|
||||||
|
|
||||||
return '输入 / 唤起更多';
|
return placeholders[~~(Math.random() * placeholders.length)];
|
||||||
},
|
},
|
||||||
showOnlyCurrent: false,
|
showOnlyCurrent: false,
|
||||||
showOnlyWhenEditable: false,
|
showOnlyWhenEditable: false,
|
||||||
|
@ -152,11 +162,17 @@ export const CollaborationKit = [
|
||||||
DocumentChildren,
|
DocumentChildren,
|
||||||
DocumentReference,
|
DocumentReference,
|
||||||
Emoji,
|
Emoji,
|
||||||
Flow,
|
Flow.configure({
|
||||||
|
getCreateUserId,
|
||||||
|
}),
|
||||||
Iframe,
|
Iframe,
|
||||||
Katex,
|
Katex.configure({
|
||||||
|
getCreateUserId,
|
||||||
|
}),
|
||||||
Mention,
|
Mention,
|
||||||
Mind,
|
Mind.configure({
|
||||||
|
getCreateUserId,
|
||||||
|
}),
|
||||||
QuickInsert,
|
QuickInsert,
|
||||||
SearchNReplace,
|
SearchNReplace,
|
||||||
Status,
|
Status,
|
||||||
|
|
Loading…
Reference in New Issue