You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

452 lines
13 KiB

  1. <template lang='pug'>
  2. .editor-code
  3. .editor-code-main
  4. .editor-code-sidebar
  5. v-tooltip(right, color='teal')
  6. template(v-slot:activator='{ on }')
  7. v-btn.animated.fadeInLeft(icon, tile, v-on='on', dark, disabled).mx-0
  8. v-icon mdi-link-plus
  9. span {{$t('editor:markup.insertLink')}}
  10. v-tooltip(right, color='teal')
  11. template(v-slot:activator='{ on }')
  12. v-btn.mt-3.animated.fadeInLeft.wait-p1s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalMedia`)').mx-0
  13. v-icon(:color='activeModal === `editorModalMedia` ? `teal` : ``') mdi-folder-multiple-image
  14. span {{$t('editor:markup.insertAssets')}}
  15. v-tooltip(right, color='teal')
  16. template(v-slot:activator='{ on }')
  17. v-btn.mt-3.animated.fadeInLeft.wait-p2s(icon, tile, v-on='on', dark, @click='toggleModal(`editorModalBlocks`)', disabled).mx-0
  18. v-icon(:color='activeModal === `editorModalBlocks` ? `teal` : ``') mdi-view-dashboard-outline
  19. span {{$t('editor:markup.insertBlock')}}
  20. v-tooltip(right, color='teal')
  21. template(v-slot:activator='{ on }')
  22. v-btn.mt-3.animated.fadeInLeft.wait-p3s(icon, tile, v-on='on', dark, disabled).mx-0
  23. v-icon mdi-code-braces
  24. span {{$t('editor:markup.insertCodeBlock')}}
  25. v-tooltip(right, color='teal')
  26. template(v-slot:activator='{ on }')
  27. v-btn.mt-3.animated.fadeInLeft.wait-p4s(icon, tile, v-on='on', dark, disabled).mx-0
  28. v-icon mdi-library-video
  29. span {{$t('editor:markup.insertVideoAudio')}}
  30. v-tooltip(right, color='teal')
  31. template(v-slot:activator='{ on }')
  32. v-btn.mt-3.animated.fadeInLeft.wait-p5s(icon, tile, v-on='on', dark, disabled).mx-0
  33. v-icon mdi-chart-multiline
  34. span {{$t('editor:markup.insertDiagram')}}
  35. v-tooltip(right, color='teal')
  36. template(v-slot:activator='{ on }')
  37. v-btn.mt-3.animated.fadeInLeft.wait-p6s(icon, tile, v-on='on', dark, disabled).mx-0
  38. v-icon mdi-function-variant
  39. span {{$t('editor:markup.insertMathExpression')}}
  40. template(v-if='$vuetify.breakpoint.mdAndUp')
  41. v-spacer
  42. v-tooltip(right, color='teal')
  43. template(v-slot:activator='{ on }')
  44. v-btn.mt-3.animated.fadeInLeft.wait-p8s(icon, tile, v-on='on', dark, @click='toggleFullscreen').mx-0
  45. v-icon mdi-arrow-expand-all
  46. span {{$t('editor:markup.distractionFreeMode')}}
  47. .editor-code-editor
  48. textarea(ref='cm')
  49. v-system-bar.editor-code-sysbar(dark, status, color='grey darken-3')
  50. .caption.editor-code-sysbar-locale {{locale.toUpperCase()}}
  51. .caption.px-3 /{{path}}
  52. template(v-if='$vuetify.breakpoint.mdAndUp')
  53. v-spacer
  54. .caption Code
  55. v-spacer
  56. .caption Ln {{cursorPos.line + 1}}, Col {{cursorPos.ch + 1}}
  57. </template>
  58. <script>
  59. import _ from 'lodash'
  60. import { get, sync } from 'vuex-pathify'
  61. // ========================================
  62. // IMPORTS
  63. // ========================================
  64. // Code Mirror
  65. import CodeMirror from 'codemirror'
  66. import 'codemirror/lib/codemirror.css'
  67. // Language
  68. import 'codemirror/mode/htmlmixed/htmlmixed.js'
  69. // Addons
  70. import 'codemirror/addon/selection/active-line.js'
  71. import 'codemirror/addon/display/fullscreen.js'
  72. import 'codemirror/addon/display/fullscreen.css'
  73. import 'codemirror/addon/selection/mark-selection.js'
  74. import 'codemirror/addon/search/searchcursor.js'
  75. // ========================================
  76. // INIT
  77. // ========================================
  78. // Platform detection
  79. // const CtrlKey = /Mac/.test(navigator.platform) ? 'Cmd' : 'Ctrl'
  80. // ========================================
  81. // Vue Component
  82. // ========================================
  83. export default {
  84. data() {
  85. return {
  86. cm: null,
  87. cursorPos: { ch: 0, line: 1 }
  88. }
  89. },
  90. computed: {
  91. isMobile() {
  92. return this.$vuetify.breakpoint.smAndDown
  93. },
  94. locale: get('page/locale'),
  95. path: get('page/path'),
  96. mode: get('editor/mode'),
  97. activeModal: sync('editor/activeModal')
  98. },
  99. methods: {
  100. toggleModal(key) {
  101. this.activeModal = (this.activeModal === key) ? '' : key
  102. this.helpShown = false
  103. },
  104. closeAllModal() {
  105. this.activeModal = ''
  106. this.helpShown = false
  107. },
  108. /**
  109. * Insert content at cursor
  110. */
  111. insertAtCursor({ content }) {
  112. const cursor = this.cm.doc.getCursor('head')
  113. this.cm.doc.replaceRange(content, cursor)
  114. },
  115. /**
  116. * Insert content after current line
  117. */
  118. insertAfter({ content, newLine }) {
  119. const curLine = this.cm.doc.getCursor('to').line
  120. const lineLength = this.cm.doc.getLine(curLine).length
  121. this.cm.doc.replaceRange(newLine ? `\n${content}\n` : content, { line: curLine, ch: lineLength + 1 })
  122. },
  123. /**
  124. * Insert content before current line
  125. */
  126. insertBeforeEachLine({ content, after }) {
  127. let lines = []
  128. if (!this.cm.doc.somethingSelected()) {
  129. lines.push(this.cm.doc.getCursor('head').line)
  130. } else {
  131. lines = _.flatten(this.cm.doc.listSelections().map(sl => {
  132. const range = Math.abs(sl.anchor.line - sl.head.line) + 1
  133. const lowestLine = (sl.anchor.line > sl.head.line) ? sl.head.line : sl.anchor.line
  134. return _.times(range, l => l + lowestLine)
  135. }))
  136. }
  137. lines.forEach(ln => {
  138. let lineContent = this.cm.doc.getLine(ln)
  139. const lineLength = lineContent.length
  140. if (_.startsWith(lineContent, content)) {
  141. lineContent = lineContent.substring(content.length)
  142. }
  143. this.cm.doc.replaceRange(content + lineContent, { line: ln, ch: 0 }, { line: ln, ch: lineLength })
  144. })
  145. if (after) {
  146. const lastLine = _.last(lines)
  147. this.cm.doc.replaceRange(`\n${after}\n`, { line: lastLine, ch: this.cm.doc.getLine(lastLine).length + 1 })
  148. }
  149. },
  150. /**
  151. * Update cursor state
  152. */
  153. positionSync(cm) {
  154. this.cursorPos = cm.getCursor('head')
  155. },
  156. toggleFullscreen () {
  157. this.cm.setOption('fullScreen', true)
  158. },
  159. refresh() {
  160. this.$nextTick(() => {
  161. this.cm.refresh()
  162. })
  163. }
  164. },
  165. mounted() {
  166. this.$store.set('editor/editorKey', 'code')
  167. if (this.mode === 'create') {
  168. this.$store.set('editor/content', '<h1>Title</h1>\n\n<p>Some text here</p>')
  169. }
  170. // Initialize CodeMirror
  171. this.cm = CodeMirror.fromTextArea(this.$refs.cm, {
  172. tabSize: 2,
  173. mode: 'text/html',
  174. theme: 'wikijs-dark',
  175. lineNumbers: true,
  176. lineWrapping: true,
  177. line: true,
  178. styleActiveLine: true,
  179. highlightSelectionMatches: {
  180. annotateScrollbar: true
  181. },
  182. viewportMargin: 50,
  183. inputStyle: 'contenteditable',
  184. allowDropFileTypes: ['image/jpg', 'image/png', 'image/svg', 'image/jpeg', 'image/gif']
  185. })
  186. this.cm.setValue(this.$store.get('editor/content'))
  187. this.cm.on('change', c => {
  188. this.$store.set('editor/content', c.getValue())
  189. })
  190. if (this.$vuetify.breakpoint.mdAndUp) {
  191. this.cm.setSize(null, 'calc(100vh - 64px - 24px)')
  192. } else {
  193. this.cm.setSize(null, 'calc(100vh - 56px - 16px)')
  194. }
  195. // Set Keybindings
  196. const keyBindings = {
  197. 'F11' (c) {
  198. c.setOption('fullScreen', !c.getOption('fullScreen'))
  199. },
  200. 'Esc' (c) {
  201. if (c.getOption('fullScreen')) c.setOption('fullScreen', false)
  202. }
  203. }
  204. this.cm.setOption('extraKeys', keyBindings)
  205. // Handle cursor movement
  206. this.cm.on('cursorActivity', c => {
  207. this.positionSync(c)
  208. })
  209. // Render initial preview
  210. this.$root.$on('editorInsert', opts => {
  211. switch (opts.kind) {
  212. case 'IMAGE':
  213. let img = `<img src="${opts.path}" alt="${opts.text}"`
  214. if (opts.align && opts.align !== '') {
  215. img += ` class="align-${opts.align}"`
  216. }
  217. img += ` />`
  218. this.insertAtCursor({
  219. content: img
  220. })
  221. break
  222. case 'BINARY':
  223. this.insertAtCursor({
  224. content: `<a href="${opts.path}" title="${opts.text}">${opts.text}</a>`
  225. })
  226. break
  227. }
  228. })
  229. // Handle save conflict
  230. this.$root.$on('saveConflict', () => {
  231. this.toggleModal(`editorModalConflict`)
  232. })
  233. this.$root.$on('overwriteEditorContent', () => {
  234. this.cm.setValue(this.$store.get('editor/content'))
  235. })
  236. },
  237. beforeDestroy() {
  238. this.$root.$off('editorInsert')
  239. }
  240. }
  241. </script>
  242. <style lang='scss'>
  243. $editor-height: calc(100vh - 64px - 24px);
  244. $editor-height-mobile: calc(100vh - 56px - 16px);
  245. .editor-code {
  246. &-main {
  247. display: flex;
  248. width: 100%;
  249. }
  250. &-editor {
  251. background-color: darken(mc('grey', '900'), 4.5%);
  252. flex: 1 1 50%;
  253. display: block;
  254. height: $editor-height;
  255. position: relative;
  256. &-title {
  257. background-color: mc('grey', '800');
  258. border-bottom-left-radius: 5px;
  259. display: inline-flex;
  260. height: 30px;
  261. justify-content: center;
  262. align-items: center;
  263. padding: 0 1rem;
  264. color: mc('grey', '500');
  265. position: absolute;
  266. top: 0;
  267. right: 0;
  268. z-index: 7;
  269. text-transform: uppercase;
  270. font-size: .7rem;
  271. cursor: pointer;
  272. @include until($tablet) {
  273. display: none;
  274. }
  275. }
  276. }
  277. &-sidebar {
  278. background-color: mc('grey', '900');
  279. width: 64px;
  280. display: flex;
  281. flex-direction: column;
  282. justify-content: flex-start;
  283. align-items: center;
  284. padding: 24px 0;
  285. @include until($tablet) {
  286. padding: 12px 0;
  287. width: 40px;
  288. }
  289. }
  290. &-sysbar {
  291. padding-left: 0;
  292. &-locale {
  293. background-color: rgba(255,255,255,.25);
  294. display:inline-flex;
  295. padding: 0 12px;
  296. height: 24px;
  297. width: 63px;
  298. justify-content: center;
  299. align-items: center;
  300. }
  301. }
  302. // ==========================================
  303. // CODE MIRROR
  304. // ==========================================
  305. .CodeMirror {
  306. height: auto;
  307. .cm-header-1 {
  308. font-size: 1.5rem;
  309. }
  310. .cm-header-2 {
  311. font-size: 1.25rem;
  312. }
  313. .cm-header-3 {
  314. font-size: 1.15rem;
  315. }
  316. .cm-header-4 {
  317. font-size: 1.1rem;
  318. }
  319. .cm-header-5 {
  320. font-size: 1.05rem;
  321. }
  322. .cm-header-6 {
  323. font-size: 1.025rem;
  324. }
  325. }
  326. .CodeMirror-focused .cm-matchhighlight {
  327. background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
  328. background-position: bottom;
  329. background-repeat: repeat-x;
  330. }
  331. .cm-matchhighlight {
  332. background-color: mc('grey', '800');
  333. }
  334. .CodeMirror-selection-highlight-scrollbar {
  335. background-color: mc('green', '600');
  336. }
  337. .cm-s-wikijs-dark.CodeMirror {
  338. background: darken(mc('grey','900'), 3%);
  339. color: #e0e0e0;
  340. }
  341. .cm-s-wikijs-dark div.CodeMirror-selected {
  342. background: mc('blue','800');
  343. }
  344. .cm-s-wikijs-dark .cm-matchhighlight {
  345. background: mc('blue','800');
  346. }
  347. .cm-s-wikijs-dark .CodeMirror-line::selection, .cm-s-wikijs-dark .CodeMirror-line > span::selection, .cm-s-wikijs-dark .CodeMirror-line > span > span::selection {
  348. background: mc('amber', '500');
  349. }
  350. .cm-s-wikijs-dark .CodeMirror-line::-moz-selection, .cm-s-wikijs-dark .CodeMirror-line > span::-moz-selection, .cm-s-wikijs-dark .CodeMirror-line > span > span::-moz-selection {
  351. background: mc('amber', '500');
  352. }
  353. .cm-s-wikijs-dark .CodeMirror-gutters {
  354. background: darken(mc('grey','900'), 6%);
  355. border-right: 1px solid mc('grey','900');
  356. }
  357. .cm-s-wikijs-dark .CodeMirror-guttermarker {
  358. color: #ac4142;
  359. }
  360. .cm-s-wikijs-dark .CodeMirror-guttermarker-subtle {
  361. color: #505050;
  362. }
  363. .cm-s-wikijs-dark .CodeMirror-linenumber {
  364. color: mc('grey','800');
  365. }
  366. .cm-s-wikijs-dark .CodeMirror-cursor {
  367. border-left: 1px solid #b0b0b0;
  368. }
  369. .cm-s-wikijs-dark span.cm-comment {
  370. color: mc('orange','800');
  371. }
  372. .cm-s-wikijs-dark span.cm-atom {
  373. color: #aa759f;
  374. }
  375. .cm-s-wikijs-dark span.cm-number {
  376. color: #aa759f;
  377. }
  378. .cm-s-wikijs-dark span.cm-property, .cm-s-wikijs-dark span.cm-attribute {
  379. color: #90a959;
  380. }
  381. .cm-s-wikijs-dark span.cm-keyword {
  382. color: #ac4142;
  383. }
  384. .cm-s-wikijs-dark span.cm-string {
  385. color: #f4bf75;
  386. }
  387. .cm-s-wikijs-dark span.cm-variable {
  388. color: #90a959;
  389. }
  390. .cm-s-wikijs-dark span.cm-variable-2 {
  391. color: #6a9fb5;
  392. }
  393. .cm-s-wikijs-dark span.cm-def {
  394. color: #d28445;
  395. }
  396. .cm-s-wikijs-dark span.cm-bracket {
  397. color: #e0e0e0;
  398. }
  399. .cm-s-wikijs-dark span.cm-tag {
  400. color: #ac4142;
  401. }
  402. .cm-s-wikijs-dark span.cm-link {
  403. color: #aa759f;
  404. }
  405. .cm-s-wikijs-dark span.cm-error {
  406. background: #ac4142;
  407. color: #b0b0b0;
  408. }
  409. .cm-s-wikijs-dark .CodeMirror-activeline-background {
  410. background: mc('grey','900');
  411. }
  412. .cm-s-wikijs-dark .CodeMirror-matchingbracket {
  413. text-decoration: underline;
  414. color: white !important;
  415. }
  416. }
  417. </style>