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,11 +8,12 @@ const getLicenses = (rootDir, callback) => {
|
|||||||
production: true,
|
production: true,
|
||||||
development: false,
|
development: false,
|
||||||
direct: true,
|
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,
|
json: true,
|
||||||
onlyAllow: 'Unlicense;WTFPL;ISC;MIT;BSD;ISC;Apache-2.0;MIT*;Apache*;BSD*',
|
onlyAllow: 'Unlicense;WTFPL;ISC;MIT;BSD;ISC;Apache-2.0;MIT*;Apache*;BSD*',
|
||||||
customPath: {
|
customPath: {
|
||||||
"licenses": "",
|
licenses: '',
|
||||||
"licenseText": "none"
|
licenseText: 'none'
|
||||||
}
|
}
|
||||||
}, function (err, packages) {
|
}, function (err, packages) {
|
||||||
callback(err, packages, checker)
|
callback(err, packages, checker)
|
||||||
|
@ -169,6 +169,7 @@ const rendererConfig = {
|
|||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
'main': path.join(__dirname, '../src/main'),
|
||||||
'@': path.join(__dirname, '../src/renderer'),
|
'@': path.join(__dirname, '../src/renderer'),
|
||||||
'common': path.join(__dirname, '../src/common'),
|
'common': path.join(__dirname, '../src/common'),
|
||||||
'muya': path.join(__dirname, '../src/muya'),
|
'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)
|
- libx11 (dev)
|
||||||
- libxkbfile (dev)
|
- libxkbfile (dev)
|
||||||
|
|
||||||
On Debian-based Linux: `sudo apt-get install libx11-dev libxkbfile-dev`
|
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`
|
On Red Hat-based Linux: `sudo dnf install libx11-devel libxkbfile-devel libsecret-devel`
|
||||||
|
|
||||||
**Let's build:**
|
**Let's build:**
|
||||||
|
|
||||||
|
@ -31,6 +31,8 @@ addons:
|
|||||||
# atom/keyboard-layout
|
# atom/keyboard-layout
|
||||||
- libx11-dev
|
- libx11-dev
|
||||||
- libxkbfile-dev
|
- libxkbfile-dev
|
||||||
|
# atom/node-keytar
|
||||||
|
- libsecret-1-dev
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
|
- 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 |
|
| `editFindPrevious` | Continue the search and find the previous match |
|
||||||
| `editReplace` | Replace the information with a replacement |
|
| `editReplace` | Replace the information with a replacement |
|
||||||
| `editAidou` | Show Aidou dialog |
|
| `editAidou` | Show Aidou dialog |
|
||||||
|
| `editScreenshot` | Get the screenshot |
|
||||||
|
|
||||||
**Paragraph menu:**
|
**Paragraph menu:**
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@
|
|||||||
"postinstall": "npm run rebuild && npm run lint:fix",
|
"postinstall": "npm run rebuild && npm run lint:fix",
|
||||||
"build:muya": "cd src/muya && webpack --progress --colors --config webpack.config.js",
|
"build:muya": "cd src/muya && webpack --progress --colors --config webpack.config.js",
|
||||||
"release:muya": "npm run build:muya && cd src/muya && npm publish",
|
"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",
|
"gen-third-party": "node tools/generateThirdPartyLicense.js",
|
||||||
"validate-licenses": "node tools/validateLicenses.js"
|
"validate-licenses": "node tools/validateLicenses.js"
|
||||||
},
|
},
|
||||||
@ -165,6 +165,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hfelix/electron-localshortcut": "^3.1.1",
|
"@hfelix/electron-localshortcut": "^3.1.1",
|
||||||
|
"@octokit/rest": "^16.26.0",
|
||||||
"arg": "^4.1.0",
|
"arg": "^4.1.0",
|
||||||
"axios": "^0.18.0",
|
"axios": "^0.18.0",
|
||||||
"chokidar": "^3.0.0",
|
"chokidar": "^3.0.0",
|
||||||
@ -188,7 +189,9 @@
|
|||||||
"html-tags": "^3.0.0",
|
"html-tags": "^3.0.0",
|
||||||
"katex": "^0.10.2",
|
"katex": "^0.10.2",
|
||||||
"keyboard-layout": "^2.0.15",
|
"keyboard-layout": "^2.0.15",
|
||||||
|
"keytar": "^4.7.0",
|
||||||
"mermaid": "^8.0.0",
|
"mermaid": "^8.0.0",
|
||||||
|
"plist": "^3.0.1",
|
||||||
"popper.js": "^1.15.0",
|
"popper.js": "^1.15.0",
|
||||||
"prismjs": "^1.16.0",
|
"prismjs": "^1.16.0",
|
||||||
"snabbdom": "^0.7.3",
|
"snabbdom": "^0.7.3",
|
||||||
|
@ -17,7 +17,9 @@ class EnvPaths {
|
|||||||
this._logPath = path.join(this._userDataPath, 'logs', `${currentDate.getFullYear()}${currentDate.getMonth() + 1}`)
|
this._logPath = path.join(this._userDataPath, 'logs', `${currentDate.getFullYear()}${currentDate.getMonth() + 1}`)
|
||||||
this._preferencesPath = userDataPath // path.join(this._userDataPath, 'preferences')
|
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...
|
// TODO(sessions): enable this...
|
||||||
// this._globalStorage = path.join(this._userDataPath, 'globalStorage')
|
// this._globalStorage = path.join(this._userDataPath, 'globalStorage')
|
||||||
@ -42,6 +44,10 @@ class EnvPaths {
|
|||||||
return this._preferencesPath
|
return this._preferencesPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get dataCenterPath () {
|
||||||
|
return this._dataCenterPath
|
||||||
|
}
|
||||||
|
|
||||||
get preferencesFilePath () {
|
get preferencesFilePath () {
|
||||||
return this._preferencesFilePath
|
return this._preferencesFilePath
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import WindowManager from '../app/windowManager'
|
import WindowManager from '../app/windowManager'
|
||||||
import Preference from '../preferences'
|
import Preference from '../preferences'
|
||||||
|
import DataCenter from '../dataCenter'
|
||||||
import Keybindings from '../keyboard/shortcutHandler'
|
import Keybindings from '../keyboard/shortcutHandler'
|
||||||
import AppMenu from '../menu'
|
import AppMenu from '../menu'
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ class Accessor {
|
|||||||
this.env = appEnvironment
|
this.env = appEnvironment
|
||||||
this.paths = appEnvironment.paths // export paths to make it better accessible
|
this.paths = appEnvironment.paths // export paths to make it better accessible
|
||||||
this.preferences = new Preference(this.paths)
|
this.preferences = new Preference(this.paths)
|
||||||
|
this.dataCenter = new DataCenter(this.paths)
|
||||||
this.keybindings = new Keybindings(userDataPath)
|
this.keybindings = new Keybindings(userDataPath)
|
||||||
this.menu = new AppMenu(this.preferences, this.keybindings, userDataPath)
|
this.menu = new AppMenu(this.preferences, this.keybindings, userDataPath)
|
||||||
this.windowManager = new WindowManager(this.menu, this.preferences)
|
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 { isLinux, isOsx } from '../config'
|
||||||
import { isDirectory, isMarkdownFileOrLink, normalizeAndResolvePath } from '../filesystem'
|
import { isDirectory, isMarkdownFileOrLink, normalizeAndResolvePath } from '../filesystem'
|
||||||
import { getMenuItemById } from '../menu'
|
import { getMenuItemById } from '../menu'
|
||||||
@ -8,6 +13,7 @@ import { watchers } from '../utils/imagePathAutoComplement'
|
|||||||
import EditorWindow from '../windows/editor'
|
import EditorWindow from '../windows/editor'
|
||||||
import SettingWindow from '../windows/setting'
|
import SettingWindow from '../windows/setting'
|
||||||
import { WindowType } from './windowManager'
|
import { WindowType } from './windowManager'
|
||||||
|
// import ShortcutCapture from 'shortcut-capture'
|
||||||
|
|
||||||
class App {
|
class App {
|
||||||
|
|
||||||
@ -21,6 +27,8 @@ class App {
|
|||||||
this._openFilesCache = []
|
this._openFilesCache = []
|
||||||
this._openFilesTimer = null
|
this._openFilesTimer = null
|
||||||
this._windowManager = this._accessor.windowManager
|
this._windowManager = this._accessor.windowManager
|
||||||
|
// this.launchScreenshotWin = null // The window which call the screenshot.
|
||||||
|
// this.shortcutCapture = null
|
||||||
|
|
||||||
this._listenForIpcMain()
|
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 = () => {
|
ready = () => {
|
||||||
const { _args: args } = this
|
const { _args: args } = this
|
||||||
if (!isOsx && args._.length) {
|
if (!isOsx && args._.length) {
|
||||||
@ -126,6 +140,27 @@ class App {
|
|||||||
} else {
|
} else {
|
||||||
this.createEditorWindow()
|
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) => {
|
openFile = (event, pathname) => {
|
||||||
@ -217,6 +252,34 @@ class App {
|
|||||||
this.createEditorWindow()
|
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', () => {
|
ipcMain.on('app-create-settings-window', () => {
|
||||||
const settingWins = this._windowManager.windowsOfType(WindowType.SETTING)
|
const settingWins = this._windowManager.windowsOfType(WindowType.SETTING)
|
||||||
if (settingWins.length >= 1) {
|
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 fs from 'fs-extra'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { hasMarkdownExtension } from '../utils'
|
import { hasMarkdownExtension } from '../utils'
|
||||||
|
import { IMAGE_EXTENSIONS } from '../config'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that a directory exist.
|
* Ensure that a directory exist.
|
||||||
@ -64,6 +65,18 @@ export const isSymbolicLink = filepath => {
|
|||||||
export const isMarkdownFile = filepath => {
|
export const isMarkdownFile = filepath => {
|
||||||
return isFile(filepath) && hasMarkdownExtension(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.
|
* 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'],
|
['editFindPrevious', 'CmdOrCtrl+Shift+U'],
|
||||||
['editReplace', 'CmdOrCtrl+Alt+F'],
|
['editReplace', 'CmdOrCtrl+Alt+F'],
|
||||||
['editAidou', 'CmdOrCtrl+/'],
|
['editAidou', 'CmdOrCtrl+/'],
|
||||||
|
['editScreenshot', 'CmdOrCtrl+Alt+A'],
|
||||||
|
|
||||||
// paragraph menu
|
// paragraph menu
|
||||||
['paragraphHeading1', 'CmdOrCtrl+1'],
|
['paragraphHeading1', 'CmdOrCtrl+1'],
|
||||||
|
@ -1,42 +1,25 @@
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { dialog, ipcMain, BrowserWindow } from 'electron'
|
import { ipcMain, BrowserWindow } from 'electron'
|
||||||
import log from 'electron-log'
|
import log from 'electron-log'
|
||||||
import { IMAGE_EXTENSIONS } from '../../config'
|
|
||||||
import { updateLineEndingMenu } from '../../menu'
|
import { updateLineEndingMenu } from '../../menu'
|
||||||
import { searchFilesAndDir } from '../../utils/imagePathAutoComplement'
|
import { searchFilesAndDir } from '../../utils/imagePathAutoComplement'
|
||||||
|
|
||||||
const getAndSendImagePath = (win, type) => {
|
ipcMain.on('mt::ask-for-image-auto-path', (e, { pathname, src, id }) => {
|
||||||
// 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 }) => {
|
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
if (src.endsWith('/') || src.endsWith('\\') || src.endsWith('.')) {
|
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 fullPath = path.isAbsolute(src) ? src : path.join(path.dirname(pathname), src)
|
||||||
const dir = path.dirname(fullPath)
|
const dir = path.dirname(fullPath)
|
||||||
const searchKey = path.basename(fullPath)
|
const searchKey = path.basename(fullPath)
|
||||||
searchFilesAndDir(dir, searchKey)
|
searchFilesAndDir(dir, searchKey)
|
||||||
.then(files => {
|
.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) => {
|
ipcMain.on('AGANI::update-line-ending-menu', (e, lineEnding) => {
|
||||||
@ -47,14 +30,10 @@ export const edit = (win, type) => {
|
|||||||
win.webContents.send('AGANI::edit', { type })
|
win.webContents.send('AGANI::edit', { type })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const screenshot = (win, type) => {
|
||||||
|
ipcMain.emit('screen-capture', win)
|
||||||
|
}
|
||||||
|
|
||||||
export const lineEnding = (win, lineEnding) => {
|
export const lineEnding = (win, lineEnding) => {
|
||||||
win.webContents.send('AGANI::set-line-ending', { lineEnding, ignoreSaveStatus: false })
|
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 * as actions from '../actions/edit'
|
||||||
|
import { isOsx } from '../../config'
|
||||||
|
|
||||||
export default function (keybindings, userPreference) {
|
export default function (keybindings, userPreference) {
|
||||||
const { aidou } = userPreference.getAll()
|
const { aidou } = userPreference.getAll()
|
||||||
@ -114,23 +115,13 @@ export default function (keybindings, userPreference) {
|
|||||||
actions.edit(browserWindow, 'aidou')
|
actions.edit(browserWindow, 'aidou')
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
label: 'Insert Image',
|
label: 'Screenshot',
|
||||||
submenu: [{
|
id: 'screenshot',
|
||||||
label: 'Absolute Path',
|
visible: isOsx,
|
||||||
|
accelerator: keybindings.getAccelerator('editScreenshot'),
|
||||||
click (menuItem, browserWindow) {
|
click (menuItem, browserWindow) {
|
||||||
actions.insertImage(browserWindow, 'absolute')
|
actions.screenshot(browserWindow, 'screenshot')
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
label: 'Relative Path',
|
|
||||||
click (menuItem, browserWindow) {
|
|
||||||
actions.insertImage(browserWindow, 'relative')
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
label: 'Upload to Cloud (EXP)',
|
|
||||||
click (menuItem, browserWindow) {
|
|
||||||
actions.insertImage(browserWindow, 'upload')
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}, {
|
}, {
|
||||||
type: 'separator'
|
type: 'separator'
|
||||||
}, {
|
}, {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import edit from './edit'
|
import edit from './edit'
|
||||||
|
import prefEdit from './prefEdit'
|
||||||
import file from './file'
|
import file from './file'
|
||||||
import help from './help'
|
import help from './help'
|
||||||
import marktext from './marktext'
|
import marktext from './marktext'
|
||||||
@ -18,6 +19,7 @@ export dockMenu from './dock'
|
|||||||
export const configSettingMenu = (keybindings) => {
|
export const configSettingMenu = (keybindings) => {
|
||||||
return [
|
return [
|
||||||
...(process.platform === 'darwin' ? [ marktext(keybindings) ] : []),
|
...(process.platform === 'darwin' ? [ marktext(keybindings) ] : []),
|
||||||
|
prefEdit(keybindings),
|
||||||
help()
|
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) {
|
if (!this.hasPreferencesFile) {
|
||||||
this.store.set(defaultSettings)
|
this.store.set(defaultSettings)
|
||||||
} else {
|
} 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
|
// Update outdated settings
|
||||||
const requiresUpdate = !hasSameKeys(defaultSettings, userSetting)
|
const requiresUpdate = !hasSameKeys(defaultSettings, userSetting)
|
||||||
|
const userSettingKeys = Object.keys(userSetting)
|
||||||
|
const defaultSettingKeys = Object.keys(defaultSettings)
|
||||||
|
|
||||||
if (requiresUpdate) {
|
if (requiresUpdate) {
|
||||||
// remove outdated settings
|
// remove outdated settings
|
||||||
for (const key in userSetting) {
|
for (const key of userSettingKeys) {
|
||||||
if (userSetting.hasOwnProperty(key) && !defaultSettings.hasOwnProperty(key)) {
|
if (!defaultSettingKeys.includes(key)) {
|
||||||
delete userSetting[key]
|
delete userSetting[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// add new setting options
|
// add new setting options
|
||||||
for (const key in defaultSettings) {
|
for (const key in defaultSettings) {
|
||||||
if (defaultSettings.hasOwnProperty(key) && !userSetting.hasOwnProperty(key)) {
|
if (!userSettingKeys.includes(key)) {
|
||||||
userSetting[key] = defaultSettings[key]
|
userSetting[key] = defaultSettings[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,14 +115,6 @@
|
|||||||
"description": "Editor--Hide hint for quickly creating paragraphs",
|
"description": "Editor--Hide hint for quickly creating paragraphs",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"imageDropAction": {
|
|
||||||
"description": "Editor--The default behavior after paste or drag the image to Mark Text",
|
|
||||||
"enum": [
|
|
||||||
"upload",
|
|
||||||
"folder",
|
|
||||||
"path"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"preferLooseListItem": {
|
"preferLooseListItem": {
|
||||||
"description": "Markdown--The preferred list type",
|
"description": "Markdown--The preferred list type",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
@ -167,5 +159,14 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"description": "Theme--Select the theme used in Mark Text",
|
"description": "Theme--Select the theme used in Mark Text",
|
||||||
"type": "string"
|
"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;
|
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 {
|
div.ag-show-quick-insert-hint p.ag-paragraph.ag-active > span.ag-paragraph-content:first-of-type:empty::after {
|
||||||
content: 'Type @ to insert';
|
content: 'Type @ to insert';
|
||||||
color: var(--editorColor10);
|
color: var(--editorColor10);
|
||||||
@ -741,6 +747,175 @@ span.ag-warn.ag-emoji-marked-text {
|
|||||||
vertical-align: middle;
|
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 {
|
.ag-image-marked-text ~ img {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@ -749,8 +924,9 @@ span.ag-warn.ag-emoji-marked-text {
|
|||||||
.ag-image-marked-text::before {
|
.ag-image-marked-text::before {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
content: '';
|
content: '';
|
||||||
width: 1em;
|
width: 1.2em;
|
||||||
height: 1em;
|
height: 1.2em;
|
||||||
|
margin-right: 5px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: sub;
|
vertical-align: sub;
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ export const emptyElementNames = ['br', 'col', 'colgroup', 'hr', 'img', 'input',
|
|||||||
export const EVENT_KEYS = generateKeyHash([
|
export const EVENT_KEYS = generateKeyHash([
|
||||||
'Enter',
|
'Enter',
|
||||||
'Backspace',
|
'Backspace',
|
||||||
|
'Space',
|
||||||
'Delete',
|
'Delete',
|
||||||
'ArrowUp',
|
'ArrowUp',
|
||||||
'ArrowDown',
|
'ArrowDown',
|
||||||
@ -95,8 +96,16 @@ export const CLASS_OR_ID = genUpper2LowerKeyHash([
|
|||||||
'AG_HTML_PREVIEW',
|
'AG_HTML_PREVIEW',
|
||||||
'AG_HTML_TAG',
|
'AG_HTML_TAG',
|
||||||
'AG_IMAGE_FAIL',
|
'AG_IMAGE_FAIL',
|
||||||
|
'AG_IMAGE_LOADING',
|
||||||
|
'AG_EMPTY_IMAGE',
|
||||||
'AG_IMAGE_MARKED_TEXT',
|
'AG_IMAGE_MARKED_TEXT',
|
||||||
'AG_IMAGE_SRC',
|
'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_INDENT_CODE',
|
||||||
'AG_INLINE_RULE',
|
'AG_INLINE_RULE',
|
||||||
'AG_LANGUAGE',
|
'AG_LANGUAGE',
|
||||||
@ -240,7 +249,14 @@ export const MUYA_DEFAULT_OPTION = {
|
|||||||
sequenceTheme: 'hand', // hand or simple
|
sequenceTheme: 'hand', // hand or simple
|
||||||
mermaidTheme: 'default', // dark / forest / default
|
mermaidTheme: 'default', // dark / forest / default
|
||||||
vegaTheme: 'latimes', // excel / ggplot2 / quartz / vox / fivethirtyeight / dark / latimes
|
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 = {
|
// 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 isInElectron = window && window.process && window.process.type === 'renderer'
|
||||||
export const isOsx = window && window.navigator && /Mac/.test(window.navigator.platform)
|
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
|
// 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
|
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
|
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) {
|
ContentState.prototype.arrowHandler = function (event) {
|
||||||
const node = selection.getSelectionStart()
|
const node = selection.getSelectionStart()
|
||||||
const paragraph = findNearestParagraph(node)
|
const paragraph = findNearestParagraph(node)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import selection from '../selection'
|
import selection from '../selection'
|
||||||
import { findNearestParagraph, findOutMostParagraph } from '../selection/dom'
|
import { findNearestParagraph, findOutMostParagraph } from '../selection/dom'
|
||||||
import { tokenizer, generator } from '../parser/'
|
import { tokenizer, generator } from '../parser/'
|
||||||
|
import { getImageInfo } from '../utils/getImageInfo'
|
||||||
|
|
||||||
const backspaceCtrl = ContentState => {
|
const backspaceCtrl = ContentState => {
|
||||||
ContentState.prototype.checkBackspaceCase = function () {
|
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) {
|
ContentState.prototype.backspaceHandler = function (event) {
|
||||||
const { start, end } = selection.getCursorRange()
|
const { start, end } = selection.getCursorRange()
|
||||||
|
|
||||||
@ -211,14 +220,50 @@ const backspaceCtrl = ContentState => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const node = selection.getSelectionStart()
|
const node = selection.getSelectionStart()
|
||||||
|
const preEleSibling = node && node.nodeType === 1 ? node.previousElementSibling : null
|
||||||
const paragraph = findNearestParagraph(node)
|
const paragraph = findNearestParagraph(node)
|
||||||
const id = paragraph.id
|
const id = paragraph.id
|
||||||
let block = this.getBlock(id)
|
let block = this.getBlock(id)
|
||||||
let parent = this.getBlock(block.parent)
|
let parent = this.getBlock(block.parent)
|
||||||
const preBlock = this.findPreBlockInLocation(block)
|
const preBlock = this.findPreBlockInLocation(block)
|
||||||
const { left } = selection.getCaretOffsets(paragraph)
|
const { left, right } = selection.getCaretOffsets(paragraph)
|
||||||
const inlineDegrade = this.checkBackspaceCase()
|
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 tableHasContent = table => {
|
||||||
const tHead = table.children[0]
|
const tHead = table.children[0]
|
||||||
const tBody = table.children[1]
|
const tBody = table.children[1]
|
||||||
|
@ -1,10 +1,30 @@
|
|||||||
import selection from '../selection'
|
import selection from '../selection'
|
||||||
|
import { isMuyaEditorElement } from '../selection/dom'
|
||||||
import { HAS_TEXT_BLOCK_REG } from '../config'
|
import { HAS_TEXT_BLOCK_REG } from '../config'
|
||||||
|
|
||||||
const clickCtrl = ContentState => {
|
const clickCtrl = ContentState => {
|
||||||
ContentState.prototype.clickHandler = function (event) {
|
ContentState.prototype.clickHandler = function (event) {
|
||||||
const { eventCenter } = this.muya
|
const { eventCenter } = this.muya
|
||||||
const { target } = event
|
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
|
// handle front menu click
|
||||||
const { start: oldStart, end: oldEnd } = this.cursor
|
const { start: oldStart, end: oldEnd } = this.cursor
|
||||||
if (oldStart && oldEnd) {
|
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()
|
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) {
|
ContentState.prototype.enterHandler = function (event) {
|
||||||
const { start, end } = selection.getCursorRange()
|
const { start, end } = selection.getCursorRange()
|
||||||
|
|
||||||
if (!start || !end) {
|
if (!start || !end) {
|
||||||
return event.preventDefault()
|
return event.preventDefault()
|
||||||
}
|
}
|
||||||
let block = this.getBlock(start.key)
|
let block = this.getBlock(start.key)
|
||||||
|
const { text } = block
|
||||||
const endBlock = this.getBlock(end.key)
|
const endBlock = this.getBlock(end.key)
|
||||||
let parent = this.getParent(block)
|
let parent = this.getParent(block)
|
||||||
|
|
||||||
@ -161,7 +190,6 @@ const enterCtrl = ContentState => {
|
|||||||
this.updateCodeLanguage(block, block.text.trim())
|
this.updateCodeLanguage(block, block.text.trim())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle select multiple blocks
|
// handle select multiple blocks
|
||||||
if (start.key !== end.key) {
|
if (start.key !== end.key) {
|
||||||
const key = start.key
|
const key = start.key
|
||||||
@ -320,7 +348,8 @@ const enterCtrl = ContentState => {
|
|||||||
block = parent
|
block = parent
|
||||||
parent = this.getParent(block)
|
parent = this.getParent(block)
|
||||||
}
|
}
|
||||||
const { left, right } = selection.getCaretOffsets(paragraph)
|
const left = start.offset
|
||||||
|
const right = text.length - left
|
||||||
const type = block.type
|
const type = block.type
|
||||||
let newBlock
|
let newBlock
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import selection from '../selection'
|
import selection from '../selection'
|
||||||
import { tokenizer, generator } from '../parser/'
|
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 getOffset = (offset, { range: { start, end }, type, tag, anchor, alt }) => {
|
||||||
const dis = offset - start
|
const dis = offset - start
|
||||||
@ -221,73 +222,6 @@ const formatCtrl = ContentState => {
|
|||||||
block.text = generator(tokens)
|
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) {
|
ContentState.prototype.format = function (type) {
|
||||||
const { start, end } = selection.getCursorRange()
|
const { start, end } = selection.getCursorRange()
|
||||||
if (!start || !end) {
|
if (!start || !end) {
|
||||||
@ -331,6 +265,23 @@ const formatCtrl = ContentState => {
|
|||||||
end.offset += end.delata
|
end.offset += end.delata
|
||||||
startBlock.text = generator(tokens)
|
startBlock.text = generator(tokens)
|
||||||
addFormat(type, startBlock, { start, end })
|
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.cursor = { start, end }
|
||||||
this.partialRender()
|
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 formatCtrl from './formatCtrl'
|
||||||
import searchCtrl from './searchCtrl'
|
import searchCtrl from './searchCtrl'
|
||||||
import containerCtrl from './containerCtrl'
|
import containerCtrl from './containerCtrl'
|
||||||
import imagePathCtrl from './imagePathCtrl'
|
|
||||||
import htmlBlockCtrl from './htmlBlock'
|
import htmlBlockCtrl from './htmlBlock'
|
||||||
import clickCtrl from './clickCtrl'
|
import clickCtrl from './clickCtrl'
|
||||||
import inputCtrl from './inputCtrl'
|
import inputCtrl from './inputCtrl'
|
||||||
import tocCtrl from './tocCtrl'
|
import tocCtrl from './tocCtrl'
|
||||||
import emojiCtrl from './emojiCtrl'
|
import emojiCtrl from './emojiCtrl'
|
||||||
|
import imageCtrl from './imageCtrl'
|
||||||
|
import dragDropCtrl from './dragDropCtrl'
|
||||||
import importMarkdown from '../utils/importMarkdown'
|
import importMarkdown from '../utils/importMarkdown'
|
||||||
import Cursor from '../selection/cursor'
|
import Cursor from '../selection/cursor'
|
||||||
|
|
||||||
@ -43,12 +44,13 @@ const prototypes = [
|
|||||||
formatCtrl,
|
formatCtrl,
|
||||||
searchCtrl,
|
searchCtrl,
|
||||||
containerCtrl,
|
containerCtrl,
|
||||||
imagePathCtrl,
|
|
||||||
htmlBlockCtrl,
|
htmlBlockCtrl,
|
||||||
clickCtrl,
|
clickCtrl,
|
||||||
inputCtrl,
|
inputCtrl,
|
||||||
tocCtrl,
|
tocCtrl,
|
||||||
emojiCtrl,
|
emojiCtrl,
|
||||||
|
imageCtrl,
|
||||||
|
dragDropCtrl,
|
||||||
importMarkdown
|
importMarkdown
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -67,6 +69,8 @@ class ContentState {
|
|||||||
this.currentCursor = null
|
this.currentCursor = null
|
||||||
// you'll select the outmost block of current cursor when you click the front icon.
|
// you'll select the outmost block of current cursor when you click the front icon.
|
||||||
this.selectedBlock = null
|
this.selectedBlock = null
|
||||||
|
this._selectedImage = null
|
||||||
|
this.dropAnchor = null
|
||||||
this.prevCursor = null
|
this.prevCursor = null
|
||||||
this.historyTimer = null
|
this.historyTimer = null
|
||||||
this.history = new History(this)
|
this.history = new History(this)
|
||||||
@ -76,6 +80,22 @@ class ContentState {
|
|||||||
this.init()
|
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) {
|
set cursor (cursor) {
|
||||||
if (!(cursor instanceof Cursor)) {
|
if (!(cursor instanceof Cursor)) {
|
||||||
cursor = new Cursor(cursor)
|
cursor = new Cursor(cursor)
|
||||||
@ -149,20 +169,29 @@ class ContentState {
|
|||||||
this.renderRange = [ startOutMostBlock.preSibling, endOutMostBlock.nextSibling ]
|
this.renderRange = [ startOutMostBlock.preSibling, endOutMostBlock.nextSibling ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
postRender () {
|
||||||
|
// do nothing.
|
||||||
|
}
|
||||||
|
|
||||||
render (isRenderCursor = true) {
|
render (isRenderCursor = true) {
|
||||||
const { blocks, cursor, searchMatches: { matches, index }, selectedBlock } = this
|
const { blocks, searchMatches: { matches, index } } = this
|
||||||
const activeBlocks = this.getActiveBlocks()
|
const activeBlocks = this.getActiveBlocks()
|
||||||
matches.forEach((m, i) => {
|
matches.forEach((m, i) => {
|
||||||
m.active = i === index
|
m.active = i === index
|
||||||
})
|
})
|
||||||
this.setNextRenderRange()
|
this.setNextRenderRange()
|
||||||
this.stateRender.collectLabels(blocks)
|
this.stateRender.collectLabels(blocks)
|
||||||
this.stateRender.render(blocks, cursor, activeBlocks, matches, selectedBlock)
|
this.stateRender.render(blocks, activeBlocks, matches)
|
||||||
if (isRenderCursor) this.setCursor()
|
if (isRenderCursor) {
|
||||||
|
this.setCursor()
|
||||||
|
} else {
|
||||||
|
this.muya.blur()
|
||||||
|
}
|
||||||
|
this.postRender()
|
||||||
}
|
}
|
||||||
|
|
||||||
partialRender (isRenderCursor = true) {
|
partialRender (isRenderCursor = true) {
|
||||||
const { blocks, cursor, searchMatches: { matches, index }, selectedBlock } = this
|
const { blocks, searchMatches: { matches, index } } = this
|
||||||
const activeBlocks = this.getActiveBlocks()
|
const activeBlocks = this.getActiveBlocks()
|
||||||
const [ startKey, endKey ] = this.renderRange
|
const [ startKey, endKey ] = this.renderRange
|
||||||
matches.forEach((m, i) => {
|
matches.forEach((m, i) => {
|
||||||
@ -174,8 +203,30 @@ class ContentState {
|
|||||||
|
|
||||||
this.setNextRenderRange()
|
this.setNextRenderRange()
|
||||||
this.stateRender.collectLabels(blocks)
|
this.stateRender.collectLabels(blocks)
|
||||||
this.stateRender.partialRender(needRenderBlocks, cursor, activeBlocks, matches, startKey, endKey, selectedBlock)
|
this.stateRender.partialRender(needRenderBlocks, activeBlocks, matches, startKey, endKey)
|
||||||
if (isRenderCursor) this.setCursor()
|
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 LIST_REG = /ul|ol/
|
||||||
const LINE_BREAKS_REG = /\n/
|
const LINE_BREAKS_REG = /\n/
|
||||||
@ -77,11 +79,113 @@ const pasteCtrl = ContentState => {
|
|||||||
return tempWrapper.innerHTML
|
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
|
// handle `normal` and `pasteAsPlainText` paste
|
||||||
ContentState.prototype.pasteHandler = function (event, type) {
|
ContentState.prototype.pasteHandler = async function (event, type = 'normal', rawText, rawHtml) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const text = event.clipboardData.getData('text/plain')
|
event.stopPropagation()
|
||||||
let html = event.clipboardData.getData('text/html')
|
const text = rawText || event.clipboardData.getData('text/plain')
|
||||||
|
let html = rawHtml || event.clipboardData.getData('text/html')
|
||||||
html = this.standardizeHTML(html)
|
html = this.standardizeHTML(html)
|
||||||
const copyType = this.checkCopyType(html, text)
|
const copyType = this.checkCopyType(html, text)
|
||||||
const { start, end } = this.cursor
|
const { start, end } = this.cursor
|
||||||
@ -94,6 +198,11 @@ const pasteCtrl = ContentState => {
|
|||||||
return this.pasteHandler(event, type)
|
return this.pasteHandler(event, type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const file = await this.pasteImage(event)
|
||||||
|
if (file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const appendHtml = (text) => {
|
const appendHtml = (text) => {
|
||||||
startBlock.text = startBlock.text.substring(0, start.offset) + text + startBlock.text.substring(start.offset)
|
startBlock.text = startBlock.text.substring(0, start.offset) + text + startBlock.text.substring(start.offset)
|
||||||
const { key } = start
|
const { key } = start
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { operateClassName } from '../utils/domManipulate'
|
import { operateClassName } from '../utils/domManipulate'
|
||||||
|
import { getImageInfo } from '../utils/getImageInfo'
|
||||||
import { CLASS_OR_ID } from '../config'
|
import { CLASS_OR_ID } from '../config'
|
||||||
import selection from '../selection'
|
import selection from '../selection'
|
||||||
|
|
||||||
@ -43,6 +44,7 @@ class ClickEvent {
|
|||||||
const { target } = event
|
const { target } = event
|
||||||
// handler table click
|
// handler table click
|
||||||
const toolItem = getToolItem(target)
|
const toolItem = getToolItem(target)
|
||||||
|
contentState.selectedImage = null
|
||||||
if (toolItem) {
|
if (toolItem) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
event.stopPropagation()
|
event.stopPropagation()
|
||||||
@ -56,6 +58,9 @@ class ClickEvent {
|
|||||||
const markedImageText = target.previousElementSibling
|
const markedImageText = target.previousElementSibling
|
||||||
const mathRender = target.closest(`.${CLASS_OR_ID['AG_MATH_RENDER']}`)
|
const mathRender = target.closest(`.${CLASS_OR_ID['AG_MATH_RENDER']}`)
|
||||||
const rubyRender = target.closest(`.${CLASS_OR_ID['AG_RUBY_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 mathText = mathRender && mathRender.previousElementSibling
|
||||||
const rubyText = rubyRender && rubyRender.previousElementSibling
|
const rubyText = rubyRender && rubyRender.previousElementSibling
|
||||||
if (markedImageText && markedImageText.classList.contains(CLASS_OR_ID['AG_IMAGE_MARKED_TEXT'])) {
|
if (markedImageText && markedImageText.classList.contains(CLASS_OR_ID['AG_IMAGE_MARKED_TEXT'])) {
|
||||||
@ -70,6 +75,51 @@ class ClickEvent {
|
|||||||
} else if (rubyText) {
|
} else if (rubyText) {
|
||||||
selectionText(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')) {
|
if (target.closest('div.ag-container-preview') || target.closest('div.ag-html-preview')) {
|
||||||
return event.stopPropagation()
|
return event.stopPropagation()
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,9 @@ class Clipboard {
|
|||||||
|
|
||||||
listen () {
|
listen () {
|
||||||
const { container, eventCenter, contentState } = this.muya
|
const { container, eventCenter, contentState } = this.muya
|
||||||
|
const docPasteHandler = event => {
|
||||||
|
contentState.docPasteHandler(event)
|
||||||
|
}
|
||||||
const copyCutHandler = event => {
|
const copyCutHandler = event => {
|
||||||
contentState.copyHandler(event, this._copyType)
|
contentState.copyHandler(event, this._copyType)
|
||||||
if (event.type === 'cut') {
|
if (event.type === 'cut') {
|
||||||
@ -22,6 +25,7 @@ class Clipboard {
|
|||||||
this._pasteType = 'normal'
|
this._pasteType = 'normal'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventCenter.attachDOMEvent(document, 'paste', docPasteHandler)
|
||||||
eventCenter.attachDOMEvent(container, 'paste', pasteHandler)
|
eventCenter.attachDOMEvent(container, 'paste', pasteHandler)
|
||||||
eventCenter.attachDOMEvent(container, 'cut', copyCutHandler)
|
eventCenter.attachDOMEvent(container, 'cut', copyCutHandler)
|
||||||
eventCenter.attachDOMEvent(container, 'copy', 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 { EVENT_KEYS } from '../config'
|
||||||
import selection from '../selection'
|
import selection from '../selection'
|
||||||
import { findNearestParagraph } from '../selection/dom'
|
import { findNearestParagraph } from '../selection/dom'
|
||||||
import { getParagraphReference } from '../utils'
|
import { getParagraphReference, getImageInfo } from '../utils'
|
||||||
import { checkEditEmoji } from '../ui/emojis'
|
import { checkEditEmoji } from '../ui/emojis'
|
||||||
|
|
||||||
class Keyboard {
|
class Keyboard {
|
||||||
@ -90,6 +90,31 @@ class Keyboard {
|
|||||||
|
|
||||||
keydownBinding () {
|
keydownBinding () {
|
||||||
const { container, eventCenter, contentState } = this.muya
|
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 => {
|
const handler = event => {
|
||||||
if (event.metaKey || event.ctrlKey) {
|
if (event.metaKey || event.ctrlKey) {
|
||||||
@ -145,6 +170,7 @@ class Keyboard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
eventCenter.attachDOMEvent(container, 'keydown', handler)
|
eventCenter.attachDOMEvent(container, 'keydown', handler)
|
||||||
|
eventCenter.attachDOMEvent(document, 'keydown', docHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputBinding () {
|
inputBinding () {
|
||||||
@ -180,7 +206,7 @@ class Keyboard {
|
|||||||
const node = selection.getSelectionStart()
|
const node = selection.getSelectionStart()
|
||||||
const paragraph = findNearestParagraph(node)
|
const paragraph = findNearestParagraph(node)
|
||||||
const emojiNode = checkEditEmoji(node)
|
const emojiNode = checkEditEmoji(node)
|
||||||
|
contentState.selectedImage = null
|
||||||
if (
|
if (
|
||||||
paragraph &&
|
paragraph &&
|
||||||
emojiNode &&
|
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)
|
const block = contentState.getBlock(anchor.key)
|
||||||
if (anchor.key === focus.key && anchor.offset !== focus.offset && block.functionType !== 'codeLine') {
|
if (anchor.key === focus.key && anchor.offset !== focus.offset && block.functionType !== 'codeLine') {
|
||||||
const reference = contentState.getPositionReference()
|
const reference = contentState.getPositionReference()
|
||||||
|
@ -2,6 +2,7 @@ import ContentState from './contentState'
|
|||||||
import EventCenter from './eventHandler/event'
|
import EventCenter from './eventHandler/event'
|
||||||
import Clipboard from './eventHandler/clipboard'
|
import Clipboard from './eventHandler/clipboard'
|
||||||
import Keyboard from './eventHandler/keyboard'
|
import Keyboard from './eventHandler/keyboard'
|
||||||
|
import DragDrop from './eventHandler/dragDrop'
|
||||||
import ClickEvent from './eventHandler/clickEvent'
|
import ClickEvent from './eventHandler/clickEvent'
|
||||||
import { CLASS_OR_ID, MUYA_DEFAULT_OPTION } from './config'
|
import { CLASS_OR_ID, MUYA_DEFAULT_OPTION } from './config'
|
||||||
import { wordCount } from './utils'
|
import { wordCount } from './utils'
|
||||||
@ -33,6 +34,7 @@ class Muya {
|
|||||||
this.clipboard = new Clipboard(this)
|
this.clipboard = new Clipboard(this)
|
||||||
this.clickEvent = new ClickEvent(this)
|
this.clickEvent = new ClickEvent(this)
|
||||||
this.keyboard = new Keyboard(this)
|
this.keyboard = new Keyboard(this)
|
||||||
|
this.dragdrop = new DragDrop(this)
|
||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,7 +42,6 @@ class Muya {
|
|||||||
const { container, contentState, eventCenter } = this
|
const { container, contentState, eventCenter } = this
|
||||||
contentState.stateRender.setContainer(container.children[0])
|
contentState.stateRender.setContainer(container.children[0])
|
||||||
eventCenter.subscribe('stateChange', this.dispatchChange)
|
eventCenter.subscribe('stateChange', this.dispatchChange)
|
||||||
contentState.listenForPathChange()
|
|
||||||
const { markdown } = this
|
const { markdown } = this
|
||||||
const { focusMode } = this.options
|
const { focusMode } = this.options
|
||||||
this.setMarkdown(markdown)
|
this.setMarkdown(markdown)
|
||||||
@ -232,20 +233,12 @@ class Muya {
|
|||||||
this.container.blur()
|
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) {
|
format (type) {
|
||||||
this.contentState.format(type)
|
this.contentState.format(type)
|
||||||
}
|
}
|
||||||
|
|
||||||
insertImage (url) {
|
insertImage (imageInfo) {
|
||||||
this.contentState.insertImage(url)
|
this.contentState.insertImage(imageInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
search (value, opt) {
|
search (value, opt) {
|
||||||
@ -288,8 +281,14 @@ class Muya {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectAll () {
|
selectAll () {
|
||||||
|
if (this.hasFocus()) {
|
||||||
this.contentState.selectAll()
|
this.contentState.selectAll()
|
||||||
}
|
}
|
||||||
|
const activeElement = document.activeElement
|
||||||
|
if (activeElement.nodeName === 'INPUT') {
|
||||||
|
activeElement.select()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
copyAsMarkdown () {
|
copyAsMarkdown () {
|
||||||
this.clipboard.copyAsMarkdown()
|
this.clipboard.copyAsMarkdown()
|
||||||
|
@ -197,9 +197,7 @@ const tokenizerFac = (src, beginRules, inlineRules, pos = 0, top, labels) => {
|
|||||||
start: pos,
|
start: pos,
|
||||||
end: pos + imageTo[0].length
|
end: pos + imageTo[0].length
|
||||||
},
|
},
|
||||||
// An image description has inline elements as its contents.
|
alt: imageTo[2],
|
||||||
// When an image is rendered to HTML, this is standardly used as the image’s alt attribute.
|
|
||||||
alt: imageTo[2].replace(/[`*{}[\]()#+\-.!_>~:|<>$]/g, ''),
|
|
||||||
backlash: {
|
backlash: {
|
||||||
first: imageTo[3],
|
first: imageTo[3],
|
||||||
second: imageTo[5]
|
second: imageTo[5]
|
||||||
|
@ -20,6 +20,7 @@ class StateRender {
|
|||||||
this.diagramCache = new Map()
|
this.diagramCache = new Map()
|
||||||
this.tokenCache = new Map()
|
this.tokenCache = new Map()
|
||||||
this.labels = new Map()
|
this.labels = new Map()
|
||||||
|
this.urlMap = new Map()
|
||||||
this.container = null
|
this.container = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +78,8 @@ class StateRender {
|
|||||||
return active ? CLASS_OR_ID['AG_HIGHLIGHT'] : CLASS_OR_ID['AG_SELECTION']
|
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 type = block.type === 'hr' ? 'p' : block.type
|
||||||
const isActive = activeBlocks.some(b => b.key === block.key) || block.key === cursor.start.key
|
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 selector = `div#${CLASS_OR_ID['AG_EDITOR_ID']}`
|
||||||
|
|
||||||
const children = blocks.map(block => {
|
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)
|
const newVdom = h(selector, children)
|
||||||
@ -160,11 +161,11 @@ class StateRender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only render the blocks which you updated
|
// 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]
|
const cursorOutMostBlock = activeBlocks[activeBlocks.length - 1]
|
||||||
// If cursor is not in render blocks, need to render cursor block independently
|
// If cursor is not in render blocks, need to render cursor block independently
|
||||||
const needRenderCursorBlock = blocks.indexOf(cursorOutMostBlock) === -1
|
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 html = toHTML(newVnode).replace(/^<section>([\s\S]+?)<\/section>$/, '$1')
|
||||||
|
|
||||||
const needToRemoved = []
|
const needToRemoved = []
|
||||||
@ -193,7 +194,7 @@ class StateRender {
|
|||||||
const cursorDom = document.querySelector(`#${key}`)
|
const cursorDom = document.querySelector(`#${key}`)
|
||||||
if (cursorDom) {
|
if (cursorDom) {
|
||||||
const oldCursorVnode = toVNode(cursorDom)
|
const oldCursorVnode = toVNode(cursorDom)
|
||||||
const newCursorVnode = this.renderBlock(cursorOutMostBlock, cursor, activeBlocks, selectedBlock, matches)
|
const newCursorVnode = this.renderBlock(cursorOutMostBlock, activeBlocks, matches)
|
||||||
patch(oldCursorVnode, newCursorVnode)
|
patch(oldCursorVnode, newCursorVnode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -202,6 +203,24 @@ class StateRender {
|
|||||||
this.renderDiagram()
|
this.renderDiagram()
|
||||||
this.codeCache.clear()
|
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)
|
mixins(StateRender, renderInlines, renderBlock)
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* [renderBlock render one block, no matter it is a container block or text block]
|
* [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
|
const method = Array.isArray(block.children) && block.children.length > 0
|
||||||
? 'renderContainerBlock'
|
? 'renderContainerBlock'
|
||||||
: 'renderLeafBlock'
|
: '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']}`
|
'vega-lite': `.${CLASS_OR_ID['AG_VEGA_LITE']}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function renderContainerBlock (block, cursor, activeBlocks, selectedBlock, matches, useCache = false) {
|
export default function renderContainerBlock (block, activeBlocks, matches, useCache = false) {
|
||||||
let selector = this.getSelector(block, cursor, activeBlocks, selectedBlock)
|
let selector = this.getSelector(block, activeBlocks)
|
||||||
const {
|
const {
|
||||||
type,
|
type,
|
||||||
headingStyle,
|
headingStyle,
|
||||||
@ -28,7 +28,7 @@ export default function renderContainerBlock (block, cursor, activeBlocks, selec
|
|||||||
isLooseListItem,
|
isLooseListItem,
|
||||||
lang
|
lang
|
||||||
} = block
|
} = 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 = {
|
const data = {
|
||||||
attrs: {},
|
attrs: {},
|
||||||
dataset: {}
|
dataset: {}
|
||||||
|
@ -49,9 +49,10 @@ const hasReferenceToken = tokens => {
|
|||||||
return result
|
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
|
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
|
// highlight search key in block
|
||||||
const highlights = matches.filter(m => m.key === block.key)
|
const highlights = matches.filter(m => m.key === block.key)
|
||||||
const {
|
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 { 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!
|
// I dont want operate dom directly, is there any better method? need help!
|
||||||
export default function image (h, cursor, block, token, outerClass) {
|
export default function image (h, cursor, block, token, outerClass) {
|
||||||
const { eventCenter } = this
|
const imageInfo = getImageInfo(token.src + encodeURI(token.backlash.second))
|
||||||
const { start: cursorStart, end: cursorEnd } = cursor
|
const { selectedImage } = this.muya.contentState
|
||||||
const { start, end } = token.range
|
const data = {
|
||||||
|
dataset: {
|
||||||
if (
|
raw: token.raw
|
||||||
cursorStart.key === cursorEnd.key &&
|
},
|
||||||
cursorStart.offset === cursorEnd.offset &&
|
attrs: {
|
||||||
cursorStart.offset === end - 1 &&
|
contenteditable: 'true'
|
||||||
!IMAGE_EXT_REG.test(token.src) &&
|
}
|
||||||
isInElectron
|
|
||||||
) {
|
|
||||||
eventCenter.dispatch('image-path', token.src)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 id
|
||||||
let isSuccess
|
let isSuccess
|
||||||
let selector
|
let { src } = imageInfo
|
||||||
const imageInfo = getImageInfo(token.src + encodeURI(token.backlash.second))
|
|
||||||
const { src } = imageInfo
|
|
||||||
const alt = token.alt + encodeURI(token.backlash.first)
|
const alt = token.alt + encodeURI(token.backlash.first)
|
||||||
const { title } = token
|
const { title } = token
|
||||||
|
|
||||||
if (src) {
|
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']}`
|
const imageIcons = [
|
||||||
|
renderIcon(h, 'ag-image-icon-success', ImageIcon),
|
||||||
if (isSuccess) {
|
renderIcon(h, 'ag-image-icon-fail', ImageFailIcon),
|
||||||
if (className === CLASS_OR_ID['AG_HIDE']) {
|
renderIcon(h, 'ag-image-icon-close', DeleteIcon)
|
||||||
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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
return isSuccess
|
||||||
? [
|
? [
|
||||||
h(selector, children),
|
h(wrapperSelector, data, [
|
||||||
h('img', { props: { alt, src, title } })
|
...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()
|
id = getUniqueId()
|
||||||
loadImage(src, isUnknownType)
|
loadImage(src, isUnknownType)
|
||||||
.then(url => {
|
.then(url => {
|
||||||
const imageWrapper = document.querySelector(`#${id}`)
|
const imageText = document.querySelector(`#${id}`)
|
||||||
const img = document.createElement('img')
|
const img = document.createElement('img')
|
||||||
img.src = url
|
img.src = url
|
||||||
if (alt) img.alt = alt
|
if (alt) img.alt = alt.replace(/[`*{}[\]()#+\-.!_>~:|<>$]/g, '')
|
||||||
if (imageClass) {
|
if (imageClass) {
|
||||||
img.classList.add(imageClass)
|
img.classList.add(imageClass)
|
||||||
}
|
}
|
||||||
if (imageWrapper) {
|
|
||||||
insertAfter(img, imageWrapper)
|
if (imageText) {
|
||||||
operateClassName(imageWrapper, 'add', className)
|
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, {
|
this.loadImageMap.set(src, {
|
||||||
id,
|
id,
|
||||||
@ -28,9 +43,17 @@ export default function loadImageAsync (imageInfo, alt, className, imageClass) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
const imageWrapper = document.querySelector(`#${id}`)
|
const imageText = document.querySelector(`#${id}`)
|
||||||
if (imageWrapper) {
|
if (imageText) {
|
||||||
operateClassName(imageWrapper, 'add', CLASS_OR_ID['AG_IMAGE_FAIL'])
|
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, {
|
this.loadImageMap.set(src, {
|
||||||
id,
|
id,
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
// render token of text type to vdom.
|
// render token of text type to vdom.
|
||||||
export default function text (h, cursor, block, token) {
|
export default function text (h, cursor, block, token) {
|
||||||
const { start, end } = token.range
|
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_link': /^\[([^\]]+?)(\\*)\](?:\[([^\]]*?)(\\*)\])?/,
|
||||||
'reference_image': /^\!\[([^\]]+?)(\\*)\](?:\[([^\]]*?)(\\*)\])?/,
|
'reference_image': /^\!\[([^\]]+?)(\\*)\](?:\[([^\]]*?)(\\*)\])?/,
|
||||||
'tail_header': /^(\s{1,}#{1,})(\s*)$/,
|
'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'),
|
'html_escape': new RegExp(`^(${escapeCharacters.join('|')})`, 'i'),
|
||||||
'soft_line_break': /^(\n)(?!\n)/,
|
'soft_line_break': /^(\n)(?!\n)/,
|
||||||
'hard_line_break': /^( {2,})(\n)(?!\n)/,
|
'hard_line_break': /^( {2,})(\n)(?!\n)/,
|
||||||
|
@ -77,13 +77,24 @@ export const getAttributes = html => {
|
|||||||
|
|
||||||
export const parseSrcAndTitle = (text = '') => {
|
export const parseSrcAndTitle = (text = '') => {
|
||||||
const parts = text.split(/\s+/)
|
const parts = text.split(/\s+/)
|
||||||
const src = parts[0]
|
if (parts.length === 1) {
|
||||||
const rawTitle = text.substring(src.length).trim()
|
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.
|
const TITLE_REG = /^('|")(.*?)\1$/ // we only support use `'` and `"` to indicate a title now.
|
||||||
let title = ''
|
let title = ''
|
||||||
if (rawTitle && TITLE_REG.test(rawTitle)) {
|
if (rawTitle && TITLE_REG.test(rawTitle)) {
|
||||||
title = rawTitle.replace(TITLE_REG, '$2')
|
title = rawTitle.replace(TITLE_REG, '$2')
|
||||||
}
|
}
|
||||||
|
if (title) {
|
||||||
|
src = text.substring(0, text.length - rawTitle.length).trim()
|
||||||
|
} else {
|
||||||
|
src = text.trim()
|
||||||
|
}
|
||||||
return { src, title }
|
return { src, title }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,23 @@ export const getTextContent = (node, blackList) => {
|
|||||||
}
|
}
|
||||||
if (node.nodeType === 3) {
|
if (node.nodeType === 3) {
|
||||||
text += node.textContent
|
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 {
|
} else {
|
||||||
const childNodes = node.childNodes
|
const childNodes = node.childNodes
|
||||||
for (const n of childNodes) {
|
for (const n of childNodes) {
|
||||||
@ -25,6 +42,23 @@ export const getTextContent = (node, blackList) => {
|
|||||||
return text
|
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 => {
|
export const findNearestParagraph = node => {
|
||||||
if (!node) {
|
if (!node) {
|
||||||
return null
|
return null
|
||||||
@ -39,7 +73,7 @@ export const findNearestParagraph = node => {
|
|||||||
export const findOutMostParagraph = node => {
|
export const findOutMostParagraph = node => {
|
||||||
do {
|
do {
|
||||||
let parentNode = node.parentNode
|
let parentNode = node.parentNode
|
||||||
if (isAganippeEditorElement(parentNode) && isAganippeParagraph(node)) return node
|
if (isMuyaEditorElement(parentNode) && isAganippeParagraph(node)) return node
|
||||||
node = parentNode
|
node = parentNode
|
||||||
} while (node)
|
} while (node)
|
||||||
}
|
}
|
||||||
@ -53,7 +87,7 @@ export const isBlockContainer = element => {
|
|||||||
blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1
|
blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isAganippeEditorElement = element => {
|
export const isMuyaEditorElement = element => {
|
||||||
return element && element.id === CLASS_OR_ID['AG_EDITOR_ID']
|
return element && element.id === CLASS_OR_ID['AG_EDITOR_ID']
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,7 +102,7 @@ export const traverseUp = (current, testElementFunction) => {
|
|||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
// do not traverse upwards past the nearest containing editor
|
// do not traverse upwards past the nearest containing editor
|
||||||
if (isAganippeEditorElement(current)) {
|
if (isMuyaEditorElement(current)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,7 +134,7 @@ export const getFirstSelectableLeafNode = element => {
|
|||||||
|
|
||||||
export const getClosestBlockContainer = node => {
|
export const getClosestBlockContainer = node => {
|
||||||
return traverseUp(node, node => {
|
return traverseUp(node, node => {
|
||||||
return isBlockContainer(node) || isAganippeEditorElement(node)
|
return isBlockContainer(node) || isMuyaEditorElement(node)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,8 @@ import {
|
|||||||
getClosestBlockContainer,
|
getClosestBlockContainer,
|
||||||
getCursorPositionWithinMarkedText,
|
getCursorPositionWithinMarkedText,
|
||||||
findNearestParagraph,
|
findNearestParagraph,
|
||||||
getTextContent
|
getTextContent,
|
||||||
|
getOffsetOfParagraph
|
||||||
} from './dom'
|
} from './dom'
|
||||||
|
|
||||||
const filterOnlyParentElements = node => {
|
const filterOnlyParentElements = node => {
|
||||||
@ -424,11 +425,48 @@ class Selection {
|
|||||||
let count = 0
|
let count = 0
|
||||||
for (i = 0; i < len; i++) {
|
for (i = 0; i < len; i++) {
|
||||||
const child = childNodes[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 (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) {
|
if (count + textLength >= offset) {
|
||||||
return getNodeAndOffset(child, offset - count)
|
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 {
|
} else {
|
||||||
count += getTextContent(child, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
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 += textLength
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { node, offset }
|
return { node, offset }
|
||||||
@ -436,10 +474,12 @@ class Selection {
|
|||||||
|
|
||||||
let { node: anchorNode, offset: anchorOffset } = getNodeAndOffset(anchorParagraph, anchor.offset)
|
let { node: anchorNode, offset: anchorOffset } = getNodeAndOffset(anchorParagraph, anchor.offset)
|
||||||
let { node: focusNode, offset: focusOffset } = getNodeAndOffset(focusParagraph, focus.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)
|
anchorOffset = Math.min(anchorOffset, anchorNode.textContent.length)
|
||||||
focusOffset = Math.min(focusOffset, focusNode.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)
|
this.select(anchorNode, anchorOffset)
|
||||||
// Secondly, set the focus node and focus offset.
|
// Secondly, set the focus node and focus offset.
|
||||||
this.setFocus(focusNode, focusOffset)
|
this.setFocus(focusNode, focusOffset)
|
||||||
@ -457,7 +497,6 @@ class Selection {
|
|||||||
|
|
||||||
getCursorRange () {
|
getCursorRange () {
|
||||||
let { anchorNode, anchorOffset, focusNode, focusOffset } = this.doc.getSelection()
|
let { anchorNode, anchorOffset, focusNode, focusOffset } = this.doc.getSelection()
|
||||||
|
|
||||||
const isAnchorValid = this.isValidCursorNode(anchorNode)
|
const isAnchorValid = this.isValidCursorNode(anchorNode)
|
||||||
const isFocusValid = this.isValidCursorNode(focusNode)
|
const isFocusValid = this.isValidCursorNode(focusNode)
|
||||||
let needFix = false
|
let needFix = false
|
||||||
@ -492,26 +531,44 @@ class Selection {
|
|||||||
|
|
||||||
const anchorParagraph = findNearestParagraph(anchorNode)
|
const anchorParagraph = findNearestParagraph(anchorNode)
|
||||||
const focusParagraph = findNearestParagraph(focusNode)
|
const focusParagraph = findNearestParagraph(focusNode)
|
||||||
|
let aOffset = getOffsetOfParagraph(anchorNode, anchorParagraph) + anchorOffset
|
||||||
const getOffsetOfParagraph = (node, paragraph) => {
|
let fOffset = getOffsetOfParagraph(focusNode, focusParagraph) + focusOffset
|
||||||
let offset = 0
|
// fix input after image.
|
||||||
let preSibling = node
|
if (
|
||||||
|
anchorNode === focusNode &&
|
||||||
if (node === paragraph) return offset
|
anchorOffset === focusOffset &&
|
||||||
|
anchorNode.parentNode.classList.contains('ag-image-container') &&
|
||||||
do {
|
anchorNode.previousElementSibling &&
|
||||||
preSibling = preSibling.previousSibling
|
anchorNode.previousElementSibling.nodeName === 'IMG'
|
||||||
if (preSibling) {
|
) {
|
||||||
offset += getTextContent(preSibling, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
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
|
||||||
}
|
}
|
||||||
} while (preSibling)
|
aOffset += getTextContent(imageWrapper, [ CLASS_OR_ID['AG_MATH_RENDER'], CLASS_OR_ID['AG_RUBY_RENDER'] ]).length
|
||||||
return (node === paragraph || node.parentNode === paragraph)
|
fOffset = aOffset
|
||||||
? offset
|
|
||||||
: offset + getOffsetOfParagraph(node.parentNode, paragraph)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const aOffset = getOffsetOfParagraph(anchorNode, anchorParagraph) + anchorOffset
|
if (
|
||||||
const fOffset = getOffsetOfParagraph(focusNode, focusParagraph) + focusOffset
|
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 anchor = { key: anchorParagraph.id, offset: aOffset }
|
||||||
const focus = { key: focusParagraph.id, offset: fOffset }
|
const focus = { key: focusParagraph.id, offset: fOffset }
|
||||||
|
@ -8,7 +8,6 @@
|
|||||||
right: -1000px;
|
right: -1000px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: var(--floatShadow);
|
box-shadow: var(--floatShadow);
|
||||||
border: 1px solid var(--floatBorderColor);
|
|
||||||
background-color: var(--floatBgColor);
|
background-color: var(--floatBgColor);
|
||||||
transition: opacity .25s ease-in-out;
|
transition: opacity .25s ease-in-out;
|
||||||
transform-origin: top;
|
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 ImageIcon from '../../assets/icons/image.svg'
|
||||||
import UploadIcon from '../../assets/icons/upload.svg'
|
import UploadIcon from '../../assets/icons/upload.svg'
|
||||||
|
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
const iconhash = {
|
const iconhash = {
|
||||||
'icon-image': ImageIcon,
|
'icon-image': ImageIcon,
|
||||||
'icon-folder': FolderIcon,
|
'icon-folder': FolderIcon,
|
||||||
@ -18,6 +20,7 @@ class ImagePathPicker extends BaseScrollFloat {
|
|||||||
this.renderArray = []
|
this.renderArray = []
|
||||||
this.oldVnode = null
|
this.oldVnode = null
|
||||||
this.activeItem = null
|
this.activeItem = null
|
||||||
|
this.floatBox.classList.add('ag-image-picker-wrapper')
|
||||||
this.listen()
|
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
|
// 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 createDOMPurify from 'dompurify'
|
||||||
import { isInElectron, URL_REG } from '../config'
|
import { isInElectron, URL_REG } from '../config'
|
||||||
|
|
||||||
@ -135,7 +133,7 @@ export const deepCopy = object => {
|
|||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loadImage = async (url, detectContentType) => {
|
export const loadImage = async (url, detectContentType = false) => {
|
||||||
if (detectContentType) {
|
if (detectContentType) {
|
||||||
const isImage = await checkImageContentType(url)
|
const isImage = await checkImageContentType(url)
|
||||||
if (!isImage) throw new Error('not an image')
|
if (!isImage) throw new Error('not an image')
|
||||||
@ -152,18 +150,36 @@ export const loadImage = async (url, detectContentType) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const checkImageContentType = async url => {
|
export const checkImageContentType = url => {
|
||||||
try {
|
const req = new XMLHttpRequest()
|
||||||
const res = await axios.head(url)
|
let settle
|
||||||
const contentType = res.headers['content-type']
|
const promise = new Promise((resolve, reject) => {
|
||||||
if (res.status === 200 && /^image\/(?:jpeg|png|gif|svg\+xml|webp)$/.test(contentType)) {
|
settle = resolve
|
||||||
return true
|
})
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
return false
|
} else {
|
||||||
} catch (err) {
|
settle(false)
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const handleError = () => {
|
||||||
|
settle(false)
|
||||||
|
}
|
||||||
|
req.open('HEAD', url)
|
||||||
|
req.onreadystatechange = handler
|
||||||
|
req.onerror = handleError
|
||||||
|
req.send()
|
||||||
|
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return image information and correct the relative image path if needed.
|
* Return image information and correct the relative image path if needed.
|
||||||
@ -302,3 +318,9 @@ export const getParagraphReference = (ele, id) => {
|
|||||||
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;
|
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 {
|
body.dark .ag-image-marked-text::before {
|
||||||
background: url(../lib/assets/icons/image_dark.png);
|
background: url(../lib/assets/icons/image_dark.png);
|
||||||
background-size: cover;
|
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;
|
--iconColor: #333;
|
||||||
--codeBgColor: #d8d8d869;
|
--codeBgColor: #d8d8d869;
|
||||||
--codeBlockBgColor: rgba(0, 0, 0, 0.03);
|
--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*/
|
/*marktext*/
|
||||||
--sideBarColor: rgba(0, 0, 0, .6);
|
--sideBarColor: rgba(0, 0, 0, .6);
|
||||||
@ -110,6 +116,23 @@ body {
|
|||||||
background-color: var(--floatBgColor);
|
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 {
|
.ag-dialog-table .dialog-title svg {
|
||||||
width: 1.5em;
|
width: 1.5em;
|
||||||
height: 1.5em;
|
height: 1.5em;
|
||||||
|
@ -24,6 +24,12 @@
|
|||||||
--iconColor: rgba(255, 255, 255, .8);
|
--iconColor: rgba(255, 255, 255, .8);
|
||||||
--codeBgColor: #424344;
|
--codeBgColor: #424344;
|
||||||
--codeBlockBgColor: #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*/
|
/*marktext*/
|
||||||
--sideBarColor: rgba(255, 255, 255, .6);
|
--sideBarColor: rgba(255, 255, 255, .6);
|
||||||
|
@ -23,6 +23,12 @@
|
|||||||
--iconColor: rgba(135, 135, 135, .8);
|
--iconColor: rgba(135, 135, 135, .8);
|
||||||
--codeBgColor: #d8d8d869;
|
--codeBgColor: #d8d8d869;
|
||||||
--codeBlockBgColor: rgba(104, 134, 170, .04);
|
--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);
|
--sideBarColor: rgba(188, 193, 197, .8);
|
||||||
--sideBarTitleColor: rgba(255, 255, 255, 1);
|
--sideBarTitleColor: rgba(255, 255, 255, 1);
|
||||||
|
@ -24,6 +24,12 @@
|
|||||||
--iconColor: rgba(255, 255, 255, .8);
|
--iconColor: rgba(255, 255, 255, .8);
|
||||||
--codeBgColor: #d8d8d869;
|
--codeBgColor: #d8d8d869;
|
||||||
--codeBlockBgColor: #3f454c;
|
--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*/
|
/*marktext*/
|
||||||
--sideBarColor: rgba(255, 255, 255, .6);
|
--sideBarColor: rgba(255, 255, 255, .6);
|
||||||
|
@ -24,6 +24,12 @@
|
|||||||
--iconColor: rgba(255, 255, 255, .8);
|
--iconColor: rgba(255, 255, 255, .8);
|
||||||
--codeBgColor: #3a3f4b;
|
--codeBgColor: #3a3f4b;
|
||||||
--codeBlockBgColor: #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*/
|
/*marktext*/
|
||||||
--sideBarColor: #9da5b4;
|
--sideBarColor: #9da5b4;
|
||||||
@ -37,7 +43,7 @@
|
|||||||
--floatBorderColor: #181a1f;
|
--floatBorderColor: #181a1f;
|
||||||
--floatShadow: rgba(0, 0, 0, 0.3);
|
--floatShadow: rgba(0, 0, 0, 0.3);
|
||||||
--maskColor: rgba(0, 0, 0, .7);
|
--maskColor: rgba(0, 0, 0, .7);
|
||||||
--editorAreaWidth: 700px;
|
--editorAreaWidth: 750px;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
@ -23,6 +23,12 @@
|
|||||||
--iconColor: rgba(101, 101, 101, .8);
|
--iconColor: rgba(101, 101, 101, .8);
|
||||||
--codeBgColor: #d8d8d869;
|
--codeBgColor: #d8d8d869;
|
||||||
--codeBlockBgColor: rgba(12, 139, 186, .04);
|
--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);
|
--sideBarColor: rgba(101, 101, 101, .6);
|
||||||
--sideBarTitleColor: rgba(101, 101, 101, 1);
|
--sideBarTitleColor: rgba(101, 101, 101, 1);
|
||||||
|
@ -81,15 +81,19 @@
|
|||||||
import CodePicker from 'muya/lib/ui/codePicker'
|
import CodePicker from 'muya/lib/ui/codePicker'
|
||||||
import EmojiPicker from 'muya/lib/ui/emojiPicker'
|
import EmojiPicker from 'muya/lib/ui/emojiPicker'
|
||||||
import ImagePathPicker from 'muya/lib/ui/imagePicker'
|
import ImagePathPicker from 'muya/lib/ui/imagePicker'
|
||||||
|
import ImageSelector from 'muya/lib/ui/imageSelector'
|
||||||
import FormatPicker from 'muya/lib/ui/formatPicker'
|
import FormatPicker from 'muya/lib/ui/formatPicker'
|
||||||
import FrontMenu from 'muya/lib/ui/frontMenu'
|
import FrontMenu from 'muya/lib/ui/frontMenu'
|
||||||
import bus from '../../bus'
|
import bus from '../../bus'
|
||||||
import Search from '../search.vue'
|
import Search from '../search.vue'
|
||||||
import { animatedScrollTo } from '../../util'
|
import { animatedScrollTo } from '../../util'
|
||||||
import { addCommonStyle } from '../../util/theme'
|
import { addCommonStyle } from '../../util/theme'
|
||||||
|
import { guessClipboardFilePath } from '../../util/guessClipBoardFilePath'
|
||||||
import { showContextMenu } from '../../contextMenu/editor'
|
import { showContextMenu } from '../../contextMenu/editor'
|
||||||
import Printer from '@/services/printService'
|
import Printer from '@/services/printService'
|
||||||
import { DEFAULT_EDITOR_FONT_FAMILY } from '@/config'
|
import { DEFAULT_EDITOR_FONT_FAMILY } from '@/config'
|
||||||
|
import { moveImageToFolder, uploadImage } from '@/util/fileSystem'
|
||||||
|
import notice from '@/services/notification'
|
||||||
|
|
||||||
import 'muya/themes/default.css'
|
import 'muya/themes/default.css'
|
||||||
import '@/assets/themes/codemirror/one-dark.css'
|
import '@/assets/themes/codemirror/one-dark.css'
|
||||||
@ -116,6 +120,7 @@
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
|
'preferences': state => state.preferences,
|
||||||
'preferLooseListItem': state => state.preferences.preferLooseListItem,
|
'preferLooseListItem': state => state.preferences.preferLooseListItem,
|
||||||
'autoPairBracket': state => state.preferences.autoPairBracket,
|
'autoPairBracket': state => state.preferences.autoPairBracket,
|
||||||
'autoPairMarkdownSyntax': state => state.preferences.autoPairMarkdownSyntax,
|
'autoPairMarkdownSyntax': state => state.preferences.autoPairMarkdownSyntax,
|
||||||
@ -132,7 +137,11 @@
|
|||||||
'darkColor': state => state.preferences.darkColor,
|
'darkColor': state => state.preferences.darkColor,
|
||||||
'editorFontFamily': state => state.preferences.editorFontFamily,
|
'editorFontFamily': state => state.preferences.editorFontFamily,
|
||||||
'hideQuickInsertHint': state => state.preferences.hideQuickInsertHint,
|
'hideQuickInsertHint': state => state.preferences.hideQuickInsertHint,
|
||||||
|
'imageInsertAction': state => state.preferences.imageInsertAction,
|
||||||
|
'imageFolderPath': state => state.preferences.imageFolderPath,
|
||||||
'theme': state => state.preferences.theme,
|
'theme': state => state.preferences.theme,
|
||||||
|
|
||||||
|
'currentFile': state => state.editor.currentFile,
|
||||||
// edit modes
|
// edit modes
|
||||||
'typewriter': state => state.preferences.typewriter,
|
'typewriter': state => state.preferences.typewriter,
|
||||||
'focus': state => state.preferences.focus,
|
'focus': state => state.preferences.focus,
|
||||||
@ -291,6 +300,7 @@
|
|||||||
Muya.use(CodePicker)
|
Muya.use(CodePicker)
|
||||||
Muya.use(EmojiPicker)
|
Muya.use(EmojiPicker)
|
||||||
Muya.use(ImagePathPicker)
|
Muya.use(ImagePathPicker)
|
||||||
|
Muya.use(ImageSelector)
|
||||||
Muya.use(FormatPicker)
|
Muya.use(FormatPicker)
|
||||||
Muya.use(FrontMenu)
|
Muya.use(FrontMenu)
|
||||||
|
|
||||||
@ -305,7 +315,11 @@
|
|||||||
orderListDelimiter,
|
orderListDelimiter,
|
||||||
tabSize,
|
tabSize,
|
||||||
listIndentation,
|
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)) {
|
if (/dark/i.test(theme)) {
|
||||||
Object.assign(options, {
|
Object.assign(options, {
|
||||||
@ -341,7 +355,6 @@
|
|||||||
bus.$on('image-uploaded', this.handleUploadedImage)
|
bus.$on('image-uploaded', this.handleUploadedImage)
|
||||||
bus.$on('file-changed', this.handleMarkdownChange)
|
bus.$on('file-changed', this.handleMarkdownChange)
|
||||||
bus.$on('editor-blur', this.blurEditor)
|
bus.$on('editor-blur', this.blurEditor)
|
||||||
bus.$on('image-auto-path', this.handleImagePath)
|
|
||||||
bus.$on('copyAsMarkdown', this.handleCopyPaste)
|
bus.$on('copyAsMarkdown', this.handleCopyPaste)
|
||||||
bus.$on('copyAsHtml', this.handleCopyPaste)
|
bus.$on('copyAsHtml', this.handleCopyPaste)
|
||||||
bus.$on('pasteAsPlainText', this.handleCopyPaste)
|
bus.$on('pasteAsPlainText', this.handleCopyPaste)
|
||||||
@ -353,19 +366,7 @@
|
|||||||
bus.$on('scroll-to-header', this.scrollToHeader)
|
bus.$on('scroll-to-header', this.scrollToHeader)
|
||||||
bus.$on('copy-block', this.handleCopyBlock)
|
bus.$on('copy-block', this.handleCopyBlock)
|
||||||
bus.$on('print', this.handlePrint)
|
bus.$on('print', this.handlePrint)
|
||||||
|
bus.$on('screenshot-captured', this.handleScreenShot)
|
||||||
// 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)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.editor.on('change', changes => {
|
this.editor.on('change', changes => {
|
||||||
// WORKAROUND: "id: 'muya'"
|
// 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 => {
|
this.editor.on('selectionChange', changes => {
|
||||||
const { y } = changes.cursorCoords
|
const { y } = changes.cursorCoords
|
||||||
if (this.typewriter) {
|
if (this.typewriter) {
|
||||||
@ -412,17 +426,52 @@
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
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) {
|
keyup (event) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
this.setImageViewerVisible(false)
|
this.setImageViewerVisible(false)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleImagePath (files) {
|
|
||||||
const { editor } = this
|
|
||||||
editor && editor.showAutoImagePath(files)
|
|
||||||
},
|
|
||||||
|
|
||||||
setImageViewerVisible (status) {
|
setImageViewerVisible (status) {
|
||||||
this.imageViewerVisible = status
|
this.imageViewerVisible = status
|
||||||
},
|
},
|
||||||
@ -452,9 +501,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleSelect (url) {
|
handleSelect (src) {
|
||||||
if (!this.sourceCode) {
|
if (!this.sourceCode) {
|
||||||
this.editor && this.editor.insertImage(url)
|
this.editor && this.editor.insertImage({ src })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -618,6 +667,12 @@
|
|||||||
|
|
||||||
handleCopyBlock (name) {
|
handleCopyBlock (name) {
|
||||||
this.editor.copy(name)
|
this.editor.copy(name)
|
||||||
|
},
|
||||||
|
|
||||||
|
handleScreenShot () {
|
||||||
|
if (this.editor) {
|
||||||
|
document.execCommand('paste')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
@ -636,7 +691,6 @@
|
|||||||
bus.$off('image-uploaded', this.handleUploadedImage)
|
bus.$off('image-uploaded', this.handleUploadedImage)
|
||||||
bus.$off('file-changed', this.handleMarkdownChange)
|
bus.$off('file-changed', this.handleMarkdownChange)
|
||||||
bus.$off('editor-blur', this.blurEditor)
|
bus.$off('editor-blur', this.blurEditor)
|
||||||
bus.$off('image-auto-path', this.handleImagePath)
|
|
||||||
bus.$off('copyAsMarkdown', this.handleCopyPaste)
|
bus.$off('copyAsMarkdown', this.handleCopyPaste)
|
||||||
bus.$off('copyAsHtml', this.handleCopyPaste)
|
bus.$off('copyAsHtml', this.handleCopyPaste)
|
||||||
bus.$off('pasteAsPlainText', this.handleCopyPaste)
|
bus.$off('pasteAsPlainText', this.handleCopyPaste)
|
||||||
@ -648,6 +702,7 @@
|
|||||||
bus.$off('scroll-to-header', this.scrollToHeader)
|
bus.$off('scroll-to-header', this.scrollToHeader)
|
||||||
bus.$off('copy-block', this.handleCopyBlock)
|
bus.$off('copy-block', this.handleCopyBlock)
|
||||||
bus.$off('print', this.handlePrint)
|
bus.$off('print', this.handlePrint)
|
||||||
|
bus.$off('screenshot-captured', this.handleScreenShot)
|
||||||
|
|
||||||
document.removeEventListener('keyup', this.keyup)
|
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'
|
import path from 'path'
|
||||||
|
import { isLinux } from './util'
|
||||||
export const isLinux = process.platform === 'linux'
|
|
||||||
|
|
||||||
export const PATH_SEPARATOR = path.sep
|
export const PATH_SEPARATOR = path.sep
|
||||||
|
|
||||||
export const THEME_STYLE_ID = 'ag-theme'
|
export const THEME_STYLE_ID = 'ag-theme'
|
||||||
|
@ -25,7 +25,11 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Select,
|
Select,
|
||||||
Option,
|
Option,
|
||||||
Radio
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Tabs,
|
||||||
|
TabPane,
|
||||||
|
Input
|
||||||
} from 'element-ui'
|
} from 'element-ui'
|
||||||
import services from './services'
|
import services from './services'
|
||||||
import routes from './router'
|
import routes from './router'
|
||||||
@ -72,6 +76,10 @@ Vue.use(Switch)
|
|||||||
Vue.use(Select)
|
Vue.use(Select)
|
||||||
Vue.use(Option)
|
Vue.use(Option)
|
||||||
Vue.use(Radio)
|
Vue.use(Radio)
|
||||||
|
Vue.use(RadioGroup)
|
||||||
|
Vue.use(Tabs)
|
||||||
|
Vue.use(TabPane)
|
||||||
|
Vue.use(Input)
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
|
@ -27,7 +27,6 @@
|
|||||||
:platform="platform"
|
:platform="platform"
|
||||||
></editor-with-tabs>
|
></editor-with-tabs>
|
||||||
<aidou></aidou>
|
<aidou></aidou>
|
||||||
<upload-image></upload-image>
|
|
||||||
<about-dialog></about-dialog>
|
<about-dialog></about-dialog>
|
||||||
<rename></rename>
|
<rename></rename>
|
||||||
<tweet></tweet>
|
<tweet></tweet>
|
||||||
@ -43,7 +42,6 @@
|
|||||||
import TitleBar from '@/components/titleBar'
|
import TitleBar from '@/components/titleBar'
|
||||||
import SideBar from '@/components/sideBar'
|
import SideBar from '@/components/sideBar'
|
||||||
import Aidou from '@/components/aidou/aidou'
|
import Aidou from '@/components/aidou/aidou'
|
||||||
import UploadImage from '@/components/uploadImage'
|
|
||||||
import AboutDialog from '@/components/about'
|
import AboutDialog from '@/components/about'
|
||||||
import Rename from '@/components/rename'
|
import Rename from '@/components/rename'
|
||||||
import Tweet from '@/components/tweet'
|
import Tweet from '@/components/tweet'
|
||||||
@ -60,7 +58,6 @@
|
|||||||
EditorWithTabs,
|
EditorWithTabs,
|
||||||
TitleBar,
|
TitleBar,
|
||||||
SideBar,
|
SideBar,
|
||||||
UploadImage,
|
|
||||||
AboutDialog,
|
AboutDialog,
|
||||||
Rename,
|
Rename,
|
||||||
Tweet,
|
Tweet,
|
||||||
@ -116,7 +113,6 @@
|
|||||||
dispatch('LISTEN_FOR_LAYOUT')
|
dispatch('LISTEN_FOR_LAYOUT')
|
||||||
dispatch('LISTEN_FOR_REQUEST_LAYOUT')
|
dispatch('LISTEN_FOR_REQUEST_LAYOUT')
|
||||||
// module: listenForMain
|
// module: listenForMain
|
||||||
dispatch('LISTEN_FOR_IMAGE_PATH')
|
|
||||||
dispatch('LISTEN_FOR_EDIT')
|
dispatch('LISTEN_FOR_EDIT')
|
||||||
dispatch('LISTEN_FOR_VIEW')
|
dispatch('LISTEN_FOR_VIEW')
|
||||||
dispatch('LISTEN_FOR_ABOUT_DIALOG')
|
dispatch('LISTEN_FOR_ABOUT_DIALOG')
|
||||||
@ -128,6 +124,7 @@
|
|||||||
// module: autoUpdates
|
// module: autoUpdates
|
||||||
dispatch('LISTEN_FOR_UPDATE')
|
dispatch('LISTEN_FOR_UPDATE')
|
||||||
// module: editor
|
// module: editor
|
||||||
|
dispatch('LISTEN_SCREEN_SHOT')
|
||||||
dispatch('ASK_FOR_USER_PREFERENCE')
|
dispatch('ASK_FOR_USER_PREFERENCE')
|
||||||
dispatch('ASK_FOR_MODE')
|
dispatch('ASK_FOR_MODE')
|
||||||
dispatch('LISTEN_FOR_CLOSE')
|
dispatch('LISTEN_FOR_CLOSE')
|
||||||
@ -138,7 +135,6 @@
|
|||||||
dispatch('LISTEN_FOR_BOOTSTRAP_WINDOW')
|
dispatch('LISTEN_FOR_BOOTSTRAP_WINDOW')
|
||||||
dispatch('LISTEN_FOR_SAVE_CLOSE')
|
dispatch('LISTEN_FOR_SAVE_CLOSE')
|
||||||
dispatch('LISTEN_FOR_EXPORT_PRINT')
|
dispatch('LISTEN_FOR_EXPORT_PRINT')
|
||||||
dispatch('LISTEN_FOR_INSERT_IMAGE')
|
|
||||||
dispatch('LISTEN_FOR_RENAME')
|
dispatch('LISTEN_FOR_RENAME')
|
||||||
dispatch('LINTEN_FOR_SET_LINE_ENDING')
|
dispatch('LINTEN_FOR_SET_LINE_ENDING')
|
||||||
dispatch('LISTEN_FOR_NEW_TAB')
|
dispatch('LISTEN_FOR_NEW_TAB')
|
||||||
@ -154,11 +150,11 @@
|
|||||||
// Cancel to allow tab drag&drop.
|
// Cancel to allow tab drag&drop.
|
||||||
if (!e.dataTransfer.types.length) return
|
if (!e.dataTransfer.types.length) return
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
if (e.dataTransfer.types.indexOf('Files') >= 0) {
|
if (e.dataTransfer.types.indexOf('Files') >= 0) {
|
||||||
if (e.dataTransfer.items.length === 1 && /png|jpg|jpeg|gif/.test(e.dataTransfer.items[0].type)) {
|
if (e.dataTransfer.items.length === 1 && e.dataTransfer.items[0].type.indexOf('image') > -1) {
|
||||||
bus.$emit('upload-image')
|
// Do nothing, because we already drag/drop image in muya.
|
||||||
} else {
|
} else {
|
||||||
|
e.preventDefault()
|
||||||
if (this.timer) {
|
if (this.timer) {
|
||||||
clearTimeout(this.timer)
|
clearTimeout(this.timer)
|
||||||
}
|
}
|
||||||
@ -167,6 +163,7 @@
|
|||||||
}, 300)
|
}, 300)
|
||||||
bus.$emit('importDialog', true)
|
bus.$emit('importDialog', true)
|
||||||
}
|
}
|
||||||
|
|
||||||
e.dataTransfer.dropEffect = 'copy'
|
e.dataTransfer.dropEffect = 'copy'
|
||||||
} else {
|
} else {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
@ -75,13 +75,6 @@
|
|||||||
:bool="hideQuickInsertHint"
|
:bool="hideQuickInsertHint"
|
||||||
:onChange="value => onSelectChange('hideQuickInsertHint', value)"
|
:onChange="value => onSelectChange('hideQuickInsertHint', value)"
|
||||||
></bool>
|
></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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -124,8 +117,7 @@ export default {
|
|||||||
textDirection: state => state.preferences.textDirection,
|
textDirection: state => state.preferences.textDirection,
|
||||||
codeFontSize: state => state.preferences.codeFontSize,
|
codeFontSize: state => state.preferences.codeFontSize,
|
||||||
codeFontFamily: state => state.preferences.codeFontFamily,
|
codeFontFamily: state => state.preferences.codeFontFamily,
|
||||||
hideQuickInsertHint: state => state.preferences.hideQuickInsertHint,
|
hideQuickInsertHint: state => state.preferences.hideQuickInsertHint
|
||||||
imageDropAction: state => state.preferences.imageDropAction
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
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 EditorIcon from '@/assets/icons/pref_editor.svg'
|
||||||
import MarkdownIcon from '@/assets/icons/pref_markdown.svg'
|
import MarkdownIcon from '@/assets/icons/pref_markdown.svg'
|
||||||
import ThemeIcon from '@/assets/icons/pref_theme.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'
|
import preferences from '../../../main/preferences/schema'
|
||||||
|
|
||||||
export const category = [{
|
export const category = [{
|
||||||
name: 'General',
|
name: 'General',
|
||||||
|
label: 'general',
|
||||||
icon: GeneralIcon,
|
icon: GeneralIcon,
|
||||||
path: '/preference/general'
|
path: '/preference/general'
|
||||||
}, {
|
}, {
|
||||||
name: 'Editor',
|
name: 'Editor',
|
||||||
|
label: 'editor',
|
||||||
icon: EditorIcon,
|
icon: EditorIcon,
|
||||||
path: '/preference/editor'
|
path: '/preference/editor'
|
||||||
}, {
|
}, {
|
||||||
name: 'Markdown',
|
name: 'Markdown',
|
||||||
|
label: 'markdown',
|
||||||
icon: MarkdownIcon,
|
icon: MarkdownIcon,
|
||||||
path: '/preference/markdown'
|
path: '/preference/markdown'
|
||||||
}, {
|
}, {
|
||||||
name: 'Theme',
|
name: 'Theme',
|
||||||
|
label: 'theme',
|
||||||
icon: ThemeIcon,
|
icon: ThemeIcon,
|
||||||
path: '/preference/theme'
|
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 => {
|
export const searchContent = Object.keys(preferences).map(k => {
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
<section class="category">
|
<section class="category">
|
||||||
<div v-for="c of category" :key="c.name" class="item"
|
<div v-for="c of category" :key="c.name" class="item"
|
||||||
@click="handleCategoryItemClick(c)"
|
@click="handleCategoryItemClick(c)"
|
||||||
:class="{active: c.name.toLowerCase() === currentCategory}"
|
:class="{active: c.label === currentCategory}"
|
||||||
>
|
>
|
||||||
<svg :viewBox="c.icon.viewBox">
|
<svg :viewBox="c.icon.viewBox">
|
||||||
<use :xlink:href="c.icon.url"></use>
|
<use :xlink:href="c.icon.url"></use>
|
||||||
|
@ -4,6 +4,8 @@ import General from '@/prefComponents/general'
|
|||||||
import Editor from '@/prefComponents/editor'
|
import Editor from '@/prefComponents/editor'
|
||||||
import Markdown from '@/prefComponents/markdown'
|
import Markdown from '@/prefComponents/markdown'
|
||||||
import Theme from '@/prefComponents/theme'
|
import Theme from '@/prefComponents/theme'
|
||||||
|
import Image from '@/prefComponents/image'
|
||||||
|
import ImageUploader from '@/prefComponents/imageUploader'
|
||||||
|
|
||||||
const routes = type => ([{
|
const routes = type => ([{
|
||||||
path: '/', redirect: type === 'editor'? '/editor' : '/preference'
|
path: '/', redirect: type === 'editor'? '/editor' : '/preference'
|
||||||
@ -21,6 +23,10 @@ const routes = type => ([{
|
|||||||
path: 'markdown', component: Markdown, name: 'markdown'
|
path: 'markdown', component: Markdown, name: 'markdown'
|
||||||
}, {
|
}, {
|
||||||
path: 'theme', component: Theme, name: 'theme'
|
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 { clipboard, ipcRenderer, shell } from 'electron'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import bus from '../bus'
|
import bus from '../bus'
|
||||||
import { hasKeys } from '../util'
|
import { hasKeys, getUniqueId } from '../util'
|
||||||
import { isSameFileSync } from '../util/fileSystem'
|
import { isSameFileSync } from '../util/fileSystem'
|
||||||
import listToTree from '../util/listToTree'
|
import listToTree from '../util/listToTree'
|
||||||
import { createDocumentState, getOptionsFromState, getSingleFileState, getBlankFileState } from './help'
|
import { createDocumentState, getOptionsFromState, getSingleFileState, getBlankFileState } from './help'
|
||||||
@ -216,18 +216,32 @@ const mutations = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const actions = {
|
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 }) {
|
FORMAT_LINK_CLICK ({ commit }, { data, dirname }) {
|
||||||
ipcRenderer.send('AGANI::format-link-click', { 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
|
// image path auto complement
|
||||||
ASK_FOR_IMAGE_AUTO_PATH ({ commit, state }, src) {
|
ASK_FOR_IMAGE_AUTO_PATH ({ commit, state }, src) {
|
||||||
const { pathname } = state.currentFile
|
const { pathname } = state.currentFile
|
||||||
if (pathname) {
|
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 }) {
|
LINTEN_FOR_SET_LINE_ENDING ({ commit, state }) {
|
||||||
ipcRenderer.on('AGANI::set-line-ending', (e, { lineEnding, ignoreSaveStatus }) => {
|
ipcRenderer.on('AGANI::set-line-ending', (e, { lineEnding, ignoreSaveStatus }) => {
|
||||||
const { lineEnding: oldLineEnding } = state.currentFile
|
const { lineEnding: oldLineEnding } = state.currentFile
|
||||||
@ -713,6 +711,10 @@ const actions = {
|
|||||||
|
|
||||||
ASK_FILE_WATCH ({ commit }, { pathname, watch }) {
|
ASK_FILE_WATCH ({ commit }, { pathname, watch }) {
|
||||||
ipcRenderer.send('AGANI::file-watch', { 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 mutations = {}
|
||||||
|
|
||||||
const actions = {
|
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 }) {
|
LISTEN_FOR_EDIT ({ commit }) {
|
||||||
ipcRenderer.on('AGANI::edit', (e, { type }) => {
|
ipcRenderer.on('AGANI::edit', (e, { type }) => {
|
||||||
bus.$emit(type, type)
|
bus.$emit(type, type)
|
||||||
|
@ -23,7 +23,7 @@ const state = {
|
|||||||
endOfLine: 'default',
|
endOfLine: 'default',
|
||||||
textDirection: 'ltr',
|
textDirection: 'ltr',
|
||||||
hideQuickInsertHint: false,
|
hideQuickInsertHint: false,
|
||||||
imageDropAction: 'folder',
|
imageInsertAction: 'folder',
|
||||||
|
|
||||||
preferLooseListItem: true,
|
preferLooseListItem: true,
|
||||||
bulletListMarker: '-',
|
bulletListMarker: '-',
|
||||||
@ -36,7 +36,20 @@ const state = {
|
|||||||
// edit modes (they are not in preference.md, but still put them here)
|
// edit modes (they are not in preference.md, but still put them here)
|
||||||
typewriter: false, // typewriter mode
|
typewriter: false, // typewriter mode
|
||||||
focus: false, // focus 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 = {}
|
const getters = {}
|
||||||
@ -57,6 +70,7 @@ const mutations = {
|
|||||||
const actions = {
|
const actions = {
|
||||||
ASK_FOR_USER_PREFERENCE ({ commit, state, rootState }) {
|
ASK_FOR_USER_PREFERENCE ({ commit, state, rootState }) {
|
||||||
ipcRenderer.send('mt::ask-for-user-preference')
|
ipcRenderer.send('mt::ask-for-user-preference')
|
||||||
|
ipcRenderer.send('mt::ask-for-user-data')
|
||||||
|
|
||||||
ipcRenderer.on('AGANI::user-preference', (e, preference) => {
|
ipcRenderer.on('AGANI::user-preference', (e, preference) => {
|
||||||
const { autoSave } = preference
|
const { autoSave } = preference
|
||||||
@ -90,6 +104,14 @@ const actions = {
|
|||||||
// commit('SET_USER_PREFERENCE', { [type]: value })
|
// commit('SET_USER_PREFERENCE', { [type]: value })
|
||||||
// save to electron-store
|
// save to electron-store
|
||||||
ipcRenderer.send('mt::set-user-preference', { [type]: value })
|
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 path from 'path'
|
||||||
import fse from 'fs-extra'
|
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) => {
|
export const create = (pathname, type) => {
|
||||||
if (type === 'directory') {
|
if (type === 'directory') {
|
||||||
@ -43,3 +48,136 @@ export const isSameFileSync = (pathA, pathB, isNormalized=false) => {
|
|||||||
}
|
}
|
||||||
return 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
|
// help functions
|
||||||
const easeInOutQuad = function (t, b, c, d) {
|
const easeInOutQuad = function (t, b, c, d) {
|
||||||
t /= d / 2
|
t /= d / 2
|
||||||
@ -187,6 +189,14 @@ export const cloneObj = (obj, deepCopy=true) => {
|
|||||||
return deepCopy ? JSON.parse(JSON.stringify(obj)) : Object.assign({}, obj)
|
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 isOsx = process.platform === 'darwin'
|
||||||
export const isWindows = process.platform === 'win32'
|
export const isWindows = process.platform === 'win32'
|
||||||
export const isLinux = process.platform === 'linux'
|
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 { dark, graphite, materialDark, oneDark, ulysses } from './themeColor'
|
||||||
|
import { isLinux } from './index'
|
||||||
import elementStyle from 'element-ui/lib/theme-chalk/index.css'
|
import elementStyle from 'element-ui/lib/theme-chalk/index.css'
|
||||||
|
|
||||||
const ORIGINAL_THEME = '#409EFF'
|
const ORIGINAL_THEME = '#409EFF'
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
"endOfLine": "default",
|
"endOfLine": "default",
|
||||||
"textDirection": "ltr",
|
"textDirection": "ltr",
|
||||||
"hideQuickInsertHint": false,
|
"hideQuickInsertHint": false,
|
||||||
"imageDropAction": "folder",
|
"imageInsertAction": "path",
|
||||||
|
|
||||||
"preferLooseListItem": true,
|
"preferLooseListItem": true,
|
||||||
"bulletListMarker": "-",
|
"bulletListMarker": "-",
|
||||||
|
245
yarn.lock
@ -150,6 +150,56 @@
|
|||||||
vow "^0.4.19"
|
vow "^0.4.19"
|
||||||
vow-fs "^0.3.6"
|
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":
|
"@types/clone@~0.1.30":
|
||||||
version "0.1.30"
|
version "0.1.30"
|
||||||
resolved "https://registry.yarnpkg.com/@types/clone/-/clone-0.1.30.tgz#e7365648c1b42136a59c7d5040637b3b5c83b614"
|
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"
|
resolved "https://registry.yarnpkg.com/atoa/-/atoa-1.0.0.tgz#0cc0e91a480e738f923ebc103676471779b34a49"
|
||||||
integrity sha1-DMDpGkgOc4+SPrwQNnZHF3mzSkk=
|
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:
|
atob@^2.1.1:
|
||||||
version "2.1.2"
|
version "2.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||||
@ -1627,6 +1682,11 @@ bcrypt-pbkdf@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tweetnacl "^0.14.3"
|
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:
|
better-assert@~1.0.0:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
|
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"
|
electron-to-chromium "^1.3.124"
|
||||||
node-releases "^1.1.14"
|
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:
|
buffer-alloc-unsafe@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
|
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:
|
optionalDependencies:
|
||||||
fsevents "^2.0.6"
|
fsevents "^2.0.6"
|
||||||
|
|
||||||
chownr@^1.1.1:
|
chownr@^1.0.1, chownr@^1.1.1:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
|
resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494"
|
||||||
integrity sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==
|
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"
|
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
|
||||||
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
|
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:
|
deep-eql@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df"
|
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"
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.3.2.tgz#1663691629d4dbfe364fa12a2a4f0aa86aa3a050"
|
||||||
integrity sha1-FmNpFinU2/42T6EqKk8KqGqjoFA=
|
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:
|
deepmerge@^1.2.0:
|
||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-1.5.2.tgz#10499d868844cdad4fee0842df8c7f6f0c95a753"
|
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"
|
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||||
integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
|
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:
|
des.js@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.0.tgz#c074d2e2aa6a8a9a07dbd61f9a15c2cd83ec8ecc"
|
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"
|
snapdragon "^0.8.1"
|
||||||
to-regex "^3.0.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:
|
expand-tilde@^2.0.0, expand-tilde@^2.0.2:
|
||||||
version "2.0.2"
|
version "2.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/expand-tilde/-/expand-tilde-2.0.2.tgz#97e801aa052df02454de46b02bf621642cdc8502"
|
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"
|
resolved "https://registry.yarnpkg.com/git-revision-webpack-plugin/-/git-revision-webpack-plugin-3.0.3.tgz#f909949d7851d1039ed530518f73f5d46594e66f"
|
||||||
integrity sha512-B2ixM0fY7VgR61ZRSXYrh0R57Er7RY+CZb+fja5OFe21Y5o9GgzQanMgdlcBwWZ+LoOVqxBogbDutTTYMXQDWw==
|
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:
|
github-markdown-css@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-3.0.1.tgz#d08db1060d2e182025e0d07d547cfe2afed30205"
|
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:
|
dependencies:
|
||||||
isobject "^3.0.1"
|
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:
|
is-promise@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
|
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"
|
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||||
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
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:
|
isstream@~0.1.2:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
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"
|
resolved "https://registry.yarnpkg.com/keypress/-/keypress-0.1.0.tgz#4a3188d4291b66b4f65edb99f806aa9ae293592a"
|
||||||
integrity sha1-SjGI1CkbZrT2XtuZ+AaqmuKTWSo=
|
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:
|
killable@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash.forown/-/lodash.forown-4.4.0.tgz#85115cf04f73ef966eced52511d3893cc46683af"
|
||||||
integrity sha1-hRFc8E9z75ZuztUlEdOJPMRmg68=
|
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:
|
lodash.isarguments@^3.0.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
|
||||||
integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
|
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:
|
lodash.sortby@^4.7.0:
|
||||||
version "4.7.0"
|
version "4.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
@ -7076,6 +7198,11 @@ lru-cache@^5.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^3.0.2"
|
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:
|
make-dir@^1.0.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
|
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"
|
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b"
|
||||||
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
|
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:
|
mini-css-extract-plugin@^0.6.0:
|
||||||
version "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"
|
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"
|
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||||
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
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"
|
version "2.13.2"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
|
||||||
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
|
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
|
||||||
@ -7508,6 +7640,11 @@ nanomatch@^1.2.1, nanomatch@^1.2.9:
|
|||||||
snapdragon "^0.8.1"
|
snapdragon "^0.8.1"
|
||||||
to-regex "^3.0.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:
|
natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
|
||||||
@ -7661,6 +7798,11 @@ node-releases@^1.1.14:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver "^5.3.0"
|
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:
|
"nopt@2 || 3", nopt@3.x:
|
||||||
version "3.0.6"
|
version "3.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
|
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
|
||||||
@ -7742,7 +7884,7 @@ npm-run-path@^2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
path-key "^2.0.0"
|
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"
|
version "4.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
|
||||||
integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==
|
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"
|
resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e"
|
||||||
integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==
|
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:
|
on-finished@~2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
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"
|
resolved "https://registry.yarnpkg.com/os-browserify/-/os-browserify-0.3.0.tgz#854373c7f5c2315914fc9bfc6bd8238fdda1ec27"
|
||||||
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
|
integrity sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=
|
||||||
|
|
||||||
os-homedir@^1.0.0:
|
os-homedir@^1.0.0, os-homedir@^1.0.1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
|
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
|
||||||
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
|
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
|
||||||
@ -7974,6 +8121,14 @@ os-locale@^3.0.0, os-locale@^3.1.0:
|
|||||||
lcid "^2.0.0"
|
lcid "^2.0.0"
|
||||||
mem "^4.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:
|
os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
|
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-parser "^0.2.0"
|
||||||
posthtml-render "^1.0.5"
|
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:
|
prelude-ls@~1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
|
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"
|
randombytes "^2.0.1"
|
||||||
safe-buffer "^5.1.2"
|
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"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
|
resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
|
||||||
integrity sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==
|
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"
|
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
|
||||||
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
|
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:
|
single-line-log@^1.1.2:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/single-line-log/-/single-line-log-1.1.2.tgz#c2f83f273a3e1a16edb0995661da0ed5ef033364"
|
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"
|
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2"
|
||||||
integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==
|
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"
|
version "1.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
|
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
|
||||||
integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
|
integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
|
||||||
@ -10919,6 +11128,13 @@ unique-string@^1.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
crypto-random-string "^1.0.0"
|
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:
|
universalify@^0.1.0:
|
||||||
version "0.1.2"
|
version "0.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
@ -11028,6 +11244,11 @@ url-slug@2.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
unidecode "0.1.8"
|
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:
|
url@^0.11.0, url@~0.11.0:
|
||||||
version "0.11.0"
|
version "0.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"
|
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"
|
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
|
||||||
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
|
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:
|
which@1, which@1.3.1, which@^1.1.1, which@^1.2.14, which@^1.2.9:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
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"
|
define-property "^1.0.0"
|
||||||
is-number "^3.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:
|
wordwrap@0.0.2:
|
||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
|
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
|
||||||
|