]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/tinymce/plugins/lists/plugin.js
WordPress 4.0
[autoinstalls/wordpress.git] / wp-includes / js / tinymce / plugins / lists / plugin.js
1 /**
2  * plugin.js
3  *
4  * Copyright, Moxiecode Systems AB
5  * Released under LGPL License.
6  *
7  * License: http://www.tinymce.com/license
8  * Contributing: http://www.tinymce.com/contributing
9  */
10
11 /*global tinymce:true */
12 /*eslint consistent-this:0 */
13
14 tinymce.PluginManager.add('lists', function(editor) {
15         var self = this;
16
17         function isListNode(node) {
18                 return node && (/^(OL|UL|DL)$/).test(node.nodeName);
19         }
20
21         function isFirstChild(node) {
22                 return node.parentNode.firstChild == node;
23         }
24
25         function isLastChild(node) {
26                 return node.parentNode.lastChild == node;
27         }
28
29         function isTextBlock(node) {
30                 return node && !!editor.schema.getTextBlockElements()[node.nodeName];
31         }
32
33         editor.on('init', function() {
34                 var dom = editor.dom, selection = editor.selection;
35
36                 /**
37                  * Returns a range bookmark. This will convert indexed bookmarks into temporary span elements with
38                  * index 0 so that they can be restored properly after the DOM has been modified. Text bookmarks will not have spans
39                  * added to them since they can be restored after a dom operation.
40                  *
41                  * So this: <p><b>|</b><b>|</b></p>
42                  * becomes: <p><b><span data-mce-type="bookmark">|</span></b><b data-mce-type="bookmark">|</span></b></p>
43                  *
44                  * @param  {DOMRange} rng DOM Range to get bookmark on.
45                  * @return {Object} Bookmark object.
46                  */
47                 function createBookmark(rng) {
48                         var bookmark = {};
49
50                         function setupEndPoint(start) {
51                                 var offsetNode, container, offset;
52
53                                 container = rng[start ? 'startContainer' : 'endContainer'];
54                                 offset = rng[start ? 'startOffset' : 'endOffset'];
55
56                                 if (container.nodeType == 1) {
57                                         offsetNode = dom.create('span', {'data-mce-type': 'bookmark'});
58
59                                         if (container.hasChildNodes()) {
60                                                 offset = Math.min(offset, container.childNodes.length - 1);
61
62                                                 if (start) {
63                                                         container.insertBefore(offsetNode, container.childNodes[offset]);
64                                                 } else {
65                                                         dom.insertAfter(offsetNode, container.childNodes[offset]);
66                                                 }
67                                         } else {
68                                                 container.appendChild(offsetNode);
69                                         }
70
71                                         container = offsetNode;
72                                         offset = 0;
73                                 }
74
75                                 bookmark[start ? 'startContainer' : 'endContainer'] = container;
76                                 bookmark[start ? 'startOffset' : 'endOffset'] = offset;
77                         }
78
79                         setupEndPoint(true);
80
81                         if (!rng.collapsed) {
82                                 setupEndPoint();
83                         }
84
85                         return bookmark;
86                 }
87
88                 /**
89                  * Moves the selection to the current bookmark and removes any selection container wrappers.
90                  *
91                  * @param {Object} bookmark Bookmark object to move selection to.
92                  */
93                 function moveToBookmark(bookmark) {
94                         function restoreEndPoint(start) {
95                                 var container, offset, node;
96
97                                 function nodeIndex(container) {
98                                         var node = container.parentNode.firstChild, idx = 0;
99
100                                         while (node) {
101                                                 if (node == container) {
102                                                         return idx;
103                                                 }
104
105                                                 // Skip data-mce-type=bookmark nodes
106                                                 if (node.nodeType != 1 || node.getAttribute('data-mce-type') != 'bookmark') {
107                                                         idx++;
108                                                 }
109
110                                                 node = node.nextSibling;
111                                         }
112
113                                         return -1;
114                                 }
115
116                                 container = node = bookmark[start ? 'startContainer' : 'endContainer'];
117                                 offset = bookmark[start ? 'startOffset' : 'endOffset'];
118
119                                 if (!container) {
120                                         return;
121                                 }
122
123                                 if (container.nodeType == 1) {
124                                         offset = nodeIndex(container);
125                                         container = container.parentNode;
126                                         dom.remove(node);
127                                 }
128
129                                 bookmark[start ? 'startContainer' : 'endContainer'] = container;
130                                 bookmark[start ? 'startOffset' : 'endOffset'] = offset;
131                         }
132
133                         restoreEndPoint(true);
134                         restoreEndPoint();
135
136                         var rng = dom.createRng();
137
138                         rng.setStart(bookmark.startContainer, bookmark.startOffset);
139
140                         if (bookmark.endContainer) {
141                                 rng.setEnd(bookmark.endContainer, bookmark.endOffset);
142                         }
143
144                         selection.setRng(rng);
145                 }
146
147                 function createNewTextBlock(contentNode, blockName) {
148                         var node, textBlock, fragment = dom.createFragment(), hasContentNode;
149                         var blockElements = editor.schema.getBlockElements();
150
151                         if (editor.settings.forced_root_block) {
152                                 blockName = blockName || editor.settings.forced_root_block;
153                         }
154
155                         if (blockName) {
156                                 textBlock = dom.create(blockName);
157
158                                 if (textBlock.tagName === editor.settings.forced_root_block) {
159                                         dom.setAttribs(textBlock, editor.settings.forced_root_block_attrs);
160                                 }
161
162                                 fragment.appendChild(textBlock);
163                         }
164
165                         if (contentNode) {
166                                 while ((node = contentNode.firstChild)) {
167                                         var nodeName = node.nodeName;
168
169                                         if (!hasContentNode && (nodeName != 'SPAN' || node.getAttribute('data-mce-type') != 'bookmark')) {
170                                                 hasContentNode = true;
171                                         }
172
173                                         if (blockElements[nodeName]) {
174                                                 fragment.appendChild(node);
175                                                 textBlock = null;
176                                         } else {
177                                                 if (blockName) {
178                                                         if (!textBlock) {
179                                                                 textBlock = dom.create(blockName);
180                                                                 fragment.appendChild(textBlock);
181                                                         }
182
183                                                         textBlock.appendChild(node);
184                                                 } else {
185                                                         fragment.appendChild(node);
186                                                 }
187                                         }
188                                 }
189                         }
190
191                         if (!editor.settings.forced_root_block) {
192                                 fragment.appendChild(dom.create('br'));
193                         } else {
194                                 // BR is needed in empty blocks on non IE browsers
195                                 if (!hasContentNode && (!tinymce.Env.ie || tinymce.Env.ie > 10)) {
196                                         textBlock.appendChild(dom.create('br', {'data-mce-bogus': '1'}));
197                                 }
198                         }
199
200                         return fragment;
201                 }
202
203                 function getSelectedListItems() {
204                         return tinymce.grep(selection.getSelectedBlocks(), function(block) {
205                                 return /^(LI|DT|DD)$/.test(block.nodeName);
206                         });
207                 }
208
209                 function splitList(ul, li, newBlock) {
210                         var tmpRng, fragment;
211
212                         var bookmarks = dom.select('span[data-mce-type="bookmark"]', ul);
213
214                         newBlock = newBlock || createNewTextBlock(li);
215                         tmpRng = dom.createRng();
216                         tmpRng.setStartAfter(li);
217                         tmpRng.setEndAfter(ul);
218                         fragment = tmpRng.extractContents();
219
220                         if (!dom.isEmpty(fragment)) {
221                                 dom.insertAfter(fragment, ul);
222                         }
223
224                         dom.insertAfter(newBlock, ul);
225
226                         if (dom.isEmpty(li.parentNode)) {
227                                 tinymce.each(bookmarks, function(node) {
228                                         li.parentNode.parentNode.insertBefore(node, li.parentNode);
229                                 });
230
231                                 dom.remove(li.parentNode);
232                         }
233
234                         dom.remove(li);
235                 }
236
237                 function mergeWithAdjacentLists(listBlock) {
238                         var sibling, node;
239
240                         sibling = listBlock.nextSibling;
241                         if (sibling && isListNode(sibling) && sibling.nodeName == listBlock.nodeName) {
242                                 while ((node = sibling.firstChild)) {
243                                         listBlock.appendChild(node);
244                                 }
245
246                                 dom.remove(sibling);
247                         }
248
249                         sibling = listBlock.previousSibling;
250                         if (sibling && isListNode(sibling) && sibling.nodeName == listBlock.nodeName) {
251                                 while ((node = sibling.firstChild)) {
252                                         listBlock.insertBefore(node, listBlock.firstChild);
253                                 }
254
255                                 dom.remove(sibling);
256                         }
257                 }
258
259                 /**
260                  * Normalizes the all lists in the specified element.
261                  */
262                 function normalizeList(element) {
263                         tinymce.each(tinymce.grep(dom.select('ol,ul', element)), function(ul) {
264                                 var sibling, parentNode = ul.parentNode;
265
266                                 // Move UL/OL to previous LI if it's the only child of a LI
267                                 if (parentNode.nodeName == 'LI' && parentNode.firstChild == ul) {
268                                         sibling = parentNode.previousSibling;
269                                         if (sibling && sibling.nodeName == 'LI') {
270                                                 sibling.appendChild(ul);
271
272                                                 if (dom.isEmpty(parentNode)) {
273                                                         dom.remove(parentNode);
274                                                 }
275                                         }
276                                 }
277
278                                 // Append OL/UL to previous LI if it's in a parent OL/UL i.e. old HTML4
279                                 if (isListNode(parentNode)) {
280                                         sibling = parentNode.previousSibling;
281                                         if (sibling && sibling.nodeName == 'LI') {
282                                                 sibling.appendChild(ul);
283                                         }
284                                 }
285                         });
286                 }
287
288                 function outdent(li) {
289                         var ul = li.parentNode, ulParent = ul.parentNode, newBlock;
290
291                         function removeEmptyLi(li) {
292                                 if (dom.isEmpty(li)) {
293                                         dom.remove(li);
294                                 }
295                         }
296
297                         if (li.nodeName == 'DD') {
298                                 dom.rename(li, 'DT');
299                                 return true;
300                         }
301
302                         if (isFirstChild(li) && isLastChild(li)) {
303                                 if (ulParent.nodeName == "LI") {
304                                         dom.insertAfter(li, ulParent);
305                                         removeEmptyLi(ulParent);
306                                         dom.remove(ul);
307                                 } else if (isListNode(ulParent)) {
308                                         dom.remove(ul, true);
309                                 } else {
310                                         ulParent.insertBefore(createNewTextBlock(li), ul);
311                                         dom.remove(ul);
312                                 }
313
314                                 return true;
315                         } else if (isFirstChild(li)) {
316                                 if (ulParent.nodeName == "LI") {
317                                         dom.insertAfter(li, ulParent);
318                                         li.appendChild(ul);
319                                         removeEmptyLi(ulParent);
320                                 } else if (isListNode(ulParent)) {
321                                         ulParent.insertBefore(li, ul);
322                                 } else {
323                                         ulParent.insertBefore(createNewTextBlock(li), ul);
324                                         dom.remove(li);
325                                 }
326
327                                 return true;
328                         } else if (isLastChild(li)) {
329                                 if (ulParent.nodeName == "LI") {
330                                         dom.insertAfter(li, ulParent);
331                                 } else if (isListNode(ulParent)) {
332                                         dom.insertAfter(li, ul);
333                                 } else {
334                                         dom.insertAfter(createNewTextBlock(li), ul);
335                                         dom.remove(li);
336                                 }
337
338                                 return true;
339                         } else {
340                                 if (ulParent.nodeName == 'LI') {
341                                         ul = ulParent;
342                                         newBlock = createNewTextBlock(li, 'LI');
343                                 } else if (isListNode(ulParent)) {
344                                         newBlock = createNewTextBlock(li, 'LI');
345                                 } else {
346                                         newBlock = createNewTextBlock(li);
347                                 }
348
349                                 splitList(ul, li, newBlock);
350                                 normalizeList(ul.parentNode);
351
352                                 return true;
353                         }
354
355                         return false;
356                 }
357
358                 function indent(li) {
359                         var sibling, newList;
360
361                         function mergeLists(from, to) {
362                                 var node;
363
364                                 if (isListNode(from)) {
365                                         while ((node = li.lastChild.firstChild)) {
366                                                 to.appendChild(node);
367                                         }
368
369                                         dom.remove(from);
370                                 }
371                         }
372
373                         if (li.nodeName == 'DT') {
374                                 dom.rename(li, 'DD');
375                                 return true;
376                         }
377
378                         sibling = li.previousSibling;
379
380                         if (sibling && isListNode(sibling)) {
381                                 sibling.appendChild(li);
382                                 return true;
383                         }
384
385                         if (sibling && sibling.nodeName == 'LI' && isListNode(sibling.lastChild)) {
386                                 sibling.lastChild.appendChild(li);
387                                 mergeLists(li.lastChild, sibling.lastChild);
388                                 return true;
389                         }
390
391                         sibling = li.nextSibling;
392
393                         if (sibling && isListNode(sibling)) {
394                                 sibling.insertBefore(li, sibling.firstChild);
395                                 return true;
396                         }
397
398                         if (sibling && sibling.nodeName == 'LI' && isListNode(li.lastChild)) {
399                                 return false;
400                         }
401
402                         sibling = li.previousSibling;
403                         if (sibling && sibling.nodeName == 'LI') {
404                                 newList = dom.create(li.parentNode.nodeName);
405                                 sibling.appendChild(newList);
406                                 newList.appendChild(li);
407                                 mergeLists(li.lastChild, newList);
408                                 return true;
409                         }
410
411                         return false;
412                 }
413
414                 function indentSelection() {
415                         var listElements = getSelectedListItems();
416
417                         if (listElements.length) {
418                                 var bookmark = createBookmark(selection.getRng(true));
419
420                                 for (var i = 0; i < listElements.length; i++) {
421                                         if (!indent(listElements[i]) && i === 0) {
422                                                 break;
423                                         }
424                                 }
425
426                                 moveToBookmark(bookmark);
427                                 editor.nodeChanged();
428
429                                 return true;
430                         }
431                 }
432
433                 function outdentSelection() {
434                         var listElements = getSelectedListItems();
435
436                         if (listElements.length) {
437                                 var bookmark = createBookmark(selection.getRng(true));
438                                 var i, y, root = editor.getBody();
439
440                                 i = listElements.length;
441                                 while (i--) {
442                                         var node = listElements[i].parentNode;
443
444                                         while (node && node != root) {
445                                                 y = listElements.length;
446                                                 while (y--) {
447                                                         if (listElements[y] === node) {
448                                                                 listElements.splice(i, 1);
449                                                                 break;
450                                                         }
451                                                 }
452
453                                                 node = node.parentNode;
454                                         }
455                                 }
456
457                                 for (i = 0; i < listElements.length; i++) {
458                                         if (!outdent(listElements[i]) && i === 0) {
459                                                 break;
460                                         }
461                                 }
462
463                                 moveToBookmark(bookmark);
464                                 editor.nodeChanged();
465
466                                 return true;
467                         }
468                 }
469
470                 function applyList(listName) {
471                         var rng = selection.getRng(true), bookmark = createBookmark(rng), listItemName = 'LI';
472
473                         listName = listName.toUpperCase();
474
475                         if (listName == 'DL') {
476                                 listItemName = 'DT';
477                         }
478
479                         function getSelectedTextBlocks() {
480                                 var textBlocks = [], root = editor.getBody();
481
482                                 function getEndPointNode(start) {
483                                         var container, offset;
484
485                                         container = rng[start ? 'startContainer' : 'endContainer'];
486                                         offset = rng[start ? 'startOffset' : 'endOffset'];
487
488                                         // Resolve node index
489                                         if (container.nodeType == 1) {
490                                                 container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container;
491                                         }
492
493                                         while (container.parentNode != root) {
494                                                 if (isTextBlock(container)) {
495                                                         return container;
496                                                 }
497
498                                                 if (/^(TD|TH)$/.test(container.parentNode.nodeName)) {
499                                                         return container;
500                                                 }
501
502                                                 container = container.parentNode;
503                                         }
504
505                                         return container;
506                                 }
507
508                                 var startNode = getEndPointNode(true);
509                                 var endNode = getEndPointNode();
510                                 var block, siblings = [];
511
512                                 for (var node = startNode; node; node = node.nextSibling) {
513                                         siblings.push(node);
514
515                                         if (node == endNode) {
516                                                 break;
517                                         }
518                                 }
519
520                                 tinymce.each(siblings, function(node) {
521                                         if (isTextBlock(node)) {
522                                                 textBlocks.push(node);
523                                                 block = null;
524                                                 return;
525                                         }
526
527                                         if (dom.isBlock(node) || node.nodeName == 'BR') {
528                                                 if (node.nodeName == 'BR') {
529                                                         dom.remove(node);
530                                                 }
531
532                                                 block = null;
533                                                 return;
534                                         }
535
536                                         var nextSibling = node.nextSibling;
537                                         if (tinymce.dom.BookmarkManager.isBookmarkNode(node)) {
538                                                 if (isTextBlock(nextSibling) || (!nextSibling && node.parentNode == root)) {
539                                                         block = null;
540                                                         return;
541                                                 }
542                                         }
543
544                                         if (!block) {
545                                                 block = dom.create('p');
546                                                 node.parentNode.insertBefore(block, node);
547                                                 textBlocks.push(block);
548                                         }
549
550                                         block.appendChild(node);
551                                 });
552
553                                 return textBlocks;
554                         }
555
556                         tinymce.each(getSelectedTextBlocks(), function(block) {
557                                 var listBlock, sibling;
558
559                                 sibling = block.previousSibling;
560                                 if (sibling && isListNode(sibling) && sibling.nodeName == listName) {
561                                         listBlock = sibling;
562                                         block = dom.rename(block, listItemName);
563                                         sibling.appendChild(block);
564                                 } else {
565                                         listBlock = dom.create(listName);
566                                         block.parentNode.insertBefore(listBlock, block);
567                                         listBlock.appendChild(block);
568                                         block = dom.rename(block, listItemName);
569                                 }
570
571                                 mergeWithAdjacentLists(listBlock);
572                         });
573
574                         moveToBookmark(bookmark);
575                 }
576
577                 function removeList() {
578                         var bookmark = createBookmark(selection.getRng(true)), root = editor.getBody();
579
580                         tinymce.each(getSelectedListItems(), function(li) {
581                                 var node, rootList;
582
583                                 if (dom.isEmpty(li)) {
584                                         outdent(li);
585                                         return;
586                                 }
587
588                                 for (node = li; node && node != root; node = node.parentNode) {
589                                         if (isListNode(node)) {
590                                                 rootList = node;
591                                         }
592                                 }
593
594                                 splitList(rootList, li);
595                         });
596
597                         moveToBookmark(bookmark);
598                 }
599
600                 function toggleList(listName) {
601                         var parentList = dom.getParent(selection.getStart(), 'OL,UL,DL');
602
603                         if (parentList) {
604                                 if (parentList.nodeName == listName) {
605                                         removeList(listName);
606                                 } else {
607                                         var bookmark = createBookmark(selection.getRng(true));
608                                         mergeWithAdjacentLists(dom.rename(parentList, listName));
609                                         moveToBookmark(bookmark);
610                                 }
611                         } else {
612                                 applyList(listName);
613                         }
614                 }
615
616                 function queryListCommandState(listName) {
617                         return function() {
618                                 var parentList = dom.getParent(editor.selection.getStart(), 'UL,OL,DL');
619
620                                 return parentList && parentList.nodeName == listName;
621                         };
622                 }
623
624                 self.backspaceDelete = function(isForward) {
625                         function findNextCaretContainer(rng, isForward) {
626                                 var node = rng.startContainer, offset = rng.startOffset;
627                                 var nonEmptyBlocks, walker;
628
629                                 if (node.nodeType == 3 && (isForward ? offset < node.data.length : offset > 0)) {
630                                         return node;
631                                 }
632
633                                 nonEmptyBlocks = editor.schema.getNonEmptyElements();
634                                 walker = new tinymce.dom.TreeWalker(rng.startContainer);
635
636                                 while ((node = walker[isForward ? 'next' : 'prev']())) {
637                                         if (node.nodeName == 'LI' && !node.hasChildNodes()) {
638                                                 return node;
639                                         }
640
641                                         if (nonEmptyBlocks[node.nodeName]) {
642                                                 return node;
643                                         }
644
645                                         if (node.nodeType == 3 && node.data.length > 0) {
646                                                 return node;
647                                         }
648                                 }
649                         }
650
651                         function mergeLiElements(fromElm, toElm) {
652                                 var node, listNode, ul = fromElm.parentNode;
653
654                                 if (isListNode(toElm.lastChild)) {
655                                         listNode = toElm.lastChild;
656                                 }
657
658                                 node = toElm.lastChild;
659                                 if (node && node.nodeName == 'BR' && fromElm.hasChildNodes()) {
660                                         dom.remove(node);
661                                 }
662
663                                 if (dom.isEmpty(toElm)) {
664                                         dom.$(toElm).empty();
665                                 }
666
667                                 if (!dom.isEmpty(fromElm)) {
668                                         while ((node = fromElm.firstChild)) {
669                                                 toElm.appendChild(node);
670                                         }
671                                 }
672
673                                 if (listNode) {
674                                         toElm.appendChild(listNode);
675                                 }
676
677                                 dom.remove(fromElm);
678
679                                 if (dom.isEmpty(ul)) {
680                                         dom.remove(ul);
681                                 }
682                         }
683
684                         if (selection.isCollapsed()) {
685                                 var li = dom.getParent(selection.getStart(), 'LI');
686
687                                 if (li) {
688                                         var rng = selection.getRng(true);
689                                         var otherLi = dom.getParent(findNextCaretContainer(rng, isForward), 'LI');
690
691                                         if (otherLi && otherLi != li) {
692                                                 var bookmark = createBookmark(rng);
693
694                                                 if (isForward) {
695                                                         mergeLiElements(otherLi, li);
696                                                 } else {
697                                                         mergeLiElements(li, otherLi);
698                                                 }
699
700                                                 moveToBookmark(bookmark);
701
702                                                 return true;
703                                         } else if (!otherLi) {
704                                                 if (!isForward && removeList(li.parentNode.nodeName)) {
705                                                         return true;
706                                                 }
707                                         }
708                                 }
709                         }
710                 };
711
712                 editor.addCommand('Indent', function() {
713                         if (!indentSelection()) {
714                                 return true;
715                         }
716                 });
717
718                 editor.addCommand('Outdent', function() {
719                         if (!outdentSelection()) {
720                                 return true;
721                         }
722                 });
723
724                 editor.addCommand('InsertUnorderedList', function() {
725                         toggleList('UL');
726                 });
727
728                 editor.addCommand('InsertOrderedList', function() {
729                         toggleList('OL');
730                 });
731
732                 editor.addCommand('InsertDefinitionList', function() {
733                         toggleList('DL');
734                 });
735
736                 editor.addQueryStateHandler('InsertUnorderedList', queryListCommandState('UL'));
737                 editor.addQueryStateHandler('InsertOrderedList', queryListCommandState('OL'));
738                 editor.addQueryStateHandler('InsertDefinitionList', queryListCommandState('DL'));
739
740                 editor.on('keydown', function(e) {
741                         if (e.keyCode == 9 && editor.dom.getParent(editor.selection.getStart(), 'LI,DT,DD')) {
742                                 e.preventDefault();
743
744                                 if (e.shiftKey) {
745                                         outdentSelection();
746                                 } else {
747                                         indentSelection();
748                                 }
749                         }
750                 });
751         });
752
753         editor.addButton('indent', {
754                 icon: 'indent',
755                 title: 'Increase indent',
756                 cmd: 'Indent',
757                 onPostRender: function() {
758                         var ctrl = this;
759
760                         editor.on('nodechange', function() {
761                                 var blocks = editor.selection.getSelectedBlocks();
762                                 var disable = false;
763
764                                 for (var i = 0, l = blocks.length; !disable && i < l; i++) {
765                                         var tag = blocks[i].nodeName;
766
767                                         disable = (tag == 'LI' && isFirstChild(blocks[i]) || tag == 'UL' || tag == 'OL' || tag == 'DD');
768                                 }
769
770                                 ctrl.disabled(disable);
771                         });
772                 }
773         });
774
775         editor.on('keydown', function(e) {
776                 if (e.keyCode == tinymce.util.VK.BACKSPACE) {
777                         if (self.backspaceDelete()) {
778                                 e.preventDefault();
779                         }
780                 } else if (e.keyCode == tinymce.util.VK.DELETE) {
781                         if (self.backspaceDelete(true)) {
782                                 e.preventDefault();
783                         }
784                 }
785         });
786 });