mercury-rails 0.1.0

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 (153) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +152 -0
  3. data/VERSION +1 -0
  4. data/app/assets/images/mercury/button.png +0 -0
  5. data/app/assets/images/mercury/clippy.png +0 -0
  6. data/app/assets/images/mercury/default-snippet.png +0 -0
  7. data/app/assets/images/mercury/loading-dark.gif +0 -0
  8. data/app/assets/images/mercury/loading-light.gif +0 -0
  9. data/app/assets/images/mercury/search-icon.png +0 -0
  10. data/app/assets/images/mercury/toolbar/editable/buttons.png +0 -0
  11. data/app/assets/images/mercury/toolbar/markupable/buttons.png +0 -0
  12. data/app/assets/images/mercury/toolbar/primary/_expander.png +0 -0
  13. data/app/assets/images/mercury/toolbar/primary/_pressed.png +0 -0
  14. data/app/assets/images/mercury/toolbar/primary/historypanel.png +0 -0
  15. data/app/assets/images/mercury/toolbar/primary/insertcharacter.png +0 -0
  16. data/app/assets/images/mercury/toolbar/primary/insertlink.png +0 -0
  17. data/app/assets/images/mercury/toolbar/primary/insertmedia.png +0 -0
  18. data/app/assets/images/mercury/toolbar/primary/inserttable.png +0 -0
  19. data/app/assets/images/mercury/toolbar/primary/inspectorpanel.png +0 -0
  20. data/app/assets/images/mercury/toolbar/primary/notespanel.png +0 -0
  21. data/app/assets/images/mercury/toolbar/primary/objectspanel.png +0 -0
  22. data/app/assets/images/mercury/toolbar/primary/preview.png +0 -0
  23. data/app/assets/images/mercury/toolbar/primary/redo.png +0 -0
  24. data/app/assets/images/mercury/toolbar/primary/save.png +0 -0
  25. data/app/assets/images/mercury/toolbar/primary/todospanel.png +0 -0
  26. data/app/assets/images/mercury/toolbar/primary/undo.png +0 -0
  27. data/app/assets/images/mercury/toolbar/snippetable/buttons.png +0 -0
  28. data/app/assets/javascripts/mercury.js +30 -0
  29. data/app/assets/javascripts/mercury/dialog.js.coffee +75 -0
  30. data/app/assets/javascripts/mercury/dialogs/backcolor.js.coffee +6 -0
  31. data/app/assets/javascripts/mercury/dialogs/forecolor.js.coffee +6 -0
  32. data/app/assets/javascripts/mercury/dialogs/formatblock.js.coffee +4 -0
  33. data/app/assets/javascripts/mercury/dialogs/objectspanel.js.coffee +10 -0
  34. data/app/assets/javascripts/mercury/dialogs/style.js.coffee +4 -0
  35. data/app/assets/javascripts/mercury/history_buffer.js.coffee +30 -0
  36. data/app/assets/javascripts/mercury/mercury.js.coffee +293 -0
  37. data/app/assets/javascripts/mercury/modal.js.coffee +177 -0
  38. data/app/assets/javascripts/mercury/modals/htmleditor.js.coffee +10 -0
  39. data/app/assets/javascripts/mercury/modals/insertcharacter.js.coffee +4 -0
  40. data/app/assets/javascripts/mercury/modals/insertlink.js.coffee +92 -0
  41. data/app/assets/javascripts/mercury/modals/insertmedia.js.coffee +72 -0
  42. data/app/assets/javascripts/mercury/modals/insertsnippet.js.coffee +11 -0
  43. data/app/assets/javascripts/mercury/modals/inserttable.js.coffee +56 -0
  44. data/app/assets/javascripts/mercury/native_extensions.js.coffee +47 -0
  45. data/app/assets/javascripts/mercury/page_editor.js.coffee +139 -0
  46. data/app/assets/javascripts/mercury/palette.js.coffee +29 -0
  47. data/app/assets/javascripts/mercury/panel.js.coffee +97 -0
  48. data/app/assets/javascripts/mercury/region.js.coffee +103 -0
  49. data/app/assets/javascripts/mercury/regions/editable.js.coffee +546 -0
  50. data/app/assets/javascripts/mercury/regions/markupable.js.coffee +380 -0
  51. data/app/assets/javascripts/mercury/regions/snippetable.js.coffee +127 -0
  52. data/app/assets/javascripts/mercury/select.js.coffee +40 -0
  53. data/app/assets/javascripts/mercury/snippet.js.coffee +92 -0
  54. data/app/assets/javascripts/mercury/snippet_toolbar.js.coffee +69 -0
  55. data/app/assets/javascripts/mercury/statusbar.js.coffee +25 -0
  56. data/app/assets/javascripts/mercury/table_editor.js.coffee +266 -0
  57. data/app/assets/javascripts/mercury/toolbar.button.js.coffee +152 -0
  58. data/app/assets/javascripts/mercury/toolbar.button_group.js.coffee +42 -0
  59. data/app/assets/javascripts/mercury/toolbar.expander.js.coffee +56 -0
  60. data/app/assets/javascripts/mercury/toolbar.js.coffee +72 -0
  61. data/app/assets/javascripts/mercury/tooltip.js.coffee +67 -0
  62. data/app/assets/javascripts/mercury/uploader.js.coffee +213 -0
  63. data/app/assets/javascripts/mercury/websocket.js.coffee +34 -0
  64. data/app/assets/stylesheets/mercury.css +31 -0
  65. data/app/assets/stylesheets/mercury/dialog.scss +178 -0
  66. data/app/assets/stylesheets/mercury/mercury.scss +119 -0
  67. data/app/assets/stylesheets/mercury/modal.scss +192 -0
  68. data/app/assets/stylesheets/mercury/statusbar.scss +23 -0
  69. data/app/assets/stylesheets/mercury/toolbar.scss +417 -0
  70. data/app/assets/stylesheets/mercury/tooltip.scss +26 -0
  71. data/app/assets/stylesheets/mercury/uploader.scss +109 -0
  72. data/app/controllers/images_controller.rb +19 -0
  73. data/app/controllers/mercury_controller.rb +20 -0
  74. data/app/models/image.rb +14 -0
  75. data/app/views/layouts/mercury.html.haml +12 -0
  76. data/app/views/mercury/modals/character.html.haml +252 -0
  77. data/app/views/mercury/modals/htmleditor.html.haml +8 -0
  78. data/app/views/mercury/modals/link.html.haml +31 -0
  79. data/app/views/mercury/modals/media.html.haml +33 -0
  80. data/app/views/mercury/modals/sanitizer.html.haml +4 -0
  81. data/app/views/mercury/modals/table.html.haml +49 -0
  82. data/app/views/mercury/palettes/backcolor.html.haml +79 -0
  83. data/app/views/mercury/palettes/forecolor.html.haml +79 -0
  84. data/app/views/mercury/panels/history.html.haml +0 -0
  85. data/app/views/mercury/panels/notes.html.haml +0 -0
  86. data/app/views/mercury/panels/snippets.html.haml +10 -0
  87. data/app/views/mercury/selects/formatblock.html.haml +10 -0
  88. data/app/views/mercury/selects/style.html.haml +4 -0
  89. data/app/views/mercury/snippets/example.html.haml +2 -0
  90. data/app/views/mercury/snippets/example_options.html.haml +16 -0
  91. data/config/engine.rb +6 -0
  92. data/config/routes.rb +15 -0
  93. data/db/migrate/20110526035601_create_images.rb +11 -0
  94. data/features/editing/basic.feature +11 -0
  95. data/features/step_definitions/debug_steps.rb +14 -0
  96. data/features/step_definitions/web_steps.rb +211 -0
  97. data/features/support/env.rb +46 -0
  98. data/features/support/paths.rb +35 -0
  99. data/features/support/selectors.rb +42 -0
  100. data/lib/mercury-rails.rb +4 -0
  101. data/log/.gitkeep +0 -0
  102. data/mercury-rails.gemspec +230 -0
  103. data/spec/javascripts/mercury/dialog_spec.js.coffee +258 -0
  104. data/spec/javascripts/mercury/history_buffer_spec.js.coffee +79 -0
  105. data/spec/javascripts/mercury/mercury_spec.js.coffee +52 -0
  106. data/spec/javascripts/mercury/native_extensions_spec.js.coffee +66 -0
  107. data/spec/javascripts/mercury/page_editor_spec.js.coffee +435 -0
  108. data/spec/javascripts/mercury/palette_spec.js.coffee +51 -0
  109. data/spec/javascripts/mercury/panel_spec.js.coffee +147 -0
  110. data/spec/javascripts/mercury/region_spec.js.coffee +261 -0
  111. data/spec/javascripts/mercury/regions/_editable_.js.coffee +0 -0
  112. data/spec/javascripts/mercury/regions/_markupable_.js.coffee +0 -0
  113. data/spec/javascripts/mercury/regions/snippetable_spec.js.coffee +368 -0
  114. data/spec/javascripts/mercury/select_spec.js.coffee +51 -0
  115. data/spec/javascripts/mercury/snippet_spec.js.coffee +246 -0
  116. data/spec/javascripts/mercury/snippet_toolbar_spec.js.coffee +186 -0
  117. data/spec/javascripts/mercury/statusbar_spec.js.coffee +78 -0
  118. data/spec/javascripts/mercury/table_editor_spec.js.coffee +192 -0
  119. data/spec/javascripts/mercury/toolbar.button_group_spec.js.coffee +92 -0
  120. data/spec/javascripts/mercury/toolbar.button_spec.js.coffee +341 -0
  121. data/spec/javascripts/mercury/toolbar.expander_spec.js.coffee +120 -0
  122. data/spec/javascripts/mercury/toolbar_spec.js.coffee +152 -0
  123. data/spec/javascripts/mercury/tooltip_spec.js.coffee +188 -0
  124. data/spec/javascripts/mercury/uploader_spec.js.coffee +512 -0
  125. data/spec/javascripts/responses/blank.html +1 -0
  126. data/spec/javascripts/spec_helper.js +513 -0
  127. data/spec/javascripts/templates/mercury/dialog.html +2 -0
  128. data/spec/javascripts/templates/mercury/page_editor.html +24 -0
  129. data/spec/javascripts/templates/mercury/palette.html +16 -0
  130. data/spec/javascripts/templates/mercury/panel.html +16 -0
  131. data/spec/javascripts/templates/mercury/region.html +2 -0
  132. data/spec/javascripts/templates/mercury/regions/snippetable.html +4 -0
  133. data/spec/javascripts/templates/mercury/select.html +16 -0
  134. data/spec/javascripts/templates/mercury/snippet.html +1 -0
  135. data/spec/javascripts/templates/mercury/snippet_toolbar.html +16 -0
  136. data/spec/javascripts/templates/mercury/statusbar.html +7 -0
  137. data/spec/javascripts/templates/mercury/table_editor.html +65 -0
  138. data/spec/javascripts/templates/mercury/toolbar.button.html +64 -0
  139. data/spec/javascripts/templates/mercury/toolbar.button_group.html +9 -0
  140. data/spec/javascripts/templates/mercury/toolbar.expander.html +18 -0
  141. data/spec/javascripts/templates/mercury/toolbar.html +10 -0
  142. data/spec/javascripts/templates/mercury/tooltip.html +12 -0
  143. data/spec/javascripts/templates/mercury/uploader.html +11 -0
  144. data/vendor/assets/javascripts/jquery-1.6.js +8865 -0
  145. data/vendor/assets/javascripts/jquery-ui-1.8.13.custom.min.js +249 -0
  146. data/vendor/assets/javascripts/jquery-ui-1.8.13.sortable.custom.js +1078 -0
  147. data/vendor/assets/javascripts/jquery.easing.js +173 -0
  148. data/vendor/assets/javascripts/jquery.json2.js +178 -0
  149. data/vendor/assets/javascripts/jquery.serialize_object.js +16 -0
  150. data/vendor/assets/javascripts/jquery.ujs.js +289 -0
  151. data/vendor/assets/javascripts/liquidmetal.js +88 -0
  152. data/vendor/assets/javascripts/showdown.js +1362 -0
  153. metadata +364 -0
@@ -0,0 +1,139 @@
1
+ class @Mercury.PageEditor
2
+
3
+ # options
4
+ # saveStyle: 'form', or 'json' (defaults to json)
5
+ # ignoredLinks: an array containing classes for links to ignore (eg. lightbox or accordian controls)
6
+ constructor: (@saveUrl = null, @options = {}) ->
7
+ throw "Mercury.PageEditor is unsupported in this client. Supported browsers are chrome 10+, firefix 4+, and safari 5+." unless Mercury.supported
8
+ throw "Mercury.PageEditor can only be instantiated once." if window.mercuryInstance
9
+
10
+ window.mercuryInstance = @
11
+ @regions = []
12
+ @initializeInterface()
13
+ Mercury.csrfToken = token if token = $('meta[name="csrf-token"]').attr('content')
14
+
15
+
16
+ initializeInterface: ->
17
+ @focusableElement = $('<input>', {type: 'text', style: 'position:absolute;opacity:0'}).appendTo(@options.appendTo ? 'body')
18
+ @iframe = $('<iframe>', {class: 'mercury-iframe', seamless: 'true', frameborder: '0', src: 'about:blank', style: 'position:absolute;top:0;width:100%;visibility:hidden'})
19
+ @iframe.load => @initializeFrame()
20
+ @iframe.attr('src', @iframeSrc())
21
+ @iframe.appendTo($(@options.appendTo).get(0) ? 'body')
22
+
23
+ @toolbar = new Mercury.Toolbar(@options)
24
+ @statusbar = new Mercury.Statusbar(@options)
25
+
26
+
27
+ initializeFrame: ->
28
+ try
29
+ return if @iframe.data('loaded')
30
+ @iframe.data('loaded', true)
31
+ @document = $(@iframe.get(0).contentWindow.document)
32
+ $("<style mercury-styles=\"true\">").html(Mercury.config.injectedStyles).appendTo(@document.find('head'))
33
+
34
+ @bindEvents()
35
+ @initializeRegions()
36
+ @finalizeInterface()
37
+
38
+ @iframe.css({visibility: 'visible'})
39
+ catch error
40
+ alert("Mercury.PageEditor failed to load: #{error}\n\nPlease try refreshing.")
41
+
42
+
43
+ initializeRegions: ->
44
+ @buildRegion($(region)) for region in $('.mercury-region', @document)
45
+ for region in @regions
46
+ if region.focus
47
+ region.focus()
48
+ break
49
+
50
+
51
+ buildRegion: (region) ->
52
+ try
53
+ type = region.data('type').titleize()
54
+ @regions.push(new Mercury.Regions[type](region, @iframe.get(0).contentWindow))
55
+ catch error
56
+ alert(error) if Mercury.debug
57
+ alert("Region type is malformed, no data-type provided, or \"#{type}\" is unknown.")
58
+
59
+
60
+ finalizeInterface: ->
61
+ @snippetToolbar = new Mercury.SnippetToolbar(@document)
62
+
63
+ @hijackLinks()
64
+ @resize()
65
+
66
+
67
+ bindEvents: ->
68
+ Mercury.bind 'initialize:frame', => setTimeout(@initializeFrame, 100)
69
+ Mercury.bind 'focus:frame', => @iframe.focus()
70
+ Mercury.bind 'focus:window', => setTimeout((=> @focusableElement.focus()), 10)
71
+
72
+ Mercury.bind 'action', (event, options) =>
73
+ @save() if options.action == 'save'
74
+
75
+ @document.mousedown (event) ->
76
+ Mercury.trigger('hide:dialogs')
77
+ Mercury.trigger('unfocus:regions') unless $(event.target).closest('.mercury-region').get(0) == Mercury.region.element.get(0)
78
+
79
+ $(window).resize => @resize()
80
+ window.onbeforeunload = @beforeUnload
81
+
82
+
83
+ resize: ->
84
+ width = $(window).width()
85
+ height = $(window).height()
86
+ toolbarHeight = @toolbar.height()
87
+ statusbarHeight = @statusbar.height()
88
+
89
+ Mercury.displayRect = {top: toolbarHeight, left: 0, width: width, height: height - statusbarHeight - toolbarHeight}
90
+
91
+ @iframe.css {
92
+ top: toolbarHeight,
93
+ width: width,
94
+ height: height - statusbarHeight - toolbarHeight
95
+ }
96
+
97
+ Mercury.trigger('resize')
98
+
99
+
100
+ iframeSrc: (url = null) ->
101
+ (url ? window.location.href).replace(/([http|https]:\/\/.[^\/]*)\/edit\/?(.*)/i, "$1/$2")
102
+
103
+
104
+ hijackLinks: ->
105
+ for link in $('a', @document)
106
+ ignored = false
107
+ for classname in @options.ignoredLinks || []
108
+ if $(link).hasClass(classname)
109
+ ignored = true
110
+ continue
111
+ if !ignored && (link.target == '' || link.target == '_self') && !$(link).closest('.mercury-region').length
112
+ $(link).attr('target', '_top')
113
+
114
+
115
+ beforeUnload: ->
116
+ if Mercury.changes && !Mercury.silent
117
+ return "You have unsaved changes. Are you sure you want to leave without saving them first?"
118
+ return null
119
+
120
+
121
+ save: ->
122
+ url = @saveUrl ? @iframeSrc()
123
+ data = @serialize()
124
+ Mercury.log('saving', data)
125
+ data = $.toJSON(data) unless @options.saveStyle == 'form'
126
+ $.ajax url, {
127
+ type: 'POST'
128
+ data: {content: data}
129
+ success: =>
130
+ Mercury.changes = false
131
+ error: =>
132
+ alert("Mercury was unable to save to the url: #{url}")
133
+ }
134
+
135
+
136
+ serialize: ->
137
+ serialized = {}
138
+ serialized[region.name] = region.serialize() for region in @regions
139
+ return serialized
@@ -0,0 +1,29 @@
1
+ class @Mercury.Palette extends Mercury.Dialog
2
+
3
+ constructor: (@url, @name, @options = {}) ->
4
+ super
5
+
6
+
7
+ build: ->
8
+ @element = $('<div>', {class: "mercury-palette mercury-#{@name}-palette loading", style: 'display:none'})
9
+ @element.appendTo($(@options.appendTo).get(0) ? 'body')
10
+
11
+
12
+ bindEvents: ->
13
+ Mercury.bind 'hide:dialogs', (event, dialog) => @hide() unless dialog == @
14
+ super
15
+
16
+
17
+ position: (keepVisible) ->
18
+ @element.css({top: 0, left: 0, display: 'block', visibility: 'hidden'})
19
+ position = @button.offset()
20
+ width = @element.width()
21
+
22
+ position.left = position.left - width + @button.width() if position.left + width > $(window).width()
23
+
24
+ @element.css {
25
+ top: position.top + @button.height(),
26
+ left: position.left,
27
+ display: if keepVisible then 'block' else 'none',
28
+ visibility: 'visible'
29
+ }
@@ -0,0 +1,97 @@
1
+ class @Mercury.Panel extends Mercury.Dialog
2
+
3
+ constructor: (@url, @name, @options = {}) ->
4
+ super
5
+
6
+
7
+ build: ->
8
+ @element = $('<div>', {class: 'mercury-panel loading', style: 'display:none;'})
9
+ @titleElement = $("<h1>#{@options.title}</h1>").appendTo(@element)
10
+ @paneElement = $('<div>', {class: 'mercury-panel-pane'}).appendTo(@element)
11
+
12
+ @element.appendTo($(@options.appendTo).get(0) ? 'body');
13
+
14
+
15
+ bindEvents: ->
16
+ Mercury.bind 'resize', => @position(@visible)
17
+ Mercury.bind 'hide:panels', (event, panel) =>
18
+ unless panel == @
19
+ @button.removeClass('pressed')
20
+ @hide()
21
+
22
+ @element.mousedown (event) -> event.stopPropagation()
23
+
24
+ super
25
+
26
+
27
+ show: ->
28
+ Mercury.trigger('hide:panels', @)
29
+ super
30
+
31
+
32
+ resize: ->
33
+ @paneElement.css({display: 'none'})
34
+ preWidth = @element.width()
35
+
36
+ @paneElement.css({visibility: 'hidden', width: 'auto', display: 'block'})
37
+ postWidth = @element.width()
38
+
39
+ @paneElement.css({visibility: 'visible', display: 'none'})
40
+ position = @element.offset()
41
+ @element.animate {left: position.left - (postWidth - preWidth), width: postWidth}, 200, 'easeInOutSine', =>
42
+ @paneElement.css({display: 'block', width: postWidth})
43
+ @makeDraggable()
44
+
45
+ @hide() unless @visible
46
+
47
+
48
+ position: (keepVisible) ->
49
+ @element.css({display: 'block', visibility: 'hidden'})
50
+ offset = @element.offset()
51
+ elementWidth = @element.width()
52
+ height = Mercury.displayRect.height - 16
53
+
54
+ paneHeight = height - @titleElement.outerHeight()
55
+ @paneElement.css({height: paneHeight, overflowY: if paneHeight < 30 then 'hidden' else 'auto'})
56
+
57
+ left = Mercury.displayRect.width - elementWidth - 20 unless @moved
58
+ left = 8 if left <= 8
59
+
60
+ if @pinned || elementWidth + offset.left > Mercury.displayRect.width - 20
61
+ left = Mercury.displayRect.width - elementWidth - 20
62
+
63
+ @element.css {
64
+ top: Mercury.displayRect.top + 8,
65
+ left: left,
66
+ height: height,
67
+ display: if keepVisible then 'block' else 'none',
68
+ visibility: 'visible'
69
+ }
70
+
71
+ @makeDraggable()
72
+ @element.hide() unless keepVisible
73
+
74
+
75
+ loadContent: (data) ->
76
+ @loaded = true
77
+ @element.removeClass('loading')
78
+ @paneElement.css({visibility: 'hidden'})
79
+ @paneElement.html(data)
80
+
81
+
82
+ makeDraggable: ->
83
+ elementWidth = @element.width()
84
+ @element.draggable {
85
+ handle: 'h1',
86
+ axis: 'x',
87
+ opacity: 0.70
88
+ scroll: false,
89
+ addClasses: false,
90
+ iframeFix: true,
91
+ containment: [8, 0, Mercury.displayRect.width - elementWidth - 20, 0] #[x1, y1, x2, y2]
92
+ stop: =>
93
+ left = @element.offset().left
94
+ @moved = true
95
+ @pinned = if left > Mercury.displayRect.width - elementWidth - 30 then true else false
96
+ return true
97
+ }
@@ -0,0 +1,103 @@
1
+ class @Mercury.Region
2
+ type = 'region'
3
+
4
+ constructor: (@element, @window, @options = {}) ->
5
+ @type = 'region' unless @type
6
+ Mercury.log("building #{@type}", @element, @options)
7
+
8
+ @document = @window.document
9
+ @name = @element.attr('id')
10
+ @history = new Mercury.HistoryBuffer()
11
+ @build()
12
+ @bindEvents()
13
+ @pushHistory()
14
+
15
+
16
+ build: ->
17
+
18
+
19
+ focus: ->
20
+
21
+
22
+ bindEvents: ->
23
+ Mercury.bind 'mode', (event, options) =>
24
+ @togglePreview() if options.mode == 'preview'
25
+
26
+ Mercury.bind 'focus:frame', =>
27
+ return if @previewing
28
+ return unless Mercury.region == @
29
+ @focus()
30
+
31
+ Mercury.bind 'action', (event, options) =>
32
+ return if @previewing
33
+ return unless Mercury.region == @
34
+ @execCommand(options.action, options) if options.action
35
+
36
+ @element.mousemove (event) =>
37
+ return if @previewing
38
+ return unless Mercury.region == @
39
+ snippet = $(event.target).closest('.mercury-snippet')
40
+ if snippet.length
41
+ @snippet = snippet
42
+ Mercury.trigger('show:toolbar', {type: 'snippet', snippet: @snippet})
43
+
44
+ @element.mouseout (event) =>
45
+ return if @previewing
46
+ Mercury.trigger('hide:toolbar', {type: 'snippet', immediately: false})
47
+
48
+
49
+ html: (value = null, filterSnippets = false) ->
50
+ if value != null
51
+ @element.html(value)
52
+ else
53
+ # sanitize the html before we return it
54
+ container = $('<div>').appendTo(@document.createDocumentFragment())
55
+ container.html(@element.html().replace(/^\s+|\s+$/g, ''))
56
+
57
+ # replace snippet contents to be an identifier
58
+ if filterSnippets then for snippet, index in container.find('.mercury-snippet')
59
+ snippet = $(snippet)
60
+ snippet.attr({contenteditable: null, 'data-version': null})
61
+ snippet.html("[#{snippet.data('snippet')}]")
62
+
63
+ return container.html()
64
+
65
+
66
+ togglePreview: ->
67
+ if @previewing
68
+ @previewing = false
69
+ @element.addClass('mercury-region').removeClass('mercury-region-preview')
70
+ @focus() if Mercury.region == @
71
+ else
72
+ @previewing = true
73
+ @element.addClass('mercury-region-preview').removeClass('mercury-region')
74
+ Mercury.trigger('region:blurred', {region: @})
75
+
76
+
77
+ execCommand: (action, options = {}) ->
78
+ @focus()
79
+ @pushHistory() unless action == 'redo'
80
+
81
+ Mercury.log('execCommand', action, options.value)
82
+ Mercury.changes = true
83
+
84
+
85
+ pushHistory: ->
86
+ @history.push(@html())
87
+
88
+
89
+ snippets: ->
90
+ snippets = {}
91
+ for element in @element.find('[data-snippet]')
92
+ snippet = Mercury.Snippet.find($(element).data('snippet'))
93
+ snippet.setVersion($(element).data('version'))
94
+ snippets[snippet.identity] = snippet.serialize()
95
+ return snippets
96
+
97
+
98
+ serialize: ->
99
+ return {
100
+ type: @type,
101
+ value: @html(null, true)
102
+ snippets: @snippets()
103
+ }
@@ -0,0 +1,546 @@
1
+ class @Mercury.Regions.Editable extends Mercury.Region
2
+ type = 'editable'
3
+
4
+ constructor: (@element, @window, @options = {}) ->
5
+ @type = 'editable'
6
+ super
7
+
8
+
9
+ build: ->
10
+ # mozilla: set some initial content so everything works correctly
11
+ @html('&nbsp;') if $.browser.mozilla && @html() == ''
12
+
13
+ # set overflow just in case
14
+ @element.data({originalOverflow: @element.css('overflow')})
15
+ @element.css({overflow: 'auto'})
16
+
17
+ # mozilla: there's some weird behavior when the element isn't a div
18
+ @specialContainer = $.browser.mozilla && @element.get(0).tagName != 'DIV'
19
+
20
+ # make it editable
21
+ # gecko: in this makes double clicking in textareas fail: https://bugzilla.mozilla.org/show_bug.cgi?id=490367
22
+ @element.get(0).contentEditable = true
23
+
24
+ # make all snippets not editable, and set their versions to 1
25
+ for element in @element.find('.mercury-snippet')
26
+ element.contentEditable = false
27
+ $(element).attr('data-version', '1')
28
+
29
+ # add the basic editor settings to the document (only once)
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
+ @document.mercuryEditing = true
36
+
37
+
38
+ bindEvents: ->
39
+ super
40
+
41
+ Mercury.bind 'region:update', =>
42
+ return if @previewing
43
+ return unless Mercury.region == @
44
+ setTimeout((=> @selection().forceSelection(@element.get(0))), 1)
45
+ currentElement = @currentElement()
46
+ if currentElement.length
47
+ # setup the table editor if we're inside a table
48
+ table = currentElement.closest('table', @element)
49
+ Mercury.tableEditor(table, currentElement) if table.length
50
+ # display a tooltip if we're in an anchor
51
+ anchor = currentElement.closest('a', @element)
52
+ if anchor.length && anchor.attr('href')
53
+ Mercury.tooltip(anchor, "<a href=\"#{anchor.attr('href')}\" target=\"_blank\">#{anchor.attr('href')}</a>", {position: 'below'})
54
+ else
55
+ Mercury.tooltip.hide()
56
+
57
+ @element.bind 'dragenter', (event) =>
58
+ return if @previewing
59
+ event.preventDefault() if event.shiftKey
60
+ event.originalEvent.dataTransfer.dropEffect = 'copy'
61
+
62
+ @element.bind 'dragover', (event) =>
63
+ return if @previewing
64
+ event.preventDefault() if event.shiftKey
65
+ event.originalEvent.dataTransfer.dropEffect = 'copy'
66
+ if $.browser.webkit
67
+ clearTimeout(@dropTimeout)
68
+ @dropTimeout = setTimeout((=> @element.trigger('possible:drop')), 10)
69
+
70
+ @element.bind 'drop', (event) =>
71
+ return if @previewing
72
+
73
+ # handle dropping snippets
74
+ clearTimeout(@dropTimeout)
75
+ @dropTimeout = setTimeout((=> @element.trigger('possible:drop')), 1)
76
+
77
+ # handle any files that were dropped
78
+ return unless event.originalEvent.dataTransfer.files.length
79
+ event.preventDefault()
80
+ @focus()
81
+ Mercury.uploader(event.originalEvent.dataTransfer.files[0])
82
+
83
+ # possible:drop custom event: we have to do this because webkit doesn't fire the drop event unless both dragover and
84
+ # dragstart default behaviors are canceled.. but when we do that and observe the drop event, the default behavior
85
+ # 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
+ # our thing we have this little hack. *sigh*
87
+ # read: http://www.quirksmode.org/blog/archives/2009/09/the_html5_drag.html
88
+ @element.bind 'possible:drop', (event) =>
89
+ return if @previewing
90
+ if snippet = @element.find('img[data-snippet]').get(0)
91
+ @focus()
92
+ Mercury.Snippet.displayOptionsFor($(snippet).data('snippet'))
93
+ @document.execCommand('undo', false, null)
94
+
95
+ # custom paste handling: we have to do some hackery to get the pasted content since it's not exposed normally
96
+ # through a clipboard in firefox (heaven forbid), and to keep the behavior across all browsers, we manually detect
97
+ # what was pasted by running a quick diff, removing it by calling undo, making our adjustments, and then putting the
98
+ # 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', =>
100
+ return if @previewing
101
+ return unless Mercury.region == @
102
+ Mercury.changes = true
103
+ html = @html()
104
+ event.preventDefault() if @specialContainer
105
+ setTimeout((=> @handlePaste(html)), 1)
106
+
107
+ @element.focus =>
108
+ return if @previewing
109
+ Mercury.region = @
110
+ setTimeout((=> @selection().forceSelection(@element.get(0))), 1)
111
+ Mercury.trigger('region:focused', {region: @})
112
+
113
+ @element.blur =>
114
+ return if @previewing
115
+ Mercury.trigger('region:blurred', {region: @})
116
+ Mercury.tooltip.hide()
117
+
118
+ @element.click (event) =>
119
+ $(event.target).closest('a').attr('target', '_top') if @previewing
120
+
121
+ @element.dblclick (event) =>
122
+ return if @previewing
123
+ image = $(event.target).closest('img', @element)
124
+ if image.length
125
+ @selection().selectNode(image.get(0), true)
126
+ Mercury.trigger('button', {action: 'insertmedia'})
127
+
128
+ @element.mouseup =>
129
+ return if @previewing
130
+ @pushHistory()
131
+ Mercury.trigger('region:update', {region: @})
132
+
133
+ @element.keydown (event) =>
134
+ return if @previewing
135
+ Mercury.changes = true
136
+ switch event.keyCode
137
+
138
+ when 90 # undo / redo
139
+ return unless event.metaKey
140
+ event.preventDefault()
141
+ if event.shiftKey then @execCommand('redo') else @execCommand('undo')
142
+ return
143
+
144
+ when 13 # enter
145
+ if $.browser.webkit && @selection().commonAncestor().closest('li, ul', @element).length == 0
146
+ event.preventDefault()
147
+ @document.execCommand('insertlinebreak', false, null)
148
+ else if @specialContainer
149
+ # mozilla: pressing enter in any elemeny besides a div handles strangely
150
+ event.preventDefault()
151
+ @document.execCommand('insertHTML', false, '<br/>')
152
+
153
+ when 9 # tab
154
+ event.preventDefault()
155
+ container = @selection().commonAncestor()
156
+ handled = false
157
+
158
+ # indent when inside of an li
159
+ if container.closest('li', @element).length
160
+ handled = true
161
+ if event.shiftKey then @execCommand('outdent') else @execCommand('indent')
162
+
163
+ @execCommand('insertHTML', {value: '&nbsp; '}) unless handled
164
+
165
+ if event.metaKey
166
+ switch event.keyCode
167
+
168
+ when 66 # b
169
+ @execCommand('bold')
170
+ event.preventDefault()
171
+
172
+ when 73 # i
173
+ @execCommand('italic')
174
+ event.preventDefault()
175
+
176
+ when 85 # u
177
+ @execCommand('underline')
178
+ event.preventDefault()
179
+
180
+ @pushHistory(event.keyCode)
181
+
182
+ @element.keyup =>
183
+ return if @previewing
184
+ Mercury.trigger('region:update', {region: @})
185
+
186
+
187
+ focus: ->
188
+ @element.focus()
189
+ setTimeout((=> @selection().forceSelection(@element.get(0))), 1)
190
+ Mercury.trigger('region:update', {region: @})
191
+
192
+
193
+ html: (value = null, filterSnippets = true, includeMarker = false) ->
194
+ if value != null
195
+ # sanitize the html before we insert it
196
+ container = $('<div>').appendTo(@document.createDocumentFragment())
197
+ container.html(value)
198
+
199
+ # fill in the snippet contents
200
+ for element in container.find('[data-snippet]')
201
+ element.contentEditable = false
202
+ element = $(element)
203
+ if snippet = Mercury.Snippet.find(element.data('snippet'))
204
+ unless element.data('version')
205
+ try
206
+ version = parseInt(element.html().match(/\/(\d+)\]/)[1])
207
+ if version
208
+ snippet.setVersion(version)
209
+ element.attr({'data-version': version})
210
+ element.html(snippet.data)
211
+ catch error
212
+
213
+ # set the html
214
+ @element.html(container.html())
215
+
216
+ # create a selection if there's markers
217
+ @selection().selectMarker(@element)
218
+ else
219
+ # remove any meta tags
220
+ @element.find('meta').remove()
221
+
222
+ # place markers for the selection
223
+ if includeMarker
224
+ selection = @selection()
225
+ selection.placeMarker()
226
+
227
+ # sanitize the html before we return it
228
+ container = $('<div>').appendTo(@document.createDocumentFragment())
229
+ container.html(@element.html().replace(/^\s+|\s+$/g, ''))
230
+
231
+ # replace snippet contents to be an identifier
232
+ if filterSnippets then for element, index in container.find('[data-snippet]')
233
+ element = $(element)
234
+ if snippet = Mercury.Snippet.find(element.data("snippet"))
235
+ snippet.data = element.html()
236
+ element.html("[#{element.data("snippet")}/#{element.data("version")}]")
237
+ element.attr({contenteditable: null, 'data-version': null})
238
+
239
+ # get the html before removing the markers
240
+ html = container.html()
241
+
242
+ # remove the markers from the dom
243
+ selection.removeMarker() if includeMarker
244
+
245
+ return html
246
+
247
+
248
+ togglePreview: ->
249
+ if @previewing
250
+ @element.get(0).contentEditable = true
251
+ @element.css({overflow: 'auto'})
252
+ else
253
+ @html(@html())
254
+ @element.get(0).contentEditable = false
255
+ @element.css({overflow: @element.data('originalOverflow')})
256
+ @element.blur()
257
+ super
258
+
259
+
260
+ execCommand: (action, options = {}) ->
261
+ super
262
+
263
+ # use a custom handler if there's one, otherwise use execCommand
264
+ if handler = Mercury.config.behaviors[action] || Mercury.Regions.Editable.actions[action]
265
+ handler.call(@, @selection(), options)
266
+ else
267
+ sibling = @element.get(0).previousSibling if action == 'indent'
268
+ options.value = $('<div>').html(options.value).html() if action == 'insertHTML' && options.value && options.value.get
269
+ try
270
+ @document.execCommand(action, false, options.value)
271
+ catch error
272
+ # mozilla: indenting when there's no br tag handles strangely
273
+ @element.prev().remove() if action == 'indent' && @element.prev() != sibling
274
+
275
+
276
+ pushHistory: (keyCode) ->
277
+ # when pressing return, delete or backspace it should push to the history
278
+ # all other times it should store if there's a 1 second pause
279
+ keyCodes = [13, 46, 8]
280
+ waitTime = 2.5
281
+ knownKeyCode = keyCodes.indexOf(keyCode) if keyCode
282
+
283
+ # clear any pushes to the history
284
+ clearTimeout(@historyTimeout)
285
+
286
+ # if the key code was return, delete, or backspace store now -- unless it was the same as last time
287
+ if knownKeyCode >= 0 && knownKeyCode != @lastKnownKeyCode # || !keyCode
288
+ @history.push(@html(null, false, true))
289
+ else if keyCode
290
+ # set a timeout for pushing to the history
291
+ @historyTimeout = setTimeout((=> @history.push(@html(null, false, true))), waitTime * 1000)
292
+ else
293
+ # push to the history immediately
294
+ @history.push(@html(null, false, true))
295
+
296
+ @lastKnownKeyCode = knownKeyCode
297
+
298
+
299
+ selection: ->
300
+ return new Mercury.Regions.Editable.Selection(@window.getSelection(), @document)
301
+
302
+
303
+ path: ->
304
+ container = @selection().commonAncestor()
305
+ return [] unless container
306
+ return if container.get(0) == @element.get(0) then [] else container.parentsUntil(@element)
307
+
308
+
309
+ currentElement: ->
310
+ element = []
311
+ selection = @selection()
312
+ if selection.range
313
+ element = selection.commonAncestor()
314
+ element = element.parent() if element.get(0).nodeType == 3
315
+ return element
316
+
317
+
318
+ handlePaste: (prePasteHTML) ->
319
+ prePasteHTML = prePasteHTML.replace(/^\<br\>/, '')
320
+
321
+ # remove any regions that might have been pasted
322
+ @element.find('.mercury-region').remove()
323
+
324
+ # handle pasting from ms office etc
325
+ html = @html()
326
+ if html.indexOf('<!--StartFragment-->') > -1 || html.indexOf('="mso-') > -1 || html.indexOf('<o:') > -1 || html.indexOf('="Mso') > -1
327
+ # clean out all the tags from the pasted contents
328
+ cleaned = prePasteHTML.singleDiff(@html()).sanitizeHTML()
329
+ try
330
+ # try to undo and put the cleaned html where the selection was
331
+ @document.execCommand('undo', false, null)
332
+ @execCommand('insertHTML', {value: cleaned})
333
+ catch error
334
+ # remove the pasted html and load up the cleaned contents into a modal
335
+ @html(prePasteHTML)
336
+ Mercury.modal '/mercury/modals/sanitizer', {
337
+ title: 'HTML Sanitizer (Starring Clippy)',
338
+ afterLoad: -> @element.find('textarea').val(cleaned.replace(/<br\/>/g, '\n'))
339
+ }
340
+ else if Mercury.config.cleanStylesOnPaste
341
+ # strip styles
342
+ pasted = prePasteHTML.singleDiff(@html())
343
+
344
+ container = $('<div>').appendTo(@document.createDocumentFragment()).html(pasted)
345
+ container.find('[style]').attr({style: null})
346
+
347
+ @document.execCommand('undo', false, null)
348
+ @execCommand('insertHTML', {value: container.html()})
349
+
350
+
351
+ # Custom actions (eg. things that execCommand doesn't do, or doesn't do well)
352
+ @actions: {
353
+ insertrowbefore: -> Mercury.tableEditor.addRow('before')
354
+
355
+ insertrowafter: -> Mercury.tableEditor.addRow('after')
356
+
357
+ insertcolumnbefore: -> Mercury.tableEditor.addColumn('before')
358
+
359
+ insertcolumnafter: -> Mercury.tableEditor.addColumn('after')
360
+
361
+ deletecolumn: -> Mercury.tableEditor.removeColumn()
362
+
363
+ deleterow: -> Mercury.tableEditor.removeRow()
364
+
365
+ increasecolspan: -> Mercury.tableEditor.increaseColspan()
366
+
367
+ decreasecolspan: -> Mercury.tableEditor.decreaseColspan()
368
+
369
+ increaserowspan: -> Mercury.tableEditor.increaseRowspan()
370
+
371
+ decreaserowspan: -> Mercury.tableEditor.decreaseRowspan()
372
+
373
+ undo: -> @html(@history.undo())
374
+
375
+ redo: -> @html(@history.redo())
376
+
377
+ removeformatting: (selection) -> selection.insertTextNode(selection.textContent())
378
+
379
+ backcolor: (selection, options) -> selection.wrap("<span style=\"background-color:#{options.value.toHex()}\">", true)
380
+
381
+ overline: (selection) -> selection.wrap('<span style="text-decoration:overline">', true)
382
+
383
+ style: (selection, options) -> selection.wrap("<span class=\"#{options.value}\">", true)
384
+
385
+ replaceHTML: (selection, options) -> @html(options.value)
386
+
387
+ insertImage: (selection, options) -> @execCommand('insertHTML', {value: $('<img/>', options.value)})
388
+
389
+ insertLink: (selection, options) ->
390
+ anchor = $("<#{options.value.tagName}>").attr(options.value.attrs).html(options.value.content)
391
+ selection.insertNode(anchor)
392
+
393
+ replaceLink: (selection, options) ->
394
+ anchor = $("<#{options.value.tagName}>").attr(options.value.attrs).html(options.value.content)
395
+ selection.selectNode(options.node)
396
+ html = $('<div>').html(selection.content()).find('a').html()
397
+ selection.replace($(anchor, selection.context).html(html))
398
+
399
+ insertsnippet: (selection, options) ->
400
+ snippet = options.value
401
+ if (existing = @element.find("[data-snippet=#{snippet.identity}]")).length
402
+ selection.selectNode(existing.get(0))
403
+ selection.insertNode(snippet.getHTML(@document))
404
+
405
+ editsnippet: ->
406
+ return unless @snippet
407
+ snippet = Mercury.Snippet.find(@snippet.data('snippet'))
408
+ snippet.displayOptions()
409
+
410
+ removesnippet: ->
411
+ @snippet.remove() if @snippet
412
+ Mercury.trigger('hide:toolbar', {type: 'snippet', immediately: true})
413
+ }
414
+
415
+
416
+ # Helper class for managing selection and getting information from it
417
+ class Mercury.Regions.Editable.Selection
418
+
419
+ constructor: (@selection, @context) ->
420
+ return unless @selection.rangeCount >= 1
421
+ @range = @selection.getRangeAt(0)
422
+ @fragment = @range.cloneContents()
423
+ @clone = @range.cloneRange()
424
+
425
+
426
+ commonAncestor: (onlyTag = false) ->
427
+ return null unless @range
428
+ ancestor = @range.commonAncestorContainer
429
+ ancestor = ancestor.parentNode if ancestor.nodeType == 3 && onlyTag
430
+ return $(ancestor)
431
+
432
+
433
+ wrap: (element, replace = false) ->
434
+ element = $(element, @context).html(@fragment)
435
+ @replace(element) if replace
436
+ return element
437
+
438
+
439
+ textContent: ->
440
+ return @range.cloneContents().textContent
441
+
442
+
443
+ content: ->
444
+ return @range.cloneContents()
445
+
446
+
447
+ is: (elementType) ->
448
+ content = @content()
449
+ return $(content.firstChild) if content.childNodes.length == 1 && $(content.firstChild).is(elementType)
450
+ return false
451
+
452
+
453
+ forceSelection: (element) ->
454
+ return unless $.browser.webkit
455
+ range = @context.createRange()
456
+
457
+ if @range
458
+ if @commonAncestor(true).closest('.mercury-snippet').length
459
+ lastChild = @context.createTextNode('\00')
460
+ element.appendChild(lastChild)
461
+ else
462
+ if element.lastChild && element.lastChild.nodeType == 3 && element.lastChild.textContent.replace(/^[\s+|\n+]|[\s+|\n+]$/, '') == ''
463
+ lastChild = element.lastChild
464
+ element.lastChild.textContent = '\00'
465
+ else
466
+ lastChild = @context.createTextNode('\00')
467
+ element.appendChild(lastChild)
468
+
469
+ if lastChild
470
+ range.setStartBefore(lastChild)
471
+ range.setEndBefore(lastChild)
472
+ @selection.addRange(range)
473
+
474
+
475
+ selectMarker: (context) ->
476
+ markers = context.find('em.mercury-marker')
477
+ return unless markers.length
478
+
479
+ range = @context.createRange()
480
+ range.setStartBefore(markers.get(0))
481
+ range.setEndBefore(markers.get(1)) if markers.length >= 2
482
+
483
+ markers.remove()
484
+
485
+ @selection.removeAllRanges()
486
+ @selection.addRange(range)
487
+
488
+
489
+ placeMarker: ->
490
+ return unless @range
491
+
492
+ @startMarker = $('<em class="mercury-marker"/>', @context).get(0)
493
+ @endMarker = $('<em class="mercury-marker"/>', @context).get(0)
494
+
495
+ # put a single marker (the end)
496
+ rangeEnd = @range.cloneRange()
497
+ rangeEnd.collapse(false)
498
+ rangeEnd.insertNode(@endMarker)
499
+
500
+ unless @range.collapsed
501
+ # put a start marker
502
+ rangeStart = @range.cloneRange()
503
+ rangeStart.collapse(true)
504
+ rangeStart.insertNode(@startMarker)
505
+
506
+ @selection.removeAllRanges()
507
+ @selection.addRange(@range)
508
+
509
+
510
+ removeMarker: ->
511
+ $(@startMarker).remove()
512
+ $(@endMarker).remove()
513
+
514
+
515
+ insertTextNode: (string) ->
516
+ node = @context.createTextNode(string)
517
+ @range.extractContents()
518
+ @range.insertNode(node)
519
+ @range.selectNodeContents(node)
520
+ @selection.addRange(@range)
521
+
522
+
523
+ insertNode: (element) ->
524
+ element = element.get(0) if element.get
525
+ element = $(element, @context).get(0) if $.type(element) == 'string'
526
+
527
+ @range.deleteContents()
528
+ @range.insertNode(element)
529
+ @range.selectNodeContents(element)
530
+ @selection.addRange(@range)
531
+
532
+
533
+ selectNode: (node, removeExisting = false) ->
534
+ @range.selectNode(node)
535
+ @selection.removeAllRanges() if removeExisting
536
+ @selection.addRange(@range)
537
+
538
+
539
+ replace: (element) ->
540
+ element = element.get(0) if element.get
541
+ element = $(element, @context).get(0) if $.type(element) == 'string'
542
+
543
+ @range.deleteContents()
544
+ @range.insertNode(element)
545
+ @range.selectNodeContents(element)
546
+ @selection.addRange(@range)