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.

444 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. },
  230. beforeDestroy() {
  231. this.$root.$off('editorInsert')
  232. }
  233. }
  234. </script>
  235. <style lang='scss'>
  236. $editor-height: calc(100vh - 64px - 24px);
  237. $editor-height-mobile: calc(100vh - 56px - 16px);
  238. .editor-code {
  239. &-main {
  240. display: flex;
  241. width: 100%;
  242. }
  243. &-editor {
  244. background-color: darken(mc('grey', '900'), 4.5%);
  245. flex: 1 1 50%;
  246. display: block;
  247. height: $editor-height;
  248. position: relative;
  249. &-title {
  250. background-color: mc('grey', '800');
  251. border-bottom-left-radius: 5px;
  252. display: inline-flex;
  253. height: 30px;
  254. justify-content: center;
  255. align-items: center;
  256. padding: 0 1rem;
  257. color: mc('grey', '500');
  258. position: absolute;
  259. top: 0;
  260. right: 0;
  261. z-index: 7;
  262. text-transform: uppercase;
  263. font-size: .7rem;
  264. cursor: pointer;
  265. @include until($tablet) {
  266. display: none;
  267. }
  268. }
  269. }
  270. &-sidebar {
  271. background-color: mc('grey', '900');
  272. width: 64px;
  273. display: flex;
  274. flex-direction: column;
  275. justify-content: flex-start;
  276. align-items: center;
  277. padding: 24px 0;
  278. @include until($tablet) {
  279. padding: 12px 0;
  280. width: 40px;
  281. }
  282. }
  283. &-sysbar {
  284. padding-left: 0;
  285. &-locale {
  286. background-color: rgba(255,255,255,.25);
  287. display:inline-flex;
  288. padding: 0 12px;
  289. height: 24px;
  290. width: 63px;
  291. justify-content: center;
  292. align-items: center;
  293. }
  294. }
  295. // ==========================================
  296. // CODE MIRROR
  297. // ==========================================
  298. .CodeMirror {
  299. height: auto;
  300. .cm-header-1 {
  301. font-size: 1.5rem;
  302. }
  303. .cm-header-2 {
  304. font-size: 1.25rem;
  305. }
  306. .cm-header-3 {
  307. font-size: 1.15rem;
  308. }
  309. .cm-header-4 {
  310. font-size: 1.1rem;
  311. }
  312. .cm-header-5 {
  313. font-size: 1.05rem;
  314. }
  315. .cm-header-6 {
  316. font-size: 1.025rem;
  317. }
  318. }
  319. .CodeMirror-focused .cm-matchhighlight {
  320. background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAFklEQVQI12NgYGBgkKzc8x9CMDAwAAAmhwSbidEoSQAAAABJRU5ErkJggg==);
  321. background-position: bottom;
  322. background-repeat: repeat-x;
  323. }
  324. .cm-matchhighlight {
  325. background-color: mc('grey', '800');
  326. }
  327. .CodeMirror-selection-highlight-scrollbar {
  328. background-color: mc('green', '600');
  329. }
  330. .cm-s-wikijs-dark.CodeMirror {
  331. background: darken(mc('grey','900'), 3%);
  332. color: #e0e0e0;
  333. }
  334. .cm-s-wikijs-dark div.CodeMirror-selected {
  335. background: mc('blue','800');
  336. }
  337. .cm-s-wikijs-dark .cm-matchhighlight {
  338. background: mc('blue','800');
  339. }
  340. .cm-s-wikijs-dark .CodeMirror-line::selection, .cm-s-wikijs-dark .CodeMirror-line > span::selection, .cm-s-wikijs-dark .CodeMirror-line > span > span::selection {
  341. background: mc('amber', '500');
  342. }
  343. .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 {
  344. background: mc('amber', '500');
  345. }
  346. .cm-s-wikijs-dark .CodeMirror-gutters {
  347. background: darken(mc('grey','900'), 6%);
  348. border-right: 1px solid mc('grey','900');
  349. }
  350. .cm-s-wikijs-dark .CodeMirror-guttermarker {
  351. color: #ac4142;
  352. }
  353. .cm-s-wikijs-dark .CodeMirror-guttermarker-subtle {
  354. color: #505050;
  355. }
  356. .cm-s-wikijs-dark .CodeMirror-linenumber {
  357. color: mc('grey','800');
  358. }
  359. .cm-s-wikijs-dark .CodeMirror-cursor {
  360. border-left: 1px solid #b0b0b0;
  361. }
  362. .cm-s-wikijs-dark span.cm-comment {
  363. color: mc('orange','800');
  364. }
  365. .cm-s-wikijs-dark span.cm-atom {
  366. color: #aa759f;
  367. }
  368. .cm-s-wikijs-dark span.cm-number {
  369. color: #aa759f;
  370. }
  371. .cm-s-wikijs-dark span.cm-property, .cm-s-wikijs-dark span.cm-attribute {
  372. color: #90a959;
  373. }
  374. .cm-s-wikijs-dark span.cm-keyword {
  375. color: #ac4142;
  376. }
  377. .cm-s-wikijs-dark span.cm-string {
  378. color: #f4bf75;
  379. }
  380. .cm-s-wikijs-dark span.cm-variable {
  381. color: #90a959;
  382. }
  383. .cm-s-wikijs-dark span.cm-variable-2 {
  384. color: #6a9fb5;
  385. }
  386. .cm-s-wikijs-dark span.cm-def {
  387. color: #d28445;
  388. }
  389. .cm-s-wikijs-dark span.cm-bracket {
  390. color: #e0e0e0;
  391. }
  392. .cm-s-wikijs-dark span.cm-tag {
  393. color: #ac4142;
  394. }
  395. .cm-s-wikijs-dark span.cm-link {
  396. color: #aa759f;
  397. }
  398. .cm-s-wikijs-dark span.cm-error {
  399. background: #ac4142;
  400. color: #b0b0b0;
  401. }
  402. .cm-s-wikijs-dark .CodeMirror-activeline-background {
  403. background: mc('grey','900');
  404. }
  405. .cm-s-wikijs-dark .CodeMirror-matchingbracket {
  406. text-decoration: underline;
  407. color: white !important;
  408. }
  409. }
  410. </style>