mercury-rails 0.2.0 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. data/POST_INSTALL +15 -0
  2. data/README.md +27 -6
  3. data/VERSION +1 -1
  4. data/app/controllers/mercury_controller.rb +4 -4
  5. data/app/views/layouts/mercury.html.erb +14 -3
  6. data/app/views/mercury/panels/snippets.html +1 -1
  7. data/app/views/mercury/snippets/{example_options.html.erb → example/options.html.erb} +0 -0
  8. data/app/views/mercury/snippets/{example.html.erb → example/preview.html.erb} +0 -0
  9. data/config/routes.rb +2 -2
  10. data/features/loading/loading.feature +22 -0
  11. data/features/loading/navigating.feature +77 -0
  12. data/features/loading/user_interface.feature +67 -0
  13. data/features/regions/editable/advanced_editing.feature +0 -0
  14. data/features/regions/editable/basic_editing.feature +195 -0
  15. data/features/regions/editable/inserting_links.feature +98 -0
  16. data/features/regions/editable/inserting_media.feature +110 -0
  17. data/features/regions/editable/inserting_snippets.feature +103 -0
  18. data/features/regions/editable/inserting_special_characters.feature +24 -0
  19. data/features/regions/editable/inserting_tables.feature +109 -0
  20. data/features/regions/editable/pasting.feature +0 -0
  21. data/features/regions/editable/uploading_images.feature +0 -0
  22. data/features/regions/markupable/advanced_editing.feature +0 -0
  23. data/features/regions/markupable/basic_editing.feature +0 -0
  24. data/features/regions/markupable/inserting_links.feature +0 -0
  25. data/features/regions/markupable/inserting_media.feature +0 -0
  26. data/features/regions/markupable/inserting_snippets.feature +0 -0
  27. data/features/regions/markupable/inserting_special_characters.feature +0 -0
  28. data/features/regions/markupable/inserting_tables.feature +0 -0
  29. data/features/regions/markupable/uploading_images.feature +0 -0
  30. data/features/regions/snippetable/advanced_editing.feature +0 -0
  31. data/features/regions/snippetable/basic_editing.feature +0 -0
  32. data/features/regions/snippetable/inserting_snippets.feature +0 -0
  33. data/features/saving/saving.feature +33 -0
  34. data/features/step_definitions/debug_steps.rb +2 -2
  35. data/features/step_definitions/mercury_steps.rb +441 -0
  36. data/features/support/env.rb +3 -3
  37. data/features/support/mercury_contents.rb +25 -0
  38. data/features/support/mercury_selectors.rb +147 -0
  39. data/features/support/paths.rb +20 -18
  40. data/features/support/selectors.rb +5 -3
  41. data/lib/generators/mercury/install/install_generator.rb +14 -0
  42. data/mercury-rails.gemspec +50 -20
  43. data/spec/javascripts/mercury/lightview_spec.js.coffee +55 -27
  44. data/spec/javascripts/mercury/mercury_spec.js.coffee +3 -3
  45. data/spec/javascripts/mercury/modal_spec.js.coffee +2 -2
  46. data/spec/javascripts/mercury/native_extensions_spec.js.coffee +0 -24
  47. data/spec/javascripts/mercury/page_editor_spec.js.coffee +148 -67
  48. data/spec/javascripts/mercury/panel_spec.js.coffee +2 -2
  49. data/spec/javascripts/mercury/region_spec.js.coffee +10 -7
  50. data/spec/javascripts/mercury/regions/editable_spec.js.coffee +0 -20
  51. data/spec/javascripts/mercury/snippet_toolbar_spec.js.coffee +2 -2
  52. data/spec/javascripts/mercury/toolbar.button_group_spec.js.coffee +1 -1
  53. data/spec/javascripts/mercury/toolbar.expander_spec.js.coffee +1 -1
  54. data/spec/javascripts/templates/mercury/page_editor.html +3 -3
  55. data/vendor/assets/images/mercury/close.png +0 -0
  56. data/vendor/assets/javascripts/mercury.js +140 -73
  57. data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/jquery-1.6.js +0 -0
  58. data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/jquery-ui-1.8.13.custom.js +0 -0
  59. data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/jquery.additions.js +0 -0
  60. data/vendor/assets/javascripts/mercury/dependencies/jquery.htmlClean.js +527 -0
  61. data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/liquidmetal.js +0 -0
  62. data/vendor/assets/javascripts/{mercury_dependencies → mercury/dependencies}/showdown.js +0 -0
  63. data/vendor/assets/javascripts/mercury/lightview.js.coffee +5 -2
  64. data/vendor/assets/javascripts/mercury/mercury.js.coffee +9 -8
  65. data/vendor/assets/javascripts/mercury/modals/htmleditor.js.coffee +3 -1
  66. data/vendor/assets/javascripts/mercury/modals/inserttable.js.coffee +2 -2
  67. data/vendor/assets/javascripts/mercury/native_extensions.js.coffee +6 -17
  68. data/vendor/assets/javascripts/mercury/page_editor.js.coffee +29 -8
  69. data/vendor/assets/javascripts/mercury/plugins/save_as_xml/mercury/page_editor.js.coffee +27 -0
  70. data/vendor/assets/javascripts/mercury/plugins/save_as_xml/plugin.js +9 -0
  71. data/vendor/assets/javascripts/mercury/region.js.coffee +2 -2
  72. data/vendor/assets/javascripts/mercury/regions/editable.js.coffee +89 -93
  73. data/vendor/assets/javascripts/mercury/regions/markupable.js.coffee +1 -1
  74. data/vendor/assets/javascripts/mercury/support/history.js +1 -0
  75. data/vendor/assets/javascripts/mercury/uploader.js.coffee +0 -1
  76. data/vendor/assets/javascripts/mercury_loader.js +4 -4
  77. data/vendor/assets/stylesheets/mercury/lightview.css +8 -0
  78. data/vendor/assets/stylesheets/mercury/mercury.css +12 -0
  79. data/vendor/assets/stylesheets/mercury/modal.css +0 -12
  80. data/vendor/assets/stylesheets/mercury/toolbar.css +1 -0
  81. data/vendor/assets/stylesheets/mercury_overrides.css +17 -0
  82. metadata +73 -45
  83. data/app/views/mercury/lightviews/imageprocessor.html +0 -3
  84. data/app/views/mercury/modals/sanitizer.html +0 -9
  85. data/features/editing/basic.feature +0 -11
  86. data/vendor/assets/images/mercury/clippy.png +0 -0
@@ -27,6 +27,8 @@ jQuery.extend Mercury.lightview, {
27
27
  @overlay = jQuery('<div>', {class: 'mercury-lightview-overlay'})
28
28
 
29
29
  @titleElement = @element.find('.mercury-lightview-title')
30
+ @titleElement.append('<a class="mercury-lightview-close"></a>') if @options.closeButton
31
+
30
32
  @contentElement = @element.find('.mercury-lightview-content')
31
33
 
32
34
  @element.appendTo(jQuery(@options.appendTo).get(0) ? 'body')
@@ -39,7 +41,8 @@ jQuery.extend Mercury.lightview, {
39
41
  Mercury.bind 'refresh', => @resize(true)
40
42
  Mercury.bind 'resize', => @position() if @visible
41
43
 
42
- @overlay.click => @hide()
44
+ @overlay.click => @hide() unless @options.closeButton
45
+ @titleElement.find('.mercury-lightview-close').click => @hide()
43
46
 
44
47
  jQuery(document).bind 'keydown', (event) =>
45
48
  @hide() if event.keyCode == 27 && @visible
@@ -52,7 +55,7 @@ jQuery.extend Mercury.lightview, {
52
55
  @overlay.animate {opacity: 1}, 200, 'easeInOutSine', =>
53
56
  @setTitle()
54
57
  @element.show().css({opacity: 0})
55
- @element.animate {opacity: 1}, 200, 'easeInOutSine', =>
58
+ @element.stop().animate {opacity: 1}, 200, 'easeInOutSine', =>
56
59
  @visible = true
57
60
  @load()
58
61
 
@@ -1,9 +1,10 @@
1
1
  # ## Require all the dependencies
2
- #= require mercury_dependencies/jquery-1.6
3
- #= require mercury_dependencies/jquery-ui-1.8.13.custom
4
- #= require mercury_dependencies/jquery.additions
5
- #= require mercury_dependencies/liquidmetal
6
- #= require mercury_dependencies/showdown
2
+ #= require mercury/dependencies/jquery-1.6
3
+ #= require mercury/dependencies/jquery-ui-1.8.13.custom
4
+ #= require mercury/dependencies/jquery.additions
5
+ #= require mercury/dependencies/jquery.htmlClean
6
+ #= require mercury/dependencies/liquidmetal
7
+ #= require mercury/dependencies/showdown
7
8
  #
8
9
  # ## Require all mercury files
9
10
  #= require_self
@@ -33,7 +34,7 @@
33
34
  #
34
35
  @Mercury ||= {}
35
36
  jQuery.extend @Mercury, {
36
- version: '0.2.0'
37
+ version: '0.2.3'
37
38
 
38
39
  # No IE support yet because it doesn't follow the W3C standards for HTML5 contentEditable (aka designMode).
39
40
  supported: document.getElementById && document.designMode && !jQuery.browser.konqueror && !jQuery.browser.msie
@@ -47,12 +48,12 @@ jQuery.extend @Mercury, {
47
48
 
48
49
  # Custom event and logging methods
49
50
  bind: (eventName, callback) ->
50
- jQuery(document).bind("mercury:#{eventName}", callback)
51
+ jQuery(top).bind("mercury:#{eventName}", callback)
51
52
 
52
53
 
53
54
  trigger: (eventName, options) ->
54
55
  Mercury.log(eventName, options)
55
- jQuery(document).trigger("mercury:#{eventName}", options)
56
+ jQuery(top).trigger("mercury:#{eventName}", options)
56
57
 
57
58
 
58
59
  log: ->
@@ -1,6 +1,8 @@
1
1
  @Mercury.modalHandlers.htmlEditor = ->
2
2
  # fill the text area with the content
3
- @element.find('textarea').val(Mercury.region.content(null, true, false))
3
+ content = Mercury.region.content(null, true, false)
4
+ # content = jQuery.htmlClean(content, {format: true, replace: [], allowedClasses: ['mercury-snippet']})
5
+ @element.find('textarea').val(content)
4
6
 
5
7
  # replace the contents on form submit
6
8
  @element.find('form').submit (event) =>
@@ -5,7 +5,7 @@
5
5
  table.click (event) =>
6
6
  cell = jQuery(event.target)
7
7
  table = cell.closest('table')
8
- table.find('.selected').removeClass('selected')
8
+ table.find('.selected').removeAttr('class')
9
9
  cell.addClass('selected')
10
10
  Mercury.tableEditor(table, cell, '&nbsp;')
11
11
 
@@ -44,7 +44,7 @@
44
44
  # build the table on form submission
45
45
  @element.find('form').submit (event) =>
46
46
  event.preventDefault()
47
- table.find('.selected').removeClass('selected')
47
+ table.find('.selected').removeAttr('class')
48
48
  table.find('td, th').html('&nbsp;')
49
49
 
50
50
  html = jQuery('<div>').html(table).html()
@@ -9,23 +9,6 @@ String::toHex = ->
9
9
  "##{parseInt(r).toHex()}#{parseInt(g).toHex()}#{parseInt(b).toHex()}"
10
10
 
11
11
 
12
- String::singleDiff = (that) ->
13
- diff = ''
14
- for char, index in that
15
- break if char == 'each'
16
- if char != @[index]
17
- re = new RegExp(@substr(index).regExpEscape().replace(/^\s+|^(&nbsp;)+/g, '') + '$', 'm')
18
- diff = that.substr(index).replace(re, '')
19
- break
20
- return diff
21
-
22
-
23
- String::regExpEscape = ->
24
- specials = ['/','.','*','+','?','|','(',')','[',']','{','}','\\']
25
- escaped = new RegExp('(\\' + specials.join('|\\') + ')', 'g')
26
- return @replace(escaped, '\\$1')
27
-
28
-
29
12
  String::sanitizeHTML = ->
30
13
  element = jQuery('<div>').html(@.toString())
31
14
  element.find('style').remove()
@@ -46,3 +29,9 @@ Number::toBytes = ->
46
29
  bytes /= 1024
47
30
  i += 1
48
31
  return if i then "#{bytes.toFixed(2)}#{['', ' kb', ' Mb', ' Gb', ' Tb', ' Pb', ' Eb'][i]}" else "#{bytes} bytes"
32
+
33
+
34
+ # make setTimeout not suck for coffeescript
35
+ window.originalSetTimeout = window.setTimeout
36
+ window.setTimeout = (arg1, arg2) ->
37
+ if typeof(arg1) == 'number' then window.originalSetTimeout(arg2, arg1) else window.originalSetTimeout(arg1, arg2)
@@ -2,6 +2,8 @@ class @Mercury.PageEditor
2
2
 
3
3
  # options
4
4
  # saveStyle: 'form', or 'json' (defaults to json)
5
+ # saveDataType: 'xml', 'json', 'jsonp', 'script', 'text', 'html' (defaults to json)
6
+ # saveMethod: 'POST', or 'PUT', create or update actions on save (defaults to POST)
5
7
  # visible: boolean, if the interface should start visible or not (defaults to true)
6
8
  constructor: (@saveUrl = null, @options = {}) ->
7
9
  throw "Mercury.PageEditor is unsupported in this client. Supported browsers are chrome 10+, firefix 4+, and safari 5+." unless Mercury.supported
@@ -17,8 +19,9 @@ class @Mercury.PageEditor
17
19
 
18
20
 
19
21
  initializeInterface: ->
20
- @focusableElement = jQuery('<input>', {type: 'text', style: 'position:absolute;opacity:0'}).appendTo(@options.appendTo ? 'body')
21
- @iframe = jQuery('<iframe>', {class: 'mercury-iframe', seamless: 'true', frameborder: '0', src: 'about:blank', style: 'position:absolute;top:0;width:100%;visibility:hidden'})
22
+ @focusableElement = jQuery('<input>', {class: 'mercury-focusable', type: 'text'}).appendTo(@options.appendTo ? 'body')
23
+
24
+ @iframe = jQuery('<iframe>', {id: 'mercury_iframe', class: 'mercury-iframe', seamless: 'true', frameborder: '0', src: 'about:blank'})
22
25
  @iframe.appendTo(jQuery(@options.appendTo).get(0) ? 'body')
23
26
 
24
27
  @iframe.load => @initializeFrame()
@@ -32,8 +35,10 @@ class @Mercury.PageEditor
32
35
  try
33
36
  return if @iframe.data('loaded')
34
37
  @iframe.data('loaded', true)
38
+ alert("Opera isn't a fully supported browser, your results may not be optimal.") if jQuery.browser.opera
35
39
  @document = jQuery(@iframe.get(0).contentWindow.document)
36
- jQuery("<style mercury-styles=\"true\">").html(Mercury.config.injectedStyles).appendTo(@document.find('head'))
40
+ stylesToInject = Mercury.config.injectedStyles.replace(/{{regionClass}}/g, Mercury.config.regionClass)
41
+ jQuery("<style mercury-styles=\"true\">").html(stylesToInject).appendTo(@document.find('head'))
37
42
 
38
43
  # jquery: make jQuery evaluate scripts within the context of the iframe window -- note that this means that we
39
44
  # can't use eval in mercury (eg. script tags in ajax responses) because it will eval in the wrong context (you can
@@ -41,13 +46,18 @@ class @Mercury.PageEditor
41
46
  # todo: look into `context` options for ajax as an alternative
42
47
  iframeWindow = @iframe.get(0).contentWindow
43
48
  jQuery.globalEval = (data) -> (iframeWindow.execScript || (data) -> iframeWindow["eval"].call(iframeWindow, data))(data) if (data && /\S/.test(data))
49
+
44
50
  iframeWindow.Mercury = Mercury
51
+ iframeWindow.History = History if window.History && History.Adapter
45
52
 
46
53
  @bindEvents()
47
54
  @resize()
48
55
  @initializeRegions()
49
56
  @finalizeInterface()
50
57
  Mercury.trigger('ready')
58
+ jQuery(iframeWindow).trigger('mercury:ready')
59
+ iframeWindow.Event.fire(iframeWindow, 'mercury:ready') if iframeWindow.Event && iframeWindow.Event.fire
60
+ iframeWindow.onMercuryReady() if iframeWindow.onMercuryReady
51
61
 
52
62
  @iframe.css({visibility: 'visible'})
53
63
  catch error
@@ -55,7 +65,7 @@ class @Mercury.PageEditor
55
65
 
56
66
 
57
67
  initializeRegions: ->
58
- @buildRegion(jQuery(region)) for region in jQuery('.mercury-region', @document)
68
+ @buildRegion(jQuery(region)) for region in jQuery(".#{Mercury.config.regionClass}", @document)
59
69
  return unless @options.visible
60
70
  for region in @regions
61
71
  if region.focus
@@ -73,6 +83,9 @@ class @Mercury.PageEditor
73
83
 
74
84
 
75
85
  finalizeInterface: ->
86
+ @santizerElement = jQuery('<div>', {id: 'mercury_sanitizer', contenteditable: 'true', style: 'position:fixed;width:100px;height:100px;top:-100px;left:-100px;opacity:0'})
87
+ @santizerElement.appendTo(@options.appendTo ? @document.find('body'))
88
+
76
89
  @snippetToolbar = new Mercury.SnippetToolbar(@document)
77
90
 
78
91
  @hijackLinksAndForms()
@@ -92,7 +105,7 @@ class @Mercury.PageEditor
92
105
  @document.mousedown (event) ->
93
106
  Mercury.trigger('hide:dialogs')
94
107
  if Mercury.region
95
- Mercury.trigger('unfocus:regions') unless jQuery(event.target).closest('.mercury-region').get(0) == Mercury.region.element.get(0)
108
+ Mercury.trigger('unfocus:regions') unless jQuery(event.target).closest(".#{Mercury.config.regionClass}").get(0) == Mercury.region.element.get(0)
96
109
 
97
110
  jQuery(window).resize => @resize()
98
111
  window.onbeforeunload = @beforeUnload
@@ -138,7 +151,7 @@ class @Mercury.PageEditor
138
151
  if jQuery(element).hasClass(classname)
139
152
  ignored = true
140
153
  continue
141
- if !ignored && (element.target == '' || element.target == '_self') && !jQuery(element).closest('.mercury-region').length
154
+ if !ignored && (element.target == '' || element.target == '_self') && !jQuery(element).closest(".#{Mercury.config.regionClass}").length
142
155
  jQuery(element).attr('target', '_top')
143
156
 
144
157
 
@@ -148,15 +161,23 @@ class @Mercury.PageEditor
148
161
  return null
149
162
 
150
163
 
164
+ getRegionByName: (id) ->
165
+ for region in @regions
166
+ return region if region.name == id
167
+ return null
168
+
169
+
151
170
  save: ->
152
171
  url = @saveUrl ? Mercury.saveURL ? @iframeSrc()
153
172
  data = @serialize()
154
173
  Mercury.log('saving', data)
155
174
  data = jQuery.toJSON(data) unless @options.saveStyle == 'form'
175
+ method = 'PUT' if @options.saveMethod == 'PUT'
156
176
  jQuery.ajax url, {
157
- type: 'POST'
177
+ type: method || 'POST'
178
+ dataType: @options.saveDataType || 'json'
158
179
  headers: @saveHeaders()
159
- data: {content: data}
180
+ data: {content: data, _method: method}
160
181
  success: =>
161
182
  Mercury.changes = false
162
183
  error: =>
@@ -0,0 +1,27 @@
1
+ class Mercury.PageEditor extends Mercury.PageEditor
2
+
3
+ save: ->
4
+ url = @saveUrl ? Mercury.saveURL ? @iframeSrc()
5
+ data = @serializeAsXml()
6
+ console.log('saving', data)
7
+ return
8
+ method = 'PUT' if @options.saveMethod == 'PUT'
9
+ jQuery.ajax url, {
10
+ type: method || 'POST'
11
+ dataType: 'xml'
12
+ data: data
13
+ success: =>
14
+ Mercury.changes = false
15
+ error: =>
16
+ alert("Mercury was unable to save to the url: #{url}")
17
+ }
18
+
19
+ serializeAsXml: ->
20
+ data = @serialize()
21
+ regionNodes = []
22
+ for regionName, regionProperties of data
23
+ snippetNodes = []
24
+ for snippetName, snippetProperties of regionProperties['snippets']
25
+ snippetNodes.push("<#{snippetName} name=\"#{snippetProperties['name']}\"><![CDATA[#{jQuery.toJSON(snippetProperties['options'])}]]></#{snippetName}>")
26
+ regionNodes.push("<region name=\"#{regionName}\" type=\"#{regionProperties['type']}\"><value>\n<![CDATA[#{regionProperties['value']}]]>\n</value><snippets>#{snippetNodes.join('')}</snippets></region>")
27
+ return "<regions>#{regionNodes.join('')}</regions>"
@@ -0,0 +1,9 @@
1
+ /*!
2
+ * This is an example plugin that will serialize to XML instead of JSON before saving. This could be useful to someone,
3
+ * but is mostly provided as an example of how to write a simple plugin.
4
+ *
5
+ * This file is could be a nice place to provide configuration options for your plugin.
6
+ *
7
+ *= require_self
8
+ *= require_tree ./mercury
9
+ */
@@ -66,11 +66,11 @@ class @Mercury.Region
66
66
  togglePreview: ->
67
67
  if @previewing
68
68
  @previewing = false
69
- @element.addClass('mercury-region').removeClass('mercury-region-preview')
69
+ @element.addClass(Mercury.config.regionClass).removeClass("#{Mercury.config.regionClass}-preview")
70
70
  @focus() if Mercury.region == @
71
71
  else
72
72
  @previewing = true
73
- @element.addClass('mercury-region-preview').removeClass('mercury-region')
73
+ @element.addClass("#{Mercury.config.regionClass}-preview").removeClass(Mercury.config.regionClass)
74
74
  Mercury.trigger('region:blurred', {region: @})
75
75
 
76
76
 
@@ -28,11 +28,14 @@ class @Mercury.Regions.Editable extends Mercury.Region
28
28
 
29
29
  # add the basic editor settings to the document (only once)
30
30
  unless @document.mercuryEditing
31
- @document.execCommand('styleWithCSS', false, false)
32
- @document.execCommand('insertBROnReturn', false, true)
33
- @document.execCommand('enableInlineTableEditing', false, false)
34
- @document.execCommand('enableObjectResizing', false, false)
35
31
  @document.mercuryEditing = true
32
+ try
33
+ @document.execCommand('styleWithCSS', false, false)
34
+ @document.execCommand('insertBROnReturn', false, true)
35
+ @document.execCommand('enableInlineTableEditing', false, false)
36
+ @document.execCommand('enableObjectResizing', false, false)
37
+ catch e
38
+ # intentionally do nothing if any of these fail, to broaden support for Opera
36
39
 
37
40
 
38
41
  bindEvents: ->
@@ -85,7 +88,7 @@ class @Mercury.Regions.Editable extends Mercury.Region
85
88
  # isn't handled (eg, putting the image where it was dropped,) so to allow the browser to do it's thing, and also do
86
89
  # our thing we have this little hack. *sigh*
87
90
  # read: http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html
88
- @element.bind 'possible:drop', (event) =>
91
+ @element.bind 'possible:drop', =>
89
92
  return if @previewing
90
93
  if snippet = @element.find('img[data-snippet]').get(0)
91
94
  @focus()
@@ -96,16 +99,15 @@ class @Mercury.Regions.Editable extends Mercury.Region
96
99
  # through a clipboard in firefox (heaven forbid), and to keep the behavior across all browsers, we manually detect
97
100
  # what was pasted by running a quick diff, removing it by calling undo, making our adjustments, and then putting the
98
101
  # content back. This is possible, so it doesn't make sense why it wouldn't be exposed in a sensible way. *sigh*
99
- @element.bind 'paste', =>
102
+ @element.bind 'paste', (event) =>
100
103
  return if @previewing
101
104
  return unless Mercury.region == @
102
- Mercury.changes = true
103
105
  if @specialContainer
104
106
  event.preventDefault()
105
107
  return
106
- content = @content()
107
- clearTimeout(@handlePasteTimeout)
108
- @handlePasteTimeout = setTimeout((=> @handlePaste(content)), 100)
108
+ return if @pasting
109
+ Mercury.changes = true
110
+ @handlePaste(event.originalEvent)
109
111
 
110
112
  @element.focus =>
111
113
  return if @previewing
@@ -144,25 +146,27 @@ class @Mercury.Regions.Editable extends Mercury.Region
144
146
  return
145
147
 
146
148
  when 13 # enter
147
- if jQuery.browser.webkit && @selection().commonAncestor().closest('li, ul', @element).length == 0
149
+ if jQuery.browser.webkit && @selection().commonAncestor().closest('li, ul, ol', @element).length == 0
148
150
  event.preventDefault()
149
- @document.execCommand('insertLineBreak', false, null)
150
- else if @specialContainer
151
- # mozilla: pressing enter in any elemeny besides a div handles strangely
151
+ @document.execCommand('insertParagraph', false, null)
152
+ else if @specialContainer || jQuery.browser.opera
153
+ # mozilla: pressing enter in any element besides a div handles strangely
152
154
  event.preventDefault()
153
155
  @document.execCommand('insertHTML', false, '<br/>')
154
156
 
155
157
  when 9 # tab
156
158
  event.preventDefault()
157
159
  container = @selection().commonAncestor()
158
- handled = false
159
160
 
160
161
  # indent when inside of an li
161
162
  if container.closest('li', @element).length
162
- handled = true
163
- if event.shiftKey then @execCommand('outdent') else @execCommand('indent')
164
-
165
- @execCommand('insertHTML', {value: '&nbsp; '}) unless handled
163
+ unless event.shiftKey
164
+ @execCommand('indent')
165
+ # do not outdent on last ul/ol parent, or we break out of the list
166
+ else if container.parents('ul, ol').length > 1
167
+ @execCommand('outdent')
168
+ else
169
+ @execCommand('insertHTML', {value: '&nbsp; '})
166
170
 
167
171
  if event.metaKey
168
172
  switch event.keyCode
@@ -320,78 +324,69 @@ class @Mercury.Regions.Editable extends Mercury.Region
320
324
  return element
321
325
 
322
326
 
323
- handlePaste: (prePasteContent) ->
324
- prePasteContent = prePasteContent.replace(/^\<br\>/, '')
325
-
326
- # remove any regions that might have been pasted
327
- @element.find('.mercury-region').remove()
328
-
329
- # handle pasting from ms office etc
330
- content = @content()
331
- if content.indexOf('<!--StartFragment-->') > -1 || content.indexOf('="mso-') > -1 || content.indexOf('<o:') > -1 || content.indexOf('="Mso') > -1
332
- # clean out all the tags from the pasted contents
333
- cleaned = prePasteContent.singleDiff(@content()).sanitizeHTML()
334
- try
335
- # try to undo and put the cleaned html where the selection was
336
- @document.execCommand('undo', false, null)
337
- @execCommand('insertHTML', {value: cleaned})
338
- catch error
339
- # remove the pasted html and load up the cleaned contents into a modal
340
- @content(prePasteContent)
341
- Mercury.modal '/mercury/modals/sanitizer', {
342
- title: 'HTML Sanitizer (Starring Clippy)',
343
- afterLoad: -> @element.find('textarea').val(cleaned.replace(/<br\/>/g, '\n'))
344
- }
345
- else if Mercury.config.cleanStylesOnPaste
346
- # strip styles
347
- pasted = prePasteContent.singleDiff(@content())
348
-
349
- container = jQuery('<div>').appendTo(@document.createDocumentFragment()).html(pasted)
350
- container.find('[style]').attr({style: null})
327
+ handlePaste: (event) ->
328
+ # get the text content from the clipboard and fall back to using the sanitizer if unavailable
329
+ if Mercury.config.pasting.sanitize == 'text' && event.clipboardData
330
+ @execCommand('insertHTML', {value: event.clipboardData.getData('text/plain')})
331
+ event.preventDefault()
332
+ return
333
+ else
334
+ console.debug('testing')
335
+ # get current selection & range
336
+ selection = @selection()
337
+ selection.placeMarker()
338
+
339
+ sanitizer = jQuery('#mercury_sanitizer', @document).focus()
340
+
341
+ # set 1ms timeout to allow paste event to complete
342
+ setTimeout 1, =>
343
+ # sanitize the content
344
+ content = @sanitize(sanitizer)
345
+
346
+ # move cursor back to original element & position
347
+ selection.selectMarker(@element)
348
+ selection.removeMarker()
349
+
350
+ # paste sanitized content
351
+ @element.focus()
352
+ @execCommand('insertHTML', {value: content})
353
+
354
+
355
+ sanitize: (sanitizer) ->
356
+ # always remove nested regions
357
+ sanitizer.find(".#{Mercury.config.regionClass}").remove()
358
+
359
+ if Mercury.config.pasting.sanitize
360
+ switch Mercury.config.pasting.sanitize
361
+ when 'blacklist'
362
+ # todo: finish writing black list functionality
363
+ sanitizer.find('[style]').removeAttr('style')
364
+ sanitizer.find('[class="Apple-style-span"]').removeClass('Apple-style-span')
365
+ content = sanitizer.html()
366
+ when 'whitelist'
367
+ for element in sanitizer.find('*')
368
+ allowed = false
369
+ for allowedTag, allowedAttributes of Mercury.config.pasting.whitelist
370
+ if element.tagName.toLowerCase() == allowedTag.toLowerCase()
371
+ allowed = true
372
+ for attr, index in element.attributes
373
+ jQuery(element).attr(attr.name, null) if attr && allowedAttributes.indexOf(attr.name) == -1
374
+ break
375
+ jQuery(element).replaceWith(jQuery(element).contents()) unless allowed
376
+ content = sanitizer.html()
377
+ else content = sanitizer.text()
378
+ else
379
+ # force text if it looks like it's from word/pages, even if there's no sanitizing requested
380
+ content = sanitizer.html()
381
+ if content.indexOf('<!--StartFragment-->') > -1 || content.indexOf('="mso-') > -1 || content.indexOf('<o:') > -1 || content.indexOf('="Mso') > -1
382
+ content = sanitizer.text()
351
383
 
352
- @document.execCommand('undo', false, null)
353
- @execCommand('insertHTML', {value: container.html()})
384
+ sanitizer.html('')
385
+ return content
354
386
 
355
387
 
356
388
  # Custom actions (eg. things that execCommand doesn't do, or doesn't do well)
357
389
  @actions: {
358
- # bold: (selection) ->
359
- # unless selection.collapsed
360
- # @document.execCommand('bold', false, null)
361
- # else
362
- ## selection.selectWordByCursor()
363
- ## @document.execCommand('bold', false, null)
364
- #
365
- # selection.placeMarker()
366
- # node = @element.find('.mercury-marker').get(0)
367
- # prev = node.previousSibling
368
- # selection.removeMarker()
369
- # next = prev.nextSibling
370
- # console.debug(prev, next)
371
- ## if prev.textContent[prev.textContent.length - 1] != ' ' &&
372
-
373
-
374
- #[some]| content
375
- # textContent = some, wholeText = some
376
- # textContent = '', wholeText = some
377
- #content |[some]
378
- #content [s|ome]
379
- #co|ntent [some]
380
- #|content [some]
381
-
382
-
383
- #
384
- # console.debug(commonAncestor)
385
- # beforeText = commonAncestor.textContent
386
- # afterText = commonAncestor.wholeText.substring(commonAncestor.wholeText.lastIndexOf(beforeText) + beforeText.length, commonAncestor.wholeText.length)
387
- # if beforeText && afterText && (beforeChar = beforeText[beforeText.length - 1]) != ' ' && (afterChar = afterText[0]) != ' '
388
- # console.debug('bolding word', beforeChar, afterChar)
389
- # else
390
- # @document.execCommand('bold', false, null)
391
- #
392
- ## selection.selectWord()
393
- ## commonAncestor.wholeText, commonAncestor.textContent
394
- # else
395
390
 
396
391
  insertRowBefore: -> Mercury.tableEditor.addRow('before')
397
392
 
@@ -417,6 +412,8 @@ class @Mercury.Regions.Editable extends Mercury.Region
417
412
 
418
413
  redo: -> @content(@history.redo())
419
414
 
415
+ horizontalRule: -> this.execCommand('insertHorizontalRule')
416
+
420
417
  removeFormatting: (selection) -> selection.insertTextNode(selection.textContent())
421
418
 
422
419
  backColor: (selection, options) -> selection.wrap("<span style=\"background-color:#{options.value.toHex()}\">", true)
@@ -490,7 +487,7 @@ class Mercury.Regions.Editable.Selection
490
487
 
491
488
  is: (elementType) ->
492
489
  content = @content()
493
- return jQuery(content.firstChild) if content.childNodes.length == 1 && jQuery(content.firstChild).is(elementType)
490
+ return jQuery(content.firstChild) if jQuery(content).length == 1 && jQuery(content.firstChild).is(elementType)
494
491
  return false
495
492
 
496
493
 
@@ -498,18 +495,16 @@ class Mercury.Regions.Editable.Selection
498
495
  return unless jQuery.browser.webkit
499
496
  range = @context.createRange()
500
497
 
501
- # todo: the \00 thing breaks when using uglifier, and is escapped to "0".. it's been fixed, but isn't available yet
502
- # https://github.com/lautis/uglifier/issues/11
503
498
  if @range
504
499
  if @commonAncestor(true).closest('.mercury-snippet').length
505
- lastChild = @context.createTextNode('\00') #\00
500
+ lastChild = @context.createTextNode('\00')
506
501
  element.appendChild(lastChild)
507
502
  else
508
503
  if element.lastChild && element.lastChild.nodeType == 3 && element.lastChild.textContent.replace(/^[\s+|\n+]|[\s+|\n+]$/, '') == ''
509
504
  lastChild = element.lastChild
510
- element.lastChild.textContent = '\00' #\00
505
+ element.lastChild.textContent = '\00'
511
506
  else
512
- lastChild = @context.createTextNode(' ') #\00
507
+ lastChild = @context.createTextNode('\00')
513
508
  element.appendChild(lastChild)
514
509
 
515
510
  if lastChild
@@ -582,7 +577,7 @@ class Mercury.Regions.Editable.Selection
582
577
  @selection.addRange(@range)
583
578
 
584
579
 
585
- replace: (element) ->
580
+ replace: (element, collapse) ->
586
581
  element = element.get(0) if element.get
587
582
  element = jQuery(element, @context).get(0) if jQuery.type(element) == 'string'
588
583
 
@@ -590,3 +585,4 @@ class Mercury.Regions.Editable.Selection
590
585
  @range.insertNode(element)
591
586
  @range.selectNodeContents(element)
592
587
  @selection.addRange(@range)
588
+ @range.collapse(false) if collapse