From 2ea347589919915979a14ec95ba14b4c85f7eab7 Mon Sep 17 00:00:00 2001
From: pftom <1043269994@qq.com>
Date: Sun, 16 May 2021 23:36:12 +0800
Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E5=88=9D=E6=AD=A5=E5=AE=8C?=
=?UTF-8?q?=E6=88=90=E7=AC=AC=E4=B8=80=E7=89=88=E4=BE=A7=E8=BE=B9=E6=A0=8F?=
=?UTF-8?q?=E8=8F=9C=E5=8D=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
package.json | 2 +
src/App.vue | 212 ++---------
src/components/BlockMenu.vue | 577 +++++++++++++++++++++++++++++
src/components/BlockMenuItem.vue | 82 ++++
src/components/LinkEditor.vue | 3 +-
src/components/LinkToolbar.vue | 11 +-
src/extensions/BlockMenuTrigger.js | 180 +++++++++
src/extensions/index.js | 1 +
src/main.js | 22 +-
src/menus/block.js | 112 ++++++
src/nodes/Notice.js | 2 +
src/utils/dictionary.js | 3 +
src/utils/environment.js | 7 +
yarn.lock | 23 +-
14 files changed, 1058 insertions(+), 179 deletions(-)
create mode 100644 src/components/BlockMenu.vue
create mode 100644 src/components/BlockMenuItem.vue
create mode 100644 src/extensions/BlockMenuTrigger.js
create mode 100644 src/menus/block.js
create mode 100644 src/utils/environment.js
diff --git a/package.json b/package.json
index 9ec6dc0..a04691e 100644
--- a/package.json
+++ b/package.json
@@ -19,6 +19,7 @@
"core-js": "^3.6.5",
"element-ui": "^2.15.1",
"highlight.js": "^10.6.0",
+ "lodash": "^4.17.21",
"lodash.some": "^4.6.0",
"medium-zoom": "^1.0.6",
"monaco-editor": "^0.23.0",
@@ -29,6 +30,7 @@
"prosemirror-tables": "^1.1.1",
"prosemirror-utils": "0.9.6",
"prosemirror-view": "^1.18.0",
+ "smooth-scroll-into-view-if-needed": "^1.1.32",
"tiptap": "^1.32.1",
"tiptap-extensions": "^1.35.1",
"vue": "^2.6.11"
diff --git a/src/App.vue b/src/App.vue
index d7ba87f..206bc7b 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,171 +1,6 @@
+
+
diff --git a/src/components/BlockMenuItem.vue b/src/components/BlockMenuItem.vue
new file mode 100644
index 0000000..5b4015f
--- /dev/null
+++ b/src/components/BlockMenuItem.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
diff --git a/src/components/LinkEditor.vue b/src/components/LinkEditor.vue
index 128cd3f..cdd5969 100644
--- a/src/components/LinkEditor.vue
+++ b/src/components/LinkEditor.vue
@@ -46,6 +46,7 @@ export default {
"onSearchLink",
"onClickLink",
"onRemoveLink",
+ "onShowToast",
],
components: {
CustomTooltip,
@@ -80,8 +81,6 @@ export default {
},
showCreateLink() {
const value = this.value;
- const results =
- this.results[value.trim()] || this.results[this.previousValue] || [];
const looksLikeUrl = value.match(/^https?:\/\//i);
diff --git a/src/components/LinkToolbar.vue b/src/components/LinkToolbar.vue
index 7400472..8955013 100644
--- a/src/components/LinkToolbar.vue
+++ b/src/components/LinkToolbar.vue
@@ -4,9 +4,10 @@
v-if="active"
:from="view.state.selection.from"
:to="view.state.selection.to"
- :on-create-link="onCreateLink"
- :on-select-link="onSelectLink"
- :on-remove-link="onClose"
+ :view="view"
+ :onCreateLink="onCreateLink"
+ :onSelectLink="handleOnSelectLink"
+ :onRemoveLink="onClose"
>
@@ -35,6 +36,9 @@ export default {
tooltip: Object,
dictionary: Object,
onCreateLink: Function,
+ onSearchLink: Function,
+ onClickLink: Function,
+ onShowToast: Function,
onClose: Function,
},
components: {
@@ -51,6 +55,7 @@ export default {
active() {
return isActive({
view: this.view,
+ isActive: this.isActive,
});
},
},
diff --git a/src/extensions/BlockMenuTrigger.js b/src/extensions/BlockMenuTrigger.js
new file mode 100644
index 0000000..14f843b
--- /dev/null
+++ b/src/extensions/BlockMenuTrigger.js
@@ -0,0 +1,180 @@
+import { InputRule } from "prosemirror-inputrules";
+import { Plugin } from "prosemirror-state";
+import { isInTable } from "prosemirror-tables";
+import { Decoration, DecorationSet } from "prosemirror-view";
+import { Extension } from "tiptap";
+import { findParentNode } from "prosemirror-utils";
+
+const MAX_MATCH = 500;
+const OPEN_REGEX = /^\/(\w+)?$/;
+const CLOSE_REGEX = /(^(?!\/(\w+)?)(.*)$|^\/((\w+)\s.*|\s)$)/;
+
+// based on the input rules code in Prosemirror, here:
+// https://github.com/ProseMirror/prosemirror-inputrules/blob/master/src/inputrules.js
+function run(view, from, to, regex, handler) {
+ if (view.composing) {
+ return false;
+ }
+ const state = view.state;
+ const $from = state.doc.resolve(from);
+ if ($from.parent.type.spec.code) {
+ return false;
+ }
+
+ const textBefore = $from.parent.textBetween(
+ Math.max(0, $from.parentOffset - MAX_MATCH),
+ $from.parentOffset,
+ null,
+ "\ufffc"
+ );
+
+ const match = regex.exec(textBefore);
+ const tr = handler(state, match, match ? from - match[0].length : from, to);
+ if (!tr) return false;
+ return true;
+}
+
+export default class BlockMenuTrigger extends Extension {
+ get name() {
+ return "blockmenu";
+ }
+
+ get plugins() {
+ const button = document.createElement("button");
+ button.className = "block-menu-trigger";
+ const icon = document.createElement("span");
+ icon.innerHTML = "+";
+ button.appendChild(icon);
+
+ return [
+ new Plugin({
+ props: {
+ handleClick: () => {
+ this.options.onClose();
+ return false;
+ },
+ handleKeyDown: (view, event) => {
+ // Prosemirror input rules are not triggered on backspace, however
+ // we need them to be evaluted for the filter trigger to work
+ // correctly. This additional handler adds inputrules-like handling.
+ if (event.key === "Backspace") {
+ // timeout ensures that the delete has been handled by prosemirror
+ // and any characters removed, before we evaluate the rule.
+ setTimeout(() => {
+ const { pos } = view.state.selection.$from;
+ return run(view, pos, pos, OPEN_REGEX, (state, match) => {
+ if (match) {
+ this.options.onOpen(match[1]);
+ } else {
+ this.options.onClose();
+ }
+ return null;
+ });
+ });
+ }
+
+ // If the query is active and we're navigating the block menu then
+ // just ignore the key events in the editor itself until we're done
+ if (
+ event.key === "Enter" ||
+ event.key === "ArrowUp" ||
+ event.key === "ArrowDown" ||
+ event.key === "Tab"
+ ) {
+ const { pos } = view.state.selection.$from;
+
+ return run(view, pos, pos, OPEN_REGEX, (state, match) => {
+ // just tell Prosemirror we handled it and not to do anything
+ return match ? true : null;
+ });
+ }
+
+ return false;
+ },
+ decorations: (state) => {
+ const parent = findParentNode(
+ (node) => node.type.name === "paragraph"
+ )(state.selection);
+
+ if (!parent) {
+ return;
+ }
+
+ const decorations = [];
+ const isEmpty = parent && parent.node.content.size === 0;
+ const isSlash = parent && parent.node.textContent === "/";
+ const isTopLevel = state.selection.$from.depth === 1;
+
+ if (isTopLevel) {
+ if (isEmpty) {
+ decorations.push(
+ Decoration.widget(parent.pos, () => {
+ button.addEventListener("click", () => {
+ this.options.onOpen("");
+ });
+ return button;
+ })
+ );
+
+ decorations.push(
+ Decoration.node(
+ parent.pos,
+ parent.pos + parent.node.nodeSize,
+ {
+ class: "placeholder",
+ "data-empty-text": this.options.dictionary.newLineEmpty,
+ }
+ )
+ );
+ }
+
+ if (isSlash) {
+ decorations.push(
+ Decoration.node(
+ parent.pos,
+ parent.pos + parent.node.nodeSize,
+ {
+ class: "placeholder",
+ "data-empty-text": ` ${this.options.dictionary.newLineWithSlash}`,
+ }
+ )
+ );
+ }
+
+ return DecorationSet.create(state.doc, decorations);
+ }
+
+ return;
+ },
+ },
+ }),
+ ];
+ }
+
+ inputRules() {
+ return [
+ // main regex should match only:
+ // /word
+ new InputRule(OPEN_REGEX, (state, match) => {
+ if (
+ match &&
+ state.selection.$from.parent.type.name === "paragraph" &&
+ !isInTable(state)
+ ) {
+ this.options.onOpen(match[1]);
+ }
+ return null;
+ }),
+ // invert regex should match some of these scenarios:
+ // /word
+ // /
+ // /word
+ new InputRule(CLOSE_REGEX, (state, match) => {
+ if (match) {
+ this.options.onClose();
+ }
+ return null;
+ }),
+ ];
+ }
+}
diff --git a/src/extensions/index.js b/src/extensions/index.js
index 7b8937b..46eb3d9 100644
--- a/src/extensions/index.js
+++ b/src/extensions/index.js
@@ -1,2 +1,3 @@
export { default as Doc } from "./Doc";
export { default as Title } from "./Title";
+export { default as BlockMenuTrigger } from "./BlockMenuTrigger";
diff --git a/src/main.js b/src/main.js
index 4279ed9..964b87b 100644
--- a/src/main.js
+++ b/src/main.js
@@ -16,6 +16,16 @@ import {
faAlignCenter,
faAlignRight,
faAlignLeft,
+ faHeading,
+ faListOl,
+ faListUl,
+ faCheck,
+ faTable,
+ faQuoteLeft,
+ faFileCode,
+ faGripHorizontal,
+ faImage,
+ faTint,
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import App from "./App.vue";
@@ -34,7 +44,17 @@ library.add(
faChevronUp,
faAlignCenter,
faAlignRight,
- faAlignLeft
+ faAlignLeft,
+ faListOl,
+ faListUl,
+ faHeading,
+ faCheck,
+ faTable,
+ faQuoteLeft,
+ faFileCode,
+ faGripHorizontal,
+ faImage,
+ faTint
);
Vue.component("font-awesome-icon", FontAwesomeIcon);
diff --git a/src/menus/block.js b/src/menus/block.js
new file mode 100644
index 0000000..6dd9afb
--- /dev/null
+++ b/src/menus/block.js
@@ -0,0 +1,112 @@
+import { IS_MAC } from "../utils/environment";
+
+const mod = IS_MAC ? "⌘" : "ctrl";
+
+export default function blockMenuItems(dictionary) {
+ return [
+ {
+ name: "heading",
+ title: dictionary.h1,
+ keywords: "h1 heading1 title",
+ icon: "heading",
+ shortcut: "^ ⇧ 1",
+ attrs: { level: 1 },
+ },
+ {
+ name: "heading",
+ title: dictionary.h2,
+ keywords: "h2 heading2",
+ icon: "heading",
+ shortcut: "^ ⇧ 2",
+ attrs: { level: 2 },
+ },
+ {
+ name: "heading",
+ title: dictionary.h3,
+ keywords: "h3 heading3",
+ icon: "heading",
+ shortcut: "^ ⇧ 3",
+ attrs: { level: 3 },
+ },
+ {
+ name: "separator",
+ },
+ {
+ name: "todo_list",
+ title: dictionary.checkboxList,
+ icon: "check",
+ keywords: "checklist checkbox task",
+ shortcut: "^ ⇧ 7",
+ },
+ {
+ name: "bullet_list",
+ title: dictionary.bulletList,
+ icon: "list-ul",
+ shortcut: "^ ⇧ 8",
+ },
+ {
+ name: "ordered_list",
+ title: dictionary.orderedList,
+ icon: "list-ol",
+ shortcut: "^ ⇧ 9",
+ },
+ {
+ name: "separator",
+ },
+ {
+ name: "table",
+ title: dictionary.table,
+ icon: "table",
+ attrs: { rowsCount: 3, colsCount: 3 },
+ },
+ {
+ name: "blockquote",
+ title: dictionary.quote,
+ icon: "quote-left",
+ shortcut: `${mod} ]`,
+ },
+ {
+ name: "code_block",
+ title: dictionary.codeBlock,
+ icon: "file-code",
+ shortcut: "^ ⇧ \\",
+ keywords: "script",
+ },
+ {
+ name: "horizontal_rule",
+ title: dictionary.hr,
+ icon: "grip-horizontal",
+ shortcut: `${mod} _`,
+ keywords: "horizontal rule break line",
+ },
+ {
+ name: "image",
+ title: dictionary.image,
+ icon: "image",
+ keywords: "picture photo",
+ },
+ {
+ name: "link",
+ title: dictionary.link,
+ icon: "link",
+ shortcut: `${mod} k`,
+ keywords: "link url uri href",
+ },
+ {
+ name: "separator",
+ },
+ {
+ name: "diff_block",
+ title: dictionary.diffBlock,
+ icon: "tint",
+ keywords: "container_notice card information",
+ },
+ {
+ name: "notice",
+ title: dictionary.default,
+ icon: "tint",
+ keywords: "container_notice card information",
+ attrs: { style: "default" },
+ },
+ ];
+}
diff --git a/src/nodes/Notice.js b/src/nodes/Notice.js
index 7ad76e8..49b387c 100644
--- a/src/nodes/Notice.js
+++ b/src/nodes/Notice.js
@@ -24,6 +24,7 @@ function getStyleFromRawMatch(rawMatch) {
export default class Notice extends Node {
get styleOptions() {
+ console.log("options", this.options);
return Object.entries({
default: this.options.dictionary.default,
primary: this.options.dictionary.primary,
@@ -82,6 +83,7 @@ export default class Notice extends Node {
}
commands({ type }) {
+ console.log("type", type);
return (attrs) => toggleWrap(type, attrs);
}
diff --git a/src/utils/dictionary.js b/src/utils/dictionary.js
index d1f739c..51a99d9 100644
--- a/src/utils/dictionary.js
+++ b/src/utils/dictionary.js
@@ -8,6 +8,7 @@ const base = {
danger: "Danger",
// Table
+ table: "Table",
addColumnAfter: "Insert column after",
addColumnBefore: "Insert column before",
addRowAfter: "Insert row after",
@@ -57,6 +58,8 @@ const base = {
strikethrough: "Strikethrough",
strong: "Bold",
subheading: "Subheading",
+
+ diffBlock: "DiffBlock",
};
export default base;
diff --git a/src/utils/environment.js b/src/utils/environment.js
new file mode 100644
index 0000000..313f7cf
--- /dev/null
+++ b/src/utils/environment.js
@@ -0,0 +1,7 @@
+export const IS_MAC =
+ typeof window != "undefined" &&
+ /Mac|iPod|iPhone|iPad/.test(window.navigator.platform);
+
+export const IS_FIREFOX =
+ typeof navigator !== "undefined" &&
+ /^(?!.*Seamonkey)(?=.*Firefox).*/i.test(navigator.userAgent);
diff --git a/yarn.lock b/yarn.lock
index f9202f8..8178188 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3045,6 +3045,11 @@ compression@^1.7.4:
safe-buffer "5.1.2"
vary "~1.1.2"
+compute-scroll-into-view@^1.0.17:
+ version "1.0.17"
+ resolved "https://registry.npm.taobao.org/compute-scroll-into-view/download/compute-scroll-into-view-1.0.17.tgz?cache=0&sync_timestamp=1614042424875&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcompute-scroll-into-view%2Fdownload%2Fcompute-scroll-into-view-1.0.17.tgz#6a88f18acd9d42e9cf4baa6bec7e0522607ab7ab"
+ integrity sha1-aojxis2dQunPS6pr7H4FImB6t6s=
+
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.npm.taobao.org/concat-map/download/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@@ -6669,9 +6674,9 @@ lodash@4.17.14:
resolved "https://registry.npm.taobao.org/lodash/download/lodash-4.17.14.tgz?cache=0&sync_timestamp=1613835817439&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.14.tgz#9ce487ae66c96254fe20b599f21b6816028078ba"
integrity sha1-nOSHrmbJYlT+ILWZ8htoFgKAeLo=
-lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@~4.17.10:
+lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.3, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@~4.17.10:
version "4.17.21"
- resolved "https://registry.npm.taobao.org/lodash/download/lodash-4.17.21.tgz?cache=0&sync_timestamp=1613835817439&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Flodash%2Fdownload%2Flodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
+ resolved "https://registry.npm.taobao.org/lodash/download/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha1-Z5WRxWTDv/quhFTPCz3zcMPWkRw=
log-symbols@^1.0.2:
@@ -9242,6 +9247,13 @@ schema-utils@^3.0.0:
ajv "^6.12.5"
ajv-keywords "^3.5.2"
+scroll-into-view-if-needed@^2.2.28:
+ version "2.2.28"
+ resolved "https://registry.npm.taobao.org/scroll-into-view-if-needed/download/scroll-into-view-if-needed-2.2.28.tgz#5a15b2f58a52642c88c8eca584644e01703d645a"
+ integrity sha1-WhWy9YpSZCyIyOylhGROAXA9ZFo=
+ dependencies:
+ compute-scroll-into-view "^1.0.17"
+
scss-tokenizer@^0.2.3:
version "0.2.3"
resolved "https://registry.npm.taobao.org/scss-tokenizer/download/scss-tokenizer-0.2.3.tgz#8eb06db9a9723333824d3f5530641149847ce5d1"
@@ -9464,6 +9476,13 @@ slice-ansi@^2.1.0:
astral-regex "^1.0.0"
is-fullwidth-code-point "^2.0.0"
+smooth-scroll-into-view-if-needed@^1.1.32:
+ version "1.1.32"
+ resolved "https://registry.nlark.com/smooth-scroll-into-view-if-needed/download/smooth-scroll-into-view-if-needed-1.1.32.tgz#57718cb2caa5265ade3e96006dfcf28b2fdcfca0"
+ integrity sha1-V3GMssqlJlrePpYAbfzyiy/c/KA=
+ dependencies:
+ scroll-into-view-if-needed "^2.2.28"
+
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.npm.taobao.org/snapdragon-node/download/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"