some other minor changes from milpeer
[fnpeditor.git] / src / editor / plugins / core / lists.js
1 define(function() {
2     
3 'use strict';
4 /* globals gettext, interpolate */
5
6
7 var getBoundariesForAList = function(fragment) {
8     var node;
9
10     if(fragment instanceof fragment.RangeFragment && fragment.hasSiblingBoundaries()) {
11         return fragment.startNode.hasSameContextRoot(fragment.endNode) && fragment.boundariesSiblingParents();
12     }
13     if(fragment instanceof fragment.NodeFragment) {
14         node = fragment.node.getNearestElementNode();
15         if(node.isContextRoot()) {
16             node = fragment.node;
17         }
18
19         return {
20             node1: node,
21             node2: node
22         };
23     }
24 };
25
26 var countItems = function(boundaries) {
27     var ptr = boundaries.node1,
28         c = 1;
29     while(ptr && !ptr.sameNode(boundaries.node2)) {
30         c++;
31         ptr = ptr.next();
32     }
33     return c;
34 };
35
36 var toggleListAction = function(type) {
37     
38     var execute = {
39         add: function(callback, params) {
40             var boundaries = getBoundariesForAList(params.fragment),
41                 listParams = {klass: type === 'Bullet' ? 'list' : 'list.enum'},
42                 action = this;
43
44             if(boundaries && boundaries.node1) {
45                 boundaries.node1.document.transaction(function() {
46                     var iterNode = boundaries.node1;
47                     while(true) {
48                         if(!iterNode.is({tagName: 'div', klass: 'p'})) {
49                             if(iterNode.is({tagName: 'header'})) {
50                                 var newNode = iterNode.setTag('div');
51                                 newNode.setClass('p');
52                                 if(iterNode.sameNode(boundaries.node1)) {
53                                     boundaries.node1 = newNode;
54                                 }
55                                 if(iterNode.sameNode(boundaries.node2)) {
56                                     boundaries.node2 = newNode;
57                                 }
58                                 iterNode = newNode;
59                             } else {
60                                 throw new Error('Invalid element');
61                             }
62                         }
63                         if(iterNode.sameNode(boundaries.node2))
64                             break;
65                         iterNode = iterNode.next();
66                     }
67                     listParams.node1 = boundaries.node1;
68                     listParams.node2 = boundaries.node2;
69                     var list = boundaries.node1.document.createList(listParams),
70                         item1 = list.object.getItem(0),
71                         text = item1 ? item1.contents()[0] : undefined, //
72                         doc = boundaries.node1.document;
73                     if(text) {
74                         return doc.createFragment(doc.CaretFragment, {node: text, offset:0});
75                     }
76                 }, {
77                     metadata: {
78                         description: action.getState().description,
79                         fragment: params.fragment
80                     },
81                     success: callback
82                 });
83             } else {
84                 throw new Error('Invalid boundaries');
85             }
86         },
87         remove: function(callback, params) {
88             /* globals Node */
89             var current = params.fragment.node,
90                 action = this;
91
92             if(current.parent().is('item') && current.parent().parent().is('list') && current.parent().next() === null) {
93                 var item = current.parent();
94                 var list = item.parent();
95                 var doc = current.document;
96                 doc.transaction(function() {
97                     var p = list.after({tagName: 'div', attrs: {'class': 'p'}});
98                     p.append({text: current.getText()});
99                     item.detach();
100                     if(list.contents().length === 0) {
101                         list.detach();
102                     }
103                     return doc.createFragment(
104                         doc.CaretFragment, {node: p.contents()[0], offset: params.fragment.offset});
105                 }, {
106                     metadata: {
107                         description: action.getState().description,
108                         fragment: params.fragment
109                     },
110                     success: callback
111                 });
112                 return;
113             }
114
115             var toSearch = current.nodeType === Node.ELEMENT_NODE ? [current] : [];
116             toSearch = toSearch.concat(current.parents());
117             toSearch.some(function(node) {
118                 var doc = node.document;
119                 if(node.is('list')) {
120                     doc.transaction(function() {
121                         var firstItem = node.object.extractListItems(),
122                             toret;
123                         if(params.fragment.isValid()) {
124                             toret = params.fragment;
125                         } else {
126                             toret = doc.createFragment(doc.NodeFragment, {node: firstItem});
127                         }
128                         return toret;
129                     }, {
130                         metadata: {
131                             description: action.getState().description,
132                             fragment: params.fragment
133                         },
134                         success: callback
135                     });
136                     
137                     return true; // break
138                 }
139             }.bind(this));
140         },
141         changeType: function(callback, params) {
142             var node = params.fragment.node,
143                 action = this;
144             node.document.transaction(function() {
145                 var list = node.getParent('list');
146                 list.setClass(type === 'Bullet' ? 'list' : 'list.enum');
147                 if(params.fragment.isValid()) {
148                     return params.fragment;
149                 } else {
150                     return node.document.createFragment(node.document.NodeFragment, {node: list.contents()[0]});
151                 }
152             }, {
153                 metadata: {
154                     description: action.getState().description,
155                     fragment: params.fragment
156                 },
157                 success: callback
158             });
159         }
160     };
161
162     var isToggled = function(params) {
163         if(params.fragment && params.fragment.node && params.fragment.node.isInside('list')) {
164             var list = params.fragment.node.getParent('list');
165             return list.getClass() === (type === 'Bullet' ? 'list' : 'list.enum');
166         }
167         return false;
168     };
169
170     var label = type === 'Bullet' ? gettext('bull. list') : gettext('num. list');
171
172     return {
173         name: 'toggle' + type + 'List',
174         context: ['fragment'],
175         params: {
176             fragment: {type: 'context', name: 'fragment'}
177         },
178         stateDefaults: {
179             label: label
180         },
181         getState: function(params) {
182             if(!params.fragment || !params.fragment.isValid()) {
183                 return false;
184             }
185
186             if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside('list')) {
187                 var list = params.fragment.node.getParent('list');
188                 if((list.getClass() === 'list' && type === 'Enum') || (list.getClass() === 'list.enum' && type === 'Bullet')) {
189                     return {
190                         allowed: true,
191                         description: interpolate(gettext('Change list type to %s'), [label]),
192                         execute: execute.changeType
193                     };
194                 }
195                 return {
196                     allowed: true,
197                     toggled: isToggled(params),
198                     description: gettext('Remove list'),
199                     execute: execute.remove
200                 };
201
202             }
203             var boundaries = getBoundariesForAList(params.fragment);
204             if(boundaries && boundaries.node1.hasSameContextRoot(boundaries.node2)) {
205                 var iterNode = boundaries.node1;
206                 while(true) {
207                     if(!iterNode.is({tagName: 'div', klass: 'p'}) && !iterNode.is({tagName: 'header'})) {
208                         return {
209                             allowed: false,
210                             description: gettext('Invalid element for a list item')
211                         }
212                     }
213                     if(iterNode.sameNode(boundaries.node2))
214                         break;
215                     iterNode = iterNode.next();
216                 }
217
218                 return {
219                     allowed: true,
220                     description: interpolate(gettext('Make %s fragment(s) into list'), [countItems(getBoundariesForAList(params.fragment))]),
221                     execute: execute.add
222                 };
223             }
224         }
225     };
226 };
227
228
229 return {
230     actions: [toggleListAction('Bullet'), toggleListAction('Enum')]
231 };
232
233 });