mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 09:46:22 +00:00
Merge remote-tracking branch 'upstream/master' into allow-no-html-escape
This commit is contained in:
@@ -7,14 +7,16 @@ import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
|
||||
import convertModeName from 'browser/lib/convertModeName'
|
||||
import eventEmitter from 'browser/main/lib/eventEmitter'
|
||||
import iconv from 'iconv-lite'
|
||||
|
||||
import crypto from 'crypto'
|
||||
import consts from 'browser/lib/consts'
|
||||
import fs from 'fs'
|
||||
const { ipcRenderer } = require('electron')
|
||||
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
|
||||
|
||||
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
|
||||
|
||||
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
|
||||
const buildCMRulers = (rulers, enableRulers) =>
|
||||
enableRulers ? rulers.map(ruler => ({ column: ruler })) : []
|
||||
enableRulers ? rulers.map(ruler => ({column: ruler})) : []
|
||||
|
||||
export default class CodeEditor extends React.Component {
|
||||
constructor (props) {
|
||||
@@ -81,8 +83,21 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
componentDidMount () {
|
||||
const { rulers, enableRulers } = this.props
|
||||
this.value = this.props.value
|
||||
const expandSnippet = this.expandSnippet.bind(this)
|
||||
|
||||
const defaultSnippet = [
|
||||
{
|
||||
id: crypto.randomBytes(16).toString('hex'),
|
||||
name: 'Dummy text',
|
||||
prefix: ['lorem', 'ipsum'],
|
||||
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
|
||||
}
|
||||
]
|
||||
if (!fs.existsSync(consts.SNIPPET_FILE)) {
|
||||
fs.writeFileSync(consts.SNIPPET_FILE, JSON.stringify(defaultSnippet, null, 4), 'utf8')
|
||||
}
|
||||
|
||||
this.value = this.props.value
|
||||
this.editor = CodeMirror(this.refs.root, {
|
||||
rulers: buildCMRulers(rulers, enableRulers),
|
||||
value: this.props.value,
|
||||
@@ -103,6 +118,8 @@ export default class CodeEditor extends React.Component {
|
||||
Tab: function (cm) {
|
||||
const cursor = cm.getCursor()
|
||||
const line = cm.getLine(cursor.line)
|
||||
const cursorPosition = cursor.ch
|
||||
const charBeforeCursor = line.substr(cursorPosition - 1, 1)
|
||||
if (cm.somethingSelected()) cm.indentSelection('add')
|
||||
else {
|
||||
const tabs = cm.getOption('indentWithTabs')
|
||||
@@ -114,6 +131,16 @@ export default class CodeEditor extends React.Component {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
cm.execCommand('goLineEnd')
|
||||
} else if (!charBeforeCursor.match(/\t|\s|\r|\n/) && cursor.ch > 1) {
|
||||
// text expansion on tab key if the char before is alphabet
|
||||
const snippets = JSON.parse(fs.readFileSync(consts.SNIPPET_FILE, 'utf8'))
|
||||
if (expandSnippet(line, cursor, cm, snippets) === false) {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
} else {
|
||||
cm.execCommand('insertSoftTab')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (tabs) {
|
||||
cm.execCommand('insertTab')
|
||||
@@ -157,6 +184,73 @@ export default class CodeEditor extends React.Component {
|
||||
CodeMirror.Vim.map('ZZ', ':q', 'normal')
|
||||
}
|
||||
|
||||
expandSnippet (line, cursor, cm, snippets) {
|
||||
const wordBeforeCursor = this.getWordBeforeCursor(line, cursor.line, cursor.ch)
|
||||
const templateCursorString = ':{}'
|
||||
for (let i = 0; i < snippets.length; i++) {
|
||||
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
|
||||
if (snippets[i].content.indexOf(templateCursorString) !== -1) {
|
||||
const snippetLines = snippets[i].content.split('\n')
|
||||
let cursorLineNumber = 0
|
||||
let cursorLinePosition = 0
|
||||
for (let j = 0; j < snippetLines.length; j++) {
|
||||
const cursorIndex = snippetLines[j].indexOf(templateCursorString)
|
||||
if (cursorIndex !== -1) {
|
||||
cursorLineNumber = j
|
||||
cursorLinePosition = cursorIndex
|
||||
cm.replaceRange(
|
||||
snippets[i].content.replace(templateCursorString, ''),
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
cm.setCursor({ line: cursor.line + cursorLineNumber, ch: cursorLinePosition })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cm.replaceRange(
|
||||
snippets[i].content,
|
||||
wordBeforeCursor.range.from,
|
||||
wordBeforeCursor.range.to
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
getWordBeforeCursor (line, lineNumber, cursorPosition) {
|
||||
let wordBeforeCursor = ''
|
||||
const originCursorPosition = cursorPosition
|
||||
const emptyChars = /\t|\s|\r|\n/
|
||||
|
||||
// to prevent the word to expand is long that will crash the whole app
|
||||
// the safeStop is there to stop user to expand words that longer than 20 chars
|
||||
const safeStop = 20
|
||||
|
||||
while (cursorPosition > 0) {
|
||||
const currentChar = line.substr(cursorPosition - 1, 1)
|
||||
// if char is not an empty char
|
||||
if (!emptyChars.test(currentChar)) {
|
||||
wordBeforeCursor = currentChar + wordBeforeCursor
|
||||
} else if (wordBeforeCursor.length >= safeStop) {
|
||||
throw new Error('Your snippet trigger is too long !')
|
||||
} else {
|
||||
break
|
||||
}
|
||||
cursorPosition--
|
||||
}
|
||||
|
||||
return {
|
||||
text: wordBeforeCursor,
|
||||
range: {
|
||||
from: {line: lineNumber, ch: originCursorPosition},
|
||||
to: {line: lineNumber, ch: cursorPosition}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quitEditor () {
|
||||
document.querySelector('textarea').blur()
|
||||
}
|
||||
@@ -174,7 +268,7 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
componentDidUpdate (prevProps, prevState) {
|
||||
let needRefresh = false
|
||||
const { rulers, enableRulers } = this.props
|
||||
const {rulers, enableRulers} = this.props
|
||||
if (prevProps.mode !== this.props.mode) {
|
||||
this.setMode(this.props.mode)
|
||||
}
|
||||
@@ -274,6 +368,7 @@ export default class CodeEditor extends React.Component {
|
||||
|
||||
handlePaste (editor, e) {
|
||||
const clipboardData = e.clipboardData
|
||||
const {storageKey, noteKey} = this.props
|
||||
const dataTransferItem = clipboardData.items[0]
|
||||
const pastedTxt = clipboardData.getData('text')
|
||||
const isURL = (str) => {
|
||||
@@ -283,22 +378,28 @@ export default class CodeEditor extends React.Component {
|
||||
const isInLinkTag = (editor) => {
|
||||
const startCursor = editor.getCursor('start')
|
||||
const prevChar = editor.getRange(
|
||||
{ line: startCursor.line, ch: startCursor.ch - 2 },
|
||||
{ line: startCursor.line, ch: startCursor.ch }
|
||||
{line: startCursor.line, ch: startCursor.ch - 2},
|
||||
{line: startCursor.line, ch: startCursor.ch}
|
||||
)
|
||||
const endCursor = editor.getCursor('end')
|
||||
const nextChar = editor.getRange(
|
||||
{ line: endCursor.line, ch: endCursor.ch },
|
||||
{ line: endCursor.line, ch: endCursor.ch + 1 }
|
||||
{line: endCursor.line, ch: endCursor.ch},
|
||||
{line: endCursor.line, ch: endCursor.ch + 1}
|
||||
)
|
||||
return prevChar === '](' && nextChar === ')'
|
||||
}
|
||||
if (dataTransferItem.type.match('image')) {
|
||||
const {storageKey, noteKey} = this.props
|
||||
attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem)
|
||||
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
|
||||
this.handlePasteUrl(e, editor, pastedTxt)
|
||||
}
|
||||
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
|
||||
attachmentManagement.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
|
||||
.then((modifiedText) => {
|
||||
this.editor.replaceSelection(modifiedText)
|
||||
})
|
||||
e.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
handleScroll (e) {
|
||||
@@ -312,24 +413,58 @@ export default class CodeEditor extends React.Component {
|
||||
const taggedUrl = `<${pastedTxt}>`
|
||||
editor.replaceSelection(taggedUrl)
|
||||
|
||||
const isImageReponse = (response) => {
|
||||
return response.headers.has('content-type') &&
|
||||
response.headers.get('content-type').match(/^image\/.+$/)
|
||||
}
|
||||
const replaceTaggedUrl = (replacement) => {
|
||||
const value = editor.getValue()
|
||||
const cursor = editor.getCursor()
|
||||
const newValue = value.replace(taggedUrl, replacement)
|
||||
const newCursor = Object.assign({}, cursor, { ch: cursor.ch + newValue.length - value.length })
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(newCursor)
|
||||
}
|
||||
|
||||
fetch(pastedTxt, {
|
||||
method: 'get'
|
||||
}).then((response) => {
|
||||
return this.decodeResponse(response)
|
||||
}).then((response) => {
|
||||
const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html')
|
||||
const value = editor.getValue()
|
||||
const cursor = editor.getCursor()
|
||||
const LinkWithTitle = `[${parsedResponse.title}](${pastedTxt})`
|
||||
const newValue = value.replace(taggedUrl, LinkWithTitle)
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(cursor)
|
||||
if (isImageReponse(response)) {
|
||||
return this.mapImageResponse(response, pastedTxt)
|
||||
} else {
|
||||
return this.mapNormalResponse(response, pastedTxt)
|
||||
}
|
||||
}).then((replacement) => {
|
||||
replaceTaggedUrl(replacement)
|
||||
}).catch((e) => {
|
||||
const value = editor.getValue()
|
||||
const newValue = value.replace(taggedUrl, pastedTxt)
|
||||
const cursor = editor.getCursor()
|
||||
editor.setValue(newValue)
|
||||
editor.setCursor(cursor)
|
||||
replaceTaggedUrl(pastedTxt)
|
||||
})
|
||||
}
|
||||
|
||||
mapNormalResponse (response, pastedTxt) {
|
||||
return this.decodeResponse(response).then((body) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const parsedBody = (new window.DOMParser()).parseFromString(body, 'text/html')
|
||||
const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
|
||||
resolve(linkWithTitle)
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
mapImageResponse (response, pastedTxt) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const url = response.url
|
||||
const name = url.substring(url.lastIndexOf('/') + 1)
|
||||
const imageLinkWithName = ``
|
||||
resolve(imageLinkWithName)
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -359,11 +494,9 @@ export default class CodeEditor extends React.Component {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { className, fontSize } = this.props
|
||||
let fontFamily = this.props.fontFamily
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
|
||||
? [fontFamily].concat(defaultEditorFontFamily)
|
||||
: defaultEditorFontFamily
|
||||
const {className, fontSize} = this.props
|
||||
const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
|
||||
const width = this.props.width
|
||||
return (
|
||||
<div
|
||||
className={className == null
|
||||
@@ -373,8 +506,9 @@ export default class CodeEditor extends React.Component {
|
||||
ref='root'
|
||||
tabIndex='-1'
|
||||
style={{
|
||||
fontFamily: fontFamily.join(', '),
|
||||
fontSize: fontSize
|
||||
fontFamily,
|
||||
fontSize: fontSize,
|
||||
width: width
|
||||
}}
|
||||
onDrop={(e) => this.handleDropImage(e)}
|
||||
/>
|
||||
|
||||
@@ -283,6 +283,8 @@ class MarkdownEditor extends React.Component {
|
||||
indentSize={editorIndentSize}
|
||||
scrollPastEnd={config.preview.scrollPastEnd}
|
||||
smartQuotes={config.preview.smartQuotes}
|
||||
smartArrows={config.preview.smartArrows}
|
||||
breaks={config.preview.breaks}
|
||||
sanitize={config.preview.sanitize}
|
||||
ref='preview'
|
||||
onContextMenu={(e) => this.handleContextMenu(e)}
|
||||
@@ -295,6 +297,8 @@ class MarkdownEditor extends React.Component {
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -22,10 +22,12 @@ const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
|
||||
|
||||
const { app } = remote
|
||||
const path = require('path')
|
||||
const fileUrl = require('file-url')
|
||||
|
||||
const dialog = remote.dialog
|
||||
|
||||
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
|
||||
const appPath = 'file://' + (process.env.NODE_ENV === 'production'
|
||||
const appPath = fileUrl(process.env.NODE_ENV === 'production'
|
||||
? app.getAppPath()
|
||||
: path.resolve())
|
||||
const CSS_FILES = [
|
||||
@@ -33,7 +35,7 @@ const CSS_FILES = [
|
||||
`${appPath}/node_modules/codemirror/lib/codemirror.css`
|
||||
]
|
||||
|
||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme) {
|
||||
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) {
|
||||
return `
|
||||
@font-face {
|
||||
font-family: 'Lato';
|
||||
@@ -53,7 +55,19 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scro
|
||||
font-weight: 700;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Material Icons';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Material Icons'),
|
||||
local('MaterialIcons-Regular'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
|
||||
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
|
||||
}
|
||||
${allowCustomCSS ? customCSS : ''}
|
||||
${markdownStyle}
|
||||
|
||||
body {
|
||||
font-family: '${fontFamily.join("','")}';
|
||||
font-size: ${fontSize}px;
|
||||
@@ -111,6 +125,9 @@ body p {
|
||||
color: #000;
|
||||
background-color: #fff;
|
||||
}
|
||||
.clipboardButton {
|
||||
display: none
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
@@ -133,7 +150,6 @@ export default class MarkdownPreview extends React.Component {
|
||||
this.mouseUpHandler = (e) => this.handleMouseUp(e)
|
||||
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
|
||||
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
|
||||
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
|
||||
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
|
||||
this.saveAsTextHandler = () => this.handleSaveAsText()
|
||||
this.saveAsMdHandler = () => this.handleSaveAsMd()
|
||||
@@ -146,29 +162,14 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
initMarkdown () {
|
||||
const { smartQuotes, sanitize } = this.props
|
||||
const { smartQuotes, sanitize, breaks } = this.props
|
||||
this.markdown = new Markdown({
|
||||
typographer: smartQuotes,
|
||||
sanitize
|
||||
sanitize,
|
||||
breaks
|
||||
})
|
||||
}
|
||||
|
||||
handlePreviewAnchorClick (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
const anchor = e.target.closest('a')
|
||||
const href = anchor.getAttribute('href')
|
||||
if (_.isString(href) && href.match(/^#/)) {
|
||||
const targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length))
|
||||
if (targetElement != null) {
|
||||
this.getWindow().scrollTo(0, targetElement.offsetTop)
|
||||
}
|
||||
} else {
|
||||
shell.openExternal(href)
|
||||
}
|
||||
}
|
||||
|
||||
handleCheckboxClick (e) {
|
||||
this.props.onCheckboxClick(e)
|
||||
}
|
||||
@@ -216,9 +217,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
handleSaveAsHtml () {
|
||||
this.exportAsDocument('html', (noteContent, exportTasks) => {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme} = this.getStyleParams()
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
||||
|
||||
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme)
|
||||
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
||||
let body = this.markdown.render(escapeHtmlCharacters(noteContent))
|
||||
|
||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||
@@ -341,7 +342,10 @@ export default class MarkdownPreview extends React.Component {
|
||||
|
||||
componentDidUpdate (prevProps) {
|
||||
if (prevProps.value !== this.props.value) this.rewriteIframe()
|
||||
if (prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize) {
|
||||
if (prevProps.smartQuotes !== this.props.smartQuotes ||
|
||||
prevProps.sanitize !== this.props.sanitize ||
|
||||
prevProps.smartArrows !== this.props.smartArrows ||
|
||||
prevProps.breaks !== this.props.breaks) {
|
||||
this.initMarkdown()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
@@ -352,14 +356,16 @@ export default class MarkdownPreview extends React.Component {
|
||||
prevProps.lineNumber !== this.props.lineNumber ||
|
||||
prevProps.showCopyNotification !== this.props.showCopyNotification ||
|
||||
prevProps.theme !== this.props.theme ||
|
||||
prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
|
||||
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
|
||||
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
|
||||
prevProps.customCSS !== this.props.customCSS) {
|
||||
this.applyStyle()
|
||||
this.rewriteIframe()
|
||||
}
|
||||
}
|
||||
|
||||
getStyleParams () {
|
||||
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme } = this.props
|
||||
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS } = this.props
|
||||
let { fontFamily, codeBlockFontFamily } = this.props
|
||||
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
|
||||
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
|
||||
@@ -368,14 +374,14 @@ export default class MarkdownPreview extends React.Component {
|
||||
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
|
||||
: defaultCodeBlockFontFamily
|
||||
|
||||
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme}
|
||||
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS}
|
||||
}
|
||||
|
||||
applyStyle () {
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme} = this.getStyleParams()
|
||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
|
||||
|
||||
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme)
|
||||
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
|
||||
}
|
||||
|
||||
GetCodeThemeLink (theme) {
|
||||
@@ -388,9 +394,6 @@ export default class MarkdownPreview extends React.Component {
|
||||
}
|
||||
|
||||
rewriteIframe () {
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
el.removeEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.removeEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
@@ -399,7 +402,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.removeEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
const { theme, indentSize, showCopyNotification, storagePath } = this.props
|
||||
const { theme, indentSize, showCopyNotification, storagePath, noteKey } = this.props
|
||||
let { value, codeBlockTheme } = this.props
|
||||
|
||||
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
|
||||
@@ -411,18 +414,15 @@ export default class MarkdownPreview extends React.Component {
|
||||
})
|
||||
}
|
||||
let renderedHTML = this.markdown.render(value)
|
||||
attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey)
|
||||
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
this.fixDecodedURI(el)
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
|
||||
el.addEventListener('click', this.checkboxClickHandler)
|
||||
})
|
||||
|
||||
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
|
||||
this.fixDecodedURI(el)
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
|
||||
@@ -473,7 +473,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, opts)
|
||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -489,7 +489,7 @@ export default class MarkdownPreview extends React.Component {
|
||||
el.innerHTML = ''
|
||||
diagram.drawSVG(el, {theme: 'simple'})
|
||||
_.forEach(el.querySelectorAll('a'), (el) => {
|
||||
el.addEventListener('click', this.anchorClickHandler)
|
||||
el.addEventListener('click', this.linkClickHandler)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -538,11 +538,6 @@ export default class MarkdownPreview extends React.Component {
|
||||
e.stopPropagation()
|
||||
|
||||
const href = e.target.href
|
||||
if (href.match(/^http/i)) {
|
||||
shell.openExternal(href)
|
||||
return
|
||||
}
|
||||
|
||||
const linkHash = href.split('/').pop()
|
||||
|
||||
const regexNoteInternalLink = /main.html#(.+)/
|
||||
@@ -574,6 +569,9 @@ export default class MarkdownPreview extends React.Component {
|
||||
eventEmitter.emit('list:jump', linkHash.split('-')[1])
|
||||
return
|
||||
}
|
||||
|
||||
// other case
|
||||
shell.openExternal(href)
|
||||
}
|
||||
|
||||
render () {
|
||||
@@ -596,9 +594,12 @@ MarkdownPreview.propTypes = {
|
||||
onDoubleClick: PropTypes.func,
|
||||
onMouseUp: PropTypes.func,
|
||||
onMouseDown: PropTypes.func,
|
||||
onContextMenu: PropTypes.func,
|
||||
className: PropTypes.string,
|
||||
value: PropTypes.string,
|
||||
showCopyNotification: PropTypes.bool,
|
||||
storagePath: PropTypes.string,
|
||||
smartQuotes: PropTypes.bool
|
||||
smartQuotes: PropTypes.bool,
|
||||
smartArrows: PropTypes.bool,
|
||||
breaks: PropTypes.bool
|
||||
}
|
||||
|
||||
@@ -14,6 +14,10 @@ class MarkdownSplitEditor extends React.Component {
|
||||
this.focus = () => this.refs.code.focus()
|
||||
this.reload = () => this.refs.code.reload()
|
||||
this.userScroll = true
|
||||
this.state = {
|
||||
isSliderFocused: false,
|
||||
codeEditorWidthInPercent: 50
|
||||
}
|
||||
}
|
||||
|
||||
handleOnChange () {
|
||||
@@ -87,6 +91,42 @@ class MarkdownSplitEditor extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove (e) {
|
||||
if (this.state.isSliderFocused) {
|
||||
const rootRect = this.refs.root.getBoundingClientRect()
|
||||
const rootWidth = rootRect.width
|
||||
const offset = rootRect.left
|
||||
let newCodeEditorWidthInPercent = (e.pageX - offset) / rootWidth * 100
|
||||
|
||||
// limit minSize to 10%, maxSize to 90%
|
||||
if (newCodeEditorWidthInPercent <= 10) {
|
||||
newCodeEditorWidthInPercent = 10
|
||||
}
|
||||
|
||||
if (newCodeEditorWidthInPercent >= 90) {
|
||||
newCodeEditorWidthInPercent = 90
|
||||
}
|
||||
|
||||
this.setState({
|
||||
codeEditorWidthInPercent: newCodeEditorWidthInPercent
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseUp (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isSliderFocused: false
|
||||
})
|
||||
}
|
||||
|
||||
handleMouseDown (e) {
|
||||
e.preventDefault()
|
||||
this.setState({
|
||||
isSliderFocused: true
|
||||
})
|
||||
}
|
||||
|
||||
render () {
|
||||
const {config, value, storageKey, noteKey} = this.props
|
||||
const storage = findStorage(storageKey)
|
||||
@@ -95,12 +135,16 @@ class MarkdownSplitEditor extends React.Component {
|
||||
let editorIndentSize = parseInt(config.editor.indentSize, 10)
|
||||
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
|
||||
const previewStyle = {}
|
||||
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none'
|
||||
previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%'
|
||||
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none'
|
||||
return (
|
||||
<div styleName='root'>
|
||||
<div styleName='root' ref='root'
|
||||
onMouseMove={e => this.handleMouseMove(e)}
|
||||
onMouseUp={e => this.handleMouseUp(e)}>
|
||||
<CodeEditor
|
||||
styleName='codeEditor'
|
||||
ref='code'
|
||||
width={this.state.codeEditorWidthInPercent + '%'}
|
||||
mode='GitHub Flavored Markdown'
|
||||
value={value}
|
||||
theme={config.editor.theme}
|
||||
@@ -119,6 +163,9 @@ class MarkdownSplitEditor extends React.Component {
|
||||
onChange={this.handleOnChange.bind(this)}
|
||||
onScroll={this.handleScroll.bind(this)}
|
||||
/>
|
||||
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
|
||||
<div styleName='slider-hitbox' />
|
||||
</div>
|
||||
<MarkdownPreview
|
||||
style={previewStyle}
|
||||
styleName='preview'
|
||||
@@ -131,6 +178,8 @@ class MarkdownSplitEditor extends React.Component {
|
||||
lineNumber={config.preview.lineNumber}
|
||||
scrollPastEnd={config.preview.scrollPastEnd}
|
||||
smartQuotes={config.preview.smartQuotes}
|
||||
smartArrows={config.preview.smartArrows}
|
||||
breaks={config.preview.breaks}
|
||||
sanitize={config.preview.sanitize}
|
||||
ref='preview'
|
||||
tabInde='0'
|
||||
@@ -140,6 +189,8 @@ class MarkdownSplitEditor extends React.Component {
|
||||
showCopyNotification={config.ui.showCopyNotification}
|
||||
storagePath={storage.path}
|
||||
noteKey={noteKey}
|
||||
customCSS={config.preview.customCSS}
|
||||
allowCustomCSS={config.preview.allowCustomCSS}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -3,7 +3,14 @@
|
||||
height 100%
|
||||
font-size 30px
|
||||
display flex
|
||||
.codeEditor
|
||||
width 50%
|
||||
.preview
|
||||
width 50%
|
||||
.slider
|
||||
absolute top bottom
|
||||
top -2px
|
||||
width 0
|
||||
z-index 0
|
||||
.slider-hitbox
|
||||
absolute top bottom left right
|
||||
width 7px
|
||||
left -3px
|
||||
z-index 10
|
||||
cursor col-resize
|
||||
|
||||
@@ -134,6 +134,7 @@ body[data-theme="dark"]
|
||||
.item-simple-wrapper
|
||||
border-color transparent
|
||||
.item-simple-title
|
||||
.item-simple-title-empty
|
||||
.item-simple-title-icon
|
||||
.item-simple-bottom-time
|
||||
color $ui-dark-text-color
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
.iconWrap
|
||||
width 20px
|
||||
text-align center
|
||||
|
||||
|
||||
.counters
|
||||
float right
|
||||
color $ui-inactive-text-color
|
||||
@@ -68,10 +68,9 @@
|
||||
.menu-button-label
|
||||
position fixed
|
||||
display inline-block
|
||||
height 32px
|
||||
height 36px
|
||||
left 44px
|
||||
padding 0 10px
|
||||
margin-top -8px
|
||||
margin-left 0
|
||||
overflow ellipsis
|
||||
z-index 10
|
||||
|
||||
@@ -58,8 +58,8 @@
|
||||
opacity 0
|
||||
border-top-right-radius 2px
|
||||
border-bottom-right-radius 2px
|
||||
height 26px
|
||||
line-height 26px
|
||||
height 34px
|
||||
line-height 32px
|
||||
|
||||
.folderList-item:hover, .folderList-item--active:hover
|
||||
.folderList-item-tooltip
|
||||
|
||||
@@ -293,6 +293,84 @@ kbd
|
||||
line-height 1
|
||||
padding 3px 5px
|
||||
|
||||
$admonition
|
||||
box-shadow 0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)
|
||||
position relative
|
||||
margin 1.5625em 0
|
||||
padding 0 1.2rem
|
||||
border-left .4rem solid #448aff
|
||||
border-radius .2rem
|
||||
overflow auto
|
||||
|
||||
html .admonition>:last-child
|
||||
margin-bottom 1.2rem
|
||||
|
||||
.admonition .admonition
|
||||
margin 1em 0
|
||||
|
||||
.admonition p
|
||||
margin-top: 0.5em
|
||||
|
||||
$admonition-icon
|
||||
position absolute
|
||||
left 1.2rem
|
||||
font-family: "Material Icons"
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-size: 24px
|
||||
display: inline-block;
|
||||
line-height: 1;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
|
||||
/* Support for all WebKit browsers. */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
/* Support for Safari and Chrome. */
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
/* Support for Firefox. */
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
/* Support for IE. */
|
||||
font-feature-settings: 'liga';
|
||||
|
||||
$admonition-title
|
||||
margin 0 -1.2rem
|
||||
padding .8rem 1.2rem .8rem 4rem
|
||||
border-bottom .1rem solid rgba(68,138,255,.1)
|
||||
background-color rgba(68,138,255,.1)
|
||||
font-weight 700
|
||||
|
||||
.admonition>.admonition-title:last-child
|
||||
margin-bottom 0
|
||||
|
||||
admonition_types = {
|
||||
note: {color: #0288D1, icon: "note"},
|
||||
hint: {color: #009688, icon: "info_outline"},
|
||||
danger: {color: #c2185b, icon: "block"},
|
||||
caution: {color: #ffa726, icon: "warning"},
|
||||
error: {color: #d32f2f, icon: "error_outline"},
|
||||
attention: {color: #455a64, icon: "priority_high"}
|
||||
}
|
||||
|
||||
for name, val in admonition_types
|
||||
.admonition.{name}
|
||||
@extend $admonition
|
||||
border-left-color: val[color]
|
||||
|
||||
.admonition.{name}>.admonition-title
|
||||
@extend $admonition-title
|
||||
border-bottom-color: .1rem solid rgba(val[color], 0.2)
|
||||
background-color: rgba(val[color], 0.2)
|
||||
|
||||
.admonition.{name}>.admonition-title:before
|
||||
@extend $admonition-icon
|
||||
color: val[color]
|
||||
content: val[icon]
|
||||
|
||||
themeDarkBackground = darken(#21252B, 10%)
|
||||
themeDarkText = #f9f9f9
|
||||
themeDarkBorder = lighten(themeDarkBackground, 20%)
|
||||
@@ -396,4 +474,6 @@ body[data-theme="monokai"]
|
||||
td
|
||||
border-color themeMonokaiTableBorder
|
||||
&:last-child
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
border-right solid 1px themeMonokaiTableBorder
|
||||
kbd
|
||||
background-color themeDarkBackground
|
||||
Reference in New Issue
Block a user