Refactor inline image to support paste/drop image (#1028)
* feat: image setting * opti: inline image * add imageSelectAction * remove axios from muya * update image selector * finish image selector ui * add load success style * delete image by click delete icon * opti structure of image html * handle arrow key * enter to edit * image preview by press space * handle backspace when the previous element is image wrapper * update codes for change another PC * emable select all in input * handle arrow and backspace key * create a new paragraph after the last paragraph if its not empty * handle backspace when the previous element is image wrapper * handle enter event in image selector * rewrite auto show image selector * modify image folder * copy file to folder * select image * handle paste image * picgo * guess image path from clipboard * drag and drop image to Mark Text * add github uploader * remove unused codes * remove unused codes * rewrite image path auto complete * support `path` imageInsertAction * doc: add image uploader doc * remove debug codes * set init value in image uploader page * fix typo * remove unused codes * drag web image to Mark Text * add save notification * opti uploading process * fix did not close image selector bug * check image content type when drag web link image * fix: unable to preview relative path image. * emit change event after paste/drop image * add url map in image selector * feat: screenshot and auto insert the screenshot image * update error handler * feat: use the native screencapture command line on macOs system * opti: drop image * fix: handle enter error when cursor is after a image * fix: hasOwnProperty error * remove debug codes * fix: backspace when the previous ele is image * fix: CI error and optimize some codes * use hash of file path to generate the copied filename * change default imageInsertAction to `path` * fix: typo * remove some unused codes and opti get image file name * fix some bugs and opti codes * update image edit icon * romove screen capture on Linux and Windows * fix: conflict * fix error that can not insert image after the existed image or before existed image
@ -8,13 +8,14 @@ const getLicenses = (rootDir, callback) => {
|
||||
production: true,
|
||||
development: false,
|
||||
direct: true,
|
||||
excludePackages: 'xmldom@0.1.27', // xmldom@0.1.27 is under MIT License, but license-checker show it's under LGPL License.
|
||||
json: true,
|
||||
onlyAllow: 'Unlicense;WTFPL;ISC;MIT;BSD;ISC;Apache-2.0;MIT*;Apache*;BSD*',
|
||||
customPath: {
|
||||
"licenses": "",
|
||||
"licenseText": "none"
|
||||
licenses: '',
|
||||
licenseText: 'none'
|
||||
}
|
||||
}, function(err, packages) {
|
||||
}, function (err, packages) {
|
||||
callback(err, packages, checker)
|
||||
})
|
||||
}
|
||||
|
@ -169,6 +169,7 @@ const rendererConfig = {
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'main': path.join(__dirname, '../src/main'),
|
||||
'@': path.join(__dirname, '../src/renderer'),
|
||||
'common': path.join(__dirname, '../src/common'),
|
||||
'muya': path.join(__dirname, '../src/muya'),
|
||||
|
4
.github/CONTRIBUTING.md
vendored
@ -81,8 +81,8 @@ Before you can get started developing, you need set up your build environment:
|
||||
- libx11 (dev)
|
||||
- libxkbfile (dev)
|
||||
|
||||
On Debian-based Linux: `sudo apt-get install libx11-dev libxkbfile-dev`
|
||||
On Red Hat-based Linux: `sudo dnf install libx11-devel libxkbfile-devel`
|
||||
On Debian-based Linux: `sudo apt-get install libx11-dev libxkbfile-dev libsecret-1-dev`
|
||||
On Red Hat-based Linux: `sudo dnf install libx11-devel libxkbfile-devel libsecret-devel`
|
||||
|
||||
**Let's build:**
|
||||
|
||||
|
@ -31,6 +31,8 @@ addons:
|
||||
# atom/keyboard-layout
|
||||
- libx11-dev
|
||||
- libxkbfile-dev
|
||||
# atom/node-keytar
|
||||
- libsecret-1-dev
|
||||
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
|
||||
|
23
doc/Image Uploader Configration.md
Normal file
@ -0,0 +1,23 @@
|
||||
#### Image Uploader Configration
|
||||
|
||||
##### SM.MS
|
||||
|
||||
No need to config, it's free uploading service, thanks!
|
||||
|
||||
##### GitHub
|
||||
|
||||
1. Step 1, Create a GitHub [repo](https://github.com/new).
|
||||
|
||||

|
||||
|
||||
2. Step 2, Create a GitHub token in [Settings/Developer settings.](https://github.com/settings/tokens)
|
||||
|
||||

|
||||
|
||||
3. Config in Mark Text Preferences window. click `CmdOrCtrl + ,` to open Mark Text Preferences window.
|
||||
|
||||

|
||||
|
||||
4. Input you `token`, `owner name` and `repo name` whick you just created. Click `Save` and `Set As default Uploader`.
|
||||
|
||||
5. Paste an image into Mark Text and open you created repo to see the uploaded image.
|
@ -57,6 +57,7 @@ Here is an example:
|
||||
| `editFindPrevious` | Continue the search and find the previous match |
|
||||
| `editReplace` | Replace the information with a replacement |
|
||||
| `editAidou` | Show Aidou dialog |
|
||||
| `editScreenshot` | Get the screenshot |
|
||||
|
||||
**Paragraph menu:**
|
||||
|
||||
|
@ -28,7 +28,7 @@
|
||||
"postinstall": "npm run rebuild && npm run lint:fix",
|
||||
"build:muya": "cd src/muya && webpack --progress --colors --config webpack.config.js",
|
||||
"release:muya": "npm run build:muya && cd src/muya && npm publish",
|
||||
"rebuild": "electron-rebuild -f -o keyboard-layout,vscode-windows-registry",
|
||||
"rebuild": "electron-rebuild -f -o keytar,keyboard-layout,vscode-windows-registry",
|
||||
"gen-third-party": "node tools/generateThirdPartyLicense.js",
|
||||
"validate-licenses": "node tools/validateLicenses.js"
|
||||
},
|
||||
@ -165,6 +165,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hfelix/electron-localshortcut": "^3.1.1",
|
||||
"@octokit/rest": "^16.26.0",
|
||||
"arg": "^4.1.0",
|
||||
"axios": "^0.18.0",
|
||||
"chokidar": "^3.0.0",
|
||||
@ -188,7 +189,9 @@
|
||||
"html-tags": "^3.0.0",
|
||||
"katex": "^0.10.2",
|
||||
"keyboard-layout": "^2.0.15",
|
||||
"keytar": "^4.7.0",
|
||||
"mermaid": "^8.0.0",
|
||||
"plist": "^3.0.1",
|
||||
"popper.js": "^1.15.0",
|
||||
"prismjs": "^1.16.0",
|
||||
"snabbdom": "^0.7.3",
|
||||
|
@ -17,7 +17,9 @@ class EnvPaths {
|
||||
this._logPath = path.join(this._userDataPath, 'logs', `${currentDate.getFullYear()}${currentDate.getMonth() + 1}`)
|
||||
this._preferencesPath = userDataPath // path.join(this._userDataPath, 'preferences')
|
||||
|
||||
this._preferencesFilePath = path.join(this._preferencesPath, 'preference.md')
|
||||
this._dataCenterPath = userDataPath
|
||||
|
||||
this._preferencesFilePath = path.join(this._preferencesPath, 'preference.json')
|
||||
|
||||
// TODO(sessions): enable this...
|
||||
// this._globalStorage = path.join(this._userDataPath, 'globalStorage')
|
||||
@ -42,6 +44,10 @@ class EnvPaths {
|
||||
return this._preferencesPath
|
||||
}
|
||||
|
||||
get dataCenterPath () {
|
||||
return this._dataCenterPath
|
||||
}
|
||||
|
||||
get preferencesFilePath () {
|
||||
return this._preferencesFilePath
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import WindowManager from '../app/windowManager'
|
||||
import Preference from '../preferences'
|
||||
import DataCenter from '../dataCenter'
|
||||
import Keybindings from '../keyboard/shortcutHandler'
|
||||
import AppMenu from '../menu'
|
||||
|
||||
@ -14,6 +15,7 @@ class Accessor {
|
||||
this.env = appEnvironment
|
||||
this.paths = appEnvironment.paths // export paths to make it better accessible
|
||||
this.preferences = new Preference(this.paths)
|
||||
this.dataCenter = new DataCenter(this.paths)
|
||||
this.keybindings = new Keybindings(userDataPath)
|
||||
this.menu = new AppMenu(this.preferences, this.keybindings, userDataPath)
|
||||
this.windowManager = new WindowManager(this.menu, this.preferences)
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { app, ipcMain, systemPreferences } from 'electron'
|
||||
import path from 'path'
|
||||
import fse from 'fs-extra'
|
||||
import log from 'electron-log'
|
||||
import { exec } from 'child_process'
|
||||
import { app, ipcMain, systemPreferences, clipboard } from 'electron'
|
||||
import dayjs from 'dayjs'
|
||||
import { isLinux, isOsx } from '../config'
|
||||
import { isDirectory, isMarkdownFileOrLink, normalizeAndResolvePath } from '../filesystem'
|
||||
import { getMenuItemById } from '../menu'
|
||||
@ -8,6 +13,7 @@ import { watchers } from '../utils/imagePathAutoComplement'
|
||||
import EditorWindow from '../windows/editor'
|
||||
import SettingWindow from '../windows/setting'
|
||||
import { WindowType } from './windowManager'
|
||||
// import ShortcutCapture from 'shortcut-capture'
|
||||
|
||||
class App {
|
||||
|
||||
@ -21,6 +27,8 @@ class App {
|
||||
this._openFilesCache = []
|
||||
this._openFilesTimer = null
|
||||
this._windowManager = this._accessor.windowManager
|
||||
// this.launchScreenshotWin = null // The window which call the screenshot.
|
||||
// this.shortcutCapture = null
|
||||
|
||||
this._listenForIpcMain()
|
||||
}
|
||||
@ -74,6 +82,12 @@ class App {
|
||||
})
|
||||
}
|
||||
|
||||
get screenshotFileName () {
|
||||
const screenshotFolderPath = this._accessor.dataCenter.getItem('screenshotFolderPath')
|
||||
const fileName = `${dayjs().format('YYYY-MM-DD-HH-mm-ss')}-screenshot.png`
|
||||
return path.join(screenshotFolderPath, fileName)
|
||||
}
|
||||
|
||||
ready = () => {
|
||||
const { _args: args } = this
|
||||
if (!isOsx && args._.length) {
|
||||
@ -126,6 +140,27 @@ class App {
|
||||
} else {
|
||||
this.createEditorWindow()
|
||||
}
|
||||
// this.shortcutCapture = new ShortcutCapture()
|
||||
// if (process.env.NODE_ENV === 'development') {
|
||||
// this.shortcutCapture.dirname = path.resolve(path.join(__dirname, '../../../node_modules/shortcut-capture'))
|
||||
// }
|
||||
// this.shortcutCapture.on('capture', async ({ dataURL }) => {
|
||||
// const { screenshotFileName } = this
|
||||
// const image = nativeImage.createFromDataURL(dataURL)
|
||||
// const bufferImage = image.toPNG()
|
||||
|
||||
// if (this.launchScreenshotWin) {
|
||||
// this.launchScreenshotWin.webContents.send('mt::screenshot-captured')
|
||||
// this.launchScreenshotWin = null
|
||||
// }
|
||||
|
||||
// try {
|
||||
// // write screenshot image into screenshot folder.
|
||||
// await fse.writeFile(screenshotFileName, bufferImage)
|
||||
// } catch (err) {
|
||||
// log.error(err)
|
||||
// }
|
||||
// })
|
||||
}
|
||||
|
||||
openFile = (event, pathname) => {
|
||||
@ -217,6 +252,34 @@ class App {
|
||||
this.createEditorWindow()
|
||||
})
|
||||
|
||||
ipcMain.on('screen-capture', win => {
|
||||
if (isOsx) {
|
||||
// Use macOs `screencapture` command line when in macOs system.
|
||||
const { screenshotFileName } = this
|
||||
exec(`screencapture -i -c`, async (err) => {
|
||||
if (err) {
|
||||
log.error(err)
|
||||
return
|
||||
}
|
||||
try {
|
||||
// Write screenshot image into screenshot folder.
|
||||
const image = clipboard.readImage()
|
||||
const bufferImage = image.toPNG()
|
||||
await fse.writeFile(screenshotFileName, bufferImage)
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
}
|
||||
win.webContents.send('mt::screenshot-captured')
|
||||
})
|
||||
} else {
|
||||
// Do nothing, maybe we'll add screenCapture later on Linux and Windows.
|
||||
// if (this.shortcutCapture) {
|
||||
// this.launchScreenshotWin = win
|
||||
// this.shortcutCapture.shortcutCapture()
|
||||
// }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('app-create-settings-window', () => {
|
||||
const settingWins = this._windowManager.windowsOfType(WindowType.SETTING)
|
||||
if (settingWins.length >= 1) {
|
||||
|
@ -312,6 +312,12 @@ class WindowManager extends EventEmitter {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('broadcast-user-data-changed', userData => {
|
||||
for (const { browserWindow } of this._windows.values()) {
|
||||
browserWindow.webContents.send('AGANI::user-preference', userData)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
202
src/main/dataCenter/index.js
Normal file
@ -0,0 +1,202 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import EventEmitter from 'events'
|
||||
import { BrowserWindow, ipcMain, dialog } from 'electron'
|
||||
import keytar from 'keytar'
|
||||
import schema from './schema'
|
||||
import Store from 'electron-store'
|
||||
import log from 'electron-log'
|
||||
import { ensureDirSync } from '../filesystem'
|
||||
import { IMAGE_EXTENSIONS } from '../config'
|
||||
|
||||
const DATA_CENTER_NAME = 'dataCenter'
|
||||
|
||||
class DataCenter extends EventEmitter {
|
||||
constructor (paths) {
|
||||
super()
|
||||
|
||||
const { dataCenterPath, userDataPath } = paths
|
||||
this.dataCenterPath = dataCenterPath
|
||||
this.userDataPath = userDataPath
|
||||
this.serviceName = 'marktext'
|
||||
this.encryptKeys = ['githubToken']
|
||||
this.hasDataCenterFile = fs.existsSync(path.join(this.dataCenterPath, `./${DATA_CENTER_NAME}.json`))
|
||||
this.store = new Store({
|
||||
schema,
|
||||
name: DATA_CENTER_NAME
|
||||
})
|
||||
|
||||
this.init()
|
||||
}
|
||||
init () {
|
||||
const defaltData = {
|
||||
imageFolderPath: path.join(this.userDataPath, 'images/'),
|
||||
screenshotFolderPath: path.join(this.userDataPath, 'screenshot/'),
|
||||
webImages: [],
|
||||
cloudImages: [],
|
||||
currentUploader: 'smms',
|
||||
imageBed: {
|
||||
github: {
|
||||
owner: '',
|
||||
repo: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.hasDataCenterFile) {
|
||||
this.store.set(defaltData)
|
||||
const imageFolderPath = this.store.get('imageFolderPath')
|
||||
const screenshotFolderPath = this.store.get('screenshotFolderPath')
|
||||
ensureDirSync(imageFolderPath)
|
||||
ensureDirSync(screenshotFolderPath)
|
||||
}
|
||||
|
||||
this._listenForIpcMain()
|
||||
}
|
||||
|
||||
async getAll () {
|
||||
const { serviceName, encryptKeys } = this
|
||||
const data = this.store.store
|
||||
try {
|
||||
const encryptData = await Promise.all(encryptKeys.map(key => {
|
||||
return keytar.getPassword(serviceName, key)
|
||||
}))
|
||||
const encryptObj = encryptKeys.reduce((acc, k, i) => {
|
||||
return {
|
||||
...acc,
|
||||
[k]: encryptData[i]
|
||||
}
|
||||
}, {})
|
||||
|
||||
return Object.assign(data, encryptObj)
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
addImage (key, url) {
|
||||
const items = this.store.get(key)
|
||||
const alreadyHas = items.some(item => item.url === url)
|
||||
let item
|
||||
if (alreadyHas) {
|
||||
item = items.find(item => item.url === url)
|
||||
item.timeStamp = +new Date()
|
||||
} else {
|
||||
item = {
|
||||
url,
|
||||
timeStamp: +new Date()
|
||||
}
|
||||
items.push(item)
|
||||
}
|
||||
|
||||
ipcMain.emit('broadcast-web-image-added', { type: key, item })
|
||||
return this.store.set(key, items)
|
||||
}
|
||||
|
||||
removeImage (type, url) {
|
||||
const items = this.store.get(type)
|
||||
const index = items.indexOf(url)
|
||||
const item = items[index]
|
||||
if (index === -1) return
|
||||
items.splice(index, 1)
|
||||
ipcMain.emit('broadcast-web-image-removed', { type, item })
|
||||
return this.store.set(type, items)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key
|
||||
* return a promise
|
||||
*/
|
||||
getItem (key) {
|
||||
const { encryptKeys, serviceName } = this
|
||||
if (encryptKeys.includes(key)) {
|
||||
return keytar.getPassword(serviceName, key)
|
||||
} else {
|
||||
const value = this.store.get(key)
|
||||
return Promise.resolve(value)
|
||||
}
|
||||
}
|
||||
|
||||
async setItem (key, value) {
|
||||
const { encryptKeys, serviceName } = this
|
||||
if (
|
||||
key === 'imageFolderPath' ||
|
||||
key === 'screenshotFolderPath'
|
||||
) {
|
||||
ensureDirSync(value)
|
||||
}
|
||||
ipcMain.emit('broadcast-user-data-changed', { [key]: value })
|
||||
if (encryptKeys.includes(key)) {
|
||||
try {
|
||||
return await keytar.setPassword(serviceName, key, value)
|
||||
} catch (err) {
|
||||
log.error(err)
|
||||
}
|
||||
} else {
|
||||
return this.store.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change multiple setting entries.
|
||||
*
|
||||
* @param {Object.<string, *>} settings A settings object or subset object with key/value entries.
|
||||
*/
|
||||
setItems (settings) {
|
||||
if (!settings) {
|
||||
log.error('Cannot change settings without entires: object is undefined or null.')
|
||||
return
|
||||
}
|
||||
|
||||
Object.keys(settings).map(key => {
|
||||
this.setItem(key, settings[key])
|
||||
})
|
||||
}
|
||||
|
||||
_listenForIpcMain () {
|
||||
// local main events
|
||||
ipcMain.on('set-image-folder-path', newPath => {
|
||||
this.setItem('imageFolderPath', newPath)
|
||||
})
|
||||
|
||||
// events from renderer process
|
||||
ipcMain.on('mt::ask-for-user-data', async e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
const userData = await this.getAll()
|
||||
win.webContents.send('AGANI::user-preference', userData)
|
||||
})
|
||||
|
||||
ipcMain.on('mt::ask-for-modify-image-folder-path', e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
const folder = dialog.showOpenDialog(win, {
|
||||
properties: [ 'openDirectory', 'createDirectory' ]
|
||||
})
|
||||
if (folder && folder[0]) {
|
||||
this.setItem('imageFolderPath', folder[0])
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('mt::set-user-data', (e, userData) =>{
|
||||
this.setItems(userData)
|
||||
})
|
||||
|
||||
ipcMain.on('mt::ask-for-image-path', e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
const files = dialog.showOpenDialog(win, {
|
||||
properties: [ 'openFile' ],
|
||||
filters: [{
|
||||
name: 'Images',
|
||||
extensions: IMAGE_EXTENSIONS
|
||||
}]
|
||||
})
|
||||
if (files && files[0]) {
|
||||
e.returnValue = files[0]
|
||||
} else {
|
||||
e.returnValue = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default DataCenter
|
26
src/main/dataCenter/schema.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"imageFolderPath": {
|
||||
"description": "The image folder to store local images.",
|
||||
"type": "string"
|
||||
},
|
||||
"screenshotFolderPath": {
|
||||
"description": "The place to store screen capture images.",
|
||||
"type": "string"
|
||||
},
|
||||
"webImages": {
|
||||
"description": "Images from web which you used in Mark Text",
|
||||
"type": "array"
|
||||
},
|
||||
"cloudImages": {
|
||||
"description": "Images which you upload to cloud",
|
||||
"type": "array"
|
||||
},
|
||||
"currentUploader": {
|
||||
"description": "The current image uploader",
|
||||
"type": "string"
|
||||
},
|
||||
"imageBed": {
|
||||
"description": "The image bed configration",
|
||||
"type": "object"
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { hasMarkdownExtension } from '../utils'
|
||||
import { IMAGE_EXTENSIONS } from '../config'
|
||||
|
||||
/**
|
||||
* Ensure that a directory exist.
|
||||
@ -64,6 +65,18 @@ export const isSymbolicLink = filepath => {
|
||||
export const isMarkdownFile = filepath => {
|
||||
return isFile(filepath) && hasMarkdownExtension(filepath)
|
||||
}
|
||||
/**
|
||||
* Returns ture if the path is an image file.
|
||||
*
|
||||
* @param {string} filepath The path
|
||||
*/
|
||||
export const isImageFile = filepath => {
|
||||
const extname = path.extname(filepath)
|
||||
return isFile(filepath) && IMAGE_EXTENSIONS.some(ext => {
|
||||
const EXT_REG = new RegExp(ext, 'i')
|
||||
return EXT_REG.test(extname)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a markdown file or symbolic link to a markdown file.
|
||||
|
@ -55,6 +55,7 @@ class Keybindings {
|
||||
['editFindPrevious', 'CmdOrCtrl+Shift+U'],
|
||||
['editReplace', 'CmdOrCtrl+Alt+F'],
|
||||
['editAidou', 'CmdOrCtrl+/'],
|
||||
['editScreenshot', 'CmdOrCtrl+Alt+A'],
|
||||
|
||||
// paragraph menu
|
||||
['paragraphHeading1', 'CmdOrCtrl+1'],
|
||||
|
@ -1,42 +1,25 @@
|
||||
import path from 'path'
|
||||
import { dialog, ipcMain, BrowserWindow } from 'electron'
|
||||
import { ipcMain, BrowserWindow } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { IMAGE_EXTENSIONS } from '../../config'
|
||||
import { updateLineEndingMenu } from '../../menu'
|
||||
import { searchFilesAndDir } from '../../utils/imagePathAutoComplement'
|
||||
|
||||
const getAndSendImagePath = (win, type) => {
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const filename = dialog.showOpenDialog(win, {
|
||||
properties: [ 'openFile' ],
|
||||
filters: [{
|
||||
name: 'Images',
|
||||
extensions: IMAGE_EXTENSIONS
|
||||
}]
|
||||
})
|
||||
if (filename && filename[0]) {
|
||||
win.webContents.send('AGANI::INSERT_IMAGE', { filename: filename[0], type })
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.on('AGANI::ask-for-insert-image', (e, type) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
getAndSendImagePath(win, type)
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::ask-for-image-auto-path', (e, { pathname, src }) => {
|
||||
ipcMain.on('mt::ask-for-image-auto-path', (e, { pathname, src, id }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
if (src.endsWith('/') || src.endsWith('\\') || src.endsWith('.')) {
|
||||
return win.webContents.send('AGANI::image-auto-path', [])
|
||||
return win.webContents.send(`mt::response-of-image-path-${id}`, [])
|
||||
}
|
||||
const fullPath = path.isAbsolute(src) ? src : path.join(path.dirname(pathname), src)
|
||||
const dir = path.dirname(fullPath)
|
||||
const searchKey = path.basename(fullPath)
|
||||
searchFilesAndDir(dir, searchKey)
|
||||
.then(files => {
|
||||
win.webContents.send('AGANI::image-auto-path', files)
|
||||
return win.webContents.send(`mt::response-of-image-path-${id}`, files)
|
||||
})
|
||||
.catch(err => {
|
||||
log.error(err)
|
||||
return win.webContents.send(`mt::response-of-image-path-${id}`, [])
|
||||
})
|
||||
.catch(log.error)
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::update-line-ending-menu', (e, lineEnding) => {
|
||||
@ -47,14 +30,10 @@ export const edit = (win, type) => {
|
||||
win.webContents.send('AGANI::edit', { type })
|
||||
}
|
||||
|
||||
export const screenshot = (win, type) => {
|
||||
ipcMain.emit('screen-capture', win)
|
||||
}
|
||||
|
||||
export const lineEnding = (win, lineEnding) => {
|
||||
win.webContents.send('AGANI::set-line-ending', { lineEnding, ignoreSaveStatus: false })
|
||||
}
|
||||
|
||||
export const insertImage = (win, type) => {
|
||||
if (type === 'absolute' || type === 'relative') {
|
||||
getAndSendImagePath(win, type)
|
||||
} else {
|
||||
win.webContents.send('AGANI::INSERT_IMAGE', { type })
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import * as actions from '../actions/edit'
|
||||
import { isOsx } from '../../config'
|
||||
|
||||
export default function (keybindings, userPreference) {
|
||||
const { aidou } = userPreference.getAll()
|
||||
@ -114,23 +115,13 @@ export default function (keybindings, userPreference) {
|
||||
actions.edit(browserWindow, 'aidou')
|
||||
}
|
||||
}, {
|
||||
label: 'Insert Image',
|
||||
submenu: [{
|
||||
label: 'Absolute Path',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.insertImage(browserWindow, 'absolute')
|
||||
}
|
||||
}, {
|
||||
label: 'Relative Path',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.insertImage(browserWindow, 'relative')
|
||||
}
|
||||
}, {
|
||||
label: 'Upload to Cloud (EXP)',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.insertImage(browserWindow, 'upload')
|
||||
}
|
||||
}]
|
||||
label: 'Screenshot',
|
||||
id: 'screenshot',
|
||||
visible: isOsx,
|
||||
accelerator: keybindings.getAccelerator('editScreenshot'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.screenshot(browserWindow, 'screenshot')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import edit from './edit'
|
||||
import prefEdit from './prefEdit'
|
||||
import file from './file'
|
||||
import help from './help'
|
||||
import marktext from './marktext'
|
||||
@ -18,6 +19,7 @@ export dockMenu from './dock'
|
||||
export const configSettingMenu = (keybindings) => {
|
||||
return [
|
||||
...(process.platform === 'darwin' ? [ marktext(keybindings) ] : []),
|
||||
prefEdit(keybindings),
|
||||
help()
|
||||
]
|
||||
}
|
||||
|
24
src/main/menu/templates/prefEdit.js
Normal file
@ -0,0 +1,24 @@
|
||||
export default function (keybindings) {
|
||||
return {
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
label: 'Cut',
|
||||
accelerator: keybindings.getAccelerator('editCut'),
|
||||
role: 'cut'
|
||||
}, {
|
||||
label: 'Copy',
|
||||
accelerator: keybindings.getAccelerator('editCopy'),
|
||||
role: 'copy'
|
||||
}, {
|
||||
label: 'Paste',
|
||||
accelerator: keybindings.getAccelerator('editPaste'),
|
||||
role: 'paste'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Select All',
|
||||
accelerator: keybindings.getAccelerator('editSelectAll'),
|
||||
role: 'selectAll'
|
||||
}]
|
||||
}
|
||||
}
|
@ -62,20 +62,24 @@ class Preference extends EventEmitter {
|
||||
if (!this.hasPreferencesFile) {
|
||||
this.store.set(defaultSettings)
|
||||
} else {
|
||||
let userSetting = this.getAll()
|
||||
|
||||
// Because `this.getAll()` will return a plainObject, so we can not use `hasOwnProperty` method
|
||||
// const plainObject = () => Object.create(null)
|
||||
const userSetting = this.getAll()
|
||||
// Update outdated settings
|
||||
const requiresUpdate = !hasSameKeys(defaultSettings, userSetting)
|
||||
const userSettingKeys = Object.keys(userSetting)
|
||||
const defaultSettingKeys = Object.keys(defaultSettings)
|
||||
|
||||
if (requiresUpdate) {
|
||||
// remove outdated settings
|
||||
for (const key in userSetting) {
|
||||
if (userSetting.hasOwnProperty(key) && !defaultSettings.hasOwnProperty(key)) {
|
||||
for (const key of userSettingKeys) {
|
||||
if (!defaultSettingKeys.includes(key)) {
|
||||
delete userSetting[key]
|
||||
}
|
||||
}
|
||||
// add new setting options
|
||||
for (const key in defaultSettings) {
|
||||
if (defaultSettings.hasOwnProperty(key) && !userSetting.hasOwnProperty(key)) {
|
||||
if (!userSettingKeys.includes(key)) {
|
||||
userSetting[key] = defaultSettings[key]
|
||||
}
|
||||
}
|
||||
|
@ -115,14 +115,6 @@
|
||||
"description": "Editor--Hide hint for quickly creating paragraphs",
|
||||
"type": "boolean"
|
||||
},
|
||||
"imageDropAction": {
|
||||
"description": "Editor--The default behavior after paste or drag the image to Mark Text",
|
||||
"enum": [
|
||||
"upload",
|
||||
"folder",
|
||||
"path"
|
||||
]
|
||||
},
|
||||
"preferLooseListItem": {
|
||||
"description": "Markdown--The preferred list type",
|
||||
"type": "boolean"
|
||||
@ -167,5 +159,14 @@
|
||||
"theme": {
|
||||
"description": "Theme--Select the theme used in Mark Text",
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
"imageInsertAction": {
|
||||
"description": "Image--The default behavior after insert image from local folder",
|
||||
"enum": [
|
||||
"upload",
|
||||
"folder",
|
||||
"path"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
BIN
src/muya/lib/assets/pngicon/image/1.png
Executable file
After Width: | Height: | Size: 371 B |
BIN
src/muya/lib/assets/pngicon/image/2.png
Executable file
After Width: | Height: | Size: 698 B |
BIN
src/muya/lib/assets/pngicon/image/3.png
Executable file
After Width: | Height: | Size: 992 B |
BIN
src/muya/lib/assets/pngicon/imageEdit/1.png
Executable file
After Width: | Height: | Size: 457 B |
BIN
src/muya/lib/assets/pngicon/imageEdit/2.png
Executable file
After Width: | Height: | Size: 924 B |
BIN
src/muya/lib/assets/pngicon/imageEdit/3.png
Executable file
After Width: | Height: | Size: 1.4 KiB |
BIN
src/muya/lib/assets/pngicon/image_fail/1.png
Executable file
After Width: | Height: | Size: 591 B |
BIN
src/muya/lib/assets/pngicon/image_fail/2.png
Executable file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/muya/lib/assets/pngicon/image_fail/3.png
Executable file
After Width: | Height: | Size: 2.0 KiB |
@ -22,6 +22,12 @@ pre {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#mu-dragover-ghost {
|
||||
height: 3px;
|
||||
position: absolute;
|
||||
background: var(--highlightColor);
|
||||
}
|
||||
|
||||
div.ag-show-quick-insert-hint p.ag-paragraph.ag-active > span.ag-paragraph-content:first-of-type:empty::after {
|
||||
content: 'Type @ to insert';
|
||||
color: var(--editorColor10);
|
||||
@ -741,6 +747,175 @@ span.ag-warn.ag-emoji-marked-text {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.ag-inline-image {
|
||||
display: flex;
|
||||
position: relative;
|
||||
background: var(--codeBlockBgColor);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.ag-inline-image .ag-image-container {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-success {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.ag-inline-image a.ag-image-icon-turninto,
|
||||
.ag-inline-image a.ag-image-icon-delete {
|
||||
opacity: 0;
|
||||
display: none;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
background: rgba(50, 54, 58, .5);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ag-inline-image a.ag-image-icon-turninto i.icon,
|
||||
.ag-inline-image a.ag-image-icon-delete i.icon {
|
||||
color: #ccc;
|
||||
transition: color .25s ease-in-out;
|
||||
}
|
||||
|
||||
.ag-inline-image a.ag-image-icon-turninto i.icon:hover,
|
||||
.ag-inline-image a.ag-image-icon-delete i.icon:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ag-inline-image a.ag-image-icon-turninto {
|
||||
right: 60px;
|
||||
}
|
||||
|
||||
.ag-inline-image a.ag-image-icon-delete {
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.ag-inline-image a.ag-image-icon-success,
|
||||
.ag-inline-image a.ag-image-icon-fail,
|
||||
.ag-inline-image a.ag-image-icon-close {
|
||||
display: none;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
}
|
||||
|
||||
.ag-inline-image a.ag-image-icon-success,
|
||||
.ag-inline-image a.ag-image-icon-fail {
|
||||
left: 15px;
|
||||
}
|
||||
|
||||
.ag-inline-image i.icon {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--iconColor);
|
||||
}
|
||||
|
||||
.ag-inline-image i.icon > i[class^=icon-] {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
filter: drop-shadow(20px 0 currentColor);
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-fail,
|
||||
.ag-inline-image.ag-image-loading,
|
||||
.ag-inline-image.ag-empty-image {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-loading::before,
|
||||
.ag-inline-image.ag-image-fail::before,
|
||||
.ag-inline-image.ag-empty-image::before {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: calc(100% - 50px);
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 50px;
|
||||
color: var(--editorColor);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-empty-image::before {
|
||||
content: 'Click to add an image';
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-fail::before {
|
||||
content: 'Load image failed';
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-loading::before {
|
||||
content: 'Loading image...';
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-loading a.ag-image-icon-success,
|
||||
.ag-inline-image.ag-empty-image a.ag-image-icon-success {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-fail a.ag-image-icon-fail {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-success:hover a.ag-image-icon-turninto,
|
||||
.ag-inline-image.ag-image-success:hover a.ag-image-icon-delete {
|
||||
opacity: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-empty-image:hover a.ag-image-icon-close,
|
||||
.ag-inline-image.ag-image-fail:hover a.ag-image-icon-close {
|
||||
opacity: .5;
|
||||
display: block;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 1;
|
||||
right: 15px;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-success.ag-inline-image-selected .ag-image-container img {
|
||||
filter: brightness(80%);
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-uploading .ag-image-container img {
|
||||
opacity: .3;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-uploading .ag-image-container a.ag-image-icon-turninto,
|
||||
.ag-inline-image.ag-image-uploading .ag-image-container a.ag-image-icon-delete {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-uploading .ag-image-container::before {
|
||||
content: 'Loading ...';
|
||||
top: 50%;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(-50%);
|
||||
color: var(--iconColor);
|
||||
}
|
||||
|
||||
.ag-image-marked-text ~ img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
@ -749,8 +924,9 @@ span.ag-warn.ag-emoji-marked-text {
|
||||
.ag-image-marked-text::before {
|
||||
background-size: cover;
|
||||
content: '';
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
width: 1.2em;
|
||||
height: 1.2em;
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
@ -47,6 +47,7 @@ export const emptyElementNames = ['br', 'col', 'colgroup', 'hr', 'img', 'input',
|
||||
export const EVENT_KEYS = generateKeyHash([
|
||||
'Enter',
|
||||
'Backspace',
|
||||
'Space',
|
||||
'Delete',
|
||||
'ArrowUp',
|
||||
'ArrowDown',
|
||||
@ -95,8 +96,16 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
|
||||
'AG_HTML_PREVIEW',
|
||||
'AG_HTML_TAG',
|
||||
'AG_IMAGE_FAIL',
|
||||
'AG_IMAGE_LOADING',
|
||||
'AG_EMPTY_IMAGE',
|
||||
'AG_IMAGE_MARKED_TEXT',
|
||||
'AG_IMAGE_SRC',
|
||||
'AG_IMAGE_CONTAINER',
|
||||
'AG_INLINE_IMAGE',
|
||||
'AG_IMAGE_SUCCESS',
|
||||
'AG_IMAGE_UPLOADING',
|
||||
'AG_INLINE_IMAGE_SELECTED',
|
||||
'AG_INLINE_IMAGE_IS_EDIT',
|
||||
'AG_INDENT_CODE',
|
||||
'AG_INLINE_RULE',
|
||||
'AG_LANGUAGE',
|
||||
@ -240,7 +249,14 @@ export const MUYA_DEFAULT_OPTION = {
|
||||
sequenceTheme: 'hand', // hand or simple
|
||||
mermaidTheme: 'default', // dark / forest / default
|
||||
vegaTheme: 'latimes', // excel / ggplot2 / quartz / vox / fivethirtyeight / dark / latimes
|
||||
hideQuickInsertHint: false
|
||||
hideQuickInsertHint: false,
|
||||
// transform the image to local folder, cloud or just return the local path
|
||||
imageAction: null,
|
||||
// Call Electron open dialog or input element type is file.
|
||||
imagePathPicker: null,
|
||||
clipboardFilePath: () => {},
|
||||
// image path auto completed when you input in image selector.
|
||||
imagePathAutoComplete: () => []
|
||||
}
|
||||
|
||||
// export const DIAGRAM_TEMPLATE = {
|
||||
@ -249,5 +265,8 @@ export const MUYA_DEFAULT_OPTION = {
|
||||
|
||||
export const isInElectron = window && window.process && window.process.type === 'renderer'
|
||||
export const isOsx = window && window.navigator && /Mac/.test(window.navigator.platform)
|
||||
export const isWin = window && window.navigator.userAgent && /win32|wow32|win64|wow64/i.test(window.navigator.userAgent)
|
||||
// http[s] (domain or IPv4 or localhost or IPv6) [port] /not-white-space
|
||||
export const URL_REG = /^http(s)?:\/\/([a-z0-9\-._~]+\.[a-z]{2,}|[0-9.]+|localhost|\[[a-f0-9.:]+\])(:[0-9]{1,5})?\/[\S]+/i
|
||||
// The smallest transparent gif base64 image.
|
||||
export const SMALLEST_BASE64 = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
||||
|
@ -53,6 +53,36 @@ const arrowCtrl = ContentState => {
|
||||
return null
|
||||
}
|
||||
|
||||
ContentState.prototype.docArrowHandler = function (event) {
|
||||
const { selectedImage } = this
|
||||
if (selectedImage) {
|
||||
const { key, token } = selectedImage
|
||||
const { start, end } = token.range
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const block = this.getBlock(key)
|
||||
switch (event.key) {
|
||||
case EVENT_KEYS.ArrowUp:
|
||||
case EVENT_KEYS.ArrowLeft: {
|
||||
this.cursor = {
|
||||
start: { key, offset: start },
|
||||
end: { key, offset: start }
|
||||
}
|
||||
break
|
||||
}
|
||||
case EVENT_KEYS.ArrowDown:
|
||||
case EVENT_KEYS.ArrowRight: {
|
||||
this.cursor = {
|
||||
start: { key, offset: end },
|
||||
end: { key, offset: end }
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return this.singleRender(block)
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.arrowHandler = function (event) {
|
||||
const node = selection.getSelectionStart()
|
||||
const paragraph = findNearestParagraph(node)
|
||||
|
@ -1,6 +1,7 @@
|
||||
import selection from '../selection'
|
||||
import { findNearestParagraph, findOutMostParagraph } from '../selection/dom'
|
||||
import { tokenizer, generator } from '../parser/'
|
||||
import { getImageInfo } from '../utils/getImageInfo'
|
||||
|
||||
const backspaceCtrl = ContentState => {
|
||||
ContentState.prototype.checkBackspaceCase = function () {
|
||||
@ -100,6 +101,14 @@ const backspaceCtrl = ContentState => {
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.docBackspaceHandler = function (event) {
|
||||
// handle delete selected image
|
||||
if (this.selectedImage) {
|
||||
event.preventDefault()
|
||||
return this.deleteImage(this.selectedImage)
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.backspaceHandler = function (event) {
|
||||
const { start, end } = selection.getCursorRange()
|
||||
|
||||
@ -211,14 +220,50 @@ const backspaceCtrl = ContentState => {
|
||||
}
|
||||
|
||||
const node = selection.getSelectionStart()
|
||||
const preEleSibling = node && node.nodeType === 1 ? node.previousElementSibling : null
|
||||
const paragraph = findNearestParagraph(node)
|
||||
const id = paragraph.id
|
||||
let block = this.getBlock(id)
|
||||
let parent = this.getBlock(block.parent)
|
||||
const preBlock = this.findPreBlockInLocation(block)
|
||||
const { left } = selection.getCaretOffsets(paragraph)
|
||||
const { left, right } = selection.getCaretOffsets(paragraph)
|
||||
const inlineDegrade = this.checkBackspaceCase()
|
||||
|
||||
// Handle backspace when the previous is an inline image.
|
||||
if (preEleSibling && preEleSibling.classList.contains('ag-inline-image')) {
|
||||
if (selection.getCaretOffsets(node).left === 0) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const imageInfo = getImageInfo(preEleSibling)
|
||||
return this.selectImage(imageInfo)
|
||||
}
|
||||
if (selection.getCaretOffsets(node).left === 1 && right === 0) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
const key = startBlock.key
|
||||
const text = startBlock.text
|
||||
|
||||
startBlock.text = text.substring(0, start.offset - 1) + text.substring(start.offset)
|
||||
const offset = start.offset - 1
|
||||
this.cursor = {
|
||||
start: { key, offset },
|
||||
end: { key, offset }
|
||||
}
|
||||
return this.singleRender(startBlock)
|
||||
}
|
||||
}
|
||||
|
||||
// handle backspace when cursor at the end of inline image.
|
||||
if (node.classList.contains('ag-image-container')) {
|
||||
const imageWrapper = node.parentNode
|
||||
const imageInfo = getImageInfo(imageWrapper)
|
||||
if (start.offset === imageInfo.token.range.end) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return this.selectImage(imageInfo)
|
||||
}
|
||||
}
|
||||
|
||||
const tableHasContent = table => {
|
||||
const tHead = table.children[0]
|
||||
const tBody = table.children[1]
|
||||
|
@ -1,10 +1,30 @@
|
||||
import selection from '../selection'
|
||||
import { isMuyaEditorElement } from '../selection/dom'
|
||||
import { HAS_TEXT_BLOCK_REG } from '../config'
|
||||
|
||||
const clickCtrl = ContentState => {
|
||||
ContentState.prototype.clickHandler = function (event) {
|
||||
const { eventCenter } = this.muya
|
||||
const { target } = event
|
||||
if (isMuyaEditorElement(target)) {
|
||||
const lastBlock = this.getLastBlock()
|
||||
const archor = this.findOutMostBlock(lastBlock)
|
||||
const archorParagraph = document.querySelector(`#${archor.key}`)
|
||||
const rect = archorParagraph.getBoundingClientRect()
|
||||
// If click below the last paragraph
|
||||
// and the last paragraph is not empty, create a new empty paragraph
|
||||
if (/\S/.test(lastBlock.text) && event.clientY > rect.top + rect.height) {
|
||||
const paragraphBlock = this.createBlockP()
|
||||
this.insertAfter(paragraphBlock, archor)
|
||||
const key = paragraphBlock.key
|
||||
const offset = 0
|
||||
this.cursor = {
|
||||
start: { key, offset },
|
||||
end: { key, offset }
|
||||
}
|
||||
return this.partialRender()
|
||||
}
|
||||
}
|
||||
// handle front menu click
|
||||
const { start: oldStart, end: oldEnd } = this.cursor
|
||||
if (oldStart && oldEnd) {
|
||||
|
180
src/muya/lib/contentState/dragDropCtrl.js
Normal file
@ -0,0 +1,180 @@
|
||||
import { findNearestParagraph, findOutMostParagraph } from '../selection/dom'
|
||||
import { verticalPositionInRect, getUniqueId, getImageInfo as getImageSrc, checkImageContentType } from '../utils'
|
||||
import { getImageInfo } from '../utils/getImageInfo'
|
||||
import { URL_REG, IMAGE_EXT_REG } from '../config'
|
||||
|
||||
const GHOST_ID = 'mu-dragover-ghost'
|
||||
const GHOST_HEIGHT = 3
|
||||
|
||||
const dragDropCtrl = ContentState => {
|
||||
ContentState.prototype.hideGhost = function () {
|
||||
this.dropAnchor = null
|
||||
const ghost = document.querySelector(`#${GHOST_ID}`)
|
||||
ghost && ghost.remove()
|
||||
}
|
||||
/**
|
||||
* create the ghost element.
|
||||
*/
|
||||
ContentState.prototype.createGhost = function (event) {
|
||||
const target = event.target
|
||||
let ghost = null
|
||||
const nearestParagraph = findNearestParagraph(target)
|
||||
const outmostParagraph = findOutMostParagraph(target)
|
||||
|
||||
if (!outmostParagraph) {
|
||||
return this.hideGhost()
|
||||
}
|
||||
|
||||
const block = this.getBlock(nearestParagraph.id)
|
||||
let anchor = this.getAnchor(block)
|
||||
|
||||
// dragover preview container
|
||||
if (!anchor && outmostParagraph) {
|
||||
anchor = this.getBlock(outmostParagraph.id)
|
||||
}
|
||||
|
||||
if (anchor) {
|
||||
const anchorParagraph = this.muya.container.querySelector(`#${anchor.key}`)
|
||||
const rect = anchorParagraph.getBoundingClientRect()
|
||||
const position = verticalPositionInRect(event, rect)
|
||||
this.dropAnchor = {
|
||||
position,
|
||||
anchor
|
||||
}
|
||||
// create ghost
|
||||
ghost = document.querySelector(`#${GHOST_ID}`)
|
||||
if (!ghost) {
|
||||
ghost = document.createElement('div')
|
||||
ghost.id = GHOST_ID
|
||||
document.body.appendChild(ghost)
|
||||
}
|
||||
|
||||
Object.assign(ghost.style, {
|
||||
width: `${rect.width}px`,
|
||||
left: `${rect.left}px`,
|
||||
top: position === 'up' ? `${rect.top - GHOST_HEIGHT}px` : `${rect.top + rect.height}px`
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.dragoverHandler = function (event) {
|
||||
// Cancel to allow tab drag&drop.
|
||||
if (!event.dataTransfer.types.length) {
|
||||
return event.dataTransfer.dropEffect = 'none'
|
||||
}
|
||||
|
||||
if (event.dataTransfer.types.includes('text/uri-list')) {
|
||||
const items = Array.from(event.dataTransfer.items)
|
||||
const hasUriItem = items.some(i => i.type === 'text/uri-list')
|
||||
const hasTextItem = items.some(i => i.type === 'text/plain')
|
||||
const hasHtmlItem = items.some(i => i.type === 'text/html')
|
||||
if (hasUriItem && hasHtmlItem && !hasTextItem) {
|
||||
this.createGhost(event)
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
}
|
||||
|
||||
if (event.dataTransfer.types.indexOf('Files') >= 0) {
|
||||
if (event.dataTransfer.items.length === 1 && event.dataTransfer.items[0].type.indexOf('image') > -1) {
|
||||
event.preventDefault()
|
||||
this.createGhost(event)
|
||||
event.dataTransfer.dropEffect = 'copy'
|
||||
}
|
||||
} else {
|
||||
event.stopPropagation()
|
||||
event.dataTransfer.dropEffect = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.dragleaveHandler = function (event) {
|
||||
return this.hideGhost()
|
||||
}
|
||||
|
||||
ContentState.prototype.dropHandler = async function (event) {
|
||||
event.preventDefault()
|
||||
const { dropAnchor } = this
|
||||
this.hideGhost()
|
||||
// handle drag/drop web link image.
|
||||
if (event.dataTransfer.items.length) {
|
||||
for (const item of event.dataTransfer.items) {
|
||||
if (item.kind === 'string' && item.type === 'text/uri-list') {
|
||||
item.getAsString(async str => {
|
||||
if (URL_REG.test(str) && dropAnchor) {
|
||||
let isImage = false
|
||||
if (IMAGE_EXT_REG.test(str)) {
|
||||
isImage = true
|
||||
}
|
||||
if (!isImage) {
|
||||
isImage = await checkImageContentType(str)
|
||||
}
|
||||
if (!isImage) return
|
||||
const text = ``
|
||||
const imageBlock = this.createBlockP(text)
|
||||
const { anchor, position } = dropAnchor
|
||||
if (position === 'up') {
|
||||
this.insertBefore(imageBlock, anchor)
|
||||
} else {
|
||||
this.insertAfter(imageBlock, anchor)
|
||||
}
|
||||
|
||||
const key = imageBlock.children[0].key
|
||||
const offset = 0
|
||||
this.cursor = {
|
||||
start: { key, offset },
|
||||
end: { key, offset }
|
||||
}
|
||||
this.render()
|
||||
this.muya.eventCenter.dispatch('stateChange')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (event.dataTransfer.files) {
|
||||
const fileList = []
|
||||
for (const file of event.dataTransfer.files) {
|
||||
fileList.push(file)
|
||||
}
|
||||
const image = fileList.find(file => /image/.test(file.type))
|
||||
if (image && dropAnchor) {
|
||||
const { name, path } = image
|
||||
const id = `loading-${getUniqueId()}`
|
||||
const text = ``
|
||||
const imageBlock = this.createBlockP(text)
|
||||
const { anchor, position } = dropAnchor
|
||||
if (position === 'up') {
|
||||
this.insertBefore(imageBlock, anchor)
|
||||
} else {
|
||||
this.insertAfter(imageBlock, anchor)
|
||||
}
|
||||
|
||||
const key = imageBlock.children[0].key
|
||||
const offset = 0
|
||||
this.cursor = {
|
||||
start: { key, offset },
|
||||
end: { key, offset }
|
||||
}
|
||||
this.render()
|
||||
|
||||
const nSrc = await this.muya.options.imageAction(path)
|
||||
const { src } = getImageSrc(path)
|
||||
if (src) {
|
||||
this.stateRender.urlMap.set(nSrc, src)
|
||||
}
|
||||
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
||||
|
||||
if (imageWrapper) {
|
||||
const imageInfo = getImageInfo(imageWrapper)
|
||||
this.replaceImage(imageInfo, {
|
||||
alt: name,
|
||||
src: nSrc
|
||||
})
|
||||
}
|
||||
}
|
||||
this.muya.eventCenter.dispatch('stateChange')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default dragDropCtrl
|
@ -144,12 +144,41 @@ const enterCtrl = ContentState => {
|
||||
return this.partialRender()
|
||||
}
|
||||
|
||||
ContentState.prototype.docEnterHandler = function (event) {
|
||||
const { eventCenter } = this.muya
|
||||
const { selectedImage } = this
|
||||
// Show image selector when you press Enter key and there is already one image selected.
|
||||
if (selectedImage) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
const { imageId, ...imageInfo } = selectedImage
|
||||
const imageWrapper = document.querySelector(`#${imageId}`)
|
||||
const rect = imageWrapper.getBoundingClientRect()
|
||||
const reference = {
|
||||
getBoundingClientRect () {
|
||||
rect.height = 0 // Put image selector bellow the top border of image.
|
||||
return rect
|
||||
}
|
||||
}
|
||||
|
||||
eventCenter.dispatch('muya-image-selector', {
|
||||
reference,
|
||||
imageInfo,
|
||||
cb: () => {}
|
||||
})
|
||||
this.selectedImage = null
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
ContentState.prototype.enterHandler = function (event) {
|
||||
const { start, end } = selection.getCursorRange()
|
||||
|
||||
if (!start || !end) {
|
||||
return event.preventDefault()
|
||||
}
|
||||
let block = this.getBlock(start.key)
|
||||
const { text } = block
|
||||
const endBlock = this.getBlock(end.key)
|
||||
let parent = this.getParent(block)
|
||||
|
||||
@ -161,7 +190,6 @@ const enterCtrl = ContentState => {
|
||||
this.updateCodeLanguage(block, block.text.trim())
|
||||
return
|
||||
}
|
||||
|
||||
// handle select multiple blocks
|
||||
if (start.key !== end.key) {
|
||||
const key = start.key
|
||||
@ -320,7 +348,8 @@ const enterCtrl = ContentState => {
|
||||
block = parent
|
||||
parent = this.getParent(block)
|
||||
}
|
||||
const { left, right } = selection.getCaretOffsets(paragraph)
|
||||
const left = start.offset
|
||||
const right = text.length - left
|
||||
const type = block.type
|
||||
let newBlock
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import selection from '../selection'
|
||||
import { tokenizer, generator } from '../parser/'
|
||||
import { FORMAT_MARKER_MAP, FORMAT_TYPES, URL_REG } from '../config'
|
||||
import { FORMAT_MARKER_MAP, FORMAT_TYPES } from '../config'
|
||||
import { getImageInfo } from '../utils/getImageInfo'
|
||||
|
||||
const getOffset = (offset, { range: { start, end }, type, tag, anchor, alt }) => {
|
||||
const dis = offset - start
|
||||
@ -221,73 +222,6 @@ const formatCtrl = ContentState => {
|
||||
block.text = generator(tokens)
|
||||
}
|
||||
|
||||
ContentState.prototype.insertImage = function (url) {
|
||||
const title = /\/?([^./]+)\.[a-z]+$/.exec(url)[1] || ''
|
||||
const { start, end } = this.cursor
|
||||
const { formats } = this.selectionFormats({ start, end })
|
||||
const { key, offset: startOffset } = start
|
||||
const { offset: endOffset } = end
|
||||
const block = this.getBlock(key)
|
||||
const { text } = block
|
||||
const imageFormat = formats.filter(f => f.type === 'image')
|
||||
|
||||
// Only encode URLs but not local paths or data URLs
|
||||
let imgUrl
|
||||
if (URL_REG.test(url)) {
|
||||
imgUrl = encodeURI(url)
|
||||
} else {
|
||||
imgUrl = url
|
||||
}
|
||||
|
||||
if (imageFormat.length === 1) {
|
||||
// Replace already existing image
|
||||
let imageTitle = title
|
||||
|
||||
// Extract title from image if there isn't an image source already (GH#562). E.g: ![old-title]()
|
||||
if (imageFormat[0].alt && !imageFormat[0].src) {
|
||||
imageTitle = imageFormat[0].alt
|
||||
}
|
||||
|
||||
const { start, end } = imageFormat[0].range
|
||||
block.text = text.substring(0, start) +
|
||||
`` +
|
||||
text.substring(end)
|
||||
|
||||
this.cursor = {
|
||||
start: { key, offset: start + 2 },
|
||||
end: { key, offset: start + 2 + imageTitle.length }
|
||||
}
|
||||
} else if (key !== end.key) {
|
||||
// Replace multi-line text
|
||||
const endBlock = this.getBlock(end.key)
|
||||
const { text } = endBlock
|
||||
endBlock.text = text.substring(0, endOffset) + `` + text.substring(endOffset)
|
||||
const offset = endOffset + 2
|
||||
this.cursor = {
|
||||
start: { key: end.key, offset },
|
||||
end: { key: end.key, offset: offset + title.length }
|
||||
}
|
||||
} else {
|
||||
// Replace single-line text
|
||||
const imageTitle = startOffset !== endOffset ? text.substring(startOffset, endOffset) : title
|
||||
block.text = text.substring(0, start.offset) +
|
||||
`` +
|
||||
text.substring(end.offset)
|
||||
|
||||
this.cursor = {
|
||||
start: {
|
||||
key,
|
||||
offset: startOffset + 2
|
||||
},
|
||||
end: {
|
||||
key,
|
||||
offset: startOffset + 2 + imageTitle.length
|
||||
}
|
||||
}
|
||||
}
|
||||
this.partialRender()
|
||||
}
|
||||
|
||||
ContentState.prototype.format = function (type) {
|
||||
const { start, end } = selection.getCursorRange()
|
||||
if (!start || !end) {
|
||||
@ -331,6 +265,23 @@ const formatCtrl = ContentState => {
|
||||
end.offset += end.delata
|
||||
startBlock.text = generator(tokens)
|
||||
addFormat(type, startBlock, { start, end })
|
||||
if (type === 'image') {
|
||||
// Show image selector when create a inline image by menu/shortcut/or just input `![]()`
|
||||
requestAnimationFrame(() => {
|
||||
const startNode = selection.getSelectionStart()
|
||||
if (startNode) {
|
||||
const imageWrapper = startNode.closest('.ag-inline-image')
|
||||
if (imageWrapper && imageWrapper.classList.contains('ag-empty-image')) {
|
||||
const imageInfo = getImageInfo(imageWrapper)
|
||||
this.muya.eventCenter.dispatch('muya-image-selector', {
|
||||
reference: imageWrapper,
|
||||
imageInfo,
|
||||
cb: () => {}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
this.cursor = { start, end }
|
||||
this.partialRender()
|
||||
|
138
src/muya/lib/contentState/imageCtrl.js
Normal file
@ -0,0 +1,138 @@
|
||||
import { URL_REG } from '../config'
|
||||
|
||||
const imageCtrl = ContentState => {
|
||||
/**
|
||||
* insert inline image at the cursor position.
|
||||
*/
|
||||
ContentState.prototype.insertImage = function ({ alt = '', src = '', title = '' }) {
|
||||
const match = /(?:\/|\\)?([^./\\]+)\.[a-z]+$/.exec(src)
|
||||
if (!alt) {
|
||||
alt = match && match[1] ? match[1] : ''
|
||||
}
|
||||
|
||||
const { start, end } = this.cursor
|
||||
const { formats } = this.selectionFormats({ start, end })
|
||||
const { key, offset: startOffset } = start
|
||||
const { offset: endOffset } = end
|
||||
const block = this.getBlock(key)
|
||||
if (
|
||||
block.type === 'span' &&
|
||||
(
|
||||
block.functionType === 'codeLine' ||
|
||||
block.functionType === 'languageInput' ||
|
||||
block.functionType === 'thematicBreakLine'
|
||||
)
|
||||
) {
|
||||
// You can not insert image into code block or language input...
|
||||
return
|
||||
}
|
||||
const { text } = block
|
||||
const imageFormat = formats.filter(f => f.type === 'image')
|
||||
// Only encode URLs but not local paths or data URLs
|
||||
let imgUrl
|
||||
if (URL_REG.test(src)) {
|
||||
imgUrl = encodeURI(src)
|
||||
} else {
|
||||
imgUrl = src
|
||||
}
|
||||
|
||||
let srcAndTitle = imgUrl
|
||||
|
||||
if (srcAndTitle && title) {
|
||||
srcAndTitle += ` "${title}"`
|
||||
}
|
||||
|
||||
if (
|
||||
imageFormat.length === 1 &&
|
||||
imageFormat[0].range.start !== startOffset &&
|
||||
imageFormat[0].range.end !== endOffset
|
||||
) {
|
||||
// Replace already existing image
|
||||
let imageAlt = alt
|
||||
|
||||
// Extract alt from image if there isn't an image source already (GH#562). E.g: ![old-alt]()
|
||||
if (imageFormat[0].alt && !imageFormat[0].src) {
|
||||
imageAlt = imageFormat[0].alt
|
||||
}
|
||||
|
||||
const { start, end } = imageFormat[0].range
|
||||
block.text = text.substring(0, start) +
|
||||
`` +
|
||||
text.substring(end)
|
||||
|
||||
this.cursor = {
|
||||
start: { key, offset: start + 2 },
|
||||
end: { key, offset: start + 2 + imageAlt.length }
|
||||
}
|
||||
} else if (key !== end.key) {
|
||||
// Replace multi-line text
|
||||
const endBlock = this.getBlock(end.key)
|
||||
const { text } = endBlock
|
||||
endBlock.text = text.substring(0, endOffset) + `` + text.substring(endOffset)
|
||||
const offset = endOffset + 2
|
||||
this.cursor = {
|
||||
start: { key: end.key, offset },
|
||||
end: { key: end.key, offset: offset + alt.length }
|
||||
}
|
||||
} else {
|
||||
// Replace single-line text
|
||||
const imageAlt = startOffset !== endOffset ? text.substring(startOffset, endOffset) : alt
|
||||
block.text = text.substring(0, start.offset) +
|
||||
`` +
|
||||
text.substring(end.offset)
|
||||
|
||||
this.cursor = {
|
||||
start: {
|
||||
key,
|
||||
offset: startOffset + 2
|
||||
},
|
||||
end: {
|
||||
key,
|
||||
offset: startOffset + 2 + imageAlt.length
|
||||
}
|
||||
}
|
||||
}
|
||||
this.partialRender()
|
||||
}
|
||||
|
||||
ContentState.prototype.replaceImage = function ({ key, token }, { alt = '', src = '', title = '' }) {
|
||||
const block = this.getBlock(key)
|
||||
const { start, end } = token.range
|
||||
const oldText = block.text
|
||||
let imageText = ' {
|
||||
imageText += src
|
||||
}
|
||||
if (title) {
|
||||
imageText += ` "${title}"`
|
||||
}
|
||||
imageText += ')'
|
||||
block.text = oldText.substring(0, start) + imageText + oldText.substring(end)
|
||||
return this.singleRender(block)
|
||||
}
|
||||
|
||||
ContentState.prototype.deleteImage = function ({ key, token }) {
|
||||
const block = this.getBlock(key)
|
||||
const oldText = block.text
|
||||
const { start, end } = token.range
|
||||
block.text = oldText.substring(0, start) + oldText.substring(end)
|
||||
|
||||
this.cursor = {
|
||||
start: { key, offset: start },
|
||||
end: { key, offset: start }
|
||||
}
|
||||
return this.singleRender(block)
|
||||
}
|
||||
|
||||
ContentState.prototype.selectImage = function (imageInfo) {
|
||||
this.selectedImage = imageInfo
|
||||
const block = this.getBlock(imageInfo.key)
|
||||
return this.singleRender(block, false)
|
||||
}
|
||||
}
|
||||
|
||||
export default imageCtrl
|
@ -1,98 +0,0 @@
|
||||
import selection from '../selection'
|
||||
import { findNearestParagraph } from '../selection/dom'
|
||||
import { CLASS_OR_ID } from '../config'
|
||||
import { getParagraphReference } from '../utils'
|
||||
|
||||
const imagePathCtrl = ContentState => {
|
||||
ContentState.prototype.getImageTextNode = function () {
|
||||
const node = selection.getSelectionStart()
|
||||
const getNode = node => {
|
||||
const parentNode = node && node.parentNode
|
||||
if (node && node.classList && node.classList.contains(CLASS_OR_ID['AG_IMAGE_MARKED_TEXT'])) {
|
||||
return node
|
||||
} else if (parentNode) {
|
||||
return getNode(parentNode)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
return getNode(node)
|
||||
}
|
||||
|
||||
ContentState.prototype.showAutoImagePath = function (list) {
|
||||
const { eventCenter } = this.muya
|
||||
const node = this.getImageTextNode()
|
||||
|
||||
if (!node) {
|
||||
return eventCenter.dispatch('muya-image-picker', { list: [] })
|
||||
}
|
||||
|
||||
const cb = item => {
|
||||
const { text } = item
|
||||
const { start: { key, offset } } = this.cursor
|
||||
const block = this.getBlock(key)
|
||||
const { text: oldText } = block
|
||||
let chop = ''
|
||||
block.text = oldText.substring(0, offset).replace(/(\/)([^/]+)$/, (m, p1, p2) => {
|
||||
chop = p2
|
||||
return p1
|
||||
}) + text + oldText.substring(offset)
|
||||
this.cursor = {
|
||||
start: { key, offset: offset + (text.length - chop.length) },
|
||||
end: { key, offset: offset + (text.length - chop.length) }
|
||||
}
|
||||
this.partialRender()
|
||||
}
|
||||
const paragraph = findNearestParagraph(node)
|
||||
const reference = getParagraphReference(node, paragraph.id)
|
||||
|
||||
eventCenter.dispatch('muya-image-picker', { reference, list, cb })
|
||||
}
|
||||
|
||||
ContentState.prototype.listenForPathChange = function () {
|
||||
const { eventCenter } = this.muya
|
||||
|
||||
eventCenter.subscribe('image-path', src => {
|
||||
const node = this.getImageTextNode()
|
||||
|
||||
if (!node) {
|
||||
return eventCenter.dispatch('muya-image-picker', { list: [] })
|
||||
}
|
||||
if (src === '') {
|
||||
const cb = item => {
|
||||
const type = item.label
|
||||
eventCenter.dispatch('insert-image', type)
|
||||
}
|
||||
|
||||
const list = [{
|
||||
text: 'Absolute Path',
|
||||
iconClass: 'icon-folder',
|
||||
label: 'absolute'
|
||||
}, {
|
||||
text: 'Relative Path',
|
||||
iconClass: 'icon-folder',
|
||||
label: 'relative'
|
||||
}, {
|
||||
text: 'Upload Image',
|
||||
iconClass: 'icon-upload',
|
||||
label: 'upload'
|
||||
}]
|
||||
|
||||
const paragraph = findNearestParagraph(node)
|
||||
const reference = getParagraphReference(node, paragraph.id)
|
||||
eventCenter.dispatch('muya-image-picker', {
|
||||
reference,
|
||||
list,
|
||||
cb
|
||||
})
|
||||
} else if (src && typeof src === 'string' && src.length) {
|
||||
eventCenter.dispatch('muya-image-picker', {
|
||||
list: []
|
||||
})
|
||||
eventCenter.dispatch('image-path-autocomplement', src)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default imagePathCtrl
|
@ -18,12 +18,13 @@ import tabCtrl from './tabCtrl'
|
||||
import formatCtrl from './formatCtrl'
|
||||
import searchCtrl from './searchCtrl'
|
||||
import containerCtrl from './containerCtrl'
|
||||
import imagePathCtrl from './imagePathCtrl'
|
||||
import htmlBlockCtrl from './htmlBlock'
|
||||
import clickCtrl from './clickCtrl'
|
||||
import inputCtrl from './inputCtrl'
|
||||
import tocCtrl from './tocCtrl'
|
||||
import emojiCtrl from './emojiCtrl'
|
||||
import imageCtrl from './imageCtrl'
|
||||
import dragDropCtrl from './dragDropCtrl'
|
||||
import importMarkdown from '../utils/importMarkdown'
|
||||
import Cursor from '../selection/cursor'
|
||||
|
||||
@ -43,12 +44,13 @@ const prototypes = [
|
||||
formatCtrl,
|
||||
searchCtrl,
|
||||
containerCtrl,
|
||||
imagePathCtrl,
|
||||
htmlBlockCtrl,
|
||||
clickCtrl,
|
||||
inputCtrl,
|
||||
tocCtrl,
|
||||
emojiCtrl,
|
||||
imageCtrl,
|
||||
dragDropCtrl,
|
||||
importMarkdown
|
||||
]
|
||||
|
||||
@ -67,6 +69,8 @@ class ContentState {
|
||||
this.currentCursor = null
|
||||
// you'll select the outmost block of current cursor when you click the front icon.
|
||||
this.selectedBlock = null
|
||||
this._selectedImage = null
|
||||
this.dropAnchor = null
|
||||
this.prevCursor = null
|
||||
this.historyTimer = null
|
||||
this.history = new History(this)
|
||||
@ -76,6 +80,22 @@ class ContentState {
|
||||
this.init()
|
||||
}
|
||||
|
||||
set selectedImage (image) {
|
||||
const oldSelectedImage = this._selectedImage
|
||||
// if there is no selected image, remove selected status of current selected image.
|
||||
if (!image && oldSelectedImage) {
|
||||
const selectedImages = this.muya.container.querySelectorAll('.ag-inline-image-selected')
|
||||
for (const img of selectedImages) {
|
||||
img.classList.remove('ag-inline-image-selected')
|
||||
}
|
||||
}
|
||||
this._selectedImage = image
|
||||
}
|
||||
|
||||
get selectedImage () {
|
||||
return this._selectedImage
|
||||
}
|
||||
|
||||
set cursor (cursor) {
|
||||
if (!(cursor instanceof Cursor)) {
|
||||
cursor = new Cursor(cursor)
|
||||
@ -149,20 +169,29 @@ class ContentState {
|
||||
this.renderRange = [ startOutMostBlock.preSibling, endOutMostBlock.nextSibling ]
|
||||
}
|
||||
|
||||
postRender () {
|
||||
// do nothing.
|
||||
}
|
||||
|
||||
render (isRenderCursor = true) {
|
||||
const { blocks, cursor, searchMatches: { matches, index }, selectedBlock } = this
|
||||
const { blocks, searchMatches: { matches, index } } = this
|
||||
const activeBlocks = this.getActiveBlocks()
|
||||
matches.forEach((m, i) => {
|
||||
m.active = i === index
|
||||
})
|
||||
this.setNextRenderRange()
|
||||
this.stateRender.collectLabels(blocks)
|
||||
this.stateRender.render(blocks, cursor, activeBlocks, matches, selectedBlock)
|
||||
if (isRenderCursor) this.setCursor()
|
||||
this.stateRender.render(blocks, activeBlocks, matches)
|
||||
if (isRenderCursor) {
|
||||
this.setCursor()
|
||||
} else {
|
||||
this.muya.blur()
|
||||
}
|
||||
this.postRender()
|
||||
}
|
||||
|
||||
partialRender (isRenderCursor = true) {
|
||||
const { blocks, cursor, searchMatches: { matches, index }, selectedBlock } = this
|
||||
const { blocks, searchMatches: { matches, index } } = this
|
||||
const activeBlocks = this.getActiveBlocks()
|
||||
const [ startKey, endKey ] = this.renderRange
|
||||
matches.forEach((m, i) => {
|
||||
@ -174,8 +203,30 @@ class ContentState {
|
||||
|
||||
this.setNextRenderRange()
|
||||
this.stateRender.collectLabels(blocks)
|
||||
this.stateRender.partialRender(needRenderBlocks, cursor, activeBlocks, matches, startKey, endKey, selectedBlock)
|
||||
if (isRenderCursor) this.setCursor()
|
||||
this.stateRender.partialRender(needRenderBlocks, activeBlocks, matches, startKey, endKey)
|
||||
if (isRenderCursor) {
|
||||
this.setCursor()
|
||||
} else {
|
||||
this.muya.blur()
|
||||
}
|
||||
this.postRender()
|
||||
}
|
||||
|
||||
singleRender (block, isRenderCursor = true) {
|
||||
const { blocks, searchMatches: { matches, index } } = this
|
||||
const activeBlocks = this.getActiveBlocks()
|
||||
matches.forEach((m, i) => {
|
||||
m.active = i === index
|
||||
})
|
||||
this.setNextRenderRange()
|
||||
this.stateRender.collectLabels(blocks)
|
||||
this.stateRender.singleRender(block, activeBlocks, matches)
|
||||
if (isRenderCursor) {
|
||||
this.setCursor()
|
||||
} else {
|
||||
this.muya.blur()
|
||||
}
|
||||
this.postRender()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { sanitize } from '../utils'
|
||||
import { PARAGRAPH_TYPES, PREVIEW_DOMPURIFY_CONFIG, HAS_TEXT_BLOCK_REG } from '../config'
|
||||
|
||||
import { PARAGRAPH_TYPES, PREVIEW_DOMPURIFY_CONFIG, HAS_TEXT_BLOCK_REG, IMAGE_EXT_REG } from '../config'
|
||||
import { sanitize, getUniqueId, getImageInfo as getImageSrc } from '../utils'
|
||||
import { getImageInfo } from '../utils/getImageInfo'
|
||||
|
||||
const LIST_REG = /ul|ol/
|
||||
const LINE_BREAKS_REG = /\n/
|
||||
@ -77,11 +79,113 @@ const pasteCtrl = ContentState => {
|
||||
return tempWrapper.innerHTML
|
||||
}
|
||||
|
||||
ContentState.prototype.pasteImage = async function (event) {
|
||||
// Try to guess the clipboard file path.
|
||||
const imagePath = this.muya.options.clipboardFilePath()
|
||||
if (imagePath && typeof imagePath === 'string' && IMAGE_EXT_REG.test(imagePath)) {
|
||||
const id = `loading-${getUniqueId()}`
|
||||
if (this.selectedImage) {
|
||||
this.replaceImage(this.selectedImage, {
|
||||
alt: id,
|
||||
src: imagePath,
|
||||
})
|
||||
} else {
|
||||
this.insertImage({
|
||||
alt: id,
|
||||
src: imagePath
|
||||
})
|
||||
}
|
||||
const nSrc = await this.muya.options.imageAction(imagePath)
|
||||
const { src } = getImageSrc(imagePath)
|
||||
if (src) {
|
||||
this.stateRender.urlMap.set(nSrc, src)
|
||||
}
|
||||
|
||||
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
||||
|
||||
if (imageWrapper) {
|
||||
const imageInfo = getImageInfo(imageWrapper)
|
||||
this.replaceImage(imageInfo, {
|
||||
src: nSrc
|
||||
})
|
||||
}
|
||||
return imagePath
|
||||
}
|
||||
|
||||
const items = event.clipboardData && event.clipboardData.items
|
||||
let file = null
|
||||
if (items && items.length) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf('image') !== -1) {
|
||||
file = items[i].getAsFile()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle paste to create inline image
|
||||
if (file) {
|
||||
const id = `loading-${getUniqueId()}`
|
||||
if (this.selectedImage) {
|
||||
this.replaceImage(this.selectedImage, {
|
||||
alt: id,
|
||||
src: ''
|
||||
})
|
||||
} else {
|
||||
this.insertImage({
|
||||
alt: id,
|
||||
src: ''
|
||||
})
|
||||
}
|
||||
|
||||
const reader = new FileReader()
|
||||
reader.onload = event => {
|
||||
const base64 = event.target.result
|
||||
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
||||
const imageContainer = this.muya.container.querySelector(`span[data-id=${id}] .ag-image-container`)
|
||||
this.stateRender.urlMap.set(id, base64)
|
||||
if (imageContainer) {
|
||||
imageWrapper.classList.remove('ag-empty-image')
|
||||
imageWrapper.classList.add('ag-image-success')
|
||||
const image = document.createElement('img')
|
||||
image.src = base64
|
||||
imageContainer.appendChild(image)
|
||||
}
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
|
||||
const nSrc = await this.muya.options.imageAction(file)
|
||||
const base64 = this.stateRender.urlMap.get(id)
|
||||
if (base64) {
|
||||
this.stateRender.urlMap.set(nSrc, base64)
|
||||
this.stateRender.urlMap.delete(id)
|
||||
}
|
||||
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
||||
|
||||
if (imageWrapper) {
|
||||
const imageInfo = getImageInfo(imageWrapper)
|
||||
this.replaceImage(imageInfo, {
|
||||
src: nSrc
|
||||
})
|
||||
}
|
||||
return file
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
ContentState.prototype.docPasteHandler = async function (event) {
|
||||
const file = await this.pasteImage(event)
|
||||
if (file) {
|
||||
return event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
// handle `normal` and `pasteAsPlainText` paste
|
||||
ContentState.prototype.pasteHandler = function (event, type) {
|
||||
ContentState.prototype.pasteHandler = async function (event, type = 'normal', rawText, rawHtml) {
|
||||
event.preventDefault()
|
||||
const text = event.clipboardData.getData('text/plain')
|
||||
let html = event.clipboardData.getData('text/html')
|
||||
event.stopPropagation()
|
||||
const text = rawText || event.clipboardData.getData('text/plain')
|
||||
let html = rawHtml || event.clipboardData.getData('text/html')
|
||||
html = this.standardizeHTML(html)
|
||||
const copyType = this.checkCopyType(html, text)
|
||||
const { start, end } = this.cursor
|
||||
@ -94,6 +198,11 @@ const pasteCtrl = ContentState => {
|
||||
return this.pasteHandler(event, type)
|
||||
}
|
||||
|
||||
const file = await this.pasteImage(event)
|
||||
if (file) {
|
||||
return
|
||||
}
|
||||
|
||||
const appendHtml = (text) => {
|
||||
startBlock.text = startBlock.text.substring(0, start.offset) + text + startBlock.text.substring(start.offset)
|
||||
const { key } = start
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { operateClassName } from '../utils/domManipulate'
|
||||
import { getImageInfo } from '../utils/getImageInfo'
|
||||
import { CLASS_OR_ID } from '../config'
|
||||
import selection from '../selection'
|
||||
|
||||
@ -43,6 +44,7 @@ class ClickEvent {
|
||||
const { target } = event
|
||||
// handler table click
|
||||
const toolItem = getToolItem(target)
|
||||
contentState.selectedImage = null
|
||||
if (toolItem) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@ -56,6 +58,9 @@ class ClickEvent {
|
||||
const markedImageText = target.previousElementSibling
|
||||
const mathRender = target.closest(`.${CLASS_OR_ID['AG_MATH_RENDER']}`)
|
||||
const rubyRender = target.closest(`.${CLASS_OR_ID['AG_RUBY_RENDER']}`)
|
||||
const imageWrapper = target.closest(`.${CLASS_OR_ID['AG_INLINE_IMAGE']}`)
|
||||
const imageTurnInto = target.closest('.ag-image-icon-turninto')
|
||||
const imageDelete = target.closest('.ag-image-icon-delete') || target.closest('.ag-image-icon-close')
|
||||
const mathText = mathRender && mathRender.previousElementSibling
|
||||
const rubyText = rubyRender && rubyRender.previousElementSibling
|
||||
if (markedImageText && markedImageText.classList.contains(CLASS_OR_ID['AG_IMAGE_MARKED_TEXT'])) {
|
||||
@ -70,6 +75,51 @@ class ClickEvent {
|
||||
} else if (rubyText) {
|
||||
selectionText(rubyText)
|
||||
}
|
||||
// Handle delete inline iamge by click delete icon.
|
||||
if (imageDelete && imageWrapper) {
|
||||
const imageInfo = getImageInfo(imageWrapper)
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
// hide image selector if needed.
|
||||
eventCenter.dispatch('muya-image-selector', { reference: null })
|
||||
return contentState.deleteImage(imageInfo)
|
||||
}
|
||||
|
||||
// Handle image click, to select the current image
|
||||
if (target.tagName === 'IMG' && imageWrapper) {
|
||||
// Handle select image
|
||||
const imageInfo = getImageInfo(imageWrapper)
|
||||
event.preventDefault()
|
||||
return contentState.selectImage(imageInfo)
|
||||
}
|
||||
|
||||
// Handle click imagewrapper when it's empty or image load failed.
|
||||
if (
|
||||
(imageTurnInto && imageWrapper) ||
|
||||
(imageWrapper &&
|
||||
(
|
||||
imageWrapper.classList.contains('ag-empty-image') ||
|
||||
imageWrapper.classList.contains('ag-image-fail')
|
||||
))
|
||||
) {
|
||||
const rect = imageWrapper.getBoundingClientRect()
|
||||
const reference = {
|
||||
getBoundingClientRect () {
|
||||
if (imageTurnInto) {
|
||||
rect.height = 0
|
||||
}
|
||||
return rect
|
||||
}
|
||||
}
|
||||
const imageInfo = getImageInfo(imageWrapper)
|
||||
eventCenter.dispatch('muya-image-selector', {
|
||||
reference,
|
||||
imageInfo,
|
||||
cb: () => {}
|
||||
})
|
||||
event.preventDefault()
|
||||
return event.stopPropagation()
|
||||
}
|
||||
if (target.closest('div.ag-container-preview') || target.closest('div.ag-html-preview')) {
|
||||
return event.stopPropagation()
|
||||
}
|
||||
|
@ -8,6 +8,9 @@ class Clipboard {
|
||||
|
||||
listen () {
|
||||
const { container, eventCenter, contentState } = this.muya
|
||||
const docPasteHandler = event => {
|
||||
contentState.docPasteHandler(event)
|
||||
}
|
||||
const copyCutHandler = event => {
|
||||
contentState.copyHandler(event, this._copyType)
|
||||
if (event.type === 'cut') {
|
||||
@ -22,6 +25,7 @@ class Clipboard {
|
||||
this._pasteType = 'normal'
|
||||
}
|
||||
|
||||
eventCenter.attachDOMEvent(document, 'paste', docPasteHandler)
|
||||
eventCenter.attachDOMEvent(container, 'paste', pasteHandler)
|
||||
eventCenter.attachDOMEvent(container, 'cut', copyCutHandler)
|
||||
eventCenter.attachDOMEvent(container, 'copy', copyCutHandler)
|
||||
|
39
src/muya/lib/eventHandler/dragDrop.js
Normal file
@ -0,0 +1,39 @@
|
||||
class DragDrop {
|
||||
constructor (muya) {
|
||||
this.muya = muya
|
||||
this.dragOverBinding()
|
||||
this.dropBinding()
|
||||
this.dragendBinding()
|
||||
}
|
||||
|
||||
dragOverBinding () {
|
||||
const { container, eventCenter, contentState } = this.muya
|
||||
|
||||
const dragoverHandler = event => {
|
||||
contentState.dragoverHandler(event)
|
||||
}
|
||||
|
||||
eventCenter.attachDOMEvent(container, 'dragover', dragoverHandler)
|
||||
}
|
||||
dropBinding () {
|
||||
const { container, eventCenter, contentState } = this.muya
|
||||
|
||||
const dropHandler = event => {
|
||||
contentState.dropHandler(event)
|
||||
}
|
||||
|
||||
eventCenter.attachDOMEvent(container, 'drop', dropHandler)
|
||||
}
|
||||
|
||||
dragendBinding () {
|
||||
const { eventCenter, contentState } = this.muya
|
||||
|
||||
const dragleaveHandler = event => {
|
||||
contentState.dragleaveHandler(event)
|
||||
}
|
||||
|
||||
eventCenter.attachDOMEvent(window, 'dragleave', dragleaveHandler)
|
||||
}
|
||||
}
|
||||
|
||||
export default DragDrop
|
@ -1,7 +1,7 @@
|
||||
import { EVENT_KEYS } from '../config'
|
||||
import selection from '../selection'
|
||||
import { findNearestParagraph } from '../selection/dom'
|
||||
import { getParagraphReference } from '../utils'
|
||||
import { getParagraphReference, getImageInfo } from '../utils'
|
||||
import { checkEditEmoji } from '../ui/emojis'
|
||||
|
||||
class Keyboard {
|
||||
@ -90,6 +90,31 @@ class Keyboard {
|
||||
|
||||
keydownBinding () {
|
||||
const { container, eventCenter, contentState } = this.muya
|
||||
const docHandler = event => {
|
||||
switch (event.code) {
|
||||
case EVENT_KEYS.Enter:
|
||||
return contentState.docEnterHandler(event)
|
||||
case EVENT_KEYS.Space: {
|
||||
if (contentState.selectedImage) {
|
||||
const { src } = getImageInfo(contentState.selectedImage.token.src)
|
||||
if (src) {
|
||||
eventCenter.dispatch('preview-image', {
|
||||
data: src
|
||||
})
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case EVENT_KEYS.Backspace: {
|
||||
return contentState.docBackspaceHandler(event)
|
||||
}
|
||||
case EVENT_KEYS.ArrowUp: // fallthrough
|
||||
case EVENT_KEYS.ArrowDown: // fallthrough
|
||||
case EVENT_KEYS.ArrowLeft: // fallthrough
|
||||
case EVENT_KEYS.ArrowRight: // fallthrough
|
||||
return contentState.docArrowHandler(event)
|
||||
}
|
||||
}
|
||||
|
||||
const handler = event => {
|
||||
if (event.metaKey || event.ctrlKey) {
|
||||
@ -145,6 +170,7 @@ class Keyboard {
|
||||
}
|
||||
|
||||
eventCenter.attachDOMEvent(container, 'keydown', handler)
|
||||
eventCenter.attachDOMEvent(document, 'keydown', docHandler)
|
||||
}
|
||||
|
||||
inputBinding () {
|
||||
@ -180,7 +206,7 @@ class Keyboard {
|
||||
const node = selection.getSelectionStart()
|
||||
const paragraph = findNearestParagraph(node)
|
||||
const emojiNode = checkEditEmoji(node)
|
||||
|
||||
contentState.selectedImage = null
|
||||
if (
|
||||
paragraph &&
|
||||
emojiNode &&
|
||||
@ -224,12 +250,6 @@ class Keyboard {
|
||||
}
|
||||
}
|
||||
|
||||
// hide image-path float box
|
||||
const imageTextNode = contentState.getImageTextNode()
|
||||
if (!imageTextNode) {
|
||||
eventCenter.dispatch('muya-image-picker', { list: [] })
|
||||
}
|
||||
|
||||
const block = contentState.getBlock(anchor.key)
|
||||
if (anchor.key === focus.key && anchor.offset !== focus.offset && block.functionType !== 'codeLine') {
|
||||
const reference = contentState.getPositionReference()
|
||||
|
@ -2,6 +2,7 @@ import ContentState from './contentState'
|
||||
import EventCenter from './eventHandler/event'
|
||||
import Clipboard from './eventHandler/clipboard'
|
||||
import Keyboard from './eventHandler/keyboard'
|
||||
import DragDrop from './eventHandler/dragDrop'
|
||||
import ClickEvent from './eventHandler/clickEvent'
|
||||
import { CLASS_OR_ID, MUYA_DEFAULT_OPTION } from './config'
|
||||
import { wordCount } from './utils'
|
||||
@ -33,6 +34,7 @@ class Muya {
|
||||
this.clipboard = new Clipboard(this)
|
||||
this.clickEvent = new ClickEvent(this)
|
||||
this.keyboard = new Keyboard(this)
|
||||
this.dragdrop = new DragDrop(this)
|
||||
this.init()
|
||||
}
|
||||
|
||||
@ -40,7 +42,6 @@ class Muya {
|
||||
const { container, contentState, eventCenter } = this
|
||||
contentState.stateRender.setContainer(container.children[0])
|
||||
eventCenter.subscribe('stateChange', this.dispatchChange)
|
||||
contentState.listenForPathChange()
|
||||
const { markdown } = this
|
||||
const { focusMode } = this.options
|
||||
this.setMarkdown(markdown)
|
||||
@ -232,20 +233,12 @@ class Muya {
|
||||
this.container.blur()
|
||||
}
|
||||
|
||||
showAutoImagePath (files) {
|
||||
const list = files.map(f => {
|
||||
const iconClass = f.type === 'directory' ? 'icon-folder' : 'icon-image'
|
||||
return Object.assign(f, { iconClass, text: f.file + (f.type === 'directory' ? '/' : '') })
|
||||
})
|
||||
this.contentState.showAutoImagePath(list)
|
||||
}
|
||||
|
||||
format (type) {
|
||||
this.contentState.format(type)
|
||||
}
|
||||
|
||||
insertImage (url) {
|
||||
this.contentState.insertImage(url)
|
||||
insertImage (imageInfo) {
|
||||
this.contentState.insertImage(imageInfo)
|
||||
}
|
||||
|
||||
search (value, opt) {
|
||||
@ -288,7 +281,13 @@ class Muya {
|
||||
}
|
||||
|
||||
selectAll () {
|
||||
this.contentState.selectAll()
|
||||
if (this.hasFocus()) {
|
||||
this.contentState.selectAll()
|
||||
}
|
||||
const activeElement = document.activeElement
|
||||
if (activeElement.nodeName === 'INPUT') {
|
||||
activeElement.select()
|
||||
}
|
||||
}
|
||||
|
||||
copyAsMarkdown () {
|
||||
|
@ -197,9 +197,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
|
||||
start: pos,
|
||||
end: pos + imageTo[0].length
|
||||
},
|
||||
// An image description has inline elements as its contents.
|
||||
// When an image is rendered to HTML, this is standardly used as the image’s alt attribute.
|
||||
alt: imageTo[2].replace(/[`*{}[\]()#+\-.!_>~:|<>$]/g, ''),
|
||||
alt: imageTo[2],
|
||||
backlash: {
|
||||
first: imageTo[3],
|
||||
second: imageTo[5]
|
||||
|
@ -20,6 +20,7 @@ class StateRender {
|
||||
this.diagramCache = new Map()
|
||||
this.tokenCache = new Map()
|
||||
this.labels = new Map()
|
||||
this.urlMap = new Map()
|
||||
this.container = null
|
||||
}
|
||||
|
||||
@ -77,7 +78,8 @@ class StateRender {
|
||||
return active ? CLASS_OR_ID['AG_HIGHLIGHT'] : CLASS_OR_ID['AG_SELECTION']
|
||||
}
|
||||
|
||||
getSelector (block, cursor, activeBlocks, selectedBlock) {
|
||||
getSelector (block, activeBlocks) {
|
||||
const { cursor, selectedBlock } = this.muya.contentState
|
||||
const type = block.type === 'hr' ? 'p' : block.type
|
||||
const isActive = activeBlocks.some(b => b.key === block.key) || block.key === cursor.start.key
|
||||
|
||||
@ -142,11 +144,10 @@ class StateRender {
|
||||
}
|
||||
}
|
||||
|
||||
render (blocks, cursor, activeBlocks, matches, selectedBlock) {
|
||||
render (blocks, activeBlocks, matches) {
|
||||
const selector = `div#${CLASS_OR_ID['AG_EDITOR_ID']}`
|
||||
|
||||
const children = blocks.map(block => {
|
||||
return this.renderBlock(block, cursor, activeBlocks, selectedBlock, matches, true)
|
||||
return this.renderBlock(block, activeBlocks, matches, true)
|
||||
})
|
||||
|
||||
const newVdom = h(selector, children)
|
||||
@ -160,11 +161,11 @@ class StateRender {
|
||||
}
|
||||
|
||||
// Only render the blocks which you updated
|
||||
partialRender (blocks, cursor, activeBlocks, matches, startKey, endKey, selectedBlock) {
|
||||
partialRender (blocks, activeBlocks, matches, startKey, endKey) {
|
||||
const cursorOutMostBlock = activeBlocks[activeBlocks.length - 1]
|
||||
// If cursor is not in render blocks, need to render cursor block independently
|
||||
const needRenderCursorBlock = blocks.indexOf(cursorOutMostBlock) === -1
|
||||
const newVnode = h('section', blocks.map(block => this.renderBlock(block, cursor, activeBlocks, selectedBlock, matches)))
|
||||
const newVnode = h('section', blocks.map(block => this.renderBlock(block, activeBlocks, matches)))
|
||||
const html = toHTML(newVnode).replace(/^<section>([\s\S]+?)<\/section>$/, '$1')
|
||||
|
||||
const needToRemoved = []
|
||||
@ -193,7 +194,7 @@ class StateRender {
|
||||
const cursorDom = document.querySelector(`#${key}`)
|
||||
if (cursorDom) {
|
||||
const oldCursorVnode = toVNode(cursorDom)
|
||||
const newCursorVnode = this.renderBlock(cursorOutMostBlock, cursor, activeBlocks, selectedBlock, matches)
|
||||
const newCursorVnode = this.renderBlock(cursorOutMostBlock, activeBlocks, matches)
|
||||
patch(oldCursorVnode, newCursorVnode)
|
||||
}
|
||||
}
|
||||
@ -202,6 +203,24 @@ class StateRender {
|
||||
this.renderDiagram()
|
||||
this.codeCache.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Only render one block.
|
||||
*
|
||||
* @param {object} block
|
||||
* @param {array} activeBlocks
|
||||
* @param {array} matches
|
||||
*/
|
||||
singleRender (block, activeBlocks, matches) {
|
||||
const selector = `#${block.key}`
|
||||
const newVdom = this.renderBlock(block, activeBlocks, matches, true)
|
||||
const rootDom = document.querySelector(selector)
|
||||
const oldVdom = toVNode(rootDom)
|
||||
patch(oldVdom, newVdom)
|
||||
this.renderMermaid()
|
||||
this.renderDiagram()
|
||||
this.codeCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
mixins(StateRender, renderInlines, renderBlock)
|
||||
|
@ -1,10 +1,10 @@
|
||||
/**
|
||||
* [renderBlock render one block, no matter it is a container block or text block]
|
||||
*/
|
||||
export default function renderBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
|
||||
export default function renderBlock (block, activeBlocks, matches, useCache = false) {
|
||||
const method = Array.isArray(block.children) && block.children.length > 0
|
||||
? 'renderContainerBlock'
|
||||
: 'renderLeafBlock'
|
||||
|
||||
return this[method](block, cursor, activeBlocks, selectedBlock, matches, useCache)
|
||||
return this[method](block, activeBlocks, matches, useCache)
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ const PRE_BLOCK_HASH = {
|
||||
'vega-lite': `.${CLASS_OR_ID['AG_VEGA_LITE']}`
|
||||
}
|
||||
|
||||
export default function renderContainerBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
|
||||
let selector = this.getSelector(block, cursor, activeBlocks, selectedBlock)
|
||||
export default function renderContainerBlock (block, activeBlocks, matches, useCache = false) {
|
||||
let selector = this.getSelector(block, activeBlocks)
|
||||
const {
|
||||
type,
|
||||
headingStyle,
|
||||
@ -28,7 +28,7 @@ export default function renderContainerBlock (block, cursor, activeBlocks, selec
|
||||
isLooseListItem,
|
||||
lang
|
||||
} = block
|
||||
const children = block.children.map(child => this.renderBlock(child, cursor, activeBlocks, selectedBlock, matches, useCache))
|
||||
const children = block.children.map(child => this.renderBlock(child, activeBlocks, matches, useCache))
|
||||
const data = {
|
||||
attrs: {},
|
||||
dataset: {}
|
||||
|
@ -49,9 +49,10 @@ const hasReferenceToken = tokens => {
|
||||
return result
|
||||
}
|
||||
|
||||
export default function renderLeafBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
|
||||
export default function renderLeafBlock (block, activeBlocks, matches, useCache = false) {
|
||||
const { loadMathMap } = this
|
||||
let selector = this.getSelector(block, cursor, activeBlocks, selectedBlock)
|
||||
const { cursor } = this.muya.contentState
|
||||
let selector = this.getSelector(block, activeBlocks)
|
||||
// highlight search key in block
|
||||
const highlights = matches.filter(m => m.key === block.key)
|
||||
const {
|
||||
|
@ -1,79 +1,138 @@
|
||||
import { CLASS_OR_ID, IMAGE_EXT_REG, isInElectron } from '../../../config'
|
||||
import { CLASS_OR_ID } from '../../../config'
|
||||
import { getImageInfo } from '../../../utils'
|
||||
import ImageIcon from '../../../assets/pngicon/image/2.png'
|
||||
import ImageFailIcon from '../../../assets/pngicon/image_fail/2.png'
|
||||
import ImageEditIcon from '../../../assets/pngicon/imageEdit/2.png'
|
||||
import DeleteIcon from '../../../assets/pngicon/delete/delete@2x.png'
|
||||
|
||||
const renderIcon = (h, className, icon) => {
|
||||
const selector = `a.${className}`
|
||||
const iconVnode = h('i.icon', h(`i.icon-inner`, {
|
||||
style: {
|
||||
background: `url(${icon}) no-repeat`,
|
||||
'background-size': '100%'
|
||||
}
|
||||
}, ''))
|
||||
|
||||
return h(selector, {
|
||||
attrs: {
|
||||
contenteditable: 'false'
|
||||
}
|
||||
}, iconVnode)
|
||||
}
|
||||
|
||||
// I dont want operate dom directly, is there any better method? need help!
|
||||
export default function image (h, cursor, block, token, outerClass) {
|
||||
const { eventCenter } = this
|
||||
const { start: cursorStart, end: cursorEnd } = cursor
|
||||
const { start, end } = token.range
|
||||
|
||||
if (
|
||||
cursorStart.key === cursorEnd.key &&
|
||||
cursorStart.offset === cursorEnd.offset &&
|
||||
cursorStart.offset === end - 1 &&
|
||||
!IMAGE_EXT_REG.test(token.src) &&
|
||||
isInElectron
|
||||
) {
|
||||
eventCenter.dispatch('image-path', token.src)
|
||||
const imageInfo = getImageInfo(token.src + encodeURI(token.backlash.second))
|
||||
const { selectedImage } = this.muya.contentState
|
||||
const data = {
|
||||
dataset: {
|
||||
raw: token.raw
|
||||
},
|
||||
attrs: {
|
||||
contenteditable: 'true'
|
||||
}
|
||||
}
|
||||
|
||||
const className = this.getClassName(outerClass, block, token, cursor)
|
||||
const imageClass = CLASS_OR_ID['AG_IMAGE_MARKED_TEXT']
|
||||
const titleContent = this.highlight(h, block, start, start + 2 + token.alt.length, token)
|
||||
const srcContent = this.highlight(
|
||||
h, block,
|
||||
start + 2 + token.alt.length + token.backlash.first.length + 2,
|
||||
start + 2 + token.alt.length + token.backlash.first.length + 2 + token.srcAndTitle.length,
|
||||
token
|
||||
)
|
||||
|
||||
const secondBracketContent = this.highlight(
|
||||
h, block,
|
||||
start + 2 + token.alt.length + token.backlash.first.length,
|
||||
start + 2 + token.alt.length + token.backlash.first.length + 2,
|
||||
token
|
||||
)
|
||||
|
||||
const lastBracketContent = this.highlight(h, block, end - 1, end, token)
|
||||
|
||||
const firstBacklashStart = start + 2 + token.alt.length
|
||||
|
||||
const secondBacklashStart = end - 1 - token.backlash.second.length
|
||||
|
||||
let id
|
||||
let isSuccess
|
||||
let selector
|
||||
const imageInfo = getImageInfo(token.src + encodeURI(token.backlash.second))
|
||||
const { src } = imageInfo
|
||||
let { src } = imageInfo
|
||||
const alt = token.alt + encodeURI(token.backlash.first)
|
||||
const { title } = token
|
||||
|
||||
if (src) {
|
||||
({ id, isSuccess } = this.loadImageAsync(imageInfo, alt, className))
|
||||
({ id, isSuccess } = this.loadImageAsync(imageInfo, alt))
|
||||
}
|
||||
let wrapperSelector = id
|
||||
? `span#${id}.${CLASS_OR_ID['AG_INLINE_IMAGE']}`
|
||||
: `span.${CLASS_OR_ID['AG_INLINE_IMAGE']}`
|
||||
|
||||
selector = id ? `span#${id}.${imageClass}.${CLASS_OR_ID['AG_REMOVE']}` : `span.${imageClass}.${CLASS_OR_ID['AG_REMOVE']}`
|
||||
|
||||
if (isSuccess) {
|
||||
if (className === CLASS_OR_ID['AG_HIDE']) {
|
||||
selector += `.${className}`
|
||||
}
|
||||
} else {
|
||||
selector += `.${CLASS_OR_ID['AG_IMAGE_FAIL']}`
|
||||
}
|
||||
const children = [
|
||||
...titleContent,
|
||||
...this.backlashInToken(h, token.backlash.first, className, firstBacklashStart, token),
|
||||
...secondBracketContent,
|
||||
h(`span.${CLASS_OR_ID['AG_IMAGE_SRC']}`, srcContent),
|
||||
...this.backlashInToken(h, token.backlash.second, className, secondBacklashStart, token),
|
||||
...lastBracketContent
|
||||
const imageIcons = [
|
||||
renderIcon(h, 'ag-image-icon-success', ImageIcon),
|
||||
renderIcon(h, 'ag-image-icon-fail', ImageFailIcon),
|
||||
renderIcon(h, 'ag-image-icon-close', DeleteIcon)
|
||||
]
|
||||
const toolIcons = [
|
||||
renderIcon(h, 'ag-image-icon-turninto', ImageEditIcon),
|
||||
renderIcon(h, 'ag-image-icon-delete', DeleteIcon)
|
||||
]
|
||||
const renderImageContainer = (...args) => {
|
||||
return h(`span.${CLASS_OR_ID['AG_IMAGE_CONTAINER']}`, {
|
||||
attrs: {
|
||||
contenteditable: 'true'
|
||||
}
|
||||
}, args)
|
||||
}
|
||||
|
||||
return isSuccess
|
||||
? [
|
||||
h(selector, children),
|
||||
h('img', { props: { alt, src, title } })
|
||||
// the src image is still loading, so use the url Map base64.
|
||||
if (this.urlMap.has(src)) {
|
||||
// fix: it will generate a new id if the image is not loaded.
|
||||
const { selectedImage } = this.muya.contentState
|
||||
if (selectedImage && selectedImage.token.src === src && selectedImage.imageId !== id) {
|
||||
selectedImage.imageId = id
|
||||
}
|
||||
src = this.urlMap.get(src)
|
||||
isSuccess = true
|
||||
}
|
||||
|
||||
if (alt.startsWith('loading-')) {
|
||||
wrapperSelector += `.${CLASS_OR_ID['AG_IMAGE_UPLOADING']}`
|
||||
Object.assign(data.dataset, {
|
||||
id: alt
|
||||
})
|
||||
if (this.urlMap.has(alt)) {
|
||||
src = this.urlMap.get(alt)
|
||||
isSuccess = true
|
||||
}
|
||||
}
|
||||
if (src) {
|
||||
// image is loading...
|
||||
if (typeof isSuccess === 'undefined') {
|
||||
wrapperSelector += `.${CLASS_OR_ID['AG_IMAGE_LOADING']}`
|
||||
} else if (isSuccess === true) {
|
||||
wrapperSelector += `.${CLASS_OR_ID['AG_IMAGE_SUCCESS']}`
|
||||
} else {
|
||||
wrapperSelector += `.${CLASS_OR_ID['AG_IMAGE_FAIL']}`
|
||||
}
|
||||
|
||||
if (selectedImage) {
|
||||
const { key, token: selectToken } = selectedImage
|
||||
if (
|
||||
key === block.key &&
|
||||
selectToken.range.start === token.range.start &&
|
||||
selectToken.range.end === token.range.end
|
||||
) {
|
||||
wrapperSelector += `.${CLASS_OR_ID['AG_INLINE_IMAGE_SELECTED']}`
|
||||
}
|
||||
}
|
||||
|
||||
return isSuccess
|
||||
? [
|
||||
h(wrapperSelector, data, [
|
||||
...imageIcons,
|
||||
renderImageContainer(
|
||||
...toolIcons,
|
||||
// An image description has inline elements as its contents.
|
||||
// When an image is rendered to HTML, this is standardly used as the image’s alt attribute.
|
||||
h('img', { props: { alt: alt.replace(/[`*{}[\]()#+\-.!_>~:|<>$]/g, ''), src, title } })
|
||||
)
|
||||
])
|
||||
]
|
||||
: [
|
||||
h(wrapperSelector, data, [
|
||||
...imageIcons,
|
||||
renderImageContainer(
|
||||
...toolIcons
|
||||
)
|
||||
])
|
||||
]
|
||||
} else {
|
||||
wrapperSelector += `.${CLASS_OR_ID['AG_EMPTY_IMAGE']}`
|
||||
return [
|
||||
h(wrapperSelector, data, [
|
||||
...imageIcons,
|
||||
renderImageContainer(
|
||||
...toolIcons
|
||||
)
|
||||
])
|
||||
]
|
||||
: [h(selector, children)]
|
||||
}
|
||||
}
|
||||
|
@ -11,16 +11,31 @@ export default function loadImageAsync (imageInfo, alt, className, imageClass) {
|
||||
id = getUniqueId()
|
||||
loadImage(src, isUnknownType)
|
||||
.then(url => {
|
||||
const imageWrapper = document.querySelector(`#${id}`)
|
||||
const imageText = document.querySelector(`#${id}`)
|
||||
const img = document.createElement('img')
|
||||
img.src = url
|
||||
if (alt) img.alt = alt
|
||||
if (alt) img.alt = alt.replace(/[`*{}[\]()#+\-.!_>~:|<>$]/g, '')
|
||||
if (imageClass) {
|
||||
img.classList.add(imageClass)
|
||||
}
|
||||
if (imageWrapper) {
|
||||
insertAfter(img, imageWrapper)
|
||||
operateClassName(imageWrapper, 'add', className)
|
||||
|
||||
if (imageText) {
|
||||
if (imageText.classList.contains('ag-inline-image')) {
|
||||
const imageContainer = imageText.querySelector('.ag-image-container')
|
||||
const oldImage = imageContainer.querySelector('img')
|
||||
if (oldImage) {
|
||||
oldImage.remove()
|
||||
}
|
||||
imageContainer.appendChild(img)
|
||||
imageText.classList.remove('ag-image-loading')
|
||||
imageText.classList.add('ag-image-success')
|
||||
} else {
|
||||
insertAfter(img, imageText)
|
||||
operateClassName(imageText, 'add', className)
|
||||
}
|
||||
}
|
||||
if (this.urlMap.has(src)) {
|
||||
this.urlMap.delete(src)
|
||||
}
|
||||
this.loadImageMap.set(src, {
|
||||
id,
|
||||
@ -28,9 +43,17 @@ export default function loadImageAsync (imageInfo, alt, className, imageClass) {
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
const imageWrapper = document.querySelector(`#${id}`)
|
||||
if (imageWrapper) {
|
||||
operateClassName(imageWrapper, 'add', CLASS_OR_ID['AG_IMAGE_FAIL'])
|
||||
const imageText = document.querySelector(`#${id}`)
|
||||
if (imageText) {
|
||||
operateClassName(imageText, 'remove', CLASS_OR_ID['AG_IMAGE_LOADING'])
|
||||
operateClassName(imageText, 'add', CLASS_OR_ID['AG_IMAGE_FAIL'])
|
||||
const image = imageText.querySelector('img')
|
||||
if (image) {
|
||||
image.remove()
|
||||
}
|
||||
}
|
||||
if (this.urlMap.has(src)) {
|
||||
this.urlMap.delete(src)
|
||||
}
|
||||
this.loadImageMap.set(src, {
|
||||
id,
|
||||
|
@ -1,5 +1,7 @@
|
||||
// render token of text type to vdom.
|
||||
export default function text (h, cursor, block, token) {
|
||||
const { start, end } = token.range
|
||||
return this.highlight(h, block, start, end, token)
|
||||
return [
|
||||
h('span.ag-plain-text', this.highlight(h, block, start, end, token))
|
||||
]
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ export const inlineRules = {
|
||||
'reference_link': /^\[([^\]]+?)(\\*)\](?:\[([^\]]*?)(\\*)\])?/,
|
||||
'reference_image': /^\!\[([^\]]+?)(\\*)\](?:\[([^\]]*?)(\\*)\])?/,
|
||||
'tail_header': /^(\s{1,}#{1,})(\s*)$/,
|
||||
'html_tag': /^(<!--[\s\S]*?-->|(<([a-zA-Z]{1}[a-zA-Z\d-]*) *[_\.\-/:a-zA-Z\d='";\? *]* *(?:\/)?>)(?:([\s\S]*?)(<\/\3 *>))?)/, // row html
|
||||
'html_tag': /^(<!--[\s\S]*?-->|(<([a-zA-Z]{1}[a-zA-Z\d-]*) *[_\.\-/:a-zA-Z\d='";\? *]* *(?:\/)?>)(?:([\s\S]*?)(<\/\3 *>))?)/, // raw html
|
||||
'html_escape': new RegExp(`^(${escapeCharacters.join('|')})`, 'i'),
|
||||
'soft_line_break': /^(\n)(?!\n)/,
|
||||
'hard_line_break': /^( {2,})(\n)(?!\n)/,
|
||||
|
@ -77,13 +77,24 @@ export const getAttributes = html => {
|
||||
|
||||
export const parseSrcAndTitle = (text = '') => {
|
||||
const parts = text.split(/\s+/)
|
||||
const src = parts[0]
|
||||
const rawTitle = text.substring(src.length).trim()
|
||||
if (parts.length === 1) {
|
||||
return {
|
||||
src: text.trim(),
|
||||
title: ''
|
||||
}
|
||||
}
|
||||
const rawTitle = parts.pop()
|
||||
let src = ''
|
||||
const TITLE_REG = /^('|")(.*?)\1$/ // we only support use `'` and `"` to indicate a title now.
|
||||
let title = ''
|
||||
if (rawTitle && TITLE_REG.test(rawTitle)) {
|
||||
title = rawTitle.replace(TITLE_REG, '$2')
|
||||
}
|
||||
if (title) {
|
||||
src = text.substring(0, text.length - rawTitle.length).trim()
|
||||
} else {
|
||||
src = text.trim()
|
||||
}
|
||||
return { src, title }
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,23 @@ export const getTextContent = (node, blackList) => {
|
||||
}
|
||||
if (node.nodeType === 3) {
|
||||
text += node.textContent
|
||||
} else if (node.nodeType === 1 && node.classList.contains('ag-inline-image')) {
|
||||
// handle inline image
|
||||
const raw = node.getAttribute('data-raw')
|
||||
const imageContainer = node.querySelector('.ag-image-container')
|
||||
const hasImg = imageContainer.querySelector('img')
|
||||
const childNodes = imageContainer.childNodes
|
||||
if (childNodes.length && hasImg) {
|
||||
for (const child of childNodes) {
|
||||
if (child.nodeType === 1 && child.nodeName === 'IMG') {
|
||||
text += raw
|
||||
} else if (child.nodeType === 3) {
|
||||
text += child.textContent
|
||||
}
|
||||
}
|
||||
} else {
|
||||
text += raw
|
||||
}
|
||||
} else {
|
||||
const childNodes = node.childNodes
|
||||
for (const n of childNodes) {
|
||||
@ -25,6 +42,23 @@ export const getTextContent = (node, blackList) => {
|
||||
return text
|
||||
}
|
||||
|
||||
export const getOffsetOfParagraph = (node, paragraph) => {
|
||||
let offset = 0
|
||||
let preSibling = node
|
||||
|
||||
if (node === paragraph) return offset
|
||||
|
||||
do {
|
||||
preSibling = preSibling.previousSibling
|
||||
if (preSibling) {
|
||||
offset += getTextContent(preSibling, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
||||
}
|
||||
} while (preSibling)
|
||||
return (node === paragraph || node.parentNode === paragraph)
|
||||
? offset
|
||||
: offset + getOffsetOfParagraph(node.parentNode, paragraph)
|
||||
}
|
||||
|
||||
export const findNearestParagraph = node => {
|
||||
if (!node) {
|
||||
return null
|
||||
@ -39,7 +73,7 @@ export const findNearestParagraph = node => {
|
||||
export const findOutMostParagraph = node => {
|
||||
do {
|
||||
let parentNode = node.parentNode
|
||||
if (isAganippeEditorElement(parentNode) && isAganippeParagraph(node)) return node
|
||||
if (isMuyaEditorElement(parentNode) && isAganippeParagraph(node)) return node
|
||||
node = parentNode
|
||||
} while (node)
|
||||
}
|
||||
@ -53,7 +87,7 @@ export const isBlockContainer = element => {
|
||||
blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1
|
||||
}
|
||||
|
||||
export const isAganippeEditorElement = element => {
|
||||
export const isMuyaEditorElement = element => {
|
||||
return element && element.id === CLASS_OR_ID['AG_EDITOR_ID']
|
||||
}
|
||||
|
||||
@ -68,7 +102,7 @@ export const traverseUp = (current, testElementFunction) => {
|
||||
return current
|
||||
}
|
||||
// do not traverse upwards past the nearest containing editor
|
||||
if (isAganippeEditorElement(current)) {
|
||||
if (isMuyaEditorElement(current)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@ -100,7 +134,7 @@ export const getFirstSelectableLeafNode = element => {
|
||||
|
||||
export const getClosestBlockContainer = node => {
|
||||
return traverseUp(node, node => {
|
||||
return isBlockContainer(node) || isAganippeEditorElement(node)
|
||||
return isBlockContainer(node) || isMuyaEditorElement(node)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,8 @@ import {
|
||||
getClosestBlockContainer,
|
||||
getCursorPositionWithinMarkedText,
|
||||
findNearestParagraph,
|
||||
getTextContent
|
||||
getTextContent,
|
||||
getOffsetOfParagraph
|
||||
} from './dom'
|
||||
|
||||
const filterOnlyParentElements = node => {
|
||||
@ -424,11 +425,48 @@ class Selection {
|
||||
let count = 0
|
||||
for (i = 0; i < len; i++) {
|
||||
const child = childNodes[i]
|
||||
const textLength = getTextContent(child, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
||||
if (child.classList && child.classList.contains(CLASS_OR_ID['AG_FRONT_ICON'])) continue
|
||||
if (count + getTextContent(child, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length >= offset) {
|
||||
return getNodeAndOffset(child, offset - count)
|
||||
if (count + textLength >= offset) {
|
||||
if (
|
||||
child.classList && child.classList.contains('ag-inline-image')
|
||||
) {
|
||||
const imageContainer = child.querySelector('.ag-image-container')
|
||||
const hasImg = imageContainer.querySelector('img')
|
||||
if (!hasImg) {
|
||||
return {
|
||||
node: child,
|
||||
offset: 0
|
||||
}
|
||||
}
|
||||
if (count + textLength === offset) {
|
||||
if (child.nextElementSibling) {
|
||||
return {
|
||||
node: child.nextElementSibling,
|
||||
offset: 0
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
node: imageContainer,
|
||||
offset: 3
|
||||
}
|
||||
}
|
||||
} else if (count === offset && count === 0) {
|
||||
return {
|
||||
node: imageContainer,
|
||||
offset: 2
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
node: child,
|
||||
offset: 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return getNodeAndOffset(child, offset - count)
|
||||
}
|
||||
} else {
|
||||
count += getTextContent(child, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
||||
count += textLength
|
||||
}
|
||||
}
|
||||
return { node, offset }
|
||||
@ -436,10 +474,12 @@ class Selection {
|
||||
|
||||
let { node: anchorNode, offset: anchorOffset } = getNodeAndOffset(anchorParagraph, anchor.offset)
|
||||
let { node: focusNode, offset: focusOffset } = getNodeAndOffset(focusParagraph, focus.offset)
|
||||
if (anchorNode.nodeType === 3 || anchorNode.nodeType === 1 && !anchorNode.classList.contains('ag-image-container')) {
|
||||
anchorOffset = Math.min(anchorOffset, anchorNode.textContent.length)
|
||||
focusOffset = Math.min(focusOffset, focusNode.textContent.length)
|
||||
}
|
||||
|
||||
anchorOffset = Math.min(anchorOffset, anchorNode.textContent.length)
|
||||
focusOffset = Math.min(focusOffset, focusNode.textContent.length)
|
||||
// First set the anchor node and anchor offeet, make it collapsed
|
||||
// First set the anchor node and anchor offset, make it collapsed
|
||||
this.select(anchorNode, anchorOffset)
|
||||
// Secondly, set the focus node and focus offset.
|
||||
this.setFocus(focusNode, focusOffset)
|
||||
@ -457,7 +497,6 @@ class Selection {
|
||||
|
||||
getCursorRange () {
|
||||
let { anchorNode, anchorOffset, focusNode, focusOffset } = this.doc.getSelection()
|
||||
|
||||
const isAnchorValid = this.isValidCursorNode(anchorNode)
|
||||
const isFocusValid = this.isValidCursorNode(focusNode)
|
||||
let needFix = false
|
||||
@ -492,26 +531,44 @@ class Selection {
|
||||
|
||||
const anchorParagraph = findNearestParagraph(anchorNode)
|
||||
const focusParagraph = findNearestParagraph(focusNode)
|
||||
|
||||
const getOffsetOfParagraph = (node, paragraph) => {
|
||||
let offset = 0
|
||||
let preSibling = node
|
||||
|
||||
if (node === paragraph) return offset
|
||||
|
||||
do {
|
||||
preSibling = preSibling.previousSibling
|
||||
if (preSibling) {
|
||||
offset += getTextContent(preSibling, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
||||
}
|
||||
} while (preSibling)
|
||||
return (node === paragraph || node.parentNode === paragraph)
|
||||
? offset
|
||||
: offset + getOffsetOfParagraph(node.parentNode, paragraph)
|
||||
let aOffset = getOffsetOfParagraph(anchorNode, anchorParagraph) + anchorOffset
|
||||
let fOffset = getOffsetOfParagraph(focusNode, focusParagraph) + focusOffset
|
||||
// fix input after image.
|
||||
if (
|
||||
anchorNode === focusNode &&
|
||||
anchorOffset === focusOffset &&
|
||||
anchorNode.parentNode.classList.contains('ag-image-container') &&
|
||||
anchorNode.previousElementSibling &&
|
||||
anchorNode.previousElementSibling.nodeName === 'IMG'
|
||||
) {
|
||||
const imageWrapper = anchorNode.parentNode.parentNode
|
||||
const preElement = imageWrapper.previousElementSibling
|
||||
aOffset = 0
|
||||
if (preElement) {
|
||||
aOffset += getOffsetOfParagraph(preElement, anchorParagraph)
|
||||
aOffset += getTextContent(preElement, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
||||
}
|
||||
aOffset += getTextContent(imageWrapper, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
||||
fOffset = aOffset
|
||||
}
|
||||
|
||||
const aOffset = getOffsetOfParagraph(anchorNode, anchorParagraph) + anchorOffset
|
||||
const fOffset = getOffsetOfParagraph(focusNode, focusParagraph) + focusOffset
|
||||
if (
|
||||
anchorNode === focusNode &&
|
||||
anchorNode.nodeType === 1 &&
|
||||
anchorNode.classList.contains('ag-image-container')
|
||||
) {
|
||||
const imageWrapper = anchorNode.parentNode
|
||||
const preElement = imageWrapper.previousElementSibling
|
||||
aOffset = 0
|
||||
if (preElement) {
|
||||
aOffset += getOffsetOfParagraph(preElement, anchorParagraph)
|
||||
aOffset += getTextContent(preElement, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
||||
}
|
||||
if (anchorOffset === 3) {
|
||||
aOffset += getTextContent(imageWrapper, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
||||
}
|
||||
fOffset = aOffset
|
||||
}
|
||||
|
||||
const anchor = { key: anchorParagraph.id, offset: aOffset }
|
||||
const focus = { key: focusParagraph.id, offset: fOffset }
|
||||
|
@ -8,7 +8,6 @@
|
||||
right: -1000px;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--floatShadow);
|
||||
border: 1px solid var(--floatBorderColor);
|
||||
background-color: var(--floatBgColor);
|
||||
transition: opacity .25s ease-in-out;
|
||||
transform-origin: top;
|
||||
|
7
src/muya/lib/ui/imagePicker/index.css
Normal file
@ -0,0 +1,7 @@
|
||||
.ag-image-picker-wrapper {
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
.ag-image-picker-wrapper .ag-list-picker {
|
||||
width: 450px;
|
||||
}
|
@ -4,6 +4,8 @@ import FolderIcon from '../../assets/icons/folder.svg'
|
||||
import ImageIcon from '../../assets/icons/image.svg'
|
||||
import UploadIcon from '../../assets/icons/upload.svg'
|
||||
|
||||
import './index.css'
|
||||
|
||||
const iconhash = {
|
||||
'icon-image': ImageIcon,
|
||||
'icon-folder': FolderIcon,
|
||||
@ -18,6 +20,7 @@ class ImagePathPicker extends BaseScrollFloat {
|
||||
this.renderArray = []
|
||||
this.oldVnode = null
|
||||
this.activeItem = null
|
||||
this.floatBox.classList.add('ag-image-picker-wrapper')
|
||||
this.listen()
|
||||
}
|
||||
|
||||
|
128
src/muya/lib/ui/imageSelector/index.css
Normal file
@ -0,0 +1,128 @@
|
||||
.ag-image-selector-wrapper {
|
||||
border-radius: 3px;
|
||||
}
|
||||
.ag-image-selector {
|
||||
width: 500px;
|
||||
height: 190px;
|
||||
}
|
||||
|
||||
.ag-image-selector ul.header {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 40px;
|
||||
border-bottom: 1px solid var(--editorColor10);
|
||||
padding-left: 40px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ag-image-selector ul.header li {
|
||||
font-size: 14px;
|
||||
color: var(--editorColor);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ag-image-selector ul.header li span {
|
||||
display: inline-block;
|
||||
padding: 0 8px;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.ag-image-selector ul.header li span:hover {
|
||||
background: var(--editorColor04);
|
||||
}
|
||||
|
||||
.ag-image-selector ul.header li.active::before {
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
display: block;
|
||||
background: var(--themeColor);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.ag-image-selector .input-container {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
margin: 20px 0 15px 0;
|
||||
padding: 0 25px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.ag-image-selector .input-container input {
|
||||
height: 30px;
|
||||
border-radius: 15px;
|
||||
outline: none;
|
||||
padding: 0 15px;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
background: var(--inputBgColor);
|
||||
color: var(--editorColor);
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.ag-image-selector .input-container input:last-of-type {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.ag-image-selector .input-container input.src {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ag-image-selector .input-container input.title,
|
||||
.ag-image-selector .input-container input.alt {
|
||||
flex: 0;
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
.ag-image-selector span.role-button {
|
||||
display: block;
|
||||
width: 280px;
|
||||
height: 32px;
|
||||
border-radius: 8px;
|
||||
color: var(--editorColor80);
|
||||
line-height: 28px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--buttonBorderColor);
|
||||
box-shadow: 0 2px 4px 0 var(--buttonShadowColor);
|
||||
background: var(--buttonBgColor);
|
||||
cursor: pointer;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 16px;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ag-image-selector span.role-button.select {
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.ag-image-selector span.role-button:hover {
|
||||
background: var(--buttonHover);
|
||||
}
|
||||
|
||||
.ag-image-selector span.role-button:active {
|
||||
background: var(--buttonActive);
|
||||
}
|
||||
|
||||
.ag-image-selector span.description {
|
||||
text-align: center;
|
||||
display: block;
|
||||
color: var(--editorColor30);
|
||||
}
|
||||
|
||||
.ag-image-selector span.description a {
|
||||
cursor: pointer;
|
||||
color: #4ca4f5;
|
||||
}
|
338
src/muya/lib/ui/imageSelector/index.js
Normal file
@ -0,0 +1,338 @@
|
||||
import BaseFloat from '../baseFloat'
|
||||
import { patch, h } from '../../parser/render/snabbdom'
|
||||
import { EVENT_KEYS, URL_REG } from '../../config'
|
||||
import { getUniqueId, getImageInfo as getImageSrc } from '../../utils'
|
||||
import { getImageInfo } from '../../utils/getImageInfo'
|
||||
|
||||
import './index.css'
|
||||
|
||||
class ImageSelector extends BaseFloat {
|
||||
static pluginName = 'imageSelector'
|
||||
constructor (muya) {
|
||||
const name = 'ag-image-selector'
|
||||
const options = {
|
||||
placement: 'bottom-center',
|
||||
modifiers: {
|
||||
offset: {
|
||||
offset: '0, 0'
|
||||
}
|
||||
},
|
||||
showArrow: false
|
||||
}
|
||||
super(muya, name, options)
|
||||
this.renderArray = []
|
||||
this.oldVnode = null
|
||||
this.imageInfo = null
|
||||
this.tab = 'link' // select or link
|
||||
this.isFullMode = false // is show title and alt input
|
||||
this.state = {
|
||||
alt: '',
|
||||
src: '',
|
||||
title: ''
|
||||
}
|
||||
const imageSelectorContainer = this.imageSelectorContainer = document.createElement('div')
|
||||
this.container.appendChild(imageSelectorContainer)
|
||||
this.floatBox.classList.add('ag-image-selector-wrapper')
|
||||
this.listen()
|
||||
}
|
||||
listen () {
|
||||
super.listen()
|
||||
const { eventCenter } = this.muya
|
||||
eventCenter.subscribe('muya-image-selector', ({ reference, cb, imageInfo }) => {
|
||||
if (reference) {
|
||||
let { alt, backlash, src, title } = imageInfo.token
|
||||
alt += encodeURI(backlash.first)
|
||||
Object.assign(this.state, { alt, title, src })
|
||||
this.imageInfo = imageInfo
|
||||
this.show(reference, cb)
|
||||
this.render()
|
||||
// Auto focus and select all content of the `src.input` element.
|
||||
const input = this.imageSelectorContainer.querySelector('input.src')
|
||||
if (input) {
|
||||
input.focus()
|
||||
input.select()
|
||||
}
|
||||
} else {
|
||||
this.hide()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
tabClick (event, tab) {
|
||||
const { value } = tab
|
||||
this.tab = value
|
||||
return this.render()
|
||||
}
|
||||
|
||||
toggleMode () {
|
||||
this.isFullMode = !this.isFullMode
|
||||
return this.render()
|
||||
}
|
||||
|
||||
inputHandler (event, type) {
|
||||
const value = event.target.value
|
||||
this.state[type] = value
|
||||
}
|
||||
|
||||
handleKeyDown (event) {
|
||||
if (event.key === EVENT_KEYS.Enter) {
|
||||
event.stopPropagation()
|
||||
this.handleLinkButtonClick()
|
||||
}
|
||||
}
|
||||
|
||||
srcInputKeyDown (event) {
|
||||
const { imagePathPicker } = this.muya
|
||||
if (!imagePathPicker.status) {
|
||||
if (event.key === EVENT_KEYS.Enter) {
|
||||
event.stopPropagation()
|
||||
this.handleLinkButtonClick()
|
||||
}
|
||||
return
|
||||
}
|
||||
switch (event.key) {
|
||||
case EVENT_KEYS.ArrowUp:
|
||||
event.preventDefault()
|
||||
imagePathPicker.step('previous')
|
||||
break
|
||||
case EVENT_KEYS.ArrowDown:
|
||||
case EVENT_KEYS.Tab:
|
||||
event.preventDefault()
|
||||
imagePathPicker.step('next')
|
||||
break
|
||||
case EVENT_KEYS.Enter:
|
||||
event.preventDefault()
|
||||
imagePathPicker.selectItem(imagePathPicker.activeItem)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
async handleKeyUp (event) {
|
||||
const { key } = event
|
||||
if (
|
||||
key === EVENT_KEYS.ArrowUp ||
|
||||
key === EVENT_KEYS.ArrowDown ||
|
||||
key === EVENT_KEYS.Tab ||
|
||||
key === EVENT_KEYS.Enter
|
||||
) {
|
||||
return
|
||||
}
|
||||
const value = event.target.value
|
||||
const { eventCenter } = this.muya
|
||||
const reference = this.imageSelectorContainer.querySelector('input.src')
|
||||
const cb = item => {
|
||||
const { text } = item
|
||||
const newValue = value.replace(/(\/)([^/]+)$/, (m, p1, p2) => {
|
||||
return p1
|
||||
}) + text
|
||||
const len = newValue.length
|
||||
reference.value = newValue
|
||||
this.state.src = newValue
|
||||
reference.focus()
|
||||
reference.setSelectionRange(
|
||||
len,
|
||||
len
|
||||
)
|
||||
}
|
||||
|
||||
let list
|
||||
if (!value) {
|
||||
list = []
|
||||
} else {
|
||||
list = await this.muya.options.imagePathAutoComplete(value)
|
||||
}
|
||||
eventCenter.dispatch('muya-image-picker', { reference, list, cb })
|
||||
}
|
||||
|
||||
handleLinkButtonClick () {
|
||||
return this.replaceImageAsync(this.state)
|
||||
}
|
||||
|
||||
async replaceImageAsync ({ alt, src, title }) {
|
||||
if (!this.muya.options.imageAction || URL_REG.test(src)) {
|
||||
const { alt: oldAlt, src: oldSrc, title: oldTitle } = this.imageInfo.token
|
||||
if (alt !== oldAlt || src !== oldSrc || title !== oldTitle) {
|
||||
this.muya.contentState.replaceImage(this.imageInfo, { alt, src, title })
|
||||
}
|
||||
this.hide()
|
||||
} else {
|
||||
if (src) {
|
||||
const id = `loading-${getUniqueId()}`
|
||||
this.muya.contentState.replaceImage(this.imageInfo, {
|
||||
alt: id,
|
||||
src,
|
||||
title
|
||||
})
|
||||
this.hide()
|
||||
const nSrc = await this.muya.options.imageAction(src)
|
||||
const { src: localPath } = getImageSrc(src)
|
||||
if (localPath) {
|
||||
this.muya.contentState.stateRender.urlMap.set(nSrc, localPath)
|
||||
}
|
||||
const imageWrapper = this.muya.container.querySelector(`span[data-id=${id}]`)
|
||||
|
||||
if (imageWrapper) {
|
||||
const imageInfo = getImageInfo(imageWrapper)
|
||||
this.muya.contentState.replaceImage(imageInfo, {
|
||||
alt,
|
||||
src: nSrc,
|
||||
title
|
||||
})
|
||||
}
|
||||
} else {
|
||||
this.hide()
|
||||
}
|
||||
}
|
||||
this.muya.eventCenter.dispatch('stateChange')
|
||||
}
|
||||
|
||||
async handleSelectButtonClick () {
|
||||
if (!this.muya.options.imagePathPicker) {
|
||||
console.warn('You need to add a imagePathPicker option')
|
||||
return
|
||||
}
|
||||
const path = await this.muya.options.imagePathPicker()
|
||||
const { alt, title } = this.state
|
||||
return this.replaceImageAsync({
|
||||
alt,
|
||||
title,
|
||||
src: path
|
||||
})
|
||||
}
|
||||
|
||||
renderHeader () {
|
||||
const tabs = [{
|
||||
label: 'Select',
|
||||
value: 'select'
|
||||
}, {
|
||||
label: 'Embed link',
|
||||
value: 'link'
|
||||
}]
|
||||
const children = tabs.map(tab => {
|
||||
const itemSelector = this.tab === tab.value ? 'li.active' : 'li'
|
||||
return h(itemSelector, h('span', {
|
||||
on: {
|
||||
click: event => {
|
||||
this.tabClick(event, tab)
|
||||
}
|
||||
}
|
||||
}, tab.label))
|
||||
})
|
||||
|
||||
return h('ul.header', children)
|
||||
}
|
||||
|
||||
renderBody () {
|
||||
const { tab, state, isFullMode } = this
|
||||
const { alt, title, src } = state
|
||||
let bodyContent = null
|
||||
if (tab === 'select') {
|
||||
bodyContent = [
|
||||
h('span.role-button.select', {
|
||||
on: {
|
||||
click: event => {
|
||||
this.handleSelectButtonClick()
|
||||
}
|
||||
}
|
||||
}, 'Choose an Image'),
|
||||
h('span.description', 'Choose image from you computer.')
|
||||
]
|
||||
} else {
|
||||
const altInput = h('input.alt', {
|
||||
props: {
|
||||
placeholder: 'Alt text',
|
||||
value: alt
|
||||
},
|
||||
on: {
|
||||
input: event => {
|
||||
this.inputHandler(event, 'alt')
|
||||
},
|
||||
paste: event => {
|
||||
this.inputHandler(event, 'alt')
|
||||
},
|
||||
keydown: event => {
|
||||
this.handleKeyDown(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
const srcInput = h('input.src', {
|
||||
props: {
|
||||
placeholder: 'Image link or local path',
|
||||
value: src
|
||||
},
|
||||
on: {
|
||||
input: event => {
|
||||
this.inputHandler(event, 'src')
|
||||
},
|
||||
paste: event => {
|
||||
this.inputHandler(event, 'src')
|
||||
},
|
||||
keydown: event => {
|
||||
this.srcInputKeyDown(event)
|
||||
},
|
||||
keyup: event => {
|
||||
this.handleKeyUp(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
const titleInput = h('input.title', {
|
||||
props: {
|
||||
placeholder: 'Image title',
|
||||
value: title
|
||||
},
|
||||
on: {
|
||||
input: event => {
|
||||
this.inputHandler(event, 'title')
|
||||
},
|
||||
paste: event => {
|
||||
this.inputHandler(event, 'title')
|
||||
},
|
||||
keydown: event => {
|
||||
this.handleKeyDown(event)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const inputWrapper = isFullMode
|
||||
? h('div.input-container', [altInput, srcInput, titleInput])
|
||||
: h('div.input-container', [srcInput])
|
||||
|
||||
const embedButton = h('span.role-button.link', {
|
||||
on: {
|
||||
click: event => {
|
||||
this.handleLinkButtonClick()
|
||||
}
|
||||
}
|
||||
}, 'Embed Image')
|
||||
const bottomDes = h('span.description', [
|
||||
h('span', 'Paste web image or local image path, '),
|
||||
h('a', {
|
||||
on: {
|
||||
click: event => {
|
||||
this.toggleMode()
|
||||
}
|
||||
}
|
||||
}, `${isFullMode ? 'simple mode' : 'full mode'}`)
|
||||
])
|
||||
bodyContent = [inputWrapper, embedButton, bottomDes]
|
||||
}
|
||||
|
||||
return h('div.image-select-body', bodyContent)
|
||||
}
|
||||
|
||||
render () {
|
||||
const { oldVnode, imageSelectorContainer } = this
|
||||
const selector = 'div'
|
||||
const vnode = h(selector, [this.renderHeader(), this.renderBody()])
|
||||
if (oldVnode) {
|
||||
patch(oldVnode, vnode)
|
||||
} else {
|
||||
patch(imageSelectorContainer, vnode)
|
||||
}
|
||||
this.oldVnode = vnode
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageSelector
|
@ -1,21 +0,0 @@
|
||||
import selection from '../selection'
|
||||
import { CLASS_OR_ID, IMAGE_EXT_REG } from '../config'
|
||||
|
||||
export const checkEditImage = () => {
|
||||
const { start, end } = selection.getCursorRange()
|
||||
if (!start || !end) {
|
||||
return false
|
||||
}
|
||||
if (start.key === end.key && start.offset === end.offset) {
|
||||
const node = selection.getSelectionStart()
|
||||
const { right } = selection.getCaretOffsets(node)
|
||||
const classList = node && node.classList
|
||||
if (classList && (classList.contains(CLASS_OR_ID['AG_IMAGE_SRC'])) && right === 0) {
|
||||
return IMAGE_EXT_REG.test(node.textContent) ? false : 'image-path'
|
||||
}
|
||||
if (classList && (classList.contains(CLASS_OR_ID['AG_IMAGE_MARKED_TEXT'])) && right === 1) {
|
||||
return 'image'
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
19
src/muya/lib/utils/getImageInfo.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { findNearestParagraph, getOffsetOfParagraph } from '../selection/dom'
|
||||
import { tokenizer } from '../parser'
|
||||
|
||||
export const getImageInfo = image => {
|
||||
const paragraph = findNearestParagraph(image)
|
||||
const raw = image.getAttribute('data-raw')
|
||||
const offset = getOffsetOfParagraph(image, paragraph)
|
||||
const tokens = tokenizer(raw)
|
||||
const token = tokens[0]
|
||||
token.range = {
|
||||
start: offset,
|
||||
end: offset + raw.length
|
||||
}
|
||||
return {
|
||||
key: paragraph.id,
|
||||
token,
|
||||
imageId: image.id
|
||||
}
|
||||
}
|
@ -1,6 +1,4 @@
|
||||
// DOTO: Don't use Node API in editor folder, remove `path` @jocs
|
||||
// todo@jocs: remove the use of `axios` in muya
|
||||
import axios from 'axios'
|
||||
import createDOMPurify from 'dompurify'
|
||||
import { isInElectron, URL_REG } from '../config'
|
||||
|
||||
@ -135,7 +133,7 @@ export const deepCopy = object => {
|
||||
return obj
|
||||
}
|
||||
|
||||
export const loadImage = async (url, detectContentType) => {
|
||||
export const loadImage = async (url, detectContentType = false) => {
|
||||
if (detectContentType) {
|
||||
const isImage = await checkImageContentType(url)
|
||||
if (!isImage) throw new Error('not an image')
|
||||
@ -152,17 +150,35 @@ export const loadImage = async (url, detectContentType) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const checkImageContentType = async url => {
|
||||
try {
|
||||
const res = await axios.head(url)
|
||||
const contentType = res.headers['content-type']
|
||||
if (res.status === 200 && /^image\/(?:jpeg|png|gif|svg\+xml|webp)$/.test(contentType)) {
|
||||
return true
|
||||
export const checkImageContentType = url => {
|
||||
const req = new XMLHttpRequest()
|
||||
let settle
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
settle = resolve
|
||||
})
|
||||
const handler = () => {
|
||||
if (req.readyState === XMLHttpRequest.DONE) {
|
||||
if (req.status === 200) {
|
||||
const contentType = req.getResponseHeader('Content-Type')
|
||||
if (/^image\/(?:jpeg|png|gif|svg\+xml|webp)$/.test(contentType)) {
|
||||
settle(true)
|
||||
} else {
|
||||
settle(false)
|
||||
}
|
||||
} else {
|
||||
settle(false)
|
||||
}
|
||||
}
|
||||
return false
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
const handleError = () => {
|
||||
settle(false)
|
||||
}
|
||||
req.open('HEAD', url)
|
||||
req.onreadystatechange = handler
|
||||
req.onerror = handleError
|
||||
req.send()
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
@ -302,3 +318,9 @@ export const getParagraphReference = (ele, id) => {
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
export const verticalPositionInRect = (event, rect) => {
|
||||
const { clientY } = event
|
||||
const { top, height } = rect
|
||||
return (clientY - top) > (height / 2) ? 'down' : 'up'
|
||||
}
|
||||
|
@ -481,6 +481,16 @@ kbd {
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-success .ag-image-marked-text::before {
|
||||
background: url(../lib/assets/icons/image_dark.png);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.ag-inline-image.ag-image-success .ag-image-marked-text.ag-image-fail::before {
|
||||
background-image: url(../lib/assets/icons/image_dark_fail.png);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
body.dark .ag-image-marked-text::before {
|
||||
background: url(../lib/assets/icons/image_dark.png);
|
||||
background-size: cover;
|
||||
|
1
src/renderer/assets/icons/pref_image.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M985.6128 84.4288 42.3936 84.4288c-22.0544 0-39.9232 17.8688-39.9232 39.9488l0 728.5888c0 22.0416 17.8688 39.9232 39.9232 39.9232l943.2192 0c22.0288 0 39.9104-17.8816 39.9104-39.9232L1025.5232 124.3648C1025.5232 102.2976 1007.6416 84.4288 985.6128 84.4288zM962.5088 833.5872 64.7808 833.5872 64.7808 663.3216l228.992-229.5552 302.7584 302.7456L769.536 541.8752l192.9728 194.6496L962.5088 833.5872zM962.5088 640.0256l-187.136-191.296L596.5312 640.0256 293.7728 347.2384 64.7808 566.8352 64.7808 150.4128l897.7408 0L962.5216 640.0256zM762.88 326.464m-49.8944 0a3.898 3.898 0 1 0 99.7888 0 3.898 3.898 0 1 0-99.7888 0Z" /></svg>
|
After Width: | Height: | Size: 893 B |
1
src/renderer/assets/icons/pref_image_uploader.svg
Normal file
@ -0,0 +1 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg class="icon" width="200px" height="200.00px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M873.984 610.816c-7.168-7.168-14.848-10.752-25.6-10.752s-18.432 3.584-25.6 10.752l-102.4 102.4c-14.848 14.848-14.848 36.352 0 51.2 14.848 14.848 36.352 14.848 51.2 0l40.448-40.448v226.816c0 18.432 14.848 36.352 36.352 36.352 22.016 0 36.352-14.848 36.352-36.352v-226.816l44.032 44.032c14.848 14.848 36.352 14.848 51.2 0 14.848-14.848 14.848-36.352 0-51.2l-105.984-105.984zM394.752 336.384c18.432-40.448 7.168-87.552-22.016-120.832-32.768-32.768-83.968-40.448-124.416-25.6-40.448 18.432-66.048 61.952-66.048 105.984 0 58.368 47.616 105.984 105.984 105.984 44.544 0.512 88.576-25.088 106.496-65.536zM256 296.448c0-10.752 3.584-18.432 10.752-25.6 3.584-10.752 14.848-14.848 25.6-14.848 18.432 0 36.352 14.848 36.352 32.768 0 18.432-14.848 36.352-32.768 40.448-21.504 0-39.936-14.848-39.936-32.768z m0 0" /><path d="M950.784 36.352H73.216C32.768 36.352 0 69.632 0 109.568v731.648c0 40.448 32.768 73.216 73.216 73.216h585.216c19.968 0 36.352-16.384 36.352-36.352 0-19.968-16.384-36.352-36.352-36.352H109.568c-22.016 0-36.352-18.432-36.352-36.352V146.432c0-18.432 14.848-36.352 36.352-36.352h804.352c22.016 0 36.352 18.432 36.352 36.352v438.784c0 19.968 16.384 36.352 36.352 36.352 19.968 0 36.352-16.384 36.352-36.352V109.568c1.024-39.936-31.744-73.216-72.192-73.216z m0 0" /><path d="M135.168 742.4c14.848 14.848 36.352 14.848 51.2 0l153.6-153.6 128 128c3.584 3.584 3.584 3.584 7.168 3.584 14.848 7.168 29.184 7.168 44.032-3.584l361.984-361.984c14.848-14.848 14.848-36.352 0-51.2-14.848-14.848-36.352-14.848-51.2 0L493.568 640l-128-131.584-3.584-3.584c-14.848-10.752-32.768-10.752-47.616 3.584L131.584 691.2c-10.752 14.848-10.752 36.352 3.584 51.2z m0 0" /></svg>
|
After Width: | Height: | Size: 1.9 KiB |
@ -29,6 +29,12 @@
|
||||
--iconColor: #333;
|
||||
--codeBgColor: #d8d8d869;
|
||||
--codeBlockBgColor: rgba(0, 0, 0, 0.03);
|
||||
--inputBgColor: rgba(0, 0, 0, .06);
|
||||
--buttonBgColor: #ffffff;
|
||||
--buttonBorderColor: rgba(0, 0, 0, 0.2);
|
||||
--buttonShadow: rgba(0, 0, 0, 0.12);
|
||||
--buttonHover: #f2f2f2;
|
||||
--buttonActive: #e5e5e5;
|
||||
|
||||
/*marktext*/
|
||||
--sideBarColor: rgba(0, 0, 0, .6);
|
||||
@ -110,6 +116,23 @@ body {
|
||||
background-color: var(--floatBgColor);
|
||||
}
|
||||
|
||||
input.el-input__inner {
|
||||
background: var(--inputBgColor);
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
background: var(--editorColor04);
|
||||
}
|
||||
|
||||
div.el-tabs__item {
|
||||
color: var(--editorColor);
|
||||
}
|
||||
|
||||
div.el-tab-pane {
|
||||
color: var(--editorColor);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ag-dialog-table .dialog-title svg {
|
||||
width: 1.5em;
|
||||
height: 1.5em;
|
||||
|
@ -24,6 +24,12 @@
|
||||
--iconColor: rgba(255, 255, 255, .8);
|
||||
--codeBgColor: #424344;
|
||||
--codeBlockBgColor: #424344;
|
||||
--inputBgColor: rgba(0, 0, 0, .1);
|
||||
--buttonBgColor: #58606a;
|
||||
--buttonBorderColor: rgba(0, 0, 0, 0.2);
|
||||
--buttonShadow: rgba(0, 0, 0, 0.12);
|
||||
--buttonHover: #4a5058;
|
||||
--buttonActive: #434a53;
|
||||
|
||||
/*marktext*/
|
||||
--sideBarColor: rgba(255, 255, 255, .6);
|
||||
|
@ -23,6 +23,12 @@
|
||||
--iconColor: rgba(135, 135, 135, .8);
|
||||
--codeBgColor: #d8d8d869;
|
||||
--codeBlockBgColor: rgba(104, 134, 170, .04);
|
||||
--inputBgColor: rgba(0, 0, 0, .06);
|
||||
--buttonBgColor: #ffffff;
|
||||
--buttonBorderColor: rgba(0, 0, 0, 0.2);
|
||||
--buttonShadow: rgba(0, 0, 0, 0.12);
|
||||
--buttonHover: #f2f2f2;
|
||||
--buttonActive: #e5e5e5;
|
||||
|
||||
--sideBarColor: rgba(188, 193, 197, .8);
|
||||
--sideBarTitleColor: rgba(255, 255, 255, 1);
|
||||
|
@ -24,6 +24,12 @@
|
||||
--iconColor: rgba(255, 255, 255, .8);
|
||||
--codeBgColor: #d8d8d869;
|
||||
--codeBlockBgColor: #3f454c;
|
||||
--inputBgColor: rgba(0, 0, 0, .1);
|
||||
--buttonBgColor: #58606a;
|
||||
--buttonBorderColor: rgba(0, 0, 0, 0.2);
|
||||
--buttonShadow: rgba(0, 0, 0, 0.12);
|
||||
--buttonHover: #4a5058;
|
||||
--buttonActive: #434a53;
|
||||
|
||||
/*marktext*/
|
||||
--sideBarColor: rgba(255, 255, 255, .6);
|
||||
|
@ -24,6 +24,12 @@
|
||||
--iconColor: rgba(255, 255, 255, .8);
|
||||
--codeBgColor: #3a3f4b;
|
||||
--codeBlockBgColor: #3a3f4b;
|
||||
--inputBgColor: rgba(0, 0, 0, .1);
|
||||
--buttonBgColor: #58606a;
|
||||
--buttonBorderColor: rgba(0, 0, 0, 0.2);
|
||||
--buttonShadow: rgba(0, 0, 0, 0.12);
|
||||
--buttonHover: #4a5058;
|
||||
--buttonActive: #434a53;
|
||||
|
||||
/*marktext*/
|
||||
--sideBarColor: #9da5b4;
|
||||
@ -37,7 +43,7 @@
|
||||
--floatBorderColor: #181a1f;
|
||||
--floatShadow: rgba(0, 0, 0, 0.3);
|
||||
--maskColor: rgba(0, 0, 0, .7);
|
||||
--editorAreaWidth: 700px;
|
||||
--editorAreaWidth: 750px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
@ -23,6 +23,12 @@
|
||||
--iconColor: rgba(101, 101, 101, .8);
|
||||
--codeBgColor: #d8d8d869;
|
||||
--codeBlockBgColor: rgba(12, 139, 186, .04);
|
||||
--inputBgColor: rgba(0, 0, 0, .06);
|
||||
--buttonBgColor: #ffffff;
|
||||
--buttonBorderColor: rgba(0, 0, 0, 0.2);
|
||||
--buttonShadow: rgba(0, 0, 0, 0.12);
|
||||
--buttonHover: #f2f2f2;
|
||||
--buttonActive: #e5e5e5;
|
||||
|
||||
--sideBarColor: rgba(101, 101, 101, .6);
|
||||
--sideBarTitleColor: rgba(101, 101, 101, 1);
|
||||
|
@ -81,15 +81,19 @@
|
||||
import CodePicker from 'muya/lib/ui/codePicker'
|
||||
import EmojiPicker from 'muya/lib/ui/emojiPicker'
|
||||
import ImagePathPicker from 'muya/lib/ui/imagePicker'
|
||||
import ImageSelector from 'muya/lib/ui/imageSelector'
|
||||
import FormatPicker from 'muya/lib/ui/formatPicker'
|
||||
import FrontMenu from 'muya/lib/ui/frontMenu'
|
||||
import bus from '../../bus'
|
||||
import Search from '../search.vue'
|
||||
import { animatedScrollTo } from '../../util'
|
||||
import { addCommonStyle } from '../../util/theme'
|
||||
import { guessClipboardFilePath } from '../../util/guessClipBoardFilePath'
|
||||
import { showContextMenu } from '../../contextMenu/editor'
|
||||
import Printer from '@/services/printService'
|
||||
import { DEFAULT_EDITOR_FONT_FAMILY } from '@/config'
|
||||
import { moveImageToFolder, uploadImage } from '@/util/fileSystem'
|
||||
import notice from '@/services/notification'
|
||||
|
||||
import 'muya/themes/default.css'
|
||||
import '@/assets/themes/codemirror/one-dark.css'
|
||||
@ -116,6 +120,7 @@
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
'preferences': state => state.preferences,
|
||||
'preferLooseListItem': state => state.preferences.preferLooseListItem,
|
||||
'autoPairBracket': state => state.preferences.autoPairBracket,
|
||||
'autoPairMarkdownSyntax': state => state.preferences.autoPairMarkdownSyntax,
|
||||
@ -132,7 +137,11 @@
|
||||
'darkColor': state => state.preferences.darkColor,
|
||||
'editorFontFamily': state => state.preferences.editorFontFamily,
|
||||
'hideQuickInsertHint': state => state.preferences.hideQuickInsertHint,
|
||||
'imageInsertAction': state => state.preferences.imageInsertAction,
|
||||
'imageFolderPath': state => state.preferences.imageFolderPath,
|
||||
'theme': state => state.preferences.theme,
|
||||
|
||||
'currentFile': state => state.editor.currentFile,
|
||||
// edit modes
|
||||
'typewriter': state => state.preferences.typewriter,
|
||||
'focus': state => state.preferences.focus,
|
||||
@ -291,6 +300,7 @@
|
||||
Muya.use(CodePicker)
|
||||
Muya.use(EmojiPicker)
|
||||
Muya.use(ImagePathPicker)
|
||||
Muya.use(ImageSelector)
|
||||
Muya.use(FormatPicker)
|
||||
Muya.use(FrontMenu)
|
||||
|
||||
@ -305,7 +315,11 @@
|
||||
orderListDelimiter,
|
||||
tabSize,
|
||||
listIndentation,
|
||||
hideQuickInsertHint
|
||||
hideQuickInsertHint,
|
||||
imageAction: this.imageAction.bind(this),
|
||||
imagePathPicker: this.imagePathPicker.bind(this),
|
||||
clipboardFilePath: guessClipboardFilePath,
|
||||
imagePathAutoComplete: this.imagePathAutoComplete.bind(this)
|
||||
}
|
||||
if (/dark/i.test(theme)) {
|
||||
Object.assign(options, {
|
||||
@ -341,7 +355,6 @@
|
||||
bus.$on('image-uploaded', this.handleUploadedImage)
|
||||
bus.$on('file-changed', this.handleMarkdownChange)
|
||||
bus.$on('editor-blur', this.blurEditor)
|
||||
bus.$on('image-auto-path', this.handleImagePath)
|
||||
bus.$on('copyAsMarkdown', this.handleCopyPaste)
|
||||
bus.$on('copyAsHtml', this.handleCopyPaste)
|
||||
bus.$on('pasteAsPlainText', this.handleCopyPaste)
|
||||
@ -353,19 +366,7 @@
|
||||
bus.$on('scroll-to-header', this.scrollToHeader)
|
||||
bus.$on('copy-block', this.handleCopyBlock)
|
||||
bus.$on('print', this.handlePrint)
|
||||
|
||||
// when cursor is in `` will emit `insert-image`
|
||||
this.editor.on('insert-image', type => {
|
||||
if (type === 'absolute' || type === 'relative') {
|
||||
this.$store.dispatch('ASK_FOR_INSERT_IMAGE', type)
|
||||
} else if (type === 'upload') {
|
||||
bus.$emit('upload-image')
|
||||
}
|
||||
})
|
||||
|
||||
this.editor.on('image-path-autocomplement', src => {
|
||||
this.$store.dispatch('ASK_FOR_IMAGE_AUTO_PATH', src)
|
||||
})
|
||||
bus.$on('screenshot-captured', this.handleScreenShot)
|
||||
|
||||
this.editor.on('change', changes => {
|
||||
// WORKAROUND: "id: 'muya'"
|
||||
@ -391,6 +392,19 @@
|
||||
}
|
||||
})
|
||||
|
||||
this.editor.on('preview-image', ({ data }) => {
|
||||
if (this.imageViewer) {
|
||||
this.imageViewer.destroy()
|
||||
}
|
||||
|
||||
this.imageViewer = new ViewImage(this.$refs.imageViewer, {
|
||||
url: data,
|
||||
snapView: true
|
||||
})
|
||||
|
||||
this.setImageViewerVisible(true)
|
||||
})
|
||||
|
||||
this.editor.on('selectionChange', changes => {
|
||||
const { y } = changes.cursorCoords
|
||||
if (this.typewriter) {
|
||||
@ -412,17 +426,52 @@
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
async imagePathAutoComplete (src) {
|
||||
const files = await this.$store.dispatch('ASK_FOR_IMAGE_AUTO_PATH', src)
|
||||
return files.map(f => {
|
||||
const iconClass = f.type === 'directory' ? 'icon-folder' : 'icon-image'
|
||||
return Object.assign(f, { iconClass, text: f.file + (f.type === 'directory' ? '/' : '') })
|
||||
})
|
||||
},
|
||||
async imageAction (image) {
|
||||
const { imageInsertAction, imageFolderPath, preferences } = this
|
||||
const { pathname } = this.currentFile
|
||||
switch (imageInsertAction) {
|
||||
case 'upload': {
|
||||
try {
|
||||
const result = await uploadImage(pathname, image, preferences)
|
||||
return result
|
||||
} catch (err) {
|
||||
notice.notify({
|
||||
title: 'Upload Image',
|
||||
type: 'info',
|
||||
message: err
|
||||
})
|
||||
return await moveImageToFolder(pathname, image, imageFolderPath)
|
||||
}
|
||||
}
|
||||
case 'folder': {
|
||||
return await moveImageToFolder(pathname, image, imageFolderPath)
|
||||
}
|
||||
case 'path': {
|
||||
if (typeof image === 'string') {
|
||||
return image
|
||||
} else {
|
||||
// Move image to image folder if it's Blob object.
|
||||
return await moveImageToFolder(pathname, image, imageFolderPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
imagePathPicker () {
|
||||
return this.$store.dispatch('ASK_FOR_IMAGE_PATH')
|
||||
},
|
||||
keyup (event) {
|
||||
if (event.key === 'Escape') {
|
||||
this.setImageViewerVisible(false)
|
||||
}
|
||||
},
|
||||
|
||||
handleImagePath (files) {
|
||||
const { editor } = this
|
||||
editor && editor.showAutoImagePath(files)
|
||||
},
|
||||
|
||||
setImageViewerVisible (status) {
|
||||
this.imageViewerVisible = status
|
||||
},
|
||||
@ -452,9 +501,9 @@
|
||||
}
|
||||
},
|
||||
|
||||
handleSelect (url) {
|
||||
handleSelect (src) {
|
||||
if (!this.sourceCode) {
|
||||
this.editor && this.editor.insertImage(url)
|
||||
this.editor && this.editor.insertImage({ src })
|
||||
}
|
||||
},
|
||||
|
||||
@ -618,6 +667,12 @@
|
||||
|
||||
handleCopyBlock (name) {
|
||||
this.editor.copy(name)
|
||||
},
|
||||
|
||||
handleScreenShot () {
|
||||
if (this.editor) {
|
||||
document.execCommand('paste')
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
@ -636,7 +691,6 @@
|
||||
bus.$off('image-uploaded', this.handleUploadedImage)
|
||||
bus.$off('file-changed', this.handleMarkdownChange)
|
||||
bus.$off('editor-blur', this.blurEditor)
|
||||
bus.$off('image-auto-path', this.handleImagePath)
|
||||
bus.$off('copyAsMarkdown', this.handleCopyPaste)
|
||||
bus.$off('copyAsHtml', this.handleCopyPaste)
|
||||
bus.$off('pasteAsPlainText', this.handleCopyPaste)
|
||||
@ -648,6 +702,7 @@
|
||||
bus.$off('scroll-to-header', this.scrollToHeader)
|
||||
bus.$off('copy-block', this.handleCopyBlock)
|
||||
bus.$off('print', this.handlePrint)
|
||||
bus.$off('screenshot-captured', this.handleScreenShot)
|
||||
|
||||
document.removeEventListener('keyup', this.keyup)
|
||||
|
||||
|
@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<div class="aidou">
|
||||
<el-dialog
|
||||
:visible.sync="showUpload"
|
||||
:show-close="false"
|
||||
:modal="true"
|
||||
custom-class="ag-dialog-table"
|
||||
width="400px"
|
||||
>
|
||||
<el-upload
|
||||
ref="uploader"
|
||||
class="upload-image"
|
||||
drag
|
||||
action="https://sm.ms/api/upload"
|
||||
name="smfile"
|
||||
:multiple="false"
|
||||
:limit="1"
|
||||
:on-success="handleResponse"
|
||||
:before-upload="handleBeforeUpload"
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">Drag image here, or <em>Click</em></div>
|
||||
<div class="el-upload__tip" slot="tip" :class="{ 'error': error }">{{ message }}</div>
|
||||
</el-upload>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import bus from '../../bus'
|
||||
|
||||
const msg = 'jpg | png | gif | jpeg only, max size 5M'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
showUpload: false,
|
||||
message: msg,
|
||||
error: false
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$nextTick(() => {
|
||||
bus.$on('upload-image', this.handleUpload)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
handleBeforeUpload (file) {
|
||||
const MAX_SIZE = 5 * 1024 * 1024
|
||||
if (!/png|jpg|jpeg|gif/.test(file.type)) {
|
||||
this.message = 'jpg | png | gif | jpeg only'
|
||||
this.error = true
|
||||
return false
|
||||
}
|
||||
if (file.size > MAX_SIZE) {
|
||||
this.message = 'Upload image limit to 5M'
|
||||
this.error = true
|
||||
return false
|
||||
}
|
||||
this.message = msg
|
||||
this.error = false
|
||||
},
|
||||
handleUpload () {
|
||||
if (!this.showUpload) {
|
||||
this.showUpload = true
|
||||
bus.$emit('editor-blur')
|
||||
}
|
||||
},
|
||||
handleResponse (res) {
|
||||
if (res.code === 'success') {
|
||||
// handle success
|
||||
const { url, delete: deletionUrl } = res.data
|
||||
this.showUpload = false
|
||||
bus.$emit('image-uploaded', url, deletionUrl)
|
||||
} else if (res.code === 'error') {
|
||||
// handle error
|
||||
this.message = res.msg
|
||||
this.error = true
|
||||
}
|
||||
this.$refs.uploader.clearFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.el-upload__tip {
|
||||
text-align: center;
|
||||
color: var(--sideBarColor);
|
||||
}
|
||||
.el-upload__tip.error {
|
||||
color: #E6A23C;
|
||||
}
|
||||
.el-upload-dragger {
|
||||
background: var(--itemBgColor) !important;
|
||||
& .el-upload__text {
|
||||
color: var(--sideBarColor);
|
||||
& em {
|
||||
color: var(--themeColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
.el-upload-dragger:hover {
|
||||
border-color: var(--themeColor);
|
||||
}
|
||||
</style>
|
@ -1,7 +1,5 @@
|
||||
import path from 'path'
|
||||
|
||||
export const isLinux = process.platform === 'linux'
|
||||
|
||||
import { isLinux } from './util'
|
||||
export const PATH_SEPARATOR = path.sep
|
||||
|
||||
export const THEME_STYLE_ID = 'ag-theme'
|
||||
|
@ -25,7 +25,11 @@ import {
|
||||
Switch,
|
||||
Select,
|
||||
Option,
|
||||
Radio
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Tabs,
|
||||
TabPane,
|
||||
Input
|
||||
} from 'element-ui'
|
||||
import services from './services'
|
||||
import routes from './router'
|
||||
@ -72,6 +76,10 @@ Vue.use(Switch)
|
||||
Vue.use(Select)
|
||||
Vue.use(Option)
|
||||
Vue.use(Radio)
|
||||
Vue.use(RadioGroup)
|
||||
Vue.use(Tabs)
|
||||
Vue.use(TabPane)
|
||||
Vue.use(Input)
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
|
@ -27,7 +27,6 @@
|
||||
:platform="platform"
|
||||
></editor-with-tabs>
|
||||
<aidou></aidou>
|
||||
<upload-image></upload-image>
|
||||
<about-dialog></about-dialog>
|
||||
<rename></rename>
|
||||
<tweet></tweet>
|
||||
@ -43,7 +42,6 @@
|
||||
import TitleBar from '@/components/titleBar'
|
||||
import SideBar from '@/components/sideBar'
|
||||
import Aidou from '@/components/aidou/aidou'
|
||||
import UploadImage from '@/components/uploadImage'
|
||||
import AboutDialog from '@/components/about'
|
||||
import Rename from '@/components/rename'
|
||||
import Tweet from '@/components/tweet'
|
||||
@ -60,7 +58,6 @@
|
||||
EditorWithTabs,
|
||||
TitleBar,
|
||||
SideBar,
|
||||
UploadImage,
|
||||
AboutDialog,
|
||||
Rename,
|
||||
Tweet,
|
||||
@ -116,7 +113,6 @@
|
||||
dispatch('LISTEN_FOR_LAYOUT')
|
||||
dispatch('LISTEN_FOR_REQUEST_LAYOUT')
|
||||
// module: listenForMain
|
||||
dispatch('LISTEN_FOR_IMAGE_PATH')
|
||||
dispatch('LISTEN_FOR_EDIT')
|
||||
dispatch('LISTEN_FOR_VIEW')
|
||||
dispatch('LISTEN_FOR_ABOUT_DIALOG')
|
||||
@ -128,6 +124,7 @@
|
||||
// module: autoUpdates
|
||||
dispatch('LISTEN_FOR_UPDATE')
|
||||
// module: editor
|
||||
dispatch('LISTEN_SCREEN_SHOT')
|
||||
dispatch('ASK_FOR_USER_PREFERENCE')
|
||||
dispatch('ASK_FOR_MODE')
|
||||
dispatch('LISTEN_FOR_CLOSE')
|
||||
@ -138,7 +135,6 @@
|
||||
dispatch('LISTEN_FOR_BOOTSTRAP_WINDOW')
|
||||
dispatch('LISTEN_FOR_SAVE_CLOSE')
|
||||
dispatch('LISTEN_FOR_EXPORT_PRINT')
|
||||
dispatch('LISTEN_FOR_INSERT_IMAGE')
|
||||
dispatch('LISTEN_FOR_RENAME')
|
||||
dispatch('LINTEN_FOR_SET_LINE_ENDING')
|
||||
dispatch('LISTEN_FOR_NEW_TAB')
|
||||
@ -154,11 +150,11 @@
|
||||
// Cancel to allow tab drag&drop.
|
||||
if (!e.dataTransfer.types.length) return
|
||||
|
||||
e.preventDefault()
|
||||
if (e.dataTransfer.types.indexOf('Files') >= 0) {
|
||||
if (e.dataTransfer.items.length === 1 && /png|jpg|jpeg|gif/.test(e.dataTransfer.items[0].type)) {
|
||||
bus.$emit('upload-image')
|
||||
if (e.dataTransfer.items.length === 1 && e.dataTransfer.items[0].type.indexOf('image') > -1) {
|
||||
// Do nothing, because we already drag/drop image in muya.
|
||||
} else {
|
||||
e.preventDefault()
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
@ -167,6 +163,7 @@
|
||||
}, 300)
|
||||
bus.$emit('importDialog', true)
|
||||
}
|
||||
|
||||
e.dataTransfer.dropEffect = 'copy'
|
||||
} else {
|
||||
e.stopPropagation()
|
||||
|
@ -75,13 +75,6 @@
|
||||
:bool="hideQuickInsertHint"
|
||||
:onChange="value => onSelectChange('hideQuickInsertHint', value)"
|
||||
></bool>
|
||||
<separator></separator>
|
||||
<section class="image-ctrl ag-underdevelop">
|
||||
<div>The default behavior after paste or drag the image to Mark Text</div>
|
||||
<el-radio v-model="imageDropAction" label="upload">Upload image to cloud</el-radio>
|
||||
<el-radio v-model="imageDropAction" label="folder">Move image to sepcial folder</el-radio>
|
||||
<el-radio v-model="imageDropAction" label="path">Insert absolute or relative path of image</el-radio>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -124,8 +117,7 @@ export default {
|
||||
textDirection: state => state.preferences.textDirection,
|
||||
codeFontSize: state => state.preferences.codeFontSize,
|
||||
codeFontFamily: state => state.preferences.codeFontFamily,
|
||||
hideQuickInsertHint: state => state.preferences.hideQuickInsertHint,
|
||||
imageDropAction: state => state.preferences.imageDropAction
|
||||
hideQuickInsertHint: state => state.preferences.hideQuickInsertHint
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
|
0
src/renderer/prefComponents/image/config.js
Normal file
98
src/renderer/prefComponents/image/index.vue
Normal file
@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div class="pref-image">
|
||||
<h4>Image</h4>
|
||||
<section class="image-ctrl">
|
||||
<div>The default behavior after insert image from local folder.
|
||||
<el-tooltip class='item' effect='dark' content='Mark Text can not get image path from paste event in Linux system.' placement='top-start'>
|
||||
<i class="el-icon-info"></i>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-radio-group v-model="imageInsertAction">
|
||||
<el-radio label="upload">Upload image to cloud by image uploader</el-radio>
|
||||
<el-radio label="folder">Move image to sepcial folder</el-radio>
|
||||
<el-radio label="path">Insert absolute or relative path of image</el-radio>
|
||||
</el-radio-group>
|
||||
</section>
|
||||
<separator></separator>
|
||||
<section class="image-folder">
|
||||
<div class="description">The local image folder.</div>
|
||||
<div class="path">{{imageFolderPath}}</div>
|
||||
<div class="button-group">
|
||||
<el-button size="mini" @click="modifyImageFolderPath">Modify</el-button>
|
||||
<el-button size="mini" @click="openImageFolder">Open Folder</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Separator from '../common/separator'
|
||||
import { shell } from 'electron'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Separator
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
imageInsertAction: {
|
||||
get: function () {
|
||||
return this.$store.state.preferences.imageInsertAction
|
||||
},
|
||||
set: function (value) {
|
||||
const type = 'imageInsertAction'
|
||||
this.$store.dispatch('SET_SINGLE_PREFERENCE', { type, value })
|
||||
}
|
||||
},
|
||||
imageFolderPath: {
|
||||
get: function () {
|
||||
return this.$store.state.preferences.imageFolderPath
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openImageFolder () {
|
||||
shell.openItem(this.imageFolderPath)
|
||||
},
|
||||
modifyImageFolderPath () {
|
||||
return this.$store.dispatch('SET_IMAGE_FOLDER_PATH')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.pref-image {
|
||||
& h4 {
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
font-weight: 100;
|
||||
}
|
||||
& .image-ctrl {
|
||||
font-size: 14px;
|
||||
margin: 20px 0;
|
||||
color: var(--editorColor);
|
||||
& label {
|
||||
display: block;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
& .image-folder {
|
||||
& div.description {
|
||||
font-size: 14px;
|
||||
color: var(--editorColor);
|
||||
}
|
||||
& div.path {
|
||||
font-size: 14px;
|
||||
color: var(--editorColor50);
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
159
src/renderer/prefComponents/imageUploader/index.vue
Normal file
@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<div class="pref-image-uploader">
|
||||
<h4>Image Uploader</h4>
|
||||
<section class="current-uploader">
|
||||
<div>The current image uploader is <span class="uploader">{{currentUploader}}</span>.</div>
|
||||
</section>
|
||||
<separator></separator>
|
||||
<section class="configration">
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="SM.MS" name="smms">
|
||||
<div class="description">Thank you <span class="link" @click="open('https://sm.ms/')">SM.MS</span> for providing free uploading services.</div>
|
||||
<el-button size="mini" @click="setCurrentUploader('smms')">Set As default Uploader</el-button>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Github" name="github">
|
||||
<div class="form-group">
|
||||
<div class="label">GitHub token:</div>
|
||||
<el-input v-model="githubToken" placeholder="Input token" size="mini"></el-input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="label">Owner name:</div>
|
||||
<el-input v-model="github.owner" placeholder="owner" size="mini"></el-input>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="label">Repo name:</div>
|
||||
<el-input v-model="github.repo" placeholder="repo" size="mini"></el-input>
|
||||
</div>
|
||||
<div class="form-group button-group">
|
||||
<el-button size="mini" :disabled="githubDisable" @click="save('github')">Save</el-button>
|
||||
<el-button size="mini" :disabled="githubDisable" @click="setCurrentUploader('github')">Set As default Uploader</el-button>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Separator from '../common/separator'
|
||||
import { shell } from 'electron'
|
||||
export default {
|
||||
components: {
|
||||
Separator
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
activeTab: 'smms',
|
||||
githubToken: '',
|
||||
github: {
|
||||
owner: '',
|
||||
repo: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentUploader: {
|
||||
get: function () {
|
||||
return this.$store.state.preferences.currentUploader
|
||||
}
|
||||
},
|
||||
imageBed: {
|
||||
get: function () {
|
||||
return this.$store.state.preferences.imageBed
|
||||
}
|
||||
},
|
||||
prefGithubToken: {
|
||||
get: function () {
|
||||
return this.$store.state.preferences.githubToken
|
||||
}
|
||||
},
|
||||
githubDisable () {
|
||||
return !this.githubToken || !this.github.owner || !this.github.repo
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
imageBed: function (value, oldValue) {
|
||||
if (value !== oldValue) {
|
||||
this.github = value.github
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$nextTick(() => {
|
||||
this.github = this.imageBed.github
|
||||
this.githubToken = this.prefGithubToken
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
handleImageChange (value) {
|
||||
// console.log(value)
|
||||
},
|
||||
open (link) {
|
||||
shell.openExternal(link)
|
||||
},
|
||||
save (type) {
|
||||
const newImageBedConfig = Object.assign({}, this.imageBed, {[type]: this[type]})
|
||||
this.$store.dispatch('SET_USER_DATA', {
|
||||
type: 'imageBed',
|
||||
value: newImageBedConfig
|
||||
})
|
||||
if (type === 'github') {
|
||||
this.$store.dispatch('SET_USER_DATA', {
|
||||
type: 'githubToken',
|
||||
value: this.githubToken
|
||||
})
|
||||
}
|
||||
new Notification('Save Image Uploader', {
|
||||
body: `The Github configration has been saved.`
|
||||
})
|
||||
},
|
||||
setCurrentUploader (value) {
|
||||
const type = 'currentUploader'
|
||||
this.$store.dispatch('SET_USER_DATA', { type, value })
|
||||
|
||||
new Notification('Set Image Uploader', {
|
||||
body: `Set ${value} as the default image uploader successfully.`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.pref-image-uploader {
|
||||
& h4 {
|
||||
text-transform: uppercase;
|
||||
margin: 0;
|
||||
font-weight: 100;
|
||||
}
|
||||
& .current-uploader {
|
||||
font-size: 14px;
|
||||
margin: 20px 0;
|
||||
color: var(--editorColor);
|
||||
& .uploader {
|
||||
color: var(--editorColor80);
|
||||
font-size: 600;
|
||||
}
|
||||
}
|
||||
& .link {
|
||||
color: var(--themeColor);
|
||||
cursor: pointer;
|
||||
}
|
||||
& .description {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
& .form-group {
|
||||
margin: 20px 0 0 0;
|
||||
}
|
||||
& .label {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
& .el-input {
|
||||
max-width: 242px;
|
||||
}
|
||||
& .button-group {
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -2,25 +2,41 @@ import GeneralIcon from '@/assets/icons/pref_general.svg'
|
||||
import EditorIcon from '@/assets/icons/pref_editor.svg'
|
||||
import MarkdownIcon from '@/assets/icons/pref_markdown.svg'
|
||||
import ThemeIcon from '@/assets/icons/pref_theme.svg'
|
||||
import ImageIcon from '@/assets/icons/pref_image.svg'
|
||||
import ImageUploaderIcon from '@/assets/icons/pref_image_uploader.svg'
|
||||
|
||||
import preferences from '../../../main/preferences/schema'
|
||||
|
||||
export const category = [{
|
||||
name: 'General',
|
||||
label: 'general',
|
||||
icon: GeneralIcon,
|
||||
path: '/preference/general'
|
||||
}, {
|
||||
name: 'Editor',
|
||||
label: 'editor',
|
||||
icon: EditorIcon,
|
||||
path: '/preference/editor'
|
||||
}, {
|
||||
name: 'Markdown',
|
||||
label: 'markdown',
|
||||
icon: MarkdownIcon,
|
||||
path: '/preference/markdown'
|
||||
}, {
|
||||
name: 'Theme',
|
||||
label: 'theme',
|
||||
icon: ThemeIcon,
|
||||
path: '/preference/theme'
|
||||
}, {
|
||||
name: 'Image',
|
||||
label: 'image',
|
||||
icon: ImageIcon,
|
||||
path: '/preference/image'
|
||||
}, {
|
||||
name: 'Image Uploader',
|
||||
label: 'imageUploader',
|
||||
icon: ImageUploaderIcon,
|
||||
path: '/preference/imageUploader'
|
||||
}]
|
||||
|
||||
export const searchContent = Object.keys(preferences).map(k => {
|
||||
|
@ -23,7 +23,7 @@
|
||||
<section class="category">
|
||||
<div v-for="c of category" :key="c.name" class="item"
|
||||
@click="handleCategoryItemClick(c)"
|
||||
:class="{active: c.name.toLowerCase() === currentCategory}"
|
||||
:class="{active: c.label === currentCategory}"
|
||||
>
|
||||
<svg :viewBox="c.icon.viewBox">
|
||||
<use :xlink:href="c.icon.url"></use>
|
||||
|
@ -4,6 +4,8 @@ import General from '@/prefComponents/general'
|
||||
import Editor from '@/prefComponents/editor'
|
||||
import Markdown from '@/prefComponents/markdown'
|
||||
import Theme from '@/prefComponents/theme'
|
||||
import Image from '@/prefComponents/image'
|
||||
import ImageUploader from '@/prefComponents/imageUploader'
|
||||
|
||||
const routes = type => ([{
|
||||
path: '/', redirect: type === 'editor'? '/editor' : '/preference'
|
||||
@ -21,6 +23,10 @@ const routes = type => ([{
|
||||
path: 'markdown', component: Markdown, name: 'markdown'
|
||||
}, {
|
||||
path: 'theme', component: Theme, name: 'theme'
|
||||
}, {
|
||||
path: 'image', component: Image, name: 'image'
|
||||
}, {
|
||||
path: 'imageUploader', component: ImageUploader, name: 'imageUploader'
|
||||
}]
|
||||
}])
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { clipboard, ipcRenderer, shell } from 'electron'
|
||||
import path from 'path'
|
||||
import bus from '../bus'
|
||||
import { hasKeys } from '../util'
|
||||
import { hasKeys, getUniqueId } from '../util'
|
||||
import { isSameFileSync } from '../util/fileSystem'
|
||||
import listToTree from '../util/listToTree'
|
||||
import { createDocumentState, getOptionsFromState, getSingleFileState, getBlankFileState } from './help'
|
||||
@ -216,18 +216,32 @@ const mutations = {
|
||||
}
|
||||
|
||||
const actions = {
|
||||
// when cursor in ``, insert image popup will be shown! `absolute` or `relative`
|
||||
ASK_FOR_INSERT_IMAGE ({ commit }, type) {
|
||||
ipcRenderer.send('AGANI::ask-for-insert-image', type)
|
||||
},
|
||||
FORMAT_LINK_CLICK ({ commit }, { data, dirname }) {
|
||||
ipcRenderer.send('AGANI::format-link-click', { data, dirname })
|
||||
},
|
||||
|
||||
LISTEN_SCREEN_SHOT ({ commit }) {
|
||||
ipcRenderer.on('mt::screenshot-captured', e => {
|
||||
bus.$emit('screenshot-captured')
|
||||
})
|
||||
},
|
||||
|
||||
// image path auto complement
|
||||
ASK_FOR_IMAGE_AUTO_PATH ({ commit, state }, src) {
|
||||
const { pathname } = state.currentFile
|
||||
if (pathname) {
|
||||
ipcRenderer.send('AGANI::ask-for-image-auto-path', { pathname, src })
|
||||
let rs
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
rs = resolve
|
||||
})
|
||||
const id = getUniqueId()
|
||||
ipcRenderer.once(`mt::response-of-image-path-${id}`, (e, files) => {
|
||||
rs(files)
|
||||
})
|
||||
ipcRenderer.send('mt::ask-for-image-auto-path', { pathname, src, id })
|
||||
return promise
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
@ -647,22 +661,6 @@ const actions = {
|
||||
})
|
||||
},
|
||||
|
||||
LISTEN_FOR_INSERT_IMAGE ({ commit, state }) {
|
||||
ipcRenderer.on('AGANI::INSERT_IMAGE', (e, { filename: imagePath, type }) => {
|
||||
if (!hasKeys(state.currentFile)) return
|
||||
if (type === 'absolute' || type === 'relative') {
|
||||
const { pathname } = state.currentFile
|
||||
if (type === 'relative' && pathname) {
|
||||
imagePath = path.relative(path.dirname(pathname), imagePath)
|
||||
}
|
||||
bus.$emit('insert-image', imagePath)
|
||||
} else {
|
||||
// upload to CM
|
||||
bus.$emit('upload-image')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
LINTEN_FOR_SET_LINE_ENDING ({ commit, state }) {
|
||||
ipcRenderer.on('AGANI::set-line-ending', (e, { lineEnding, ignoreSaveStatus }) => {
|
||||
const { lineEnding: oldLineEnding } = state.currentFile
|
||||
@ -713,6 +711,10 @@ const actions = {
|
||||
|
||||
ASK_FILE_WATCH ({ commit }, { pathname, watch }) {
|
||||
ipcRenderer.send('AGANI::file-watch', { pathname, watch })
|
||||
},
|
||||
|
||||
ASK_FOR_IMAGE_PATH ({ commit }) {
|
||||
return ipcRenderer.sendSync('mt::ask-for-image-path')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,12 +9,6 @@ const getters = {}
|
||||
const mutations = {}
|
||||
|
||||
const actions = {
|
||||
LISTEN_FOR_IMAGE_PATH ({ commit }) {
|
||||
ipcRenderer.on('AGANI::image-auto-path', (e, files) => {
|
||||
bus.$emit('image-auto-path', files)
|
||||
})
|
||||
},
|
||||
|
||||
LISTEN_FOR_EDIT ({ commit }) {
|
||||
ipcRenderer.on('AGANI::edit', (e, { type }) => {
|
||||
bus.$emit(type, type)
|
||||
|
@ -23,7 +23,7 @@ const state = {
|
||||
endOfLine: 'default',
|
||||
textDirection: 'ltr',
|
||||
hideQuickInsertHint: false,
|
||||
imageDropAction: 'folder',
|
||||
imageInsertAction: 'folder',
|
||||
|
||||
preferLooseListItem: true,
|
||||
bulletListMarker: '-',
|
||||
@ -36,7 +36,20 @@ const state = {
|
||||
// edit modes (they are not in preference.md, but still put them here)
|
||||
typewriter: false, // typewriter mode
|
||||
focus: false, // focus mode
|
||||
sourceCode: false // source code mode
|
||||
sourceCode: false, // source code mode
|
||||
|
||||
// user configration
|
||||
imageFolderPath: '',
|
||||
webImages: [],
|
||||
cloudImages: [],
|
||||
currentUploader: 'smms',
|
||||
githubToken: '',
|
||||
imageBed: {
|
||||
github: {
|
||||
owner: '',
|
||||
repo: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getters = {}
|
||||
@ -57,6 +70,7 @@ const mutations = {
|
||||
const actions = {
|
||||
ASK_FOR_USER_PREFERENCE ({ commit, state, rootState }) {
|
||||
ipcRenderer.send('mt::ask-for-user-preference')
|
||||
ipcRenderer.send('mt::ask-for-user-data')
|
||||
|
||||
ipcRenderer.on('AGANI::user-preference', (e, preference) => {
|
||||
const { autoSave } = preference
|
||||
@ -90,6 +104,14 @@ const actions = {
|
||||
// commit('SET_USER_PREFERENCE', { [type]: value })
|
||||
// save to electron-store
|
||||
ipcRenderer.send('mt::set-user-preference', { [type]: value })
|
||||
},
|
||||
|
||||
SET_USER_DATA ({ commit }, { type, value }) {
|
||||
ipcRenderer.send('mt::set-user-data', { [type]: value })
|
||||
},
|
||||
|
||||
SET_IMAGE_FOLDER_PATH ({ commit }) {
|
||||
ipcRenderer.send('mt::ask-for-modify-image-folder-path')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,10 @@
|
||||
import path from 'path'
|
||||
import fse from 'fs-extra'
|
||||
import dayjs from 'dayjs'
|
||||
import Octokit from '@octokit/rest'
|
||||
import { isImageFile } from '../../main/filesystem'
|
||||
import { dataURItoBlob, getContentHash } from './index'
|
||||
import axios from 'axios'
|
||||
|
||||
export const create = (pathname, type) => {
|
||||
if (type === 'directory') {
|
||||
@ -43,3 +48,136 @@ export const isSameFileSync = (pathA, pathB, isNormalized=false) => {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export const moveImageToFolder = async (pathname, image, dir) => {
|
||||
const isPath = typeof image === 'string'
|
||||
if (isPath) {
|
||||
const dirname = path.dirname(pathname)
|
||||
const imagePath = path.resolve(dirname, image)
|
||||
const isImage = isImageFile(imagePath)
|
||||
if (isImage) {
|
||||
const filename = path.basename(imagePath)
|
||||
const extname = path.extname(imagePath)
|
||||
const noHashPath = path.join(dir, filename)
|
||||
if (noHashPath === imagePath) {
|
||||
return imagePath
|
||||
}
|
||||
const hash = getContentHash(imagePath)
|
||||
// To avoid name conflict.
|
||||
const hashFilePath = path.join(dir, `${hash}${extname}`)
|
||||
await fse.copy(imagePath, hashFilePath)
|
||||
return hashFilePath
|
||||
} else {
|
||||
return Promise.resolve(image)
|
||||
}
|
||||
} else {
|
||||
const imagePath = path.join(dir, `${dayjs().format('YYYY-MM-DD-HH-mm-ss')}-${image.name}`)
|
||||
|
||||
const binaryString = await new Promise((resolve, reject) => {
|
||||
const fileReader = new FileReader()
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result)
|
||||
}
|
||||
|
||||
fileReader.readAsBinaryString(image)
|
||||
})
|
||||
await fse.writeFile(imagePath, binaryString, 'binary')
|
||||
return imagePath
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @jocs todo, rewrite it use class
|
||||
*/
|
||||
export const uploadImage = async (pathname, image, preferences) => {
|
||||
const { currentUploader } = preferences
|
||||
const { owner, repo } = preferences.imageBed.github
|
||||
const token = preferences.githubToken
|
||||
const isPath = typeof image === 'string'
|
||||
const MAX_SIZE = 5 * 1024 * 1024
|
||||
let re
|
||||
let rj
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
re = resolve
|
||||
rj = reject
|
||||
})
|
||||
|
||||
const uploadToSMMS = file => {
|
||||
const api = 'https://sm.ms/api/upload'
|
||||
const formData = new window.FormData()
|
||||
formData.append('smfile', file)
|
||||
axios.post(api, formData).then((res) => {
|
||||
re(res.data.data.url)
|
||||
})
|
||||
.catch(err => {
|
||||
rj('Upload failed, the image will be copied to the image folder')
|
||||
})
|
||||
}
|
||||
|
||||
const uploadByGithub = (content, filename) => {
|
||||
const octokit = new Octokit({
|
||||
auth: `token ${token}`
|
||||
|
||||
})
|
||||
const path = dayjs().format('YYYY/MM') + `/${dayjs().format('DD-HH-mm-ss')}-${filename}`
|
||||
const message = `Upload by Mark Text at ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`
|
||||
octokit.repos.createFile({
|
||||
owner,
|
||||
repo,
|
||||
path,
|
||||
message,
|
||||
content
|
||||
}).then(result => {
|
||||
re(result.data.content.download_url)
|
||||
})
|
||||
.catch(err => {
|
||||
rj('Upload failed, the image will be copied to the image folder')
|
||||
})
|
||||
}
|
||||
|
||||
const notification = () => {
|
||||
rj('Cannot upload more than 5M image, the image will be copied to the image folder')
|
||||
}
|
||||
|
||||
if (isPath) {
|
||||
const dirname = path.dirname(pathname)
|
||||
const imagePath = path.resolve(dirname, image)
|
||||
const isImage = isImageFile(imagePath)
|
||||
if (isImage) {
|
||||
const { size } = await fse.stat(imagePath)
|
||||
if (size > MAX_SIZE) {
|
||||
notification()
|
||||
} else {
|
||||
const imageFile = await fse.readFile(imagePath)
|
||||
const blobFile = new Blob([imageFile])
|
||||
if (currentUploader === 'smms') {
|
||||
uploadToSMMS(blobFile)
|
||||
} else {
|
||||
const base64 = Buffer.from(imageFile).toString('base64')
|
||||
uploadByGithub(base64, path.basename(imagePath))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
re(image)
|
||||
}
|
||||
} else {
|
||||
const { size } = image
|
||||
if (size > MAX_SIZE) {
|
||||
notification()
|
||||
} else {
|
||||
const reader = new FileReader()
|
||||
reader.onload = async () => {
|
||||
const blobFile = dataURItoBlob(reader.result, image.name)
|
||||
if (currentUploader === 'smms') {
|
||||
uploadToSMMS(blobFile)
|
||||
} else {
|
||||
uploadByGithub(reader.result, image.name)
|
||||
}
|
||||
}
|
||||
|
||||
reader.readAsDataURL(image)
|
||||
}
|
||||
}
|
||||
|
||||
return promise
|
||||
}
|
||||
|
26
src/renderer/util/guessClipBoardFilePath.js
Normal file
@ -0,0 +1,26 @@
|
||||
import { isLinux, isOsx, isWindows } from './index'
|
||||
import plist from 'plist'
|
||||
import { remote } from 'electron'
|
||||
|
||||
const hasClipboardFiles = () => {
|
||||
return remote.clipboard.has('NSFilenamesPboardType')
|
||||
}
|
||||
|
||||
const getClipboardFiles = () => {
|
||||
if (!hasClipboardFiles()) { return [] }
|
||||
return plist.parse(remote.clipboard.read('NSFilenamesPboardType'))
|
||||
}
|
||||
|
||||
export const guessClipboardFilePath = () => {
|
||||
if (isLinux) return ''
|
||||
if (isOsx) {
|
||||
const result = getClipboardFiles()
|
||||
return Array.isArray(result) && result.length ? result[0] : ''
|
||||
} else if (isWindows) {
|
||||
const rawFilePath = remote.clipboard.read('FileNameW')
|
||||
const filePath = rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '')
|
||||
return filePath && typeof filePath === 'string' ? filePath : ''
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import crypto from 'crypto'
|
||||
|
||||
// help functions
|
||||
const easeInOutQuad = function (t, b, c, d) {
|
||||
t /= d / 2
|
||||
@ -187,6 +189,14 @@ export const cloneObj = (obj, deepCopy=true) => {
|
||||
return deepCopy ? JSON.parse(JSON.stringify(obj)) : Object.assign({}, obj)
|
||||
}
|
||||
|
||||
export const getHash = (content, encoding, type) => {
|
||||
return crypto.createHash(type).update(content, encoding).digest('hex')
|
||||
}
|
||||
|
||||
export const getContentHash = content => {
|
||||
return getHash(content, 'utf8', 'sha1')
|
||||
}
|
||||
|
||||
export const isOsx = process.platform === 'darwin'
|
||||
export const isWindows = process.platform === 'win32'
|
||||
export const isLinux = process.platform === 'linux'
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { isLinux, THEME_STYLE_ID, COMMON_STYLE_ID, DEFAULT_CODE_FONT_FAMILY, oneDarkThemes, railscastsThemes } from '../config'
|
||||
import { THEME_STYLE_ID, COMMON_STYLE_ID, DEFAULT_CODE_FONT_FAMILY, oneDarkThemes, railscastsThemes } from '../config'
|
||||
import { dark, graphite, materialDark, oneDark, ulysses } from './themeColor'
|
||||
import { isLinux } from './index'
|
||||
import elementStyle from 'element-ui/lib/theme-chalk/index.css'
|
||||
|
||||
const ORIGINAL_THEME = '#409EFF'
|
||||
|
@ -19,7 +19,7 @@
|
||||
"endOfLine": "default",
|
||||
"textDirection": "ltr",
|
||||
"hideQuickInsertHint": false,
|
||||
"imageDropAction": "folder",
|
||||
"imageInsertAction": "path",
|
||||
|
||||
"preferLooseListItem": true,
|
||||
"bulletListMarker": "-",
|
||||
|
245
yarn.lock
@ -150,6 +150,56 @@
|
||||
vow "^0.4.19"
|
||||
vow-fs "^0.3.6"
|
||||
|
||||
"@octokit/endpoint@^5.1.0":
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-5.1.2.tgz#45fd879e33a25ee10fa4cffc4d098ee04135afe6"
|
||||
integrity sha512-bBGGmcRFq1x0jrB29G/9KjYmO3cdHfk3476B2JOHRvLsNw1Pn3l+ZvbiqtcO9qAS4Ti+zFedLB84ziHZRZclQA==
|
||||
dependencies:
|
||||
deepmerge "3.2.0"
|
||||
is-plain-object "^3.0.0"
|
||||
universal-user-agent "^2.1.0"
|
||||
url-template "^2.0.8"
|
||||
|
||||
"@octokit/request-error@^1.0.1", "@octokit/request-error@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.0.2.tgz#e6dbc5be13be1041ef8eca9225520982add574cf"
|
||||
integrity sha512-T9swMS/Vc4QlfWrvyeSyp/GjhXtYaBzCcibjGywV4k4D2qVrQKfEMPy8OxMDEj7zkIIdpHwqdpVbKCvnUPqkXw==
|
||||
dependencies:
|
||||
deprecation "^2.0.0"
|
||||
once "^1.4.0"
|
||||
|
||||
"@octokit/request@^4.0.1":
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-4.1.0.tgz#e85dc377113baf2fe24433af8feb20e8a32e21b0"
|
||||
integrity sha512-RvpQAba4i+BNH0z8i0gPRc1ShlHidj4puQjI/Tno6s+Q3/Mzb0XRSHJiOhpeFrZ22V7Mwjq1E7QS27P5CgpWYA==
|
||||
dependencies:
|
||||
"@octokit/endpoint" "^5.1.0"
|
||||
"@octokit/request-error" "^1.0.1"
|
||||
deprecation "^2.0.0"
|
||||
is-plain-object "^3.0.0"
|
||||
node-fetch "^2.3.0"
|
||||
once "^1.4.0"
|
||||
universal-user-agent "^2.1.0"
|
||||
|
||||
"@octokit/rest@^16.26.0":
|
||||
version "16.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.26.0.tgz#5c12b28763219045e1c9a15182e8dfaed10004e8"
|
||||
integrity sha512-NBpzre44ZAQWZhlH+zUYTgqI0pHN+c9rNj4d+pCydGEiKTGc1HKmoTghEUyr9GxazDyoAvmpx9nL0I7QS1Olvg==
|
||||
dependencies:
|
||||
"@octokit/request" "^4.0.1"
|
||||
"@octokit/request-error" "^1.0.2"
|
||||
atob-lite "^2.0.0"
|
||||
before-after-hook "^1.4.0"
|
||||
btoa-lite "^1.0.0"
|
||||
deprecation "^2.0.0"
|
||||
lodash.get "^4.4.2"
|
||||
lodash.set "^4.3.2"
|
||||
lodash.uniq "^4.5.0"
|
||||
octokit-pagination-methods "^1.1.0"
|
||||
once "^1.4.0"
|
||||
universal-user-agent "^2.0.0"
|
||||
url-template "^2.0.8"
|
||||
|
||||
"@types/clone@~0.1.30":
|
||||
version "0.1.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/clone/-/clone-0.1.30.tgz#e7365648c1b42136a59c7d5040637b3b5c83b614"
|
||||
@ -803,6 +853,11 @@ atoa@1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/atoa/-/atoa-1.0.0.tgz#0cc0e91a480e738f923ebc103676471779b34a49"
|
||||
integrity sha1-DMDpGkgOc4+SPrwQNnZHF3mzSkk=
|
||||
|
||||
atob-lite@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696"
|
||||
integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY=
|
||||
|
||||
atob@^2.1.1:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
@ -1627,6 +1682,11 @@ bcrypt-pbkdf@^1.0.0:
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
before-after-hook@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d"
|
||||
integrity sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg==
|
||||
|
||||
better-assert@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
|
||||
@ -1890,6 +1950,11 @@ browserslist@^4.4.2, browserslist@^4.5.4:
|
||||
electron-to-chromium "^1.3.124"
|
||||
node-releases "^1.1.14"
|
||||
|
||||
btoa-lite@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337"
|
||||
integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc=
|
||||
|
||||
buffer-alloc-unsafe@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
|
||||
@ -2259,7 +2324,7 @@ chokidar@^3.0.0:
|
||||
optionalDependencies:
|
||||
fsevents "^2.0.6"
|
||||
|
||||
chownr@^1.1.1:
|
||||
chownr@^1.0.1, chownr@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
|
||||
integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
|
||||
@ -3426,6 +3491,13 @@ decode-uri-component@^0.2.0:
|
||||
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
|
||||
|
||||
decompress-response@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3"
|
||||
integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=
|
||||
dependencies:
|
||||
mimic-response "^1.0.0"
|
||||
|
||||
deep-eql@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
|
||||
@ -3453,6 +3525,11 @@ deepmerge@1.3.2:
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.3.2.tgz#1663691629d4dbfe364fa12a2a4f0aa86aa3a050"
|
||||
integrity sha1-FmNpFinU2/42T6EqKk8KqGqjoFA=
|
||||
|
||||
deepmerge@3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.2.0.tgz#58ef463a57c08d376547f8869fdc5bcee957f44e"
|
||||
integrity sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow==
|
||||
|
||||
deepmerge@^1.2.0:
|
||||
version "1.5.2"
|
||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
|
||||
@ -3552,6 +3629,11 @@ depd@~1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
||||
|
||||
deprecation@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.0.0.tgz#dd0427cd920c78bc575ec39dab2f22e7c304fb9d"
|
||||
integrity sha512-lbQN037mB3VfA2JFuguM5GCJ+zPinMeCrFe+AfSZ6eqrnJA/Fs+EYMnd6Nb2mn9lf2jO9xwEd9o9lic+D4vkcw==
|
||||
|
||||
des.js@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
|
||||
@ -4619,6 +4701,11 @@ expand-brackets@^2.1.4:
|
||||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
expand-template@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
|
||||
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
|
||||
|
||||
expand-tilde@^2.0.0, expand-tilde@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
|
||||
@ -5184,6 +5271,11 @@ git-revision-webpack-plugin@^3.0.3:
|
||||
resolved "https://registry.yarnpkg.com/git-revision-webpack-plugin/-/git-revision-webpack-plugin-3.0.3.tgz#f909949d7851d1039ed530518f73f5d46594e66f"
|
||||
integrity sha512-B2ixM0fY7VgR61ZRSXYrh0R57Er7RY+CZb+fja5OFe21Y5o9GgzQanMgdlcBwWZ+LoOVqxBogbDutTTYMXQDWw==
|
||||
|
||||
github-from-package@0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
|
||||
integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
|
||||
|
||||
github-markdown-css@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-3.0.1.tgz#d08db1060d2e182025e0d07d547cfe2afed30205"
|
||||
@ -6161,6 +6253,13 @@ is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4:
|
||||
dependencies:
|
||||
isobject "^3.0.1"
|
||||
|
||||
is-plain-object@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.0.tgz#47bfc5da1b5d50d64110806c199359482e75a928"
|
||||
integrity sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==
|
||||
dependencies:
|
||||
isobject "^4.0.0"
|
||||
|
||||
is-promise@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
|
||||
@ -6276,6 +6375,11 @@ isobject@^3.0.0, isobject@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
||||
|
||||
isobject@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
|
||||
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
@ -6597,6 +6701,14 @@ keypress@0.1.x:
|
||||
resolved "https://registry.yarnpkg.com/keypress/-/keypress-0.1.0.tgz#4a3188d4291b66b4f65edb99f806aa9ae293592a"
|
||||
integrity sha1-SjGI1CkbZrT2XtuZ+AaqmuKTWSo=
|
||||
|
||||
keytar@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/keytar/-/keytar-4.7.0.tgz#992f646ace0862ee72a513a9f6cf7c21ef97078f"
|
||||
integrity sha512-0hLlRRkhdR0068fVQo21hnIndGvacsh9PtAHGAPMPzxFjJwP8idAkVAcbdb1P5B+gterCBa3+4hxL0NPMDlZtw==
|
||||
dependencies:
|
||||
nan "2.13.2"
|
||||
prebuild-install "5.3.0"
|
||||
|
||||
killable@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
|
||||
@ -6864,6 +6976,11 @@ lodash.forown@^4.4.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.forown/-/lodash.forown-4.4.0.tgz#85115cf04f73ef966eced52511d3893cc46683af"
|
||||
integrity sha1-hRFc8E9z75ZuztUlEdOJPMRmg68=
|
||||
|
||||
lodash.get@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=
|
||||
|
||||
lodash.isarguments@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
|
||||
@ -6947,6 +7064,11 @@ lodash.restparam@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
|
||||
integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
|
||||
|
||||
lodash.set@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.set/-/lodash.set-4.3.2.tgz#d8757b1da807dde24816b0d6a84bea1a76230b23"
|
||||
integrity sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=
|
||||
|
||||
lodash.sortby@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
@ -7076,6 +7198,11 @@ lru-cache@^5.1.1:
|
||||
dependencies:
|
||||
yallist "^3.0.2"
|
||||
|
||||
macos-release@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.2.0.tgz#ab58d55dd4714f0a05ad4b0e90f4370fef5cdea8"
|
||||
integrity sha512-iV2IDxZaX8dIcM7fG6cI46uNmHUxHE4yN+Z8tKHAW1TBPMZDIKHf/3L+YnOuj/FK9il14UaVdHmiQ1tsi90ltA==
|
||||
|
||||
make-dir@^1.0.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
|
||||
@ -7300,6 +7427,11 @@ mimic-fn@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
||||
|
||||
mimic-response@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b"
|
||||
integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==
|
||||
|
||||
mini-css-extract-plugin@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.6.0.tgz#a3f13372d6fcde912f3ee4cd039665704801e3b9"
|
||||
@ -7486,7 +7618,7 @@ mute-stream@0.0.7:
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
||||
|
||||
nan@^2.10.0, nan@^2.12.1:
|
||||
nan@2.13.2, nan@^2.10.0, nan@^2.12.1:
|
||||
version "2.13.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
|
||||
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
|
||||
@ -7508,6 +7640,11 @@ nanomatch@^1.2.1, nanomatch@^1.2.9:
|
||||
snapdragon "^0.8.1"
|
||||
to-regex "^3.0.1"
|
||||
|
||||
napi-build-utils@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508"
|
||||
integrity sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==
|
||||
|
||||
natural-compare@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||
@ -7661,6 +7798,11 @@ node-releases@^1.1.14:
|
||||
dependencies:
|
||||
semver "^5.3.0"
|
||||
|
||||
noop-logger@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2"
|
||||
integrity sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=
|
||||
|
||||
"nopt@2 || 3", nopt@3.x:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
|
||||
@ -7742,7 +7884,7 @@ npm-run-path@^2.0.0:
|
||||
dependencies:
|
||||
path-key "^2.0.0"
|
||||
|
||||
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2:
|
||||
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.1, npmlog@^4.0.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
||||
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
|
||||
@ -7873,6 +8015,11 @@ obuf@^1.0.0, obuf@^1.1.2:
|
||||
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
|
||||
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
|
||||
|
||||
octokit-pagination-methods@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4"
|
||||
integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ==
|
||||
|
||||
on-finished@~2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||
@ -7960,7 +8107,7 @@ os-browserify@^0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
||||
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
|
||||
|
||||
os-homedir@^1.0.0:
|
||||
os-homedir@^1.0.0, os-homedir@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
|
||||
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
|
||||
@ -7974,6 +8121,14 @@ os-locale@^3.0.0, os-locale@^3.1.0:
|
||||
lcid "^2.0.0"
|
||||
mem "^4.0.0"
|
||||
|
||||
os-name@^3.0.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801"
|
||||
integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==
|
||||
dependencies:
|
||||
macos-release "^2.2.0"
|
||||
windows-release "^3.1.0"
|
||||
|
||||
os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
||||
@ -8787,6 +8942,28 @@ posthtml@^0.9.2:
|
||||
posthtml-parser "^0.2.0"
|
||||
posthtml-render "^1.0.5"
|
||||
|
||||
prebuild-install@5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.3.0.tgz#58b4d8344e03590990931ee088dd5401b03004c8"
|
||||
integrity sha512-aaLVANlj4HgZweKttFNUVNRxDukytuIuxeK2boIMHjagNJCiVKWFsKF4tCE3ql3GbrD2tExPQ7/pwtEJcHNZeg==
|
||||
dependencies:
|
||||
detect-libc "^1.0.3"
|
||||
expand-template "^2.0.3"
|
||||
github-from-package "0.0.0"
|
||||
minimist "^1.2.0"
|
||||
mkdirp "^0.5.1"
|
||||
napi-build-utils "^1.0.1"
|
||||
node-abi "^2.7.0"
|
||||
noop-logger "^0.1.1"
|
||||
npmlog "^4.0.1"
|
||||
os-homedir "^1.0.1"
|
||||
pump "^2.0.1"
|
||||
rc "^1.2.7"
|
||||
simple-get "^2.7.0"
|
||||
tar-fs "^1.13.0"
|
||||
tunnel-agent "^0.6.0"
|
||||
which-pm-runs "^1.0.0"
|
||||
|
||||
prelude-ls@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
||||
@ -8898,7 +9075,15 @@ public-encrypt@^4.0.0:
|
||||
randombytes "^2.0.1"
|
||||
safe-buffer "^5.1.2"
|
||||
|
||||
pump@^2.0.0:
|
||||
pump@^1.0.0:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954"
|
||||
integrity sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
pump@^2.0.0, pump@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
|
||||
integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
|
||||
@ -9745,6 +9930,20 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
|
||||
|
||||
simple-concat@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6"
|
||||
integrity sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=
|
||||
|
||||
simple-get@^2.7.0:
|
||||
version "2.8.1"
|
||||
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.1.tgz#0e22e91d4575d87620620bc91308d57a77f44b5d"
|
||||
integrity sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==
|
||||
dependencies:
|
||||
decompress-response "^3.3.0"
|
||||
once "^1.3.1"
|
||||
simple-concat "^1.0.0"
|
||||
|
||||
single-line-log@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364"
|
||||
@ -10470,7 +10669,17 @@ tapable@^1.0.0, tapable@^1.1.0:
|
||||
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
|
||||
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
|
||||
|
||||
tar-stream@^1.5.0:
|
||||
tar-fs@^1.13.0:
|
||||
version "1.16.3"
|
||||
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509"
|
||||
integrity sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==
|
||||
dependencies:
|
||||
chownr "^1.0.1"
|
||||
mkdirp "^0.5.1"
|
||||
pump "^1.0.0"
|
||||
tar-stream "^1.1.2"
|
||||
|
||||
tar-stream@^1.1.2, tar-stream@^1.5.0:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
|
||||
integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
|
||||
@ -10919,6 +11128,13 @@ unique-string@^1.0.0:
|
||||
dependencies:
|
||||
crypto-random-string "^1.0.0"
|
||||
|
||||
universal-user-agent@^2.0.0, universal-user-agent@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.1.0.tgz#5abfbcc036a1ba490cb941f8fd68c46d3669e8e4"
|
||||
integrity sha512-8itiX7G05Tu3mGDTdNY2fB4KJ8MgZLS54RdG6PkkfwMAavrXu1mV/lls/GABx9O3Rw4PnTtasxrvbMQoBYY92Q==
|
||||
dependencies:
|
||||
os-name "^3.0.0"
|
||||
|
||||
universalify@^0.1.0:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||
@ -11028,6 +11244,11 @@ url-slug@2.0.0:
|
||||
dependencies:
|
||||
unidecode "0.1.8"
|
||||
|
||||
url-template@^2.0.8:
|
||||
version "2.0.8"
|
||||
resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
|
||||
integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE=
|
||||
|
||||
url@^0.11.0, url@~0.11.0:
|
||||
version "0.11.0"
|
||||
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
|
||||
@ -11905,6 +12126,11 @@ which-module@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
|
||||
|
||||
which-pm-runs@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb"
|
||||
integrity sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=
|
||||
|
||||
which@1, which@1.3.1, which@^1.1.1, which@^1.2.14, which@^1.2.9:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
@ -11939,6 +12165,13 @@ window-size@^1.1.1:
|
||||
define-property "^1.0.0"
|
||||
is-number "^3.0.0"
|
||||
|
||||
windows-release@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f"
|
||||
integrity sha512-QTlz2hKLrdqukrsapKsINzqMgOUpQW268eJ0OaOpJN32h272waxR9fkB9VoWRtK7uKHG5EHJcTXQBD8XZVJkFA==
|
||||
dependencies:
|
||||
execa "^1.0.0"
|
||||
|
||||
wordwrap@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
|
||||
|