marktext/src/muya/lib/utils/index.js
Ran Luo c239e99f1b
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
2019-05-26 23:55:13 +08:00

327 lines
8.2 KiB
JavaScript

// DOTO: Don't use Node API in editor folder, remove `path` @jocs
import createDOMPurify from 'dompurify'
import { isInElectron, URL_REG } from '../config'
const ID_PREFIX = 'ag-'
let id = 0
export const getUniqueId = () => `${ID_PREFIX}${id++}`
export const getLongUniqueId = () => `${getUniqueId()}-${(+new Date()).toString(32)}`
export const isMetaKey = ({ key }) => key === 'Shift' || key === 'Control' || key === 'Alt' || key === 'Meta'
export const noop = () => {}
export const identity = i => i
export const isOdd = number => Math.abs(number) % 2 === 1
export const isEven = number => Math.abs(number) % 2 === 0
export const isLengthEven = (str = '') => str.length % 2 === 0
export const snakeToCamel = name => name.replace(/_([a-z])/g, (p0, p1) => p1.toUpperCase())
export const camelToSnake = name => name.replace(/([A-Z])/g, (_, p) => `-${p.toLowerCase()}`)
/**
* Are two arrays have intersection
*/
export const conflict = (arr1, arr2) => {
return !(arr1[1] < arr2[0] || arr2[1] < arr1[0])
}
export const union = ({ start: tStart, end: tEnd }, { start: lStart, end: lEnd, active }) => {
if (!(tEnd <= lStart || lEnd <= tStart)) {
if (lStart < tStart) {
return {
start: tStart,
end: tEnd < lEnd ? tEnd : lEnd,
active
}
} else {
return {
start: lStart,
end: tEnd < lEnd ? tEnd : lEnd,
active
}
}
}
return null
}
// https://github.com/jashkenas/underscore
export const throttle = (func, wait = 50) => {
let context
let args
let result
let timeout = null
let previous = 0
const later = () => {
previous = Date.now()
timeout = null
result = func.apply(context, args)
if (!timeout) {
context = args = null
}
}
return function () {
const now = Date.now()
const remaining = wait - (now - previous)
context = this
args = arguments
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
result = func.apply(context, args)
if (!timeout) {
context = args = null
}
} else if (!timeout) {
timeout = setTimeout(later, remaining)
}
return result
}
}
// simple implementation...
export const debounce = (func, wait = 50) => {
let timer = null
return function (...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func(...args)
}, wait)
}
}
export const deepCopyArray = array => {
const result = []
const len = array.length
let i
for (i = 0; i < len; i++) {
if (typeof array[i] === 'object' && array[i] !== null) {
if (Array.isArray(array[i])) {
result.push(deepCopyArray(array[i]))
} else {
result.push(deepCopy(array[i]))
}
} else {
result.push(array[i])
}
}
return result
}
// TODO: @jocs rewrite deepCopy
export const deepCopy = object => {
const obj = {}
Object.keys(object).forEach(key => {
if (typeof object[key] === 'object' && object[key] !== null) {
if (Array.isArray(object[key])) {
obj[key] = deepCopyArray(object[key])
} else {
obj[key] = deepCopy(object[key])
}
} else {
obj[key] = object[key]
}
})
return obj
}
export const loadImage = async (url, detectContentType = false) => {
if (detectContentType) {
const isImage = await checkImageContentType(url)
if (!isImage) throw new Error('not an image')
}
return new Promise((resolve, reject) => {
const image = new Image()
image.onload = () => {
resolve(url)
}
image.onerror = err => {
reject(err)
}
image.src = url
})
}
export const checkImageContentType = url => {
const req = new XMLHttpRequest()
let settle
const promise = new Promise((resolve, reject) => {
settle = resolve
})
const handler = () => {
if (req.readyState === XMLHttpRequest.DONE) {
if (req.status === 200) {
const contentType = req.getResponseHeader('Content-Type')
if (/^image\/(?:jpeg|png|gif|svg\+xml|webp)$/.test(contentType)) {
settle(true)
} else {
settle(false)
}
} else {
settle(false)
}
}
}
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.
*
* @param {string} src Image url
* @param {string} baseUrl Base path; used on desktop to fix the relative image path.
*/
export const getImageInfo = (src, baseUrl = window.DIRNAME) => {
const EXT_REG = /\.(jpeg|jpg|png|gif|svg|webp)(?=\?|$)/i
// data:[<MIME-type>][;charset=<encoding>][;base64],<data>
const DATA_URL_REG = /^data:image\/[\w+-]+(;[\w-]+=[\w-]+|;base64)*,[a-zA-Z0-9+/]+={0,2}$/
const imageExtension = EXT_REG.test(src)
const isUrl = URL_REG.test(src)
// Treat an URL with valid extension as image
if (imageExtension) {
const isAbsoluteLocal = /^(?:\/|\\\\|[a-zA-Z]:\\).+/.test(src)
if (isUrl || (!isAbsoluteLocal && !baseUrl)) {
if (!isUrl && !baseUrl && isInElectron) {
console.warn('"baseUrl" is not defined!')
}
return {
isUnknownType: false,
src
}
} else if (isInElectron) {
// Correct relative path on desktop. If we resolve a absolute path "path.resolve" doesn't do anything.
return {
isUnknownType: false,
src: 'file://' + require('path').resolve(baseUrl, src)
}
}
// else: Forbid the request due absolute or relative path in browser
} else if (isUrl && !imageExtension) {
// Assume it's a valid image and make a http request later
return {
isUnknownType: true,
src
}
}
// Data url
if (DATA_URL_REG.test(src)) {
return {
isUnknownType: false,
src
}
}
// Url type is unknown
return {
isUnknownType: false,
src: ''
}
}
export const escapeHtml = html => {
return html
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
}
export const unescapeHtml = text => {
return text
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, `'`)
}
export const escapeInBlockHtml = html => {
return html
.replace(/(<(style|script|title)[^<>]*>)([\s\S]*?)(<\/\2>)/g, (m, p1, p2, p3, p4) => {
return `${escapeHtml(p1)}${p3}${escapeHtml(p4)}`
})
}
export const wordCount = markdown => {
const paragraph = markdown.split(/\n{2,}/).filter(line => line).length
let word = 0
let character = 0
let all = 0
const removedChinese = markdown.replace(/[\u4e00-\u9fa5]/g, '')
const tokens = removedChinese.split(/[\s\n]+/).filter(t => t)
const chineseWordLength = markdown.length - removedChinese.length
word += chineseWordLength + tokens.length
character += tokens.reduce((acc, t) => acc + t.length, 0) + chineseWordLength
all += markdown.length
return { word, paragraph, character, all }
}
/**
* [genUpper2LowerKeyHash generate constants map hash, the value is lowercase of the key,
* also translate `_` to `-`]
*/
export const genUpper2LowerKeyHash = keys => {
return keys.reduce((acc, key) => {
const value = key.toLowerCase().replace(/_/g, '-')
return Object.assign(acc, { [key]: value })
}, {})
}
/**
* generate constants map, the value is the key.
*/
export const generateKeyHash = keys => {
return keys.reduce((acc, key) => {
return Object.assign(acc, { [key]: key })
}, {})
}
// mixins
export const mixins = (constructor, ...object) => {
return Object.assign(constructor.prototype, ...object)
}
export const sanitize = (html, options) => {
const DOMPurify = createDOMPurify(window)
return DOMPurify.sanitize(escapeInBlockHtml(html), options)
}
export const getParagraphReference = (ele, id) => {
const { x, y, left, top, bottom, height } = ele.getBoundingClientRect()
return {
getBoundingClientRect () {
return { x, y, left, top, bottom, height, width: 0, right: left }
},
clientWidth: 0,
clientHeight: height,
id
}
}
export const verticalPositionInRect = (event, rect) => {
const { clientY } = event
const { top, height } = rect
return (clientY - top) > (height / 2) ? 'down' : 'up'
}