Van 6 år sedan
förälder
incheckning
9fe18fd5ca

+ 7 - 7
demo/demo.js

@@ -4,10 +4,10 @@ import Vditor from '../src/index'
 // import Vditor from '../dist/index.min'
 
 const vditor = new Vditor('vditor', {
-  toolbar: [
-    'emoji', {
-      name: 'bold',
-      icon: '123',
-      tip: 'bbbb',
-    }],
-})
+  toolbar: [{
+    name: 'emoji',
+    tail: '<a href="https://hacpai.com/settings/function" target="_blank">设置常用表情</a>',
+  }],
+})
+
+const vditor2 = new Vditor('vditor2')

+ 1 - 0
demo/index.html

@@ -45,5 +45,6 @@
 </head>
 <body>
 <div id="vditor"></div>
+<div id="vditor2"></div>
 </body>
 </html>

+ 18 - 0
src/assets/_panel.scss

@@ -0,0 +1,18 @@
+/**
+ * panel.
+ *
+ * @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
+ * @version 0.1.0.0, Jan 28, 2019
+ */
+
+.vditor-panel {
+  background-color: #fff;
+  line-height: 20px;
+  position: absolute;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, .2);
+  border-radius: 3px;
+  padding: 5px;
+  z-index: 1;
+  font-size: 14px;
+  display: none;
+}

+ 8 - 0
src/assets/_toolbar.scss

@@ -0,0 +1,8 @@
+/**
+ * toolbar.
+ *
+ * @author <a href="http://vanessa.b3log.org">Liyuan Li</a>
+ * @version 0.1.0.0, Jan 28, 2019
+ */
+.vditor-toolbar {
+}

+ 1 - 1
src/assets/_tooltipped.scss

@@ -14,7 +14,7 @@
   }
 }
 
-.tooltipped {
+.vditor-tooltipped {
   position: relative;
   cursor: pointer;
   &::after {

+ 3 - 1
src/assets/classic.scss

@@ -1 +1,3 @@
-@import "tooltipped";
+@import "tooltipped";
+@import "panel";
+@import "toolbar";

+ 5 - 1
src/index.ts

@@ -3,6 +3,7 @@ import {Toolbar} from "./ts/toolbar/index";
 import {OptionsClass} from "./ts/util/OptionsClass";
 import {Ui} from "./ts/ui/Ui";
 import {Editor} from "./ts/editor/index";
+import {Hotkey} from "./ts/hotkey/index";
 
 class Vditor {
     readonly version: string;
@@ -17,8 +18,11 @@ class Vditor {
         const editorElement: HTMLTextAreaElement = editor.genElement()
 
         const toolbar = new Toolbar(mergedOptions, editorElement)
+        const toolbarElements = toolbar.genElement()
 
-        new Ui(id, toolbar.genElement(), editorElement)
+        new Hotkey(toolbarElements, editorElement, mergedOptions)
+
+        new Ui(id, toolbarElements, editorElement)
     }
 }
 

+ 79 - 2
src/ts/editor/index.ts

@@ -1,4 +1,6 @@
-export class Editor {
+import {commandable} from '../util/commandable'
+
+class Editor {
     element: HTMLTextAreaElement
 
     constructor() {
@@ -8,4 +10,79 @@ export class Editor {
     genElement(): HTMLTextAreaElement {
         return this.element
     }
-}
+}
+
+const insertTextAtCaret = (textarea: HTMLTextAreaElement, prefix: string, suffix: string, replace?: string) => {
+    if (typeof textarea.selectionStart === 'number' &&
+        typeof textarea.selectionEnd === 'number') {
+        const startPos = textarea.selectionStart
+        const endPos = textarea.selectionEnd
+        const tmpStr = textarea.value
+        textarea.focus()
+        if (!commandable()) {
+            if (startPos === endPos) {
+                // no selection
+                textarea.value = tmpStr.substring(0, startPos) + prefix + suffix +
+                    tmpStr.substring(endPos, tmpStr.length)
+                textarea.selectionEnd = textarea.selectionStart = endPos + prefix.length
+            } else {
+                if (replace) {
+                    textarea.value = tmpStr.substring(0, startPos) + prefix + suffix +
+                        tmpStr.substring(endPos, tmpStr.length)
+                    textarea.selectionEnd = startPos + prefix.length + suffix.length
+                } else {
+                    if (tmpStr.substring(startPos - prefix.length, startPos) === prefix &&
+                        tmpStr.substring(endPos, endPos + suffix.length) === suffix) {
+                        // broke circle, avoid repeat
+                        textarea.value = tmpStr.substring(0, startPos - prefix.length) +
+                            tmpStr.substring(startPos, endPos) +
+                            tmpStr.substring(endPos + suffix.length, tmpStr.length)
+                        textarea.selectionStart = startPos - prefix.length
+                        textarea.selectionEnd = endPos - prefix.length
+                    } else {
+                        // insert
+                        textarea.value = tmpStr.substring(0, startPos) + prefix +
+                            tmpStr.substring(startPos, endPos) +
+                            suffix + tmpStr.substring(endPos, tmpStr.length)
+                        textarea.selectionStart = startPos + prefix.length
+                        textarea.selectionEnd = endPos + prefix.length
+                    }
+                }
+            }
+            return
+        }
+        if (startPos === endPos) {
+            // no selection
+            document.execCommand('insertText', false, prefix + suffix)
+            textarea.selectionStart = textarea.selectionEnd = textarea.selectionStart - suffix.length
+        } else {
+            if (replace) {
+                document.execCommand('insertText', false, prefix + suffix)
+            } else {
+                if (tmpStr.substring(startPos - prefix.length, startPos) === prefix &&
+                    tmpStr.substring(endPos, endPos + suffix.length) === suffix) {
+                    // broke circle, avoid repeat
+                    document.execCommand('delete', false)
+                    for (let i = 0, iMax = prefix.length; i < iMax; i++) {
+                        document.execCommand('delete', false)
+                    }
+                    for (let j = 0, jMax = suffix.length; j < jMax; j++) {
+                        document.execCommand('forwardDelete', false)
+                    }
+                    document.execCommand('insertText', false,
+                        tmpStr.substring(startPos, endPos))
+                    textarea.selectionStart = startPos - prefix.length
+                    textarea.selectionEnd = endPos - prefix.length
+                } else {
+                    // insert
+                    document.execCommand('insertText', false,
+                        prefix + tmpStr.substring(startPos, endPos) + suffix)
+                    textarea.selectionStart = startPos + prefix.length
+                    textarea.selectionEnd = endPos + prefix.length
+                }
+            }
+        }
+    }
+}
+
+export {Editor, insertTextAtCaret}

+ 31 - 0
src/ts/hotkey/index.ts

@@ -0,0 +1,31 @@
+export class Hotkey {
+    editorElement: HTMLTextAreaElement
+    toolbarElements: any
+    options: Options
+
+    constructor(toolbarElements: any, editorElement: HTMLTextAreaElement, options: Options) {
+        this.editorElement = editorElement
+        this.toolbarElements = toolbarElements
+        this.options = options
+        this.bindHotkey()
+    }
+
+    bindHotkey(): void {
+        this.editorElement.addEventListener('keydown', (event) => {
+            this.options.toolbar.forEach((menuItem: MenuItem) => {
+                const hotkeys = menuItem.hotkey.split(' ')
+                if ((hotkeys[0] === 'ctrl' || hotkeys[0] === '⌘') && (event.metaKey || event.ctrlKey)) {
+                    if (event.key === hotkeys[1]) {
+                        if (menuItem.name === 'emoji') {
+                            // TODO: panel
+                        } else {
+                            this.toolbarElements[menuItem.name].click()
+                        }
+                        event.preventDefault()
+                        event.stopPropagation()
+                    }
+                }
+            })
+        })
+    }
+}

+ 2 - 3
src/ts/toolbar/Bold.ts

@@ -2,8 +2,7 @@ import boldSVG from "../../assets/icons/bold.svg";
 import {MenuItemClass} from "./MenuItemClass";
 
 export class Bold extends MenuItemClass {
-    genElement(menuItem: MenuItem, i18n: string, editorElement: HTMLTextAreaElement): HTMLElement {
-        menuItem.icon = menuItem.icon || boldSVG
-        return super.genElement(menuItem, i18n, editorElement)
+    genElement(menuItem: MenuItem, lang: string, editorElement: HTMLTextAreaElement): HTMLElement {
+        return super.genElement(Object.assign({}, menuItem, {icon: menuItem.icon || boldSVG}), lang, editorElement)
     }
 }

+ 22 - 3
src/ts/toolbar/Emoji.ts

@@ -1,9 +1,28 @@
 import emojiSVG from "../../assets/icons/emoji.svg";
 import {MenuItemClass} from "./MenuItemClass";
+import {i18n} from "../i18n/index";
 
 export class Emoji extends MenuItemClass {
-    genElement(menuItem: MenuItem, i18n: string, editorElement:HTMLTextAreaElement): HTMLElement {
-        menuItem.icon = menuItem.icon || emojiSVG
-        return super.genElement(menuItem, i18n, editorElement)
+    element: HTMLElement
+
+    genElement(menuItem: MenuItem, lang: string, editorElement: HTMLTextAreaElement): HTMLElement {
+        this.element = document.createElement('div')
+
+        this.element.className = 'vditor-tooltipped vditor-tooltipped__e'
+        this.element.setAttribute('aria-label', menuItem.tip || i18n[lang][menuItem.name])
+        this.element.innerHTML = menuItem.icon || emojiSVG
+        const emojiPanelElement = document.createElement('div')
+        emojiPanelElement.className = 'vditor-panel'
+
+        if (menuItem.tail) {
+            emojiPanelElement.innerHTML = `<div>${menuItem.tail}</div>`
+            this.element.appendChild(emojiPanelElement)
+        }
+
+        this.element.addEventListener('click', () => {
+            emojiPanelElement.style.display = emojiPanelElement.style.display === 'block' ? 'none' : 'block'
+        })
+
+        return this.element
     }
 }

+ 5 - 3
src/ts/toolbar/MenuItemClass.ts

@@ -1,4 +1,5 @@
 import {i18n} from "../i18n/index";
+import {insertTextAtCaret} from "../editor/index";
 
 
 export class MenuItemClass {
@@ -11,8 +12,9 @@ export class MenuItemClass {
         this.editorElement = editorElement
         this.element = document.createElement('div')
 
-        const tip = this.menuItem.tip || i18n[lang][this.menuItem.name]
-        this.element.innerHTML = `<span class="tooltipped tooltipped__e" aria-label="${tip}">${this.menuItem.icon}</span>`
+        this.element.className = 'vditor-tooltipped vditor-tooltipped__e'
+        this.element.setAttribute('aria-label', this.menuItem.tip || i18n[lang][this.menuItem.name])
+        this.element.innerHTML = this.menuItem.icon
 
         this.bindEvent()
         return this.element
@@ -20,7 +22,7 @@ export class MenuItemClass {
 
     private bindEvent() {
         this.element.addEventListener('click', () => {
-            this.editorElement.value = JSON.stringify(this.menuItem)
+            insertTextAtCaret(this.editorElement, this.menuItem.prefix || '', this.menuItem.suffix || '')
         })
     }
 }

+ 3 - 34
src/ts/toolbar/index.ts

@@ -2,31 +2,16 @@ import {Emoji} from './emoji'
 import {Bold} from './Bold'
 
 export class Toolbar {
-    menuElements: Array<HTMLElement>
+    menuElements: any
     private options: Options
-    private toolbar: Array<MenuItem> = [
-        {
-            name: 'emoji',
-            icon: '',
-            tip: '',
-            hotkey: 'emoji hotkey'
-        },
-        {
-            name: 'bold',
-            icon: '',
-            tip: '',
-            hotkey: 'bold hotkey'
-        }
-    ]
 
     constructor(options: Options, editorElement: HTMLTextAreaElement) {
         this.options = options
         this.menuElements = []
 
         this.options.toolbar.forEach((menuItem: MenuItem) => {
-            const currentMenuItem = this.getMenuItem(menuItem)
             let menuItemObj
-            switch (currentMenuItem.name) {
+            switch (menuItem.name) {
                 case 'emoji':
                     menuItemObj = new Emoji()
                     break
@@ -37,26 +22,10 @@ export class Toolbar {
                     console.log('menu item no matched')
                     break
             }
-            this.menuElements.push(menuItemObj.genElement(currentMenuItem, options.i18n, editorElement))
+            this.menuElements[menuItem.name] = menuItemObj.genElement(menuItem, options.i18n, editorElement)
         })
     }
 
-    private getMenuItem(menuItem: MenuItem): MenuItem {
-        let currentMenuItem: MenuItem
-
-        this.toolbar.forEach((data: MenuItem) => {
-            if (typeof menuItem === 'string') {
-                if (data.name === menuItem) {
-                    currentMenuItem = data
-                }
-            } else {
-                currentMenuItem = Object.assign({}, data, menuItem)
-            }
-        })
-
-        return currentMenuItem
-    }
-
     genElement() {
         return this.menuElements
     }

+ 23 - 20
src/ts/types/custom.d.ts

@@ -1,6 +1,6 @@
 declare module "*.svg" {
-    const content: string;
-    export default content;
+    const content: string
+    export default content
 }
 
 interface Classes {
@@ -8,31 +8,34 @@ interface Classes {
 }
 
 interface Upload {
-    imgPath: string;
-    max: number;
-    LinkToImgPath: string;
+    imgPath: string
+    max: number
+    LinkToImgPath: string
 }
 
 interface MenuItem {
-    name: string;
-    icon?: string;
-    tip?: string;
-    hotkey?: string;
+    name: string
+    icon?: string
+    tip?: string
+    hotkey?: string
+    suffix?: string
+    prefix?: string
+    tail?: string
 }
 
 interface Options {
-    height?: number;
-    width?: number | string;
-    theme?: string;
-    placeholder?: string;
+    height?: number
+    width?: number | string
+    theme?: string
+    placeholder?: string
     i18n?: string
-    draggable?: boolean;
-    previewShow?: boolean;
+    draggable?: boolean
+    previewShow?: boolean
     counter?: number
-    upload?: Upload;
-    classes?: Classes;
-    staticServePath?: string;
-    atUserCallback?: object | string;
-    commonEmoji?: object;
+    upload?: Upload
+    classes?: Classes
+    staticServePath?: string
+    atUserCallback?: object | string
+    commonEmoji?: object
     toolbar?: Array<string | MenuItem>
 }

+ 8 - 3
src/ts/ui/Ui.ts

@@ -1,9 +1,14 @@
 export class Ui {
-    constructor(id: string, toolbar: Array<HTMLElement>, editor: HTMLElement) {
+    constructor(id: string, toolbar: any, editor: HTMLElement) {
         const vditorElement = document.getElementById(id)
-        toolbar.forEach((element) => {
-            vditorElement.appendChild(element)
+
+        const toolbarElement = document.createElement('div')
+        // toolbarElement.className = 'vditor-toolbar'
+        Object.keys(toolbar).forEach((key) => {
+            toolbarElement.appendChild(toolbar[key])
         })
+
+        vditorElement.appendChild(toolbarElement)
         vditorElement.appendChild(editor)
     }
 }

+ 34 - 3
src/ts/util/OptionsClass.ts

@@ -27,15 +27,46 @@ export class OptionsClass {
             "8ball": "🎱",
             "a": "🅰",
         },
-        toolbar: ['emoji', 'bold']
+        toolbar: [{
+            name: 'emoji',
+            hotkey: '⌘ /'
+        }, {
+            name: 'bold',
+            prefix: '**',
+            suffix: '**',
+            hotkey: '⌘ b'
+        }]
     }
 
     constructor(options: Options) {
         this.options = options
-        this.merge()
     }
 
     merge(): Options {
-        return Object.assign({}, this.defaultOptions, this.options)
+        let toolbar: Array<MenuItem> = []
+        if (this.options && this.options.toolbar) {
+            this.options.toolbar.forEach((menuItem) => {
+                let currentMenuItem: MenuItem
+                this.defaultOptions.toolbar.forEach((defaultMenuItem: MenuItem) => {
+                    if (typeof menuItem === 'string' && defaultMenuItem.name === menuItem) {
+                        currentMenuItem = defaultMenuItem
+                    }
+                    if (typeof menuItem === 'object' && defaultMenuItem.name === menuItem.name) {
+                        currentMenuItem = Object.assign({}, defaultMenuItem, menuItem)
+                    }
+                })
+                toolbar.push(currentMenuItem)
+            })
+        }
+
+        const mergedOptions = Object.assign({}, this.defaultOptions, this.options)
+
+        if (toolbar.length > 0) {
+            mergedOptions.toolbar = toolbar
+        }
+
+        console.log(this.defaultOptions, mergedOptions)
+
+        return mergedOptions
     }
 }

+ 7 - 0
src/ts/util/commandable.ts

@@ -0,0 +1,7 @@
+export const commandable = (): boolean => {
+    if (/firefox/i.test(navigator.userAgent) || /edge/i.test(navigator.userAgent)
+        || /msie/i.test(navigator.userAgent) || /trident/i.test(navigator.userAgent)) {
+        return false
+    }
+    return true
+}