Refit the Block UI logic

Previously contained in richeditor (this is a bit messy, rich editor should be generic), the block UI logic has been moved to its own "figures" plugin
This commit is contained in:
Samuel Georges 2016-05-21 06:58:34 +10:00
parent 85decc4086
commit 115fc0c1ef
4 changed files with 359 additions and 313 deletions

File diff suppressed because one or more lines are too long

View File

@ -37,6 +37,7 @@
=require ../vendor/froala_drm/js/plugins/draggable.min.js
=require ../vendor/froala_drm/js/plugins/code_beautifier.min.js
=require plugins/figures.js
=require richeditor.js
*/

View File

@ -0,0 +1,211 @@
(function ($) {
// Add an option for your plugin.
// $.FroalaEditor.DEFAULTS = $.extend($.FroalaEditor.DEFAULTS, {
// myOption: false
// });
$.FroalaEditor.PLUGINS.figures = function (editor) {
/**
* Insert UI Blocks
*/
function insert($el) {
var html = $('<div />').append($el.clone()).remove().html()
editor.html.insert(html)
editor.selection.restore()
editor.html.cleanEmptyTags()
// Clean up wrapping paragraphs or empty paragraphs
$('figure', editor.$el).each(function() {
var $this = $(this),
$parent = $this.parent('p'),
$next = $this.next('p')
if (!!$parent.length) {
$this.unwrap()
}
if (!!$next.length && $.trim($next.text()).length == 0) {
$next.remove()
}
})
}
/**
* Init UI Blocks
*/
function _initUiBlocks () {
$('[data-video], [data-audio]', editor.$el).each(function() {
$(this).attr({
'data-ui-block': true,
'tabindex': '0'
})
this.contentEditable = false
})
}
function _handleUiBlocksKeydown(ev) {
if (ev.which == 40 || ev.which == 38 || ev.which == 8 || ev.which == 46) {
var $block = $(editor.selection.element())
if ($block.is('br')) {
$block = $block.parent()
}
if (!!$block.length) {
switch (ev.which) {
case 38:
// Up arrow
_handleUiBlockCaretIn($block.prev())
break
case 40:
// Down arrow
_handleUiBlockCaretIn($block.next())
break
case 46:
// Delete key
_handleUiBlockCaretClearEmpty($block.next(), $block)
break
case 8:
// Backspace key
_handleUiBlockCaretClearEmpty($block.prev(), $block)
break
}
}
}
}
function _handleUiBlockCaretClearEmpty($block, $p) {
if ($block.attr('data-ui-block') !== undefined && $.trim($p.text()).length == 0) {
$p.remove()
_handleUiBlockCaretIn($block)
editor.undo.saveStep()
}
}
function _handleUiBlockCaretIn($block) {
if ($block.attr('data-ui-block') !== undefined) {
$block.focus()
editor.selection.clear()
return true
}
return false
}
function _uiBlockKeyDown(ev, block) {
if (ev.which == 40 || ev.which == 38 || ev.which == 13 || ev.which == 8 || ev.which == 46) {
switch (ev.which) {
case 40:
// Down arrow
_focusUiBlockOrText($(block).next(), true)
break
case 38:
// Up arrow
_focusUiBlockOrText($(block).prev(), false)
break
case 13:
// Enter key
var $paragraph = $('<p><br/></p>')
$paragraph.insertAfter(block)
editor.selection.setAfter(block)
editor.selection.restore()
editor.undo.saveStep()
break
case 8:
case 46:
// Delete / Backspace key
var $nextFocus = $(block).next(),
gotoStart = true
if ($nextFocus.length == 0) {
$nextFocus = $(block).prev()
gotoStart = false
}
_focusUiBlockOrText($nextFocus, gotoStart)
$(block).remove()
editor.undo.saveStep()
break
}
ev.preventDefault()
}
}
function _focusUiBlockOrText($block, gotoStart) {
if (!!$block.length) {
if (!_handleUiBlockCaretIn($block)) {
if (gotoStart) {
editor.selection.setAtStart($block.get(0))
editor.selection.restore()
}
else {
editor.selection.setAtEnd($block.get(0))
editor.selection.restore()
}
}
}
}
/**
* Keydown
*/
function _onKeydown (ev) {
_handleUiBlocksKeydown(ev)
if (ev.isDefaultPrevented()) {
return false
}
}
function _onFigureKeydown(ev) {
if (ev.target && $(ev.target).attr('data-ui-block') !== undefined) {
_uiBlockKeyDown(ev, ev.target)
}
if (ev.isDefaultPrevented()) {
return false
}
}
/**
* Sync
*/
function _onSync(html) {
var $domTree = $('<div>' + html + '</div>')
$domTree.find('[data-video], [data-audio]').each(function(){
$(this).removeAttr('contenteditable data-ui-block tabindex')
})
return $domTree.html()
}
// The start point for your plugin.
function _init () {
editor.events.on('initialized', _initUiBlocks)
editor.events.on('html.get', _onSync)
editor.events.on('keydown', _onKeydown)
editor.events.on('destroy', _destroy, true)
editor.$el.on('keydown', 'figure', _onFigureKeydown)
}
/**
* Destroy.
*/
function _destroy () {
editor.$el.off('keydown', 'figure', _onFigureKeydown)
}
return {
_init: _init,
insert: insert
}
}
})(jQuery);

View File

@ -8,7 +8,7 @@
* $('textarea').richEditor()
*
* Dependancies:
* - Redactor Editor (redactor.js)
* - Froala Editor (froala_editor.js)
*/
+function ($) { "use strict";
var Base = $.oc.foundation.base,
@ -22,8 +22,7 @@
this.$el = $(element)
this.$textarea = this.$el.find('>textarea:first')
this.$form = this.$el.closest('form')
this.$editor = null
this.redactor = null
this.editor = null
$.oc.foundation.controlUtils.markDisposable(element)
@ -95,6 +94,8 @@
froalaOptions.toolbarButtonsXS = froalaOptions.toolbarButtons
froalaOptions.htmlAllowedEmptyTags = ['figure', 'textarea', 'a', 'iframe', 'object', 'video', 'style', 'script']
froalaOptions.htmlDoNotWrapTags = ['figure', 'script', 'style']
froalaOptions.lineBreakerTags = ['figure', 'table', 'hr', 'iframe', 'form', 'dl']
froalaOptions.shortcutsEnabled = ['show', 'bold', 'italic', 'underline', 'indent', 'outdent', 'undo', 'redo']
var placeholder = this.$textarea.attr('placeholder')
froalaOptions.placeholderText = placeholder ? placeholder : ''
@ -110,8 +111,16 @@
}
this.$textarea.on('froalaEditor.initialized', this.proxy(this.build))
this.$textarea.on('froalaEditor.contentChanged', this.proxy(this.onChange))
this.$textarea.on('froalaEditor.keydown', this.proxy(this.onKeydown))
this.$textarea.on('froalaEditor.html.get', this.proxy(this.onSyncContent))
this.$textarea.on('froalaEditor.html.set', this.proxy(this.onSetContent))
this.$textarea.froalaEditor(froalaOptions)
this.editor = this.$textarea.data('froala.editor')
this.$el.on('keydown', '.fr-view figure', this.proxy(this.onFigureKeydown))
}
RichEditor.prototype.dispose = function() {
@ -125,12 +134,20 @@
this.$el = null
this.$textarea = null
this.$form = null
this.$editor = null
this.editor = null
BaseProto.dispose.call(this)
}
RichEditor.prototype.unregisterHandlers = function() {
this.$el.off('keydown', '.fr-view figure', this.proxy(this.onFigureKeydown))
this.$textarea.off('froalaEditor.initialized', this.proxy(this.build))
this.$textarea.off('froalaEditor.contentChanged', this.proxy(this.onChange))
this.$textarea.off('froalaEditor.keydown', this.proxy(this.onKeydown))
this.$textarea.off('froalaEditor.html.get', this.proxy(this.onSyncContent))
this.$textarea.off('froalaEditor.html.set', this.proxy(this.onSetContent))
$(window).off('resize', this.proxy(this.updateLayout))
$(window).off('oc.updateUi', this.proxy(this.updateLayout))
this.$el.off('dispose-control', this.proxy(this.dispose))
@ -143,15 +160,6 @@
$(window).on('oc.updateUi', this.proxy(this.updateLayout))
this.$textarea.trigger('init.oc.richeditor', [this])
this.initUiBlocks()
// var self = this
// redactor.default = {
// onShow: function($figure, $toolbar) {
// self.onShowFigureToolbar($figure, $toolbar)
// }
// }
}
RichEditor.prototype.getElement = function() {
@ -159,17 +167,23 @@
}
RichEditor.prototype.getEditor = function() {
return this.$editor
return this.editor
}
RichEditor.prototype.getTextarea = function() {
return this.$textarea
}
RichEditor.prototype.setContent = function(html) {
this.$textarea.redactor('code.set', html)
RichEditor.prototype.getContent = function() {
return this.$textarea.froalaEditor('html.get')
}
this.$textarea.trigger('setContent.oc.richeditor', [this])
RichEditor.prototype.setContent = function(html) {
this.$textarea.froalaEditor('html.set', html)
}
RichEditor.prototype.syncContent = function() {
this.editor.events.trigger('contentChanged')
}
RichEditor.prototype.updateLayout = function() {
@ -193,210 +207,37 @@
}
}
RichEditor.prototype.sanityCheckContent = function() {
// First and last elements should always be paragraphs, lists or pre
var safeElements = 'p, h1, h2, h3, h4, h5, pre, figure, ol, ul';
if (!this.$editor.children(':last-child').is(safeElements)) {
this.$editor.append('<p><br></p>')
}
if (!this.$editor.children(':first-child').is(safeElements)) {
this.$editor.prepend('<p><br></p>')
}
this.$textarea.trigger('sanitize.oc.richeditor', [this])
RichEditor.prototype.insertHtml = function(html) {
this.editor.html.insert(html)
this.editor.selection.restore()
}
RichEditor.prototype.syncBefore = function(html) {
var container = {
html: html
}
this.$textarea.trigger('syncBefore.oc.richeditor', [this, container])
var $domTree = $('<div>'+container.html+'</div>')
// This code removes Redactor-specific attributes and tags from the code.
// It seems to be a known problem with Redactor, try googling for
// "data-redactor-tag" or "redactor-invisible-space" (with quotes)
$('*', $domTree).removeAttr('data-redactor-tag')
$domTree.find('span[data-redactor-class="redactor-invisible-space"]').each(function(){
$(this).children().insertBefore(this)
$(this).remove()
})
$domTree.find('span.redactor-invisible-space').each(function(){
$(this).children().insertBefore(this)
$(this).remove()
})
$domTree.find('[data-video], [data-audio]').each(function(){
$(this).removeAttr('contenteditable data-ui-block tabindex')
})
$domTree.find('div.oc-figure-controls').remove()
return $domTree.html()
}
RichEditor.prototype.onShowFigureToolbar = function($figure, $toolbar) {
// Deal with the case when the toolbar top has negative value
var toolbarTop = $figure.position().top - $toolbar.height() - 10
$toolbar.toggleClass('bottom', toolbarTop < 0)
RichEditor.prototype.insertElement = function($el) {
this.insertHtml($('<div />').append($el.clone()).remove().html())
}
/*
* Inserts non-editable block (used for snippets, audio and video)
*/
RichEditor.prototype.insertUiBlock = function($node) {
var current = this.redactor.selection.getCurrent(),
inserted = false
if (current === false) {
this.redactor.focus.setStart()
}
current = this.redactor.selection.getCurrent()
if (current !== false) {
// If the block is inserted into a paragraph, insert it after the paragraph.
var $paragraph = $(current).closest('p')
if ($paragraph.length > 0) {
this.redactor.caret.setAfter($paragraph.get(0))
// If the paragraph is empty, remove it.
if ($.trim($paragraph.text()).length == 0) {
$paragraph.remove()
}
}
else {
// If block is inserted into another UI block, insert it after the existing block.
var $closestBlock = $(current).closest('[data-ui-block]')
if ($closestBlock.length > 0) {
$node.insertBefore($closestBlock.get(0))
inserted = true
}
}
}
if (!inserted) {
this.redactor.insert.node($node)
}
this.redactor.code.sync()
$node.focus()
}
RichEditor.prototype.initUiBlocks = function() {
$('.fr-wrapper [data-video], .fr-wrapper [data-audio]', this.$el).each(function() {
$(this).attr({
'data-ui-block': true,
'tabindex': '0'
})
this.contentEditable = false
})
}
RichEditor.prototype.handleUiBlocksKeydown = function(ev) {
if (this.$textarea === undefined) {
return
}
if (ev.target && $(ev.target).attr('data-ui-block') !== undefined) {
this.uiBlockKeyDown(ev, ev.target)
ev.preventDefault()
return
}
switch (ev.which) {
case 38:
// Up arrow
var block = this.redactor.selection.getBlock()
if (block)
this.handleUiBlockCaretIn($(block).prev())
break
case 40:
// Down arrow
var block = this.redactor.selection.getBlock()
if (block)
this.handleUiBlockCaretIn($(block).next())
break
}
}
RichEditor.prototype.handleUiBlockCaretIn = function($block) {
if ($block.attr('data-ui-block') !== undefined) {
$block.focus()
this.redactor.selection.remove()
return true
}
return false
}
RichEditor.prototype.uiBlockKeyDown = function(ev, block) {
if (ev.which == 40 || ev.which == 38 || ev.which == 13 || ev.which == 8) {
switch (ev.which) {
case 40:
// Down arrow
this.focusUiBlockOrText($(block).next(), true)
break
case 38:
// Up arrow
this.focusUiBlockOrText($(block).prev(), false)
break
case 13:
// Enter key
var $paragraph = $('<p><br/></p>')
$paragraph.insertAfter(block)
this.redactor.caret.setStart($paragraph.get(0))
break
case 8:
// Backspace key
var $nextFocus = $(block).next(),
gotoStart = true
if ($nextFocus.length == 0) {
$nextFocus = $(block).prev()
gotoStart = false
}
this.focusUiBlockOrText($nextFocus, gotoStart)
$(block).remove()
break
}
}
}
RichEditor.prototype.focusUiBlockOrText = function($block, gotoStart) {
if ($block.length > 0) {
if (!this.handleUiBlockCaretIn($block, this.redactor)) {
if (gotoStart) {
this.redactor.caret.setStart($block.get(0))
}
else {
this.redactor.caret.setEnd($block.get(0))
}
}
}
this.$textarea.froalaEditor('figures.insert', $node)
}
// EVENT HANDLERS
// ============================
RichEditor.prototype.onVisualMode = function(html) {
this.$textarea.trigger('visualMode.oc.richeditor', [this])
RichEditor.prototype.onSetContent = function(ev, editor) {
this.$textarea.trigger('setContent.oc.richeditor', [this])
}
RichEditor.prototype.onSyncBefore = function(html) {
return this.syncBefore(html)
RichEditor.prototype.onSyncContent = function(ev, editor, html) {
var container = {
html: html
}
this.$textarea.trigger('syncContent.oc.richeditor', [this, container])
return container.html
}
RichEditor.prototype.onFocus = function() {
@ -407,28 +248,12 @@
this.$el.removeClass('editor-focus')
}
RichEditor.prototype.onKeydown = function(ev) {
this.$textarea.trigger('keydown.oc.richeditor', [ev, this])
if (ev.isDefaultPrevented()) {
return false
}
this.handleUiBlocksKeydown(ev)
if (ev.isDefaultPrevented()) {
return false
}
RichEditor.prototype.onFigureKeydown = function(ev) {
this.$textarea.trigger('figureKeydown.oc.richeditor', [ev, this])
}
RichEditor.prototype.onEnter = function(ev) {
this.$textarea.trigger('enter.oc.richeditor', [ev, this])
if (ev.isDefaultPrevented()) {
return false
}
this.handleUiBlocksKeydown(ev)
RichEditor.prototype.onKeydown = function(ev, editor, keyEv) {
this.$textarea.trigger('keydown.oc.richeditor', [keyEv, this])
if (ev.isDefaultPrevented()) {
return false
@ -436,8 +261,6 @@
}
RichEditor.prototype.onChange = function(ev) {
this.sanityCheckContent()
this.$editor.trigger('mutate')
this.$form.trigger('change')
}