mercury-rails 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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)