1ff6ccd44590aed0924760e6e108d0982068497e
[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                 current.document.transaction(function() {
96                     var p = list.after({tagName: 'div', attrs: {'class': 'p'}});
97                     p.append({text: current.getText()});
98                     item.detach();
99                     if(list.contents().length === 0) {
100                         list.detach();
101                     }
102                     return current.document.createFragment(current.document.NodeFragment, {node: p});
103                 }, {
104                     metadata: {
105                         description: action.getState().description,
106                         fragment: params.fragment
107                     },
108                     success: callback
109                 });
110                 return;
111             }
112
113             var toSearch = current.nodeType === Node.ELEMENT_NODE ? [current] : [];
114             toSearch = toSearch.concat(current.parents());
115             toSearch.some(function(node) {
116                 if(node.is('list')) {
117                     node.document.transaction(function() {
118                         var firstItem = node.object.extractListItems(),
119                             toret;
120                         if(params.fragment.isValid()) {
121                             toret = params.fragment;
122                         } else {
123                             toret = node.document.createFragment(node.document.NodeFragment, {node: firstItem});
124                         }
125                         return toret;
126                     }, {
127                         metadata: {
128                             description: action.getState().description,
129                             fragment: params.fragment
130                         },
131                         success: callback
132                     });
133                     
134                     return true; // break
135                 }
136             }.bind(this));
137         },
138         changeType: function(callback, params) {
139             var node = params.fragment.node,
140                 action = this;
141             node.document.transaction(function() {
142                 var list = node.getParent('list');
143                 list.setClass(type === 'Bullet' ? 'list' : 'list.enum');
144                 if(params.fragment.isValid()) {
145                     return params.fragment;
146                 } else {
147                     return node.document.createFragment(node.document.NodeFragment, {node: list.contents()[0]});
148                 }
149             }, {
150                 metadata: {
151                     description: action.getState().description,
152                     fragment: params.fragment
153                 },
154                 success: callback
155             });
156         }
157     };
158
159     var isToggled = function(params) {
160         if(params.fragment && params.fragment.node && params.fragment.node.isInside('list')) {
161             var list = params.fragment.node.getParent('list');
162             return list.getClass() === (type === 'Bullet' ? 'list' : 'list.enum');
163         }
164         return false;
165     };
166
167     var label = type === 'Bullet' ? gettext('bull. list') : gettext('num. list');
168
169     return {
170         name: 'toggle' + type + 'List',
171         context: ['fragment'],
172         params: {
173             fragment: {type: 'context', name: 'fragment'}
174         },
175         stateDefaults: {
176             label: label
177         },
178         getState: function(params) {
179             if(!params.fragment || !params.fragment.isValid()) {
180                 return false;
181             }
182
183             if(params.fragment instanceof params.fragment.CaretFragment && params.fragment.node.isInside('list')) {
184                 var list = params.fragment.node.getParent('list');
185                 if((list.getClass() === 'list' && type === 'Enum') || (list.getClass() === 'list.enum' && type === 'Bullet')) {
186                     return {
187                         allowed: true,
188                         description: interpolate(gettext('Change list type to %s'), [label]),
189                         execute: execute.changeType
190                     };
191                 }
192                 return {
193                     allowed: true,
194                     toggled: isToggled(params),
195                     description: gettext('Remove list'),
196                     execute: execute.remove
197                 };
198
199             }
200             var boundaries = getBoundariesForAList(params.fragment);
201             if(boundaries && boundaries.node1.hasSameContextRoot(boundaries.node2)) {
202                 var iterNode = boundaries.node1;
203                 while(true) {
204                     if(!iterNode.is({tagName: 'div', klass: 'p'}) && !iterNode.is({tagName: 'header'})) {
205                         return {
206                             allowed: false,
207                             description: gettext('Invalid element for a list item')
208                         }
209                     }
210                     if(iterNode.sameNode(boundaries.node2))
211                         break;
212                     iterNode = iterNode.next();
213                 }
214
215                 return {
216                     allowed: true,
217                     description: interpolate(gettext('Make %s fragment(s) into list'), [countItems(getBoundariesForAList(params.fragment))]),
218                     execute: execute.add
219                 };
220             }
221         }
222     };
223 };
224
225
226 return {
227     actions: [toggleListAction('Bullet'), toggleListAction('Enum')]
228 };
229
230 });