Skip to content
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
450 lines (381 sloc) 13.2 KB
Hallo {{ VERSION }} - a rich text editing jQuery UI widget
(c) 2011 Henri Bergius, IKS Consortium
Hallo may be freely distributed under the MIT license
((jQuery) ->
# Hallo provides a jQuery UI widget `hallo`. Usage:
# jQuery('p').hallo();
# Getting out of the editing state:
# jQuery('p').hallo({editable: false});
# When content is in editable state, users can just click on
# an editable element in order to start modifying it. This
# relies on browser having support for the HTML5 contentEditable
# functionality, which means that some mobile browsers are not
# supported.
# If plugins providing toolbar buttons have been enabled for
# Hallo, then a toolbar will be rendered when an area is active.
# ## Toolbar
# Hallo ships with different toolbar options, including:
# * `halloToolbarContextual`: a toolbar that appears as a popover
# dialog when user makes a selection
# * `halloToolbarFixed`: a toolbar that is constantly visible above
# the editable area when the area is activated
# The toolbar can be defined by the `toolbar` configuration key,
# which has to conform to the toolbar widget being used.
# Just like with plugins, it is possible to use Hallo with your own
# custom toolbar implementation.
# ## Events
# The Hallo editor provides several jQuery events that web
# applications can use for integration:
# ### Activated
# When user activates an editable (usually by clicking or tabbing
# to an editable element), a `halloactivated` event will be fired.
# jQuery('p').on('halloactivated', function() {
# console.log("Activated");
# });
# ### Deactivated
# When user gets out of an editable element, a `hallodeactivated`
# event will be fired.
# jQuery('p').on('hallodeactivated', function() {
# console.log("Deactivated");
# });
# ### Modified
# When contents in an editable have been modified, a
# `hallomodified` event will be fired.
# jQuery('p').on('hallomodified', function(event, data) {
# console.log("New contents are " + data.content);
# });
# ### Restored
# When contents are restored through calling
# `.hallo("restoreOriginalContent")` or the user pressing ESC while
# the cursor is in the editable element, a 'hallorestored' event will
# be fired.
# jQuery('p').on('hallorestored', function(event, data) {
# console.log("The thrown contents are " + data.thrown);
# console.log("The restored contents are " + data.content);
# });
jQuery.widget 'IKS.hallo',
toolbar: null
bound: false
originalContent: ''
previousContent: ''
uuid: ''
selection: null
_keepActivated: false
originalHref: null
editable: true
plugins: {}
toolbar: 'halloToolbarContextual'
parentElement: 'body'
buttonCssClass: null
toolbarCssClass: null
toolbarPositionAbove: false
toolbarOptions: {}
placeholder: ''
forceStructured: true
checkTouch: true
touchScreen: null
_create: ->
@id = @_generateUUID()
@checkTouch() if @options.checkTouch and @options.touchScreen is null
for plugin, options of @options.plugins
options = {} unless jQuery.isPlainObject options
jQuery.extend options,
editable: this
uuid: @id
buttonCssClass: @options.buttonCssClass
jQuery(@element)[plugin] options 'halloactivated', =>
# We will populate the toolbar the first time this
# editable is activated. This will make multiple
# Hallo instances on same page load much faster
@originalContent = @getContents()
_init: ->
if @options.editable
destroy: ->
if @toolbar
@element[@options.toolbar] 'destroy'
for plugin, options of @options.plugins
jQuery(@element)[plugin] 'destroy' @
# Disable an editable
disable: ->
@element.attr "contentEditable", false "focus", @_activated "blur", @_deactivated "keyup paste change", @_checkModified "keyup", @_keys "keyup mouseup", @_checkSelection
@bound = false
jQuery(@element).removeClass 'isModified'
jQuery(@element).removeClass 'inEditMode'
@element.parents('a').addBack().each (idx, elem) =>
element = jQuery elem
return unless 'a'
return unless @originalHref
element.attr 'href', @originalHref
@_trigger "disabled", null
# Enable an editable
enable: ->
@element.parents('a[href]').addBack().each (idx, elem) =>
element = jQuery elem
return unless 'a[href]'
@originalHref = element.attr 'href'
element.removeAttr 'href'
@element.attr "contentEditable", true
unless jQuery.parseHTML(@element.html())
@element.html this.options.placeholder
jQuery(@element).addClass 'inPlaceholderMode'
'min-width': @element.innerWidth()
'min-height': @element.innerHeight()
unless @bound
@element.on "focus", this, @_activated
@element.on "blur", this, @_deactivated
@element.on "keyup paste change", this, @_checkModified
@element.on "keyup", this, @_keys
@element.on "keyup mouseup", this, @_checkSelection
@bound = true
@_forceStructured() if @options.forceStructured
@_trigger "enabled", null
# Activate an editable for editing
activate: ->
# Checks whether the editable element contains the current selection
containsSelection: ->
range = @getSelection()
return @element.has(range.startContainer).length > 0
# Only supports one range for now (i.e. no multiselection)
getSelection: ->
sel = rangy.getSelection()
range = null
if sel.rangeCount > 0
range = sel.getRangeAt(0)
range = rangy.createRange()
return range
restoreSelection: (range) ->
sel = rangy.getSelection()
replaceSelection: (cb) ->
if navigator.appName is 'Microsoft Internet Explorer'
t = document.selection.createRange().text
r = document.selection.createRange()
sel = window.getSelection()
range = sel.getRangeAt(0)
newTextNode = document.createTextNode(cb(range.extractContents()))
removeAllSelections: () ->
if navigator.appName is 'Microsoft Internet Explorer'
getPluginInstance: (plugin) ->
# jQuery UI 1.10 or newer
instance = jQuery(@element).data "IKS-#{plugin}"
return instance if instance
# Older jQuery UI
instance = jQuery(@element).data plugin
return instance if instance
throw new Error "Plugin #{plugin} not found"
# Get contents of an editable as HTML string
getContents: ->
for plugin of @options.plugins
instance = @getPluginInstance(plugin)
continue unless instance
cleanup = instance.cleanupContentClone
continue unless jQuery.isFunction cleanup
jQuery(@element)[plugin] 'cleanupContentClone', @element
# Set the contents of an editable
setContents: (contents) ->
@element.html contents
# Check whether the editable has been modified
isModified: ->
@previousContent = @originalContent unless @previousContent
@previousContent isnt @getContents()
# Set the editable as unmodified
setUnmodified: ->
jQuery(@element).removeClass 'isModified'
@previousContent = @getContents()
# Set the editable as modified
setModified: ->
jQuery(@element).addClass 'isModified'
@._trigger 'modified', null,
editable: @
content: @getContents()
# Restore the content original
restoreOriginalContent: () ->
# Execute a contentEditable command
execute: (command, value) ->
if document.execCommand command, false, value
@element.trigger "change"
protectFocusFrom: (el) ->
el.on "mousedown", (event) =>
@_protectToolbarFocus = true
setTimeout =>
@_protectToolbarFocus = false
, 300
keepActivated: (@_keepActivated) ->
_generateUUID: ->
S4 = ->
((1 + Math.random()) * 0x10000|0).toString(16).substring 1
_prepareToolbar: ->
@toolbar = jQuery('<div class="hallotoolbar"></div>').hide()
@toolbar.addClass @options.toolbarCssClass if @options.toolbarCssClass
defaults =
editable: @
parentElement: @options.parentElement
toolbar: @toolbar
positionAbove: @options.toolbarPositionAbove
toolbarOptions = jQuery.extend({}, defaults, @options.toolbarOptions)
@element[@options.toolbar] toolbarOptions
for plugin of @options.plugins
instance = @getPluginInstance(plugin)
continue unless instance
populate = instance.populateToolbar
continue unless jQuery.isFunction populate
@element[plugin] 'populateToolbar', @toolbar
@element[@options.toolbar] 'setPosition'
@protectFocusFrom @toolbar
changeToolbar: (element, toolbar, hide = false) ->
originalToolbar = @options.toolbar
@options.parentElement = element
@options.toolbar = toolbar if toolbar
return unless @toolbar
@element[originalToolbar] 'destroy'
do @toolbar.remove
do @_prepareToolbar
@toolbar.hide() if hide
_checkModified: (event) ->
widget =
widget.setModified() if widget.isModified()
_keys: (event) ->
widget =
if event.keyCode == 27
old = widget.getContents()
widget._trigger "restored", null,
editable: widget
content: widget.getContents()
thrown: old
_rangesEqual: (r1, r2) ->
return false unless r1.startContainer is r2.startContainer
return false unless r1.startOffset is r2.startOffset
return false unless r1.endContainer is r2.endContainer
return false unless r1.endOffset is r2.endOffset
# Check if some text is selected, and if this selection has changed.
# If it changed, trigger the "halloselected" event
_checkSelection: (event) ->
if event.keyCode == 27
widget =
# The mouseup event triggers before the text selection is updated.
# I did not find a better solution than setTimeout in 0 ms
setTimeout ->
sel = widget.getSelection()
if widget._isEmptySelection(sel) or widget._isEmptyRange(sel)
if widget.selection
widget.selection = null
widget._trigger "unselected", null,
editable: widget
originalEvent: event
if !widget.selection or not widget._rangesEqual sel, widget.selection
widget.selection = sel.cloneRange()
widget._trigger "selected", null,
editable: widget
selection: widget.selection
ranges: [widget.selection]
originalEvent: event
, 0
_isEmptySelection: (selection) ->
if selection.type is "Caret"
return true
return false
_isEmptyRange: (range) ->
if range.collapsed
return true
if range.isCollapsed
return range.isCollapsed() if typeof range.isCollapsed is 'function'
return range.isCollapsed
return false
turnOn: () ->
if this.getContents() is this.options.placeholder
this.setContents ''
jQuery(@element).removeClass 'inPlaceholderMode'
jQuery(@element).addClass 'inEditMode'
@_trigger "activated", null, @
turnOff: () ->
jQuery(@element).removeClass 'inEditMode'
@_trigger "deactivated", null, @
unless @getContents()
jQuery(@element).addClass 'inPlaceholderMode'
@setContents @options.placeholder
_activated: (event) ->
_deactivated: (event) ->
return if
unless is true
setTimeout ->
, 300
_forceStructured: (event) ->
document.execCommand 'styleWithCSS', 0, false
catch e
document.execCommand 'useCSS', 0, true
catch e
document.execCommand 'styleWithCSS', false, false
catch e
checkTouch: ->
@options.touchScreen = !!('createTouch' of document)
You can’t perform that action at this time.