locomotive-aloha-rails 0.20.1.2 → 0.20.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/Rakefile +1 -1
  2. data/lib/aloha/rails/version.rb +2 -2
  3. data/vendor/assets/javascripts/aloha/css/aloha.css +3 -0
  4. data/vendor/assets/javascripts/aloha/css/ext-aloha.css +3 -0
  5. data/vendor/assets/javascripts/aloha/lib/aloha-bootstrap.js +4565 -3934
  6. data/vendor/assets/javascripts/aloha/lib/aloha.js +1357 -702
  7. data/vendor/assets/javascripts/aloha/lib/aloha/command.js +16 -13
  8. data/vendor/assets/javascripts/aloha/lib/aloha/core.js +23 -3
  9. data/vendor/assets/javascripts/aloha/lib/aloha/ecma5shims.js +23 -7
  10. data/vendor/assets/javascripts/aloha/lib/aloha/editable.js +57 -14
  11. data/vendor/assets/javascripts/aloha/lib/aloha/engine.js +9 -5
  12. data/vendor/assets/javascripts/aloha/lib/aloha/floatingmenu.js +288 -96
  13. data/vendor/assets/javascripts/aloha/lib/aloha/jquery.js +11 -1
  14. data/vendor/assets/javascripts/aloha/lib/aloha/markup.js +318 -40
  15. data/vendor/assets/javascripts/aloha/lib/aloha/repositorymanager.js +11 -10
  16. data/vendor/assets/javascripts/aloha/lib/aloha/selection.js +20 -1
  17. data/vendor/assets/javascripts/aloha/lib/aloha/sidebar.js +11 -1
  18. data/vendor/assets/javascripts/aloha/lib/jquery-plugin.js +10 -7
  19. data/vendor/assets/javascripts/aloha/lib/util/dom.js +18 -6
  20. data/vendor/assets/javascripts/aloha/lib/util/range.js +6 -6
  21. data/vendor/assets/javascripts/aloha/lib/vendor/ext-3.2.1/ext-all-debug.js +26 -2
  22. data/vendor/assets/javascripts/aloha/lib/vendor/jquery.store.js +39 -15
  23. data/vendor/assets/javascripts/aloha/plugins/common/abbr/lib/abbr-plugin.js +1 -0
  24. data/vendor/assets/javascripts/aloha/plugins/common/align/lib/align-plugin.js +344 -334
  25. data/vendor/assets/javascripts/aloha/plugins/common/block/css/block.css +65 -12
  26. data/vendor/assets/javascripts/aloha/plugins/common/block/lib/block-plugin.js +12 -15
  27. data/vendor/assets/javascripts/aloha/plugins/common/block/lib/block.js +796 -180
  28. data/vendor/assets/javascripts/aloha/plugins/common/block/lib/blockcontenthandler.js +54 -13
  29. data/vendor/assets/javascripts/aloha/plugins/common/block/lib/blockmanager.js +315 -78
  30. data/vendor/assets/javascripts/aloha/plugins/common/block/lib/editor.js +111 -8
  31. data/vendor/assets/javascripts/aloha/plugins/common/block/lib/editormanager.js +2 -0
  32. data/vendor/assets/javascripts/aloha/plugins/common/block/lib/jquery-ui-1.8.16.custom.min.js +198 -0
  33. data/vendor/assets/javascripts/aloha/plugins/common/block/lib/sidebarattributeeditor.js +7 -20
  34. data/vendor/assets/javascripts/aloha/plugins/common/characterpicker/lib/characterpicker-plugin.js +15 -3
  35. data/vendor/assets/javascripts/aloha/plugins/common/contenthandler/lib/sanitizecontenthandler.js +3 -2
  36. data/vendor/assets/javascripts/aloha/plugins/common/contenthandler/lib/wordcontenthandler.js +111 -5
  37. data/vendor/assets/javascripts/aloha/plugins/common/dom-to-xhtml/lib/dom-to-xhtml-plugin.js +29 -0
  38. data/vendor/assets/javascripts/aloha/plugins/common/dom-to-xhtml/lib/dom-to-xhtml.js +306 -0
  39. data/vendor/assets/javascripts/aloha/plugins/common/format/lib/format-plugin.js +59 -5
  40. data/vendor/assets/javascripts/aloha/plugins/common/format/nls/i18n.js +1 -1
  41. data/vendor/assets/javascripts/aloha/plugins/common/horizontalruler/lib/horizontalruler-plugin.js +18 -3
  42. data/vendor/assets/javascripts/aloha/plugins/common/image/img/crop-buttons.gif +0 -0
  43. data/vendor/assets/javascripts/aloha/plugins/common/image/lib/image-plugin.js +1629 -1601
  44. data/vendor/assets/javascripts/aloha/plugins/common/image/vendor/jcrop/jquery.jcrop.css +11 -0
  45. data/vendor/assets/javascripts/aloha/plugins/common/link/extra/linklist.js +8 -6
  46. data/vendor/assets/javascripts/aloha/plugins/common/link/lib/link-plugin.js +26 -10
  47. data/vendor/assets/javascripts/aloha/plugins/common/list/nls/de/i18n.js +5 -1
  48. data/vendor/assets/javascripts/aloha/plugins/common/paste/lib/paste-plugin.js +3 -4
  49. data/vendor/assets/javascripts/aloha/plugins/common/table/lib/table-cell.js +13 -12
  50. data/vendor/assets/javascripts/aloha/plugins/common/table/lib/table-plugin.js +108 -61
  51. data/vendor/assets/javascripts/aloha/plugins/common/table/lib/table-selection.js +61 -1
  52. data/vendor/assets/javascripts/aloha/plugins/common/table/lib/table.js +1 -0
  53. data/vendor/assets/javascripts/aloha/plugins/common/table/nls/de/i18n.js +28 -1
  54. data/vendor/assets/javascripts/aloha/plugins/common/table/nls/i18n.js +36 -10
  55. data/vendor/assets/javascripts/aloha/plugins/extra/browser/css/browser.jqgrid.css +292 -292
  56. data/vendor/assets/javascripts/aloha/plugins/extra/browser/lib/browser.js +28 -5
  57. data/vendor/assets/javascripts/aloha/plugins/extra/browser/lib/locale.js +2 -2
  58. data/vendor/assets/javascripts/aloha/plugins/extra/browser/vendor/grid.locale.de.js +6 -1
  59. data/vendor/assets/javascripts/aloha/plugins/extra/browser/vendor/grid.locale.en.js +5 -0
  60. data/vendor/assets/javascripts/aloha/plugins/extra/browser/vendor/jquery.jqGrid.js +5 -0
  61. data/vendor/assets/javascripts/aloha/plugins/extra/browser/vendor/jquery.jstree.js +6 -1
  62. data/vendor/assets/javascripts/aloha/plugins/extra/browser/vendor/jquery.ui.js +6 -1
  63. data/vendor/assets/javascripts/aloha/plugins/extra/browser/vendor/ui-layout.js +6 -1
  64. data/vendor/assets/javascripts/aloha/plugins/extra/cite/lib/cite-plugin.js +18 -4
  65. data/vendor/assets/javascripts/aloha/plugins/extra/formatlesspaste/lib/formatlesspaste-plugin.js +1 -1
  66. data/vendor/assets/javascripts/aloha/plugins/extra/linkbrowser/lib/linkbrowser-plugin.js +14 -2
  67. data/vendor/assets/javascripts/aloha/plugins/extra/numerated-headers/demo/js/aloha-config.js +2 -2
  68. data/vendor/assets/javascripts/aloha/plugins/extra/toc/lib/toc-plugin.js +382 -0
  69. data/vendor/assets/javascripts/aloha/plugins/extra/toc/nls/de/i18n.js +3 -0
  70. data/vendor/assets/javascripts/aloha/plugins/extra/toc/nls/i18n.js +4 -0
  71. metadata +15 -11
  72. data/vendor/assets/javascripts/aloha/plugins/extra/toc/i18n/de.json +0 -1
  73. data/vendor/assets/javascripts/aloha/plugins/extra/toc/i18n/en.json +0 -1
  74. data/vendor/assets/javascripts/aloha/plugins/extra/toc/src/toc.js +0 -350
@@ -6,25 +6,71 @@
6
6
  padding: 4px;
7
7
  width: 100%;
8
8
  }
9
- .aloha-block:hover > .aloha-block-inner {
10
- background: #FFFCE2;
9
+ .aloha-editable-active .aloha-block, .aloha-block.aloha-block-highlighted {
11
10
  cursor: pointer;
12
- outline: 1px solid #FFE98A;
11
+ box-shadow: 0 0 0px 3px #FFE767; /* We use box-shadow because of a Firefox rendering bug with "outline" */
13
12
  }
13
+ .aloha-block.aloha-block-active, .aloha-block.aloha-block-active:hover {
14
+ box-shadow: 0 0 0px 3px #80B5F2; /* We use box-shadow because of a Firefox rendering bug with "outline" */
15
+ }
16
+
17
+
18
+ /* IE8 will use outline for highlighting */
19
+ .ext-ie8 .aloha-block:hover {
20
+ outline: 3px solid #FFE767; /* IE does not support box-shadow, thus we need to use border */
21
+ }
22
+ .ext-ie8 .aloha-block.aloha-block-active, .ext-ie8 .aloha-block.aloha-block-active:hover {
23
+ outline: 3px solid #80B5F2; /* IE does not support box-shadow, thus we need to use border */
24
+ }
25
+ /* IE7 will use border for outlining */
26
+ .ext-ie7 .aloha-block:hover {
27
+ border: 3px solid #FFE767; /* IE does not support box-shadow, thus we need to use border */
28
+ margin: -3px;
29
+ }
30
+ .ext-ie7 .aloha-block.aloha-block-active, .ext-ie7 .aloha-block.aloha-block-active:hover {
31
+ border: 3px solid #80B5F2; /* IE does not support box-shadow, thus we need to use border */
32
+ margin: -3px;
33
+ }
34
+
35
+ /** DRAG DROP **/
36
+ /* Display cursor for drag/drop */
37
+ .aloha-block-droppable {
38
+ border-left: 1px solid red;
39
+ margin-left:-1px;
40
+ }
41
+ .ext-ie7 .aloha-block-droppable {
42
+ margin-left:0; /* IE7 cannot handle the negative margin... */
43
+ }
44
+
45
+ .aloha-block-droppable.aloha-block-droppable-right {
46
+ margin-left:0 !important;
47
+ border-left: none !important;
48
+ margin-right:-1px;
49
+ border-right: 1px solid red;
50
+ }
51
+
52
+ .aloha-block-droppable-blocklevel {
53
+ position:relative;
54
+ }
55
+ .aloha-block-blockleveldragdropline {
56
+ position:absolute;
57
+ width: 100%;
58
+ height:2px;
59
+ background-color:red;
60
+ bottom:0;
61
+
62
+ }
63
+ /** OTHER **/
64
+
14
65
  .aloha-block .aloha-editable {
15
66
  cursor: auto;
16
67
  }
17
- .aloha-block-active > .aloha-block-inner,
18
- .aloha-block-active:hover > .aloha-block-inner {
19
- outline: 1px solid #2F7ECC;
20
- background: #B4D7F8;
21
- }
22
68
 
23
69
  .aloha-block {
24
70
  position:relative;
25
71
  }
26
- .aloha-block:hover .aloha-block-draghandle,
27
- .aloha-block-active .aloha-block-draghandle {
72
+ .aloha-block:hover > .aloha-block-draghandle,
73
+ .aloha-block-active > .aloha-block-draghandle {
28
74
  display:block;
29
75
  }
30
76
  .aloha-block-draghandle {
@@ -42,10 +88,17 @@
42
88
  border-top-left-radius:5px;
43
89
  border-top-right-radius:5px;
44
90
  }
91
+ /* Hide drag handle while dragging takes place */
92
+ .aloha-block.ui-draggable-dragging .aloha-block-draghandle {
93
+ display: none;
94
+ }
45
95
 
46
96
  .aloha-block-editor label {
47
97
  display:block;
48
98
  }
49
99
 
50
- .aloha-block ::selection { background: transparent; }
51
- .aloha-block ::-moz-selection { background: transparent; }
100
+
101
+
102
+ .aloha-block-dropInlineElementIntoEmptyBlock {
103
+ border: 1px solid red;
104
+ }
@@ -20,16 +20,16 @@ define([
20
20
  'block/editormanager',
21
21
  'block/blockcontenthandler',
22
22
  'block/editor',
23
- 'css!block/css/block.css'
23
+ 'css!block/css/block.css',
24
+ 'block/jquery-ui-1.8.16.custom.min'
24
25
  ], function(Aloha, Plugin, jQuery, ContentHandlerManager, BlockManager, SidebarAttributeEditor, block, EditorManager, BlockContentHandler, editor) {
25
26
 
26
-
27
27
  /**
28
- * Register the plugin with unique name
28
+ * Register the 'block' plugin
29
29
  */
30
30
  var BlockPlugin = Plugin.create( 'block', {
31
31
  settings: {},
32
- // dependencies: [ 'contenthandler' ],
32
+ // dependencies: [ 'paste' ],
33
33
 
34
34
  init: function () {
35
35
  var that = this;
@@ -42,11 +42,14 @@ define([
42
42
  EditorManager.register('number', editor.NumberEditor);
43
43
  EditorManager.register('url', editor.UrlEditor);
44
44
  EditorManager.register('email', editor.EmailEditor);
45
-
45
+ EditorManager.register('select', editor.SelectEditor);
46
+ EditorManager.register('button', editor.ButtonEditor);
47
+
46
48
  // register content handler for block plugin
47
49
  ContentHandlerManager.register('block', BlockContentHandler);
48
50
 
49
51
  BlockManager.registerEventHandlers();
52
+ BlockManager.initializeBlockLevelDragDrop();
50
53
 
51
54
  Aloha.bind('aloha-ready', function() {
52
55
  // When Aloha is fully loaded, we initialize the blocks.
@@ -56,9 +59,11 @@ define([
56
59
  }
57
60
  });
58
61
  },
59
- _createBlocks: function() {
60
- var defaultBlockSettings;
61
62
 
63
+ /**
64
+ * Create blocks from default settings
65
+ */
66
+ _createBlocks: function() {
62
67
  if (!this.settings.defaults) {
63
68
  this.settings.defaults = {};
64
69
  }
@@ -68,14 +73,6 @@ define([
68
73
  }
69
74
  });
70
75
 
71
- /**
72
- * See (http://jquery.com/).
73
- * @name jQuery
74
- * @class
75
- * See the jQuery Library (http://jquery.com/) for full details. This just
76
- * documents the function and classes that are added to jQuery by this plug-in.
77
- */
78
-
79
76
  /**
80
77
  * See (http://jquery.com/).
81
78
  * @name jQuery.fn
@@ -6,14 +6,22 @@
6
6
  */
7
7
 
8
8
  /**
9
+ * Module which contains the base class for Blocks, and a Default/Debug block.
10
+ *
9
11
  * @name block.block
10
- * @namespace Block models
12
+ * @namespace block/block
11
13
  */
12
14
  define(['aloha', 'aloha/jquery', 'block/blockmanager', 'aloha/observable', 'aloha/floatingmenu'],
13
15
  function(Aloha, jQuery, BlockManager, Observable, FloatingMenu) {
14
16
 
15
17
 
18
+ var GENTICS = window.GENTICS;
19
+
16
20
  /**
21
+ * An aloha block has the following special properties, being readable through the
22
+ * "attr" function:
23
+ * - aloha-block-type -- TYPE of the AlohaBlock as registered by the BlockManager
24
+ *
17
25
  * @name block.block.AbstractBlock
18
26
  * @class An abstract block that must be used as a base class for custom blocks
19
27
  */
@@ -22,41 +30,32 @@ function(Aloha, jQuery, BlockManager, Observable, FloatingMenu) {
22
30
  {
23
31
 
24
32
  /**
33
+ * Event which is triggered if the block attributes change.
34
+ *
25
35
  * @name block.block.AbstractBlock#change
26
36
  * @event
27
37
  */
28
38
 
29
39
  /**
30
- * Title for the block, used to display the name in the sidebar.
40
+ * Title of the block, used to display the name in the sidebar editor.
41
+ *
31
42
  * @type String
32
43
  * @api
33
44
  */
34
45
  title: null,
35
46
 
36
47
  /**
37
- * Id of the assigned element, used to identify a block
48
+ * Id of the underlying $element, used to identify the block.
38
49
  * @type String
39
50
  */
40
51
  id: null,
41
52
 
42
53
  /**
43
- * The wrapper element around the inner element
44
- * @type jQuery
45
- */
46
- // TODO: Rename to $element
47
- element: null,
48
-
49
- /**
50
- * The inner element which is containing the actual user-provided content
54
+ * The wrapping element of the block.
51
55
  * @type jQuery
56
+ * @api
52
57
  */
53
- $innerElement: null,
54
-
55
- /**
56
- * Either "inline" or "block", will be guessed from the original block dom element
57
- * @type String
58
- */
59
- _domElementType: null,
58
+ $element: null,
60
59
 
61
60
  /**
62
61
  * if TRUE, the rendering is currently taking place. Used to prevent recursion
@@ -66,181 +65,333 @@ function(Aloha, jQuery, BlockManager, Observable, FloatingMenu) {
66
65
  _currentlyRendering: false,
67
66
 
68
67
  /**
69
- * set to TRUE once the block is fully initialized and should be rendered.
68
+ * set to TRUE once the block is fully initialized.
70
69
  *
71
70
  * @type Boolean
72
71
  */
73
72
  _initialized: false,
74
73
 
75
74
  /**
76
- * @param {jQuery} $innerElement Element that declares the block
75
+ * Set to TRUE if the last click activated a *nested editable*.
76
+ * If FALSE; the block itself is selected.
77
+ * This is needed when a block is deleted in IE7/8.
78
+ */
79
+ _isInsideNestedEditable: false,
80
+
81
+ /**************************
82
+ * SECTION: Initialization and Lifecycle
83
+ **************************/
84
+
85
+ /**
86
+ * Initialize the basic block. Do not call directly; instead use jQuery(...).alohaBlock() to
87
+ * create new blocks.
88
+ *
89
+ * This function shall only be called through the BlockManager. See BlockManager::_blockify().
90
+ *
91
+ * @param {jQuery} $element Element that declares the block
77
92
  * @constructor
78
93
  */
79
- _constructor: function($innerElement) {
94
+ _constructor: function($element) {
80
95
  var that = this;
81
- this.id = GENTICS.Utils.guid();
82
96
 
83
- this.$innerElement = $innerElement;
97
+ this.$element = $element;
84
98
 
85
- this._domElementType = GENTICS.Utils.Dom.isBlockLevelElement($innerElement[0]) ? 'block' : 'inline';
86
- $innerElement.wrap('<' + this._getWrapperElementType() + ' />');
87
- this.element = $innerElement.parent();
88
- this.element.contentEditable(false);
99
+ if ($element.attr('id')) {
100
+ this.id = $element.attr('id');
101
+ } else {
102
+ this.id = GENTICS.Utils.guid();
103
+ $element.attr('id', this.id);
104
+ }
89
105
 
90
- this.element.attr('id', this.id);
106
+ $element.contentEditable(false);
91
107
 
92
- this.element.addClass('aloha-block');
93
- $innerElement.addClass('aloha-block-inner');
108
+ $element.addClass('aloha-block');
94
109
 
95
- // Register event handlers for activating an Aloha Block
96
- this.element.bind('click', function(event) {
97
- that.activate(event.target);
98
- return false;
99
- });
110
+ if (this.isDraggable()) {
111
+ // Remove default drag/drop behavior of the browser
112
+ $element.find('img').attr('draggable', 'false');
113
+ $element.find('a').attr('draggable', 'false');
114
+ }
100
115
 
101
- Aloha.bind('aloha-block-selected', function(event,obj) {
102
- if (that.element.get(0) === obj) {
103
- that.activate();
116
+ // While the event handler is defined here, it is connected to the DOM element inside "_connectThisBlockToDomElement"
117
+ this._onElementClickHandler = function(event) {
118
+ // We only activate ourselves if we are the innermost aloha-block.
119
+ // If we are not the innermost aloha-block, we get highlighted (but not activated) automatically
120
+ // by the innermost block.
121
+ if (jQuery(event.target).closest('.aloha-block').get(0) === that.$element.get(0)) {
122
+ that._fixScrollPositionBugsInIE();
123
+ that.activate(event.target, event);
104
124
  }
105
- });
125
+ };
106
126
 
107
- // We need to tell Aloha that we handle the event already;
108
- // else a selection of block contents will *not* select
109
- // the block.
110
- this.element.bind('mousedown', function() {
111
- Aloha.eventHandled = true;
112
- }).bind('focus', function() {
113
- Aloha.eventHandled = true;
114
- }).bind('dblclick', function() {
115
- Aloha.eventHandled = true;
116
- });
117
- this.init();
127
+ // Register event handlers on the block
128
+ this._connectThisBlockToDomElement($element);
118
129
 
119
- this._registerAsBlockified();
120
- },
121
130
 
122
- serialize: function() {
123
- // TODO: use ALL attributes, not just data-....
124
- return {
125
- tag: this.element[0].tagName,
126
- attributes: this._getAttributes(), // contains data-properties AND about
127
- classes: this.$innerElement.attr('class') // TODO: filter out aloha-block-active...
128
- }
129
- },
131
+ // This is executed when a block is selected through caret handling
132
+ // TODO!
133
+ //Aloha.bind('aloha-block-selected', function(event,obj) {
134
+ // if (that.$element.get(0) === obj) {
135
+ // that.activate();
136
+ // }
137
+ //});
138
+
130
139
 
131
- _registerAsBlockified: function() {
132
140
  this._initialized = true;
133
- this.element.trigger('block-initialized');
134
141
  },
135
142
 
136
143
  /**
137
- * Template method to initialize the block
138
- * @api
144
+ * Is set inside the constructor to the event handler function
145
+ * which should be executed when the element is clicked.
146
+ *
147
+ * NOTE: Purely internal, "this" is not available inside this method!
139
148
  */
140
- init: function() {},
149
+ _onElementClickHandler: null,
141
150
 
142
151
  /**
143
- * Get a schema of attributes with
152
+ * We need to tell Aloha that we handle the event already;
153
+ * else a selection of a nested editable will *not* select
154
+ * the block.
144
155
  *
145
- * TODO Document schema format
156
+ * This callback is bound to the mousedown, focus and dblclick events.
146
157
  *
147
- * @api
148
- * @returns {Object}
158
+ * NOTE: Purely internal, "this" is not available inside this method!
149
159
  */
150
- getSchema: function() {
151
- return null;
160
+ _preventSelectionChangedEventHandler: function() {
161
+ Aloha.Selection.preventSelectionChanged();
152
162
  },
153
163
 
154
164
  /**
155
- * Template Method which should return the block title
165
+ * This method connects this block object to the passed DOM element.
166
+ * In detail, this method does the following:
167
+ *
168
+ * - if this.$element is already set, remove all block event handlers
169
+ * - sets this.$element = jQuery(newElement)
170
+ * - initialize event listeners on this.$element
171
+ * - call init()
172
+ *
173
+ * The method is called in two contexts: First, when a block is constructed
174
+ * to initialize the event listeners etc. Second, it is ALSO called when
175
+ * a block inside a nested block with editable in between is detected
176
+ * as inconsistent.
156
177
  */
157
- getTitle: function() {
158
- return this.title;
178
+ _connectThisBlockToDomElement: function(newElement) {
179
+ var that = this;
180
+ var $newElement = jQuery(newElement);
181
+ if (this.$element) {
182
+ this.$element.unbind('click', this._onElementClickHandler);
183
+ this.$element.unbind('mousedown', this._preventSelectionChangedEventHandler);
184
+ this.$element.unbind('focus', this._preventSelectionChangedEventHandler);
185
+ this.$element.unbind('dblclick', this._preventSelectionChangedEventHandler);
186
+ }
187
+ this.$element = $newElement;
188
+
189
+ this.$element.bind('click', this._onElementClickHandler);
190
+ this.$element.bind('mousedown', this._preventSelectionChangedEventHandler);
191
+ this.$element.bind('focus', this._preventSelectionChangedEventHandler);
192
+ this.$element.bind('dblclick', this._preventSelectionChangedEventHandler);
193
+
194
+ this.init(this.$element, function() {
195
+ // WORKAROUND against loading order dependencies. If we have
196
+ // nested Blocks inside each other (with no editables in between)
197
+ // it could be that the *inner* block is initialized *before* the outer one.
198
+ //
199
+ // However, the inner block needs to know whether it shall render drag handles or not,
200
+ // and this depends on whether it is inside an editable or a block.
201
+ //
202
+ // In order to fix this case, we delay the the drag-handle-rendering (and all the other
203
+ // post-processing) to the next JavaScript Run Loop using a small timeout.
204
+ window.setTimeout(function() {
205
+ that._postProcessElementIfNeeded();
206
+ }, 5);
207
+ });
159
208
  },
160
209
 
161
210
  /**
162
- * activates the block
163
- * will select the block's contents, highlight it, update the floating menu and update the sidebar (if needed)
164
- * @param {DOMNode} clickedDomNode The DOM node which has been clicked. Should only be set INTERNALLY, if you call activate() programmatically, DO NOT SET THIS PARAM! We need the DOM node to see whether we clicked inside an embedded editable or not.
211
+ * IE HACK: Our beloved Internet Explorer sometimes scrolls to the top
212
+ * of the page when activating an aloha block, and on numerous other occasions
213
+ * like when an <span> block is moved via drag/drop.
214
+ *
215
+ * We can detect this and scroll right back; although this will flicker
216
+ * a little (but still a lot better than before)
217
+ */
218
+ _fixScrollPositionBugsInIE: function() {
219
+ var scrollPositionBefore = jQuery(window).scrollTop();
220
+ window.setTimeout(function() {
221
+ if (jQuery(window).scrollTop() !== scrollPositionBefore) {
222
+ jQuery(window).scrollTop(scrollPositionBefore);
223
+ }
224
+ }, 10);
225
+ },
226
+ /**
227
+ * Template method to initialize the block. Can be used to set attributes
228
+ * on the block, depending on the block contents. You will most probably
229
+ * use $element and this.attr() inside this function.
230
+ *
231
+ * !!! This method can be called *multiple times*, as it is called each time
232
+ * when $element has been disconnected from the DOM (which can happen because of various reasons)
233
+ * and the block needs to re-initialize. So make sure this method can be called *MULTIPLE TIMES*
234
+ * and always returns predictable results. This method must be idempotent, same as update().
235
+ *
236
+ * Furthermore, always when this method is finished, you need to call postProcessFn() afterwards.
237
+ * This function adds drag handles and other controls if necessary.
238
+ *
239
+ * @param {jQuery} $element a shortcut to the block's DOM element (this.$element) for easy processing
240
+ * @param {Function} postProcessFn this function MUST be called at all times the $element has been updated; as it adds drag/drop/delete/... handles if necessary
165
241
  * @api
166
242
  */
167
- activate: function(clickedDomNode) {
168
- var previouslyActiveBlocks = BlockManager.getActiveBlocks(),
169
- activeBlocks = [];
170
-
171
- delete previouslyActiveBlocks[this.id];
172
-
173
- this._selectBlock(clickedDomNode);
174
-
175
- // Set scope to current block
176
- FloatingMenu.setScope('Aloha.Block.' + this.attr('block-type'));
177
-
178
- this._highlight();
179
- activeBlocks.push(this);
180
-
181
- this.element.parents('.aloha-block').each(function() {
182
- var block = BlockManager.getBlock(this);
183
- delete previouslyActiveBlocks[block.id];
184
-
185
- block._highlight();
186
- activeBlocks.push(block);
187
- });
188
- jQuery.each(previouslyActiveBlocks, function() {
189
- this.deactivate();
190
- });
191
-
192
- BlockManager.trigger('block-selection-change', activeBlocks);
243
+ init: function($element, postProcessFn) {
244
+ postProcessFn();
245
+ },
193
246
 
194
- return false;
247
+ /**
248
+ * Callback which is executed when somebody triggers destroy().
249
+ *
250
+ * This only allows destruction if the block is *inside* an aloha-editable and *not* inside an aloha-block.
251
+ *
252
+ * @return {Boolean} true of destruction should happen, false otherwise
253
+ */
254
+ shouldDestroy: function() {
255
+ var $closest = this.$element.parent().closest('.aloha-block,.aloha-editable,.aloha-block-collection');
256
+ if ($closest.hasClass('aloha-block-collection') && this.$element[0].tagName.toLowerCase() === 'div') {
257
+ return true;
258
+ } else {
259
+ return $closest.hasClass('aloha-editable');
260
+ }
195
261
  },
196
262
 
197
263
  /**
198
264
  * Destroy this block instance completely. Removes the element from the DOM,
199
- * unregisters it, and triggers a delete event on the BlockManager.
265
+ * unregisters it, and triggers a block-delete event on the BlockManager.
200
266
  *
201
- * @return
267
+ * @param {Boolean} force TRUE if you want to force deletion, despite shouldDestroy() returning false.
202
268
  * @api
203
269
  */
204
- destroy: function() {
270
+ destroy: function(force) {
271
+ if (!this.shouldDestroy() && force !== true) return;
272
+
205
273
  var that = this;
274
+ var newRange = new GENTICS.Utils.RangeObject();
275
+
276
+ newRange.startContainer = newRange.endContainer = this.$element.parent()[0];
277
+ newRange.startOffset = newRange.endOffset = GENTICS.Utils.Dom.getIndexInParent(this.$element[0]);
278
+
206
279
  BlockManager.trigger('block-delete', this);
207
280
  BlockManager._unregisterBlock(this);
208
281
 
209
282
  this.unbindAll();
210
283
 
211
- this.element.fadeOut('fast', function() {
212
- that.element.remove();
284
+ var isInlineElement = this.$element[0].tagName.toLowerCase() === 'span';
285
+
286
+ this.$element.fadeOut('fast', function() {
287
+ that.$element.remove();
213
288
  BlockManager.trigger('block-selection-change', []);
289
+ window.setTimeout(function() {
290
+ if (isInlineElement) {
291
+ newRange.select();
292
+ }
293
+ }, 5);
214
294
  });
215
295
  },
216
296
 
297
+ /**************************
298
+ * SECTION: Getters and Helpers
299
+ **************************/
300
+
217
301
  /**
218
- * Activated when the block is clicked
302
+ * Get the id of the block
303
+ * @returns {String}
219
304
  */
220
- _highlight: function() {
221
- BlockManager._setActive(this);
222
- this.element.addClass('aloha-block-active');
305
+ getId: function() {
306
+ return this.id;
223
307
  },
224
308
 
309
+ /**
310
+ * Get a schema of attributes which shall be rendered / edited
311
+ * in the sidebar.
312
+ *
313
+ * @api
314
+ * @returns {Object}
315
+ */
316
+ getSchema: function() {
317
+ return null;
318
+ },
225
319
 
226
- _unhighlight: function() {
227
- BlockManager._setInactive(this);
228
- this.element.removeClass('aloha-block-active');
320
+ /**
321
+ * Template Method which should return the block title. Needed for editing sidebar.
322
+ * By default, the block title is returned.
323
+ *
324
+ * @api
325
+ */
326
+ getTitle: function() {
327
+ return this.title;
229
328
  },
230
329
 
231
- _selectBlock: function(domNode) {
232
- if (!domNode || jQuery(domNode).is('.aloha-editable') || jQuery(domNode).parents('.aloha-block, .aloha-editable').first().is('.aloha-editable')) {
233
- // It was clicked on a Aloha-Editable inside a block; so we do not
234
- // want to select the whole block and do an early return.
235
- return;
330
+ /**
331
+ * Returns true if the block is draggable because it is inside an aloha-editable, false otherwise.
332
+ *
333
+ * You cannot depend on this method's result during the *init* phase of the Aloha Block, as the
334
+ * outer block might not be initialized at that point yet. Thus, do not call this method inside init().
335
+ *
336
+ * @return Boolean
337
+ */
338
+ isDraggable: function() {
339
+ if (this.$element[0].tagName.toLowerCase() === 'div' && this.$element.parents('.aloha-editable,.aloha-block,.aloha-block-collection').first().hasClass('aloha-block-collection')) {
340
+ // Here, we are inside an aloha-block-collection, and thus also need to be draggable.
341
+ return true;
236
342
  }
343
+ return this.$element.parents('.aloha-editable,.aloha-block').first().hasClass('aloha-editable');
344
+ },
345
+
346
+ /**************************
347
+ * SECTION: Activation / Deactivation
348
+ **************************/
349
+
350
+ /**
351
+ * activates the block
352
+ * will select the block's contents, highlight it, update the floating menu and update the sidebar (if needed).
353
+ *
354
+ * When calling programmatically, do not set eventTarget or event arguments.
355
+ * @api
356
+ */
357
+ activate: function(eventTarget, event) {
358
+ var highlightedBlocks = [];
359
+
360
+ // Deactivate currently highlighted blocks
361
+ jQuery.each(BlockManager._getHighlightedBlocks(), function() {
362
+ this.deactivate();
363
+ });
237
364
 
238
- if (this.element.parents('.aloha-editable').length == 0) {
239
- // If the block is not inside an editable, there is no need to select it (as it gets highlighted in an ugly way then)
240
- return;
365
+ // Activate current block
366
+ if (this.$element.attr('data-block-skip-scope') !== 'true') {
367
+ FloatingMenu.setScope('Aloha.Block.' + this.attr('aloha-block-type'));
241
368
  }
369
+ this.$element.addClass('aloha-block-active');
370
+ this._highlight();
371
+ highlightedBlocks.push(this);
242
372
 
243
- GENTICS.Utils.Dom.selectDomNode(this.element[0]);
373
+ // Highlight parent blocks
374
+ this.$element.parents('.aloha-block').each(function() {
375
+ var block = BlockManager.getBlock(this);
376
+ block._highlight();
377
+ highlightedBlocks.push(block);
378
+ });
379
+
380
+ // Browsers do not remove the cursor, so we enforce it when an aditable is clicked.
381
+ // However, when the user clicked inside a nested editable, we will not remove the cursor (as the user wants to start typing then)
382
+ // small HACK: we also do not deactivate if we are inside an aloha-table-cell-editable.
383
+ if (jQuery(eventTarget).closest('.aloha-editable,.aloha-block,.aloha-table-cell-editable').first().hasClass('aloha-block')) {
384
+ this._isInsideNestedEditable = false;
385
+ Aloha.getSelection().removeAllRanges();
386
+ } else {
387
+ this._isInsideNestedEditable = true;
388
+ if (event) {
389
+ // We now update the selection, as you clicked *inside* an editable inside the block
390
+ Aloha.Selection.updateSelection(event);
391
+ }
392
+ }
393
+ // Trigger selection change event
394
+ BlockManager.trigger('block-selection-change', highlightedBlocks);
244
395
  },
245
396
 
246
397
  /**
@@ -249,92 +400,545 @@ function(Aloha, jQuery, BlockManager, Observable, FloatingMenu) {
249
400
  deactivate: function() {
250
401
  var that = this;
251
402
  this._unhighlight();
252
- this.element.parents('.aloha-block').each(function() {
403
+ this.$element.parents('.aloha-block').each(function() {
253
404
  that._unhighlight();
254
405
  });
406
+
407
+ this.$element.removeClass('aloha-block-active');
255
408
  BlockManager.trigger('block-selection-change', []);
256
- // TODO: remove the current selection here
257
409
  },
258
410
 
259
411
  /**
260
412
  * @returns {Boolean} True if this block is active
261
413
  */
262
414
  isActive: function() {
263
- return this.element.hasClass('aloha-block-active');
415
+ return this.$element.hasClass('aloha-block-active');
264
416
  },
265
417
 
266
418
  /**
267
- * Get the id of the block
268
- * @returns {String}
419
+ * Internal helper which sets a block as highlighted, because the block itself
420
+ * or a child block has been activated.
269
421
  */
270
- getId: function() {
271
- return this.id;
422
+ _highlight: function() {
423
+ this.$element.addClass('aloha-block-highlighted');
424
+ BlockManager._setHighlighted(this);
272
425
  },
273
426
 
274
427
  /**
275
- * Template method to render contents of the block, must be implemented by specific block type
276
- *
277
- * The renderer must manually take care of flushing the inner element if it needs that.
278
- *
279
- * @api
428
+ * Internal helper which sets a block as un-highlighted.
280
429
  */
281
- render: function() {},
430
+ _unhighlight: function() {
431
+ this.$element.removeClass('aloha-block-highlighted');
432
+ BlockManager._setUnhighlighted(this);
433
+ },
434
+
435
+ /**************************
436
+ * SECTION: Block Rendering
437
+ **************************/
282
438
 
283
- _renderAndSetContent: function() {
439
+ /**
440
+ * Internal _update method, which needs to be called internally if a property
441
+ * changed. This is just a wrapper around update().
442
+ */
443
+ _update: function() {
444
+ var that = this;
284
445
  if (this._currentlyRendering) return;
285
446
  if (!this._initialized) return;
286
447
 
287
448
  this._currentlyRendering = true;
288
449
 
289
- var result = this.render(this.$innerElement);
450
+ this.update(this.$element, function() {
451
+ that._postProcessElementIfNeeded();
452
+ });
290
453
 
291
- // Convenience for simple string content
292
- if (typeof result === 'string') {
293
- this.$innerElement.html(result);
454
+ this._currentlyRendering = false;
455
+ },
456
+
457
+ /**
458
+ * Template method to render contents of the block, must be implemented by specific block type.
459
+ * $element can be augumented by additional DOM elements like drag/drop handles. If you do
460
+ * any jQuery selection, you need to ignore all results which have a "aloha-block-handle" class
461
+ * set.
462
+ *
463
+ * Furthermore, always when you update $element, you need to call postProcessFn() afterwards.
464
+ * This function adds drag handles and other controls if necessary.
465
+ *
466
+ * This method should *only* be called from the internal _update method.
467
+ *
468
+ * @param {jQuery} $element a shortcut to the block's DOM element (this.$element) for easy processing
469
+ * @param {Function} postProcessFn this function MUST be called at all times the $element has been updated; as it adds drag/drop/delete/... handles if necessary
470
+ *
471
+ * @api
472
+ */
473
+ update: function($element, postProcessFn) {
474
+ postProcessFn();
475
+ },
476
+
477
+
478
+ /**
479
+ * Post processor, being called to augument the Block Element's DOM by drag handles etc.
480
+ *
481
+ * This method must be idempotent. I.e. it must produce the same results
482
+ * when called once or twice.
483
+ */
484
+ _postProcessElementIfNeeded: function() {
485
+ this.createEditablesIfNeeded();
486
+ this._checkThatNestedBlocksAreStillConsistent();
487
+ this._makeNestedBlockCollectionsSortable();
488
+
489
+ this.renderBlockHandlesIfNeeded();
490
+ if (this.isDraggable() && this.$element[0].tagName.toLowerCase() === 'span') {
491
+ this._setupDragDropForInlineElements();
492
+ this._disableUglyInternetExplorerDragHandles();
493
+ } else if (this.isDraggable() && this.$element[0].tagName.toLowerCase() === 'div') {
494
+ this._setupDragDropForBlockElements();
495
+ this._disableUglyInternetExplorerDragHandles();
294
496
  }
497
+ },
295
498
 
296
- this._renderSurroundingElements();
499
+ /**
500
+ * Due to indeterminate initialization order of nested blocks,
501
+ * it can happen that blockifying a parent block deconnects $element inside
502
+ * child blocks.
503
+ *
504
+ * This is the case we detect here; and if it happens, we reconnect the
505
+ * block to its currently visible DOM element.
506
+ */
507
+ _checkThatNestedBlocksAreStillConsistent: function() {
508
+ this.$element.find('.aloha-block').each(function() {
509
+ var block = BlockManager.getBlock(this);
510
+ if (block && block.$element[0] !== this) {
511
+ block._connectThisBlockToDomElement(this);
512
+ }
513
+ });
514
+ },
297
515
 
298
- this._currentlyRendering = false;
516
+ /**
517
+ * If a nested element is marked as "aloha-block-collection",
518
+ * we want to make it sortable, by calling the appropriate Block Manager
519
+ * function.
520
+ */
521
+ _makeNestedBlockCollectionsSortable: function() {
522
+ var that = this;
523
+ this.$element.find('.aloha-block-collection').each(function() {
524
+ var $blockCollection = jQuery(this);
525
+ if ($blockCollection.closest('.aloha-block').get(0) === that.$element.get(0)) {
526
+ // We are only responsible for one-level-down Block Collections, not
527
+ // for nested ones.
528
+ BlockManager.createBlockLevelSortableForEditableOrBlockCollection($blockCollection);
529
+ }
530
+ })
531
+ },
532
+
533
+ /**
534
+ * Helper which disables the ugly IE drag handles. They are still shown, but at
535
+ * least they do not work anymore
536
+ */
537
+ _disableUglyInternetExplorerDragHandles: function() {
538
+ this.$element.get( 0 ).onresizestart = function ( e ) { return false; };
539
+ this.$element.get( 0 ).oncontrolselect = function ( e ) { return false; };
540
+ // We do NOT abort the "ondragstart" event as it is required for drag/drop.
541
+ this.$element.get( 0 ).onmovestart = function ( e ) { return false; };
542
+ this.$element.get( 0 ).onselectstart = function ( e ) { return false; };
299
543
  },
300
544
 
301
- _renderSurroundingElements: function() {
302
- this.element.empty();
303
- this.element.append(this.$innerElement);
545
+ /**************************
546
+ * SECTION: Drag&Drop for INLINE elements
547
+ **************************/
548
+ _setupDragDropForInlineElements: function() {
549
+ var that = this;
304
550
 
305
- this.createEditables(this.$innerElement);
551
+ // Here, we store the character DOM element which has been hovered upon recently.
552
+ // This is needed as somehow, the "drop" event on the character is not fired.
553
+ // Furthermore, we use it to know whether we need to "revert" the draggable to the original state or not.
554
+ var lastHoveredCharacter = null;
555
+
556
+ // HACK for IE7: Internet Explorer 7 has a very weird behavior in
557
+ // not always firing the "drop" callback of the inner droppable... However,
558
+ // the "over" and "out" callbacks are fired correctly.
559
+ // Because of this, we handle the "drop" inside the "stop" callback in IE7
560
+ // instead of the "drop" callback (where it is handled in all other browsers)
561
+
562
+ // This $currentDraggable is also needed as part of the IE 7 hack.
563
+ // $currentDraggable contains a reference to the current draggable, but
564
+ // only makes sense to read when lastHoveredCharacter !== NULL.
565
+ var $currentDraggable = null;
566
+
567
+ // This dropFn is the callback which handles the actual moving of
568
+ // nodes. We created a separate function for it, as it is called inside the "stop" callback
569
+ // in IE7 and inside the "drop" callback in all other browsers.
570
+ var dropFn = function() {
571
+ if (lastHoveredCharacter) {
572
+ // the user recently hovered over a character
573
+ var $dropReferenceNode = jQuery(lastHoveredCharacter);
574
+
575
+ if ($dropReferenceNode.is('.aloha-block-dropInlineElementIntoEmptyBlock')) {
576
+ // the user wanted to drop INTO an empty block!
577
+ $dropReferenceNode.children().remove();
578
+ $dropReferenceNode.append($currentDraggable);
579
+ } else if ($dropReferenceNode.is('.aloha-block-droppable-right')) {
580
+ $dropReferenceNode.html($dropReferenceNode.html() + ' ');
581
+
582
+ // Move draggable after drop reference node
583
+ $dropReferenceNode.after($currentDraggable);
584
+ } else {
585
+ // Insert space in the beginning of the drop reference node
586
+ if ($dropReferenceNode.prev('[data-i]').length > 0) {
587
+ // If not the last element, insert space in front of next element (i.e. after the moved block)
588
+ $dropReferenceNode.prev('[data-i]').html($dropReferenceNode.prev('[data-i]').html() + ' ');
589
+ }
590
+ $dropReferenceNode.html(' ' + $dropReferenceNode.html());
591
+
592
+ // Move draggable before drop reference node
593
+ $dropReferenceNode.before($currentDraggable);
594
+ }
306
595
 
307
- this.renderToolbar();
596
+ $currentDraggable.removeClass('ui-draggable').css({'left': 0, 'top': 0}); // Remove "draggable" options... somehow "Destroy" does not work
597
+ that._fixScrollPositionBugsInIE();
598
+ }
599
+ jQuery('.aloha-block-dropInlineElementIntoEmptyBlock').removeClass('aloha-block-dropInlineElementIntoEmptyBlock');
600
+ };
601
+ var editablesWhichNeedToBeCleaned = [];
602
+ this.$element.draggable({
603
+ handle: '.aloha-block-draghandle',
604
+ scope: 'aloha-block-inlinedragdrop',
605
+ revert: function() {
606
+ return (lastHoveredCharacter === null);
607
+ },
608
+ revertDuration: 250,
609
+ stop: function() {
610
+ if (Ext.isIE7) {
611
+ dropFn();
612
+ }
613
+ jQuery.each(editablesWhichNeedToBeCleaned, function() {
614
+ that._dd_traverseDomTreeAndRemoveSpans(this);
615
+ })
616
+ $currentDraggable = null;
617
+
618
+ editablesWhichNeedToBeCleaned = [];
619
+ },
620
+ start: function() {
621
+ editablesWhichNeedToBeCleaned = [];
622
+
623
+ // In order to make Inline Blocks droppable into empty paragraphs, we insert a &nbsp; manually before the placeholder-br.
624
+ // -> for IE
625
+ jQuery('.aloha-editable').children('p:empty').html('&nbsp;');
626
+
627
+
628
+ // Make **ALL** editables on the page droppable, such that it is possible
629
+ // to drag/drop *across* editable boundaries
630
+ var droppableCfg = {
631
+ // make block elements droppable
632
+ tolerance: 'pointer',
633
+ addClasses: false, // performance optimization
634
+ scope: 'aloha-block-inlinedragdrop',
635
+
636
+ /**
637
+ * When hovering over a paragraph, we make convert its contents into spans, to make
638
+ * them droppable.
639
+ */
640
+ over: function(event, ui) {
641
+ if (editablesWhichNeedToBeCleaned.indexOf(this) === -1) {
642
+ editablesWhichNeedToBeCleaned.push(this);
643
+ }
644
+
645
+ $currentDraggable = ui.draggable;
646
+ if (jQuery(this).is(':empty') || jQuery(this).children('br.aloha-end-br').length > 0 || jQuery(this).html() === '&nbsp;') {
647
+ // the user tries to drop into an empty container, thus we highlight the container and do an early return
648
+ jQuery(this).addClass('aloha-block-dropInlineElementIntoEmptyBlock');
649
+ lastHoveredCharacter = this;
650
+ return;
651
+ }
652
+
653
+ that._dd_traverseDomTreeAndWrapCharactersWithSpans(this);
654
+ jQuery('span[data-i]', this).droppable({
655
+ tolerance: 'pointer',
656
+ addClasses: false,
657
+ scope: 'aloha-block-inlinedragdrop',
658
+ over: function() {
659
+ if (lastHoveredCharacter) {
660
+ // Just to be sure, we remove the css class of the last hovered character.
661
+ // This is needed such that spans are deselected which contain multiple
662
+ // lines.
663
+ jQuery(lastHoveredCharacter).removeClass('aloha-block-droppable');
664
+ }
665
+ lastHoveredCharacter = this;
666
+ jQuery(this).addClass('aloha-block-droppable');
667
+ },
668
+ out: function() {
669
+ jQuery(this).removeClass('aloha-block-droppable');
670
+ if (lastHoveredCharacter === this) {
671
+ lastHoveredCharacter = null;
672
+ }
673
+ }
674
+ });
675
+ // Now that we updated the droppables in the system, we need to recalculate
676
+ // the Drag Drop offsets.
677
+ jQuery.ui.ddmanager.prepareOffsets(ui.draggable.data('draggable'), event);
678
+ },
679
+ out: function() {
680
+ jQuery(this).removeClass('aloha-block-dropInlineElementIntoEmptyBlock');
681
+ },
682
+
683
+ /**
684
+ * When dropping over a paragraph, we use the "lastHoveredCharacter"
685
+ * as drop target.
686
+ */
687
+ drop: function() {
688
+ if (!Ext.isIE7) {
689
+ dropFn();
690
+ }
691
+ }
692
+ };
693
+
694
+
695
+ jQuery('.aloha-editable').children(':not(.aloha-block)').droppable(droppableCfg);
696
+ // Small HACK: Also make table cells droppable
697
+ jQuery('.aloha-table-cell-editable').droppable(droppableCfg);
698
+ }
699
+ });
308
700
  },
309
701
 
310
- _getWrapperElementType: function() {
311
- return this._domElementType === 'block' ? 'div' : 'span';
702
+ /**
703
+ * Helper which traverses the DOM tree starting from el and wraps all non-empty texts with spans,
704
+ * such that they can act as drop target.
705
+ *
706
+ * @param {DomElement} el
707
+ */
708
+ _dd_traverseDomTreeAndWrapCharactersWithSpans: function(el) {
709
+ var child;
710
+ for(var i=0, l=el.childNodes.length; i < l; i++) {
711
+ child = el.childNodes[i];
712
+ if (child.nodeType === 1) { // DOM Nodes
713
+ if (!~child.className.indexOf('aloha-block') && child.attributes['data-i'] === undefined) {
714
+ // We only recurse if child does NOT have the class "aloha-block", and is NOT data-i
715
+ this._dd_traverseDomTreeAndWrapCharactersWithSpans(child);
716
+ } else if (child.attributes['data-i']) {
717
+ // data-i set -> we have converted this hierarchy level already --> early return!
718
+ return;
719
+ }
720
+ } else if (child.nodeType === 3) { // Text Nodes
721
+ var numberOfSpansInserted = this._dd_insertSpans(child);
722
+ i += numberOfSpansInserted;
723
+ l += numberOfSpansInserted;
724
+ }
725
+ }
312
726
  },
313
727
 
728
+ /**
729
+ * Helper which splits text on word boundaries, adding whitespaces to the following element.
730
+ * Examples:
731
+ * - "Hello world" -> ["Hello", " world"]
732
+ * - " Hello world" -> [" Hello", " world"]
733
+ * --> see the unit tests for the specification
734
+ */
735
+ _dd_splitText: function(text) {
736
+ var textParts = text.split(/(?=\b)/);
737
+ var cleanedTextParts = [];
738
+
739
+ var isWhitespace = false;
740
+ for (var i=0,l=textParts.length; i<l; i++) {
741
+ if (!/[^\t\n\r ]/.test(textParts[i])) {
742
+ // if the current text part is just whitespace, we add a flag...
743
+ isWhitespace = true;
744
+ } else {
745
+ if (isWhitespace) {
746
+ // we have a whitespace to add
747
+ cleanedTextParts.push(' ' + textParts[i]);
748
+ isWhitespace = false;
749
+ } else {
750
+ cleanedTextParts.push(textParts[i]);
751
+ }
752
+ }
753
+ }
754
+ if (isWhitespace) {
755
+ cleanedTextParts[cleanedTextParts.length - 1] += ' ';
756
+ }
757
+ return cleanedTextParts;
758
+ },
759
+
760
+ /**
761
+ * This is a helper for _dd_traverseDomTreeAndWrapCharactersWithSpans,
762
+ * performing the actual conversion.
763
+ *
764
+ * This function returns the number of additional DOM elements inserted.
765
+ * This is "numberOfSpansCreated - 1" (because one text node has been initially there)
766
+ */
767
+ _dd_insertSpans: function(el) {
768
+ var text = el.nodeValue;
769
+
770
+ // If node just contains empty strings, we do not do anything.
771
+ // Use ECMA-262 Edition 3 String and RegExp features
772
+ if (!/[^\t\n\r ]/.test(text)) {
773
+ return 0;
774
+ }
775
+ var newNodes = document.createDocumentFragment();
776
+
777
+ var splitText = this._dd_splitText(text);
778
+
779
+ var l = splitText.length;
780
+ var x, word, leftWordPartLength, t;
781
+ var numberOfSpansInserted = 0;
782
+
783
+ for (var i=0; i<l; i++) {
784
+ // left half of word
785
+ word = splitText[i];
786
+ if (word.length === 0) continue;
787
+ // We use "floor" here such that sentence delimiters like "!" can have a block placed afterwards
788
+ leftWordPartLength = Math.floor(word.length/2);
789
+
790
+ // For Internet Explorer, we only make dropping AFTER words possible to improve performance
791
+ if (Ext.isIE7 || Ext.isIE8) {
792
+ leftWordPartLength = 0;
793
+ }
794
+
795
+ if (leftWordPartLength > 0) {
796
+ x = document.createElement('span');
797
+ x.appendChild(document.createTextNode(word.substr(0, leftWordPartLength)));
798
+ x.setAttribute('data-i', i);
799
+
800
+ newNodes.appendChild(x);
801
+ numberOfSpansInserted++;
802
+ }
803
+
804
+ // right half of word
805
+ x = document.createElement('span');
806
+ t = word.substr(leftWordPartLength);
807
+ x.appendChild(document.createTextNode(t));
808
+ x.setAttribute('data-i', i);
809
+ x.setAttribute('class', 'aloha-block-droppable-right');
810
+
811
+ newNodes.appendChild(x);
812
+ numberOfSpansInserted++;
813
+ }
814
+ el.parentNode.replaceChild(newNodes, el);
815
+ return numberOfSpansInserted-1;
816
+ },
817
+
818
+ /**
819
+ * After the Drag/Drop operation, we need to remove the SPAN elements
820
+ * again.
821
+ */
822
+ _dd_traverseDomTreeAndRemoveSpans: function(el) {
823
+ var nodesToDelete = [], convertBack;
824
+ convertBack = function(el) {
825
+ var currentlyTraversingExpandedText = false, currentText, lastNode;
826
+ var child;
827
+ for(var i=0, l=el.childNodes.length; i < l; i++) {
828
+ child = el.childNodes[i];
829
+ if (child.nodeType === 1) { // Node
830
+ if (child.attributes['data-i'] !== undefined) {
831
+ if (!currentlyTraversingExpandedText) {
832
+ // We did not traverse expanded text before, and just entered an expanded text section
833
+ // thus, we reset all variables to their initial state
834
+ currentlyTraversingExpandedText = true;
835
+ currentText = '';
836
+ lastNode = undefined;
837
+ }
838
+ if (currentlyTraversingExpandedText) {
839
+ // We are currently traversing the expanded text nodes, so we collect their data
840
+ // together in the currentText variable. We know that they only
841
+ // have one TextNode child, as this is the way we constructed them.
842
+ //
843
+ // Note: we do NOT use child.innerHTML here, as this returns HTML entities;
844
+ // but we need the HTML entities already processed:
845
+ // - child.innerHTML returns "Hello&nbsp;World"
846
+ // - child.firstChild.nodeValue returns "Hello World"
847
+ currentText += child.firstChild.nodeValue;
848
+
849
+ if (lastNode) {
850
+ nodesToDelete.push(lastNode);
851
+ }
852
+ lastNode = child;
853
+ }
854
+ } else {
855
+ if (currentlyTraversingExpandedText) {
856
+ currentlyTraversingExpandedText = false;
857
+ // We just left a region with data-i elements set.
858
+ // so, we need to store the currentText back to the region.
859
+ // We do this by using the last visited node as anchor.
860
+ lastNode.parentNode.replaceChild(document.createTextNode(currentText), lastNode);
861
+ }
862
+ // Recursion
863
+ if (!~child.className.indexOf('aloha-block')) {
864
+ // If child does not have the class "aloha-block", we iterate into it
865
+ convertBack(child);
866
+ }
867
+ }
868
+ }
869
+ }
870
+ if (currentlyTraversingExpandedText) {
871
+ // Special case: the last child node *is* a wrapped text node and we are at the end of the collection.
872
+ // In this case, we convert the text as well.
873
+ lastNode.parentNode.replaceChild(document.createTextNode(currentText), lastNode);
874
+ }
875
+ };
876
+
877
+ convertBack(el);
878
+
879
+ for (var i=0, l=nodesToDelete.length; i<l; i++) {
880
+ nodesToDelete[i].parentNode.removeChild(nodesToDelete[i]);
881
+ }
882
+ },
883
+
884
+ /**************************
885
+ * SECTION: Drag&Drop for Block elements
886
+ **************************/
887
+ _setupDragDropForBlockElements: function() {
888
+ // Mark the drag handle with an extra CSS class, such that it is picked up by BlockManager.initializeBlockLevelDragDrop()
889
+ this.$element.find('.aloha-block-draghandle').addClass('aloha-block-draghandle-blocklevel');
890
+ },
891
+
892
+
893
+ /**************************
894
+ * SECTION: Other Rendering Helpers
895
+ **************************/
896
+
314
897
  /**
315
898
  * Create editables from the inner content that was
316
899
  * rendered for this block.
317
900
  *
901
+ * This method must be idempotent. I.e. it must produce the same results
902
+ * when called once or twice.
903
+ *
318
904
  * Override to use a custom implementation and to pass
319
905
  * special configuration to .aloha()
320
- *
321
- * @param {jQuery} innerElement
322
906
  */
323
- createEditables: function(innerElement) {
324
- innerElement.find('.aloha-editable').aloha();
907
+ createEditablesIfNeeded: function() {
908
+ // TODO: only create them if they are no aloha element yet...
909
+ // TODO: should only happen inside Aloha
910
+ this.$element.find('.aloha-editable').aloha();
325
911
  },
326
912
 
327
913
  /**
328
914
  * Render block toolbar elements
329
915
  *
916
+ * This method must be idempotent. I.e. it must produce the same results
917
+ * when called once or twice.
918
+ *
330
919
  * Template method to render custom block UI.
920
+ * @api
331
921
  */
332
- renderToolbar: function() {
333
- this.element.prepend('<span class="aloha-block-draghandle"></span>');
922
+ renderBlockHandlesIfNeeded: function() {
923
+ if (this.isDraggable()) {
924
+ if (this.$element.children('.aloha-block-draghandle').length === 0) {
925
+ this.$element.prepend('<span class="aloha-block-handle aloha-block-draghandle"></span>');
926
+ }
927
+ }
334
928
  },
335
929
 
930
+ /**************************
931
+ * SECTION: Attribute Handling
932
+ **************************/
933
+
336
934
  /**
337
- * Get or set one or many attributes
935
+ * Get or set one or many attribute, similar to the jQuery attr() function.
936
+ *
937
+ * The attribute keys are converted internally to lowercase,
938
+ * so attr('foo', 'bar') and attr('FoO', 'bar') are the same internally.
939
+ * The same applies to reading.
940
+ *
941
+ * It is not allowed to set internal attributes (starting with aloha-block-) through this API.
338
942
  *
339
943
  * @api
340
944
  * @param {String|Object} attributeNameOrObject
@@ -345,12 +949,20 @@ function(Aloha, jQuery, BlockManager, Observable, FloatingMenu) {
345
949
  var that = this, attributeChanged = false;
346
950
 
347
951
  if (arguments.length >= 2) {
952
+ if (attributeNameOrObject.substr(0, 12) === 'aloha-block-') {
953
+ Aloha.Log.error('block/block', 'It is not allowed to set internal block attributes (starting with aloha-block-) through Block.attr() (You tried to set ' + attributeNameOrObject + ')');
954
+ return;
955
+ }
348
956
  if (this._getAttribute(attributeNameOrObject) !== attributeValue) {
349
957
  attributeChanged = true;
350
958
  }
351
959
  this._setAttribute(attributeNameOrObject, attributeValue);
352
960
  } else if (typeof attributeNameOrObject === 'object') {
353
961
  jQuery.each(attributeNameOrObject, function(key, value) {
962
+ if (key.substr(0, 12) === 'aloha-block-') {
963
+ Aloha.Log.error('block/block', 'It is not allowed to set internal block attributes (starting with aloha-block-) through Block.attr() (You tried to set ' + key + ')');
964
+ return;
965
+ }
354
966
  if (that._getAttribute(key) !== value) {
355
967
  attributeChanged = true;
356
968
  }
@@ -362,33 +974,36 @@ function(Aloha, jQuery, BlockManager, Observable, FloatingMenu) {
362
974
  return this._getAttributes();
363
975
  }
364
976
  if (attributeChanged && !suppressEvents) {
365
- this._renderAndSetContent();
977
+ this._update();
366
978
  this.trigger('change');
367
979
  }
368
- return this;
980
+ return null;
369
981
  },
370
982
 
983
+ /**
984
+ * Internal helper for setting a single attribute.
985
+ */
371
986
  _setAttribute: function(name, value) {
372
- if (name === 'about') {
373
- this.element.attr('about', value);
374
- } else {
375
- this.element.attr('data-' + name, value);
376
- }
987
+ this.$element.attr('data-' + name.toLowerCase(), value);
377
988
  },
378
989
 
990
+ /**
991
+ * Internal helper for getting an attribute
992
+ */
379
993
  _getAttribute: function(name) {
380
- return this._getAttributes()[name];
994
+ return this.$element.attr('data-' + name.toLowerCase());
381
995
  },
382
996
 
997
+ /**
998
+ * Internal helper for getting all attributes
999
+ */
383
1000
  _getAttributes: function() {
384
1001
  var attributes = {};
385
1002
 
386
1003
  // element.data() not always up-to-date, that's why we iterate over the attributes directly.
387
- jQuery.each(this.element[0].attributes, function(i, attribute) {
388
- if (attribute.name === 'about') {
389
- attributes['about'] = attribute.value;
390
- } else if (attribute.name.substr(0, 5) === 'data-') {
391
- attributes[attribute.name.substr(5)] = attribute.value;
1004
+ jQuery.each(this.$element[0].attributes, function(i, attribute) {
1005
+ if (attribute.name.substr(0, 5) === 'data-') {
1006
+ attributes[attribute.name.substr(5).toLowerCase()] = attribute.value;
392
1007
  }
393
1008
  });
394
1009
 
@@ -404,11 +1019,8 @@ function(Aloha, jQuery, BlockManager, Observable, FloatingMenu) {
404
1019
  var DefaultBlock = AbstractBlock.extend(
405
1020
  /** @lends block.block.DefaultBlock */
406
1021
  {
407
- init: function() {
408
- this.attr('default-content', this.element.html());
409
- },
410
- render: function() {
411
- return this.attr('default-content');
1022
+ update: function($element, postProcessFn) {
1023
+ postProcessFn();
412
1024
  }
413
1025
  });
414
1026
 
@@ -421,8 +1033,11 @@ function(Aloha, jQuery, BlockManager, Observable, FloatingMenu) {
421
1033
  /** @lends block.block.DebugBlock */
422
1034
  {
423
1035
  title: 'Debugging',
424
- render: function() {
425
- this.element.css({display: 'block'});
1036
+ init: function($element, postProcessFn) {
1037
+ this.update($element, postProcessFn);
1038
+ },
1039
+ update: function($element, postProcessFn) {
1040
+ $element.css({display: 'block'});
426
1041
  var renderedAttributes = '<table class="debug-block">';
427
1042
  jQuery.each(this.attr(), function(k, v) {
428
1043
  renderedAttributes += '<tr><th>' + k + '</th><td>' + v + '</td></tr>';
@@ -430,7 +1045,8 @@ function(Aloha, jQuery, BlockManager, Observable, FloatingMenu) {
430
1045
 
431
1046
  renderedAttributes += '</table>';
432
1047
 
433
- return renderedAttributes;
1048
+ $element.html(renderedAttributes);
1049
+ postProcessFn();
434
1050
  }
435
1051
  });
436
1052