mercury-rails 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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