diff --git a/modules/backend/formwidgets/MarkdownEditor.php b/modules/backend/formwidgets/MarkdownEditor.php index d17fc6e83..70e490205 100644 --- a/modules/backend/formwidgets/MarkdownEditor.php +++ b/modules/backend/formwidgets/MarkdownEditor.php @@ -17,6 +17,11 @@ class MarkdownEditor extends FormWidgetBase // Configurable properties // + /** + * @var bool Display mode: split, tab. + */ + public $mode = 'tab'; + // // Object properties // @@ -31,7 +36,9 @@ class MarkdownEditor extends FormWidgetBase */ public function init() { - $this->fillFromConfig([]); + $this->fillFromConfig([ + 'mode', + ]); } /** @@ -48,6 +55,7 @@ class MarkdownEditor extends FormWidgetBase */ public function prepareVars() { + $this->vars['mode'] = $this->mode; $this->vars['stretch'] = $this->formField->stretch; $this->vars['size'] = $this->formField->size; $this->vars['name'] = $this->formField->getName(); diff --git a/modules/backend/formwidgets/markdowneditor/assets/css/markdowneditor.css b/modules/backend/formwidgets/markdowneditor/assets/css/markdowneditor.css index 6f86999c7..c4a22f216 100644 --- a/modules/backend/formwidgets/markdowneditor/assets/css/markdowneditor.css +++ b/modules/backend/formwidgets/markdowneditor/assets/css/markdowneditor.css @@ -5,6 +5,8 @@ background: #fff; -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; border-radius: 5px; } .field-markdowneditor textarea { @@ -18,35 +20,58 @@ .field-markdowneditor.editor-focus { border: 1px solid #808c8d; } -.field-markdowneditor.size-tiny .editor-write, +.field-markdowneditor.size-tiny .editor-write { + min-height: 50px; +} .field-markdowneditor.size-tiny .editor-preview { height: 50px; } -.field-markdowneditor.size-small .editor-write, +.field-markdowneditor.size-small .editor-write { + min-height: 100px; +} .field-markdowneditor.size-small .editor-preview { height: 100px; } -.field-markdowneditor.size-large .editor-write, +.field-markdowneditor.size-large .editor-write { + min-height: 200px; +} .field-markdowneditor.size-large .editor-preview { height: 200px; } -.field-markdowneditor.size-huge .editor-write, +.field-markdowneditor.size-huge .editor-write { + min-height: 250px; +} .field-markdowneditor.size-huge .editor-preview { height: 250px; } -.field-markdowneditor.size-giant .editor-write, +.field-markdowneditor.size-giant .editor-write { + min-height: 350px; +} .field-markdowneditor.size-giant .editor-preview { height: 350px; } .field-markdowneditor .editor-write { position: relative; - margin: 15px; } .field-markdowneditor .editor-preview { padding: 15px; + overflow: auto; +} +.field-markdowneditor .editor-preview-loader { + display: block; + width: 20px; + height: 20px; + position: absolute; + right: 10px; + top: 10px; + margin-top: 40px; + background-image: url('../../../../../system/assets/ui/images/loader-transparent.svg'); + background-size: 20px 20px; + background-position: 50% 50%; + -webkit-animation: spin 1s linear infinite; + animation: spin 1s linear infinite; } .field-markdowneditor.mode-tab .editor-preview { - overflow: auto; display: none; } .field-markdowneditor.mode-tab.is-preview .editor-write { @@ -55,3 +80,119 @@ .field-markdowneditor.mode-tab.is-preview .editor-preview { display: block; } +.field-markdowneditor.mode-split .editor-preview { + float: right; + width: 50%; +} +.field-markdowneditor.mode-split .editor-write { + float: left; + width: 50%; +} +.field-markdowneditor.mode-split .editor-write .editor-code { + border-right: 2px solid #808C8D; +} +.field-markdowneditor.stretch, +.field-markdowneditor.stretch .editor-toolbar { + border-radius: 0 !important; +} +.field-markdowneditor.stretch .editor-toolbar { + height: auto; +} +.field-markdowneditor.stretch .editor-write, +.field-markdowneditor.stretch .editor-preview { + float: none; + height: auto; + position: absolute; + right: 0; + top: 0; + bottom: 0; + margin-top: 40px; +} +.field-markdowneditor.stretch .editor-write { + left: 0; + right: auto; +} +.field-markdowneditor.is-fullscreen { + z-index: 1020; + position: fixed !important; + top: 0; + left: 0; + width: 100%; +} +.field-markdowneditor.is-fullscreen, +.field-markdowneditor.is-fullscreen .editor-toolbar { + border-radius: 0 !important; + border: none; +} +.field-markdowneditor .editor-preview { + color: #515c5d; + font-family: "Helvetica", sans-serif; + line-height: 180%; +} +.field-markdowneditor .editor-preview h1, +.field-markdowneditor .editor-preview h2, +.field-markdowneditor .editor-preview h3, +.field-markdowneditor .editor-preview h4, +.field-markdowneditor .editor-preview h5, +.field-markdowneditor .editor-preview h6 { + margin-top: 20px; + font-weight: bold; +} +.field-markdowneditor .editor-preview h1:first-child, +.field-markdowneditor .editor-preview h2:first-child, +.field-markdowneditor .editor-preview h3:first-child, +.field-markdowneditor .editor-preview h4:first-child, +.field-markdowneditor .editor-preview h5:first-child, +.field-markdowneditor .editor-preview h6:first-child { + margin-top: 0; +} +.field-markdowneditor .editor-preview *:last-child { + margin-bottom: 0; +} +.field-markdowneditor .editor-preview h1 { + font-size: 30px; +} +.field-markdowneditor .editor-preview h2 { + font-size: 26px; +} +.field-markdowneditor .editor-preview h3 { + font-size: 24px; +} +.field-markdowneditor .editor-preview h4 { + font-size: 22px; +} +.field-markdowneditor .editor-preview h5 { + font-size: 20px; +} +.field-markdowneditor .editor-preview h6 { + font-size: 18px; +} +.field-markdowneditor .editor-preview p, +.field-markdowneditor .editor-preview ol, +.field-markdowneditor .editor-preview ul { + font-size: 14px; +} +.field-markdowneditor .editor-preview h1, +.field-markdowneditor .editor-preview h2, +.field-markdowneditor .editor-preview h3, +.field-markdowneditor .editor-preview h4, +.field-markdowneditor .editor-preview h5, +.field-markdowneditor .editor-preview h6, +.field-markdowneditor .editor-preview p, +.field-markdowneditor .editor-preview ol, +.field-markdowneditor .editor-preview ul { + margin-bottom: 15px; +} +.field-markdowneditor .editor-preview pre.prettyprint { + border-width: 0; + padding: 13px 16px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + line-height: 130%; +} +.field-markdowneditor .editor-preview img { + display: block; + max-width: 100%; + height: auto; +} diff --git a/modules/backend/formwidgets/markdowneditor/assets/js/markdowneditor.js b/modules/backend/formwidgets/markdowneditor/assets/js/markdowneditor.js index 1f5c1fb17..6f2fc4e3e 100644 --- a/modules/backend/formwidgets/markdowneditor/assets/js/markdowneditor.js +++ b/modules/backend/formwidgets/markdowneditor/assets/js/markdowneditor.js @@ -14,6 +14,8 @@ this.$form = null this.$buttons = null this.$fixedButtons = null + this.$indicator = null + this.editorPadding = 15 $.oc.foundation.controlUtils.markDisposable(element) Base.call(this) @@ -33,14 +35,17 @@ this.$el.attr('id', 'element-' + Math.random().toString(36).substring(7)) } - this.$el.addClass('mode-' + this.options.viewMode) this.$form = this.$el.closest('form') this.createCodeContainer() this.createToolbar() + this.createIndicator() + this.setViewMode(this.options.viewMode) this.$toolbar.on('click', '.btn, .md-dropdown-button', this.proxy(this.onClickToolbarButton)) this.$form.on('oc.beforeRequest', this.proxy(this.onBeforeRequest)) + this.editor.on('change', this.proxy(this.onEditorChange)) + this.editor.getSession().on('changeScrollTop', this.proxy(this.onEditorScrollTop)) $('[data-control="tooltip"]', this.$toolbar).tooltip() $('[data-toggle="dropdown"]', this.$toolbar).dropdown() @@ -48,8 +53,11 @@ MarkdownEditor.prototype.dispose = function() { this.$el.off('dispose-control', this.proxy(this.dispose)) + this.$toolbar.off('click', '.btn, .md-dropdown-button', this.proxy(this.onClickToolbarButton)) this.$form.off('oc.beforeRequest', this.proxy(this.onBeforeRequest)) + this.editor.off('change', this.proxy(this.onEditorChange)) + $(window).off('resize', this.proxy(this.updateFullscreen)) this.$el.removeData('oc.markdownEditor') @@ -63,31 +71,18 @@ this.$form = null this.$buttons = null this.$fixedButtons = null + this.$indicator = null - this.isSplitMode = false - this.isPreview = false - this.isFullscreen = false + this.isSplitMode = null + this.isPreview = null + this.isFullscreen = null + this.dataTrackInputTimer = null this.options = null BaseProto.dispose.call(this) } - MarkdownEditor.prototype.onClickToolbarButton = function(ev) { - var $button = $(ev.target), - action = $button.data('button-action'), - template = $button.data('button-template') - - $button.blur() - - if (template) { - this[action](template) - } - else { - this[action]() - } - } - MarkdownEditor.prototype.createToolbar = function() { var self = this, $button, @@ -152,6 +147,10 @@ 'data-button-action': childButton.action }) + if (childButton.template) { + $childButton.attr('data-button-template', childButton.template) + } + if (childButton.cssClass) { $childButton.addClass(childButton.cssClass) } @@ -187,8 +186,22 @@ */ var editor = this.editor = ace.edit(this.$code.attr('id')) + // Fixes a weird notice about scrolling + editor.$blockScrolling = Infinity + editor.getSession().setValue(this.$textarea.val()) + /* + * Set theme, anticipated languages should be preloaded + */ + assetManager.load({ + js:[ + this.options.vendorPath + '/theme-github.js' + ] + }, function(){ + editor.setTheme('ace/theme/github') + }) + editor.getSession().setMode({ path: 'ace/mode/markdown' }) editor.setHighlightActiveLine(false) editor.renderer.setShowGutter(false) @@ -197,17 +210,88 @@ editor.setFontSize(14) editor.on('blur', this.proxy(this.onBlur)) editor.on('focus', this.proxy(this.onFocus)) + + // Set the vendor path for Ace's require path + ace.require('ace/config').set('basePath', this.options.vendorPath) + + editor.renderer.setPadding(this.editorPadding) + editor.renderer.setScrollMargin(this.editorPadding, this.editorPadding, 0, 0) + + setTimeout(function() { + editor.resize() + }, 100) } - MarkdownEditor.prototype.updatePreview = function() { + // + // Events + // + + MarkdownEditor.prototype.onClickToolbarButton = function(ev) { + var $button = $(ev.target), + action = $button.data('button-action'), + template = $button.data('button-template') + + $button.blur() + + if (template) { + this[action](template) + } + else { + this[action]() + } + } + + MarkdownEditor.prototype.onEditorScrollTop = function(scroll) { + if (!this.isSplitMode) return + + var canvasHeight = this.$preview.height(), + editorHeight, + previewHeight, + scrollRatio + + if (canvasHeight != this.$el.data('markdowneditor-canvas-height')) { + + editorHeight = + (this.editor.getSession().getScreenLength() * + this.editor.renderer.lineHeight) - + canvasHeight + + previewHeight = this.$preview.get(0).scrollHeight - canvasHeight + + scrollRatio = previewHeight / editorHeight + + this.$el.data('markdowneditor-canvas-height', canvasHeight) + this.$el.data('markdowneditor-scroll-ratio', scrollRatio) + } + else { + scrollRatio = this.$el.data('markdowneditor-scroll-ratio') + } + + scroll += this.editorPadding + this.$preview.scrollTop(scroll * scrollRatio) + } + + MarkdownEditor.prototype.onEditorChange = function() { + this.$form.trigger('change') + var self = this - this.$el.request(this.options.refreshHandler, { - success: function(data) { - this.success(data) - self.$preview.html(data.preview) + if (!this.isSplitMode) return + + if (this.loading) { + if (this.dataTrackInputTimer === undefined) { + this.dataTrackInputTimer = window.setInterval(function(){ + self.onEditorChange() + }, 100) } - }) + + return + } + + window.clearTimeout(this.dataTrackInputTimer) + this.dataTrackInputTimer = undefined + + self.updatePreview() } MarkdownEditor.prototype.onBeforeRequest = function() { @@ -226,21 +310,171 @@ this.$el.addClass('editor-focus') } - /* - * Button actions - */ + // + // Preview + // + + MarkdownEditor.prototype.updatePreview = function() { + var self = this + + this.loading = true + this.showIndicator() + + this.$el.request(this.options.refreshHandler, { + success: function(data) { + this.success(data) + self.$preview.html(data.preview) + self.initPreview() + } + }).done(function() { + self.hideIndicator() + self.loading = false + }) + } + + MarkdownEditor.prototype.initPreview = function() { + $('pre', this.$preview).addClass('prettyprint') + prettyPrint() + } + + // + // Loader + // + + MarkdownEditor.prototype.createIndicator = function() { + this.$indicator = $('
') + this.$el.prepend(this.$indicator) + this.$indicator.css('display', 'none') + } + + MarkdownEditor.prototype.showIndicator = function() { + this.$indicator.css('display', 'block') + } + + MarkdownEditor.prototype.hideIndicator = function() { + this.$indicator.css('display', 'none') + } + + // + // View mode + // + + MarkdownEditor.prototype.setViewMode = function(value) { + this.isSplitMode = value == 'split' + + $('[data-button-code="preview"]', this.$toolbar).toggle(!this.isSplitMode) + + this.$el + .removeClass('mode-tab mode-split') + .addClass('mode-' + value) + + if (this.isSplitMode) { + this.updatePreview() + } + } + + // + // Full screen + // + + MarkdownEditor.prototype.setFullscreen = function(value) { + this.isFullscreen = value + this.$el.toggleClass('is-fullscreen', value) + + if (value) { + $('body, html').css('overflow', 'hidden') + this.updateFullscreen() + this.setViewMode('split') + $(window).on('resize', this.proxy(this.updateFullscreen)) + } + else { + this.setViewMode(this.options.viewMode) + + this.$preview.css('height', '') + this.$write.css('height', '') + $('body, html').css('overflow', '') + + $(window).off('resize', this.proxy(this.updateFullscreen)) + this.editor.resize() + } + + $(window).trigger('oc.updateUi') + } + + MarkdownEditor.prototype.updateFullscreen = function() { + if (!this.isFullscreen) return + + var fullscreenCss = { + height: $(document).height() - this.$toolbar.outerHeight() + } + + this.$preview.css(fullscreenCss) + this.$write.css(fullscreenCss) + this.editor.resize() + } + + // + // Media Manager + // + + MarkdownEditor.prototype.launchMediaManager = function(onSuccess) { + var self = this + + new $.oc.mediaManager.popup({ + alias: 'ocmediamanager', + cropAndInsertButton: true, + onInsert: function(items) { + if (!items.length) { + alert('Please select image(s) to insert.') + return + } + + if (items.length > 1) { + alert('Please select a single item.') + return + } + + var path, publicUrl + for (var i=0, len=items.length; i