]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/tinymce/plugins/wpview/plugin.js
WordPress 3.9.1
[autoinstalls/wordpress.git] / wp-includes / js / tinymce / plugins / wpview / plugin.js
1 /* global tinymce */
2 /**
3  * WordPress View plugin.
4  */
5 tinymce.PluginManager.add( 'wpview', function( editor ) {
6         var selected,
7                 VK = tinymce.util.VK,
8                 TreeWalker = tinymce.dom.TreeWalker,
9                 toRemove = false;
10
11         function getParentView( node ) {
12                 while ( node && node.nodeName !== 'BODY' ) {
13                         if ( isView( node ) ) {
14                                 return node;
15                         }
16
17                         node = node.parentNode;
18                 }
19         }
20
21         function isView( node ) {
22                 return node && /\bwpview-wrap\b/.test( node.className );
23         }
24
25         function createPadNode() {
26                 return editor.dom.create( 'p', { 'data-wpview-pad': 1 },
27                         ( tinymce.Env.ie && tinymce.Env.ie < 11 ) ? '' : '<br data-mce-bogus="1" />' );
28         }
29
30         /**
31          * Get the text/shortcode string for a view.
32          *
33          * @param view The view wrapper's HTML id or node
34          * @returns string The text/shoercode string of the view
35          */
36         function getViewText( view ) {
37                 view = getParentView( typeof view === 'string' ? editor.dom.get( view ) : view );
38
39                 if ( view ) {
40                         return window.decodeURIComponent( editor.dom.getAttrib( view, 'data-wpview-text' ) || '' );
41                 }
42                 return '';
43         }
44
45         /**
46          * Set the view's original text/shortcode string
47          *
48          * @param view The view wrapper's HTML id or node
49          * @param text The text string to be set
50          */
51         function setViewText( view, text ) {
52                 view = getParentView( typeof view === 'string' ? editor.dom.get( view ) : view );
53
54                 if ( view ) {
55                         editor.dom.setAttrib( view, 'data-wpview-text', window.encodeURIComponent( text || '' ) );
56                         return true;
57                 }
58                 return false;
59         }
60
61         function _stop( event ) {
62                 event.stopPropagation();
63         }
64
65         function select( viewNode ) {
66                 var clipboard,
67                         dom = editor.dom;
68
69                 // Bail if node is already selected.
70                 if ( viewNode === selected ) {
71                         return;
72                 }
73
74                 deselect();
75                 selected = viewNode;
76                 dom.addClass( viewNode, 'selected' );
77
78                 clipboard = dom.create( 'div', {
79                         'class': 'wpview-clipboard',
80                         'contenteditable': 'true'
81                 }, getViewText( viewNode ) );
82
83                 // Prepend inside the wrapper
84                 viewNode.insertBefore( clipboard, viewNode.firstChild );
85
86                 // Both of the following are necessary to prevent manipulating the selection/focus
87                 dom.bind( clipboard, 'beforedeactivate focusin focusout', _stop );
88                 dom.bind( selected, 'beforedeactivate focusin focusout', _stop );
89
90                 // Make sure that the editor is focused.
91                 // It is possible that the editor is not focused when the mouse event fires
92                 // without focus, the selection will not work properly.
93                 editor.getBody().focus();
94
95                 // select the hidden div
96                 editor.selection.select( clipboard, true );
97         }
98
99         /**
100          * Deselect a selected view and remove clipboard
101          */
102         function deselect() {
103                 var clipboard,
104                         dom = editor.dom;
105
106                 if ( selected ) {
107                         clipboard = editor.dom.select( '.wpview-clipboard', selected )[0];
108                         dom.unbind( clipboard );
109                         dom.remove( clipboard );
110
111                         dom.unbind( selected, 'beforedeactivate focusin focusout click mouseup', _stop );
112                         dom.removeClass( selected, 'selected' );
113                 }
114
115                 selected = null;
116         }
117
118         function selectSiblingView( node, direction ) {
119                 var body = editor.getBody(),
120                         sibling = direction === 'previous' ? 'previousSibling' : 'nextSibling';
121
122                 while ( node && node.parentNode !== body ) {
123                         if ( node[sibling] ) {
124                                 // The caret will be in another element
125                                 return false;
126                         }
127
128                         node = node.parentNode;
129                 }
130
131                 if ( isView( node[sibling] ) ) {
132                         select( node[sibling] );
133                         return true;
134                 }
135
136                 return false;
137         }
138
139         // Check if the `wp.mce` API exists.
140         if ( typeof wp === 'undefined' || ! wp.mce ) {
141                 return;
142         }
143
144         // Remove the content of view wrappers from HTML string
145         function emptyViews( content ) {
146                 return content.replace(/(<div[^>]+wpview-wrap[^>]+>)[\s\S]+?data-wpview-end[^>]*><\/ins><\/div>/g, '$1</div>' );
147         }
148
149         // Prevent adding undo levels on changes inside a view wrapper
150         editor.on( 'BeforeAddUndo', function( event ) {
151                 if ( event.lastLevel && emptyViews( event.level.content ) === emptyViews( event.lastLevel.content ) ) {
152                         event.preventDefault();
153                 }
154         });
155
156         // When the editor's content changes, scan the new content for
157         // matching view patterns, and transform the matches into
158         // view wrappers.
159         editor.on( 'BeforeSetContent', function( event ) {
160                 if ( ! event.content ) {
161                         return;
162                 }
163
164                 if ( ! event.initial ) {
165                         wp.mce.views.unbind( editor );
166                 }
167
168                 event.content = wp.mce.views.toViews( event.content );
169         });
170
171         // When the editor's content has been updated and the DOM has been
172         // processed, render the views in the document.
173         editor.on( 'SetContent', function( event ) {
174                 var body, padNode;
175
176                 wp.mce.views.render();
177
178                 // Add padding <p> if the noneditable node is last
179                 if ( event.load || ! event.set ) {
180                         body = editor.getBody();
181
182                         if ( isView( body.lastChild ) ) {
183                                 padNode = createPadNode();
184                                 body.appendChild( padNode );
185
186                                 if ( ! event.initial ) {
187                                         editor.selection.setCursorLocation( padNode, 0 );
188                                 }
189                         }
190                 }
191         });
192
193         // Detect mouse down events that are adjacent to a view when a view is the first view or the last view
194         editor.on( 'click', function( event ) {
195                 var body = editor.getBody(),
196                         doc = editor.getDoc(),
197                         scrollTop = doc.documentElement.scrollTop || body.scrollTop || 0,
198                         x, y, firstNode, lastNode, padNode;
199
200                 if ( event.target.nodeName === 'HTML' && ! event.metaKey && ! event.ctrlKey ) {
201                         firstNode = body.firstChild;
202                         lastNode = body.lastChild;
203                         x = event.clientX;
204                         y = event.clientY;
205
206                         // Detect clicks above or to the left if the first node is a wpview
207                         if ( isView( firstNode ) && ( ( x < firstNode.offsetLeft && y < ( firstNode.offsetHeight - scrollTop ) ) ||
208                                 y < firstNode.offsetTop ) ) {
209
210                                 padNode = createPadNode();
211                                 body.insertBefore( padNode, firstNode );
212
213                         // Detect clicks to the right and below the last view
214                         } else if ( isView( lastNode ) && ( x > ( lastNode.offsetLeft + lastNode.offsetWidth ) ||
215                                 ( ( scrollTop + y ) - ( lastNode.offsetTop + lastNode.offsetHeight ) ) > 0 ) ) {
216
217                                 padNode = createPadNode();
218                                 body.appendChild( padNode );
219                         }
220
221                         if ( padNode ) {
222                                 // Make sure that a selected view is deselected so that focus and selection are handled properly
223                                 deselect();
224                                 editor.getBody().focus();
225                                 editor.selection.setCursorLocation( padNode, 0 );
226                         }
227                 }
228         });
229
230         editor.on( 'init', function() {
231                 var selection = editor.selection;
232                 // When a view is selected, ensure content that is being pasted
233                 // or inserted is added to a text node (instead of the view).
234                 editor.on( 'BeforeSetContent', function() {
235                         var walker, target,
236                                 view = getParentView( selection.getNode() );
237
238                         // If the selection is not within a view, bail.
239                         if ( ! view ) {
240                                 return;
241                         }
242
243                         if ( ! view.nextSibling || isView( view.nextSibling ) ) {
244                                 // If there are no additional nodes or the next node is a
245                                 // view, create a text node after the current view.
246                                 target = editor.getDoc().createTextNode('');
247                                 editor.dom.insertAfter( target, view );
248                         } else {
249                                 // Otherwise, find the next text node.
250                                 walker = new TreeWalker( view.nextSibling, view.nextSibling );
251                                 target = walker.next();
252                         }
253
254                         // Select the `target` text node.
255                         selection.select( target );
256                         selection.collapse( true );
257                 });
258
259                 // When the selection's content changes, scan any new content
260                 // for matching views.
261                 //
262                 // Runs on paste and on inserting nodes/html.
263                 editor.on( 'SetContent', function( e ) {
264                         if ( ! e.context ) {
265                                 return;
266                         }
267
268                         var node = selection.getNode();
269
270                         if ( ! node.innerHTML ) {
271                                 return;
272                         }
273
274                         node.innerHTML = wp.mce.views.toViews( node.innerHTML );
275                 });
276
277                 editor.dom.bind( editor.getBody(), 'mousedown mouseup click', function( event ) {
278                         var view = getParentView( event.target ),
279                                 deselectEventType;
280
281                         // Contain clicks inside the view wrapper
282                         if ( view ) {
283                                 event.stopPropagation();
284
285                                 // Hack to try and keep the block resize handles from appearing. They will show on mousedown and then be removed on mouseup.
286                                 if ( tinymce.Env.ie <= 10 ) {
287                                         deselect();
288                                 }
289
290                                 select( view );
291
292                                 if ( event.type === 'click' && ! event.metaKey && ! event.ctrlKey ) {
293                                         if ( editor.dom.hasClass( event.target, 'edit' ) ) {
294                                                 wp.mce.views.edit( view );
295                                         } else if ( editor.dom.hasClass( event.target, 'remove' ) ) {
296                                                 editor.dom.remove( view );
297                                         }
298                                 }
299                                 // Returning false stops the ugly bars from appearing in IE11 and stops the view being selected as a range in FF.
300                                 // Unfortunately, it also inhibits the dragging of views to a new location.
301                                 return false;
302                         } else {
303
304                                 // Fix issue with deselecting a view in IE8. Without this hack, clicking content above the view wouldn't actually deselect it
305                                 // and the caret wouldn't be placed at the mouse location
306                                 if ( tinymce.Env.ie && tinymce.Env.ie <= 8 ) {
307                                         deselectEventType = 'mouseup';
308                                 } else {
309                                         deselectEventType = 'mousedown';
310                                 }
311
312                                 if ( event.type === deselectEventType ) {
313                                         deselect();
314                                 }
315                         }
316                 });
317         });
318
319         editor.on( 'PreProcess', function( event ) {
320                 var dom = editor.dom;
321
322                 // Remove empty padding nodes
323                 tinymce.each( dom.select( 'p[data-wpview-pad]', event.node ), function( node ) {
324                         if ( dom.isEmpty( node ) ) {
325                                 dom.remove( node );
326                         } else {
327                                 dom.setAttrib( node, 'data-wpview-pad', null );
328                         }
329                 });
330
331                 // Replace the wpview node with the wpview string/shortcode?
332                 tinymce.each( dom.select( 'div[data-wpview-text]', event.node ), function( node ) {
333                         // Empty the wrap node
334                         if ( 'textContent' in node ) {
335                                 node.textContent = '\u00a0';
336                         } else {
337                                 node.innerText = '\u00a0';
338                         }
339                 });
340     });
341
342     editor.on( 'PostProcess', function( event ) {
343                 if ( event.content ) {
344                         event.content = event.content.replace( /<div [^>]*?data-wpview-text="([^"]*)"[^>]*>[\s\S]*?<\/div>/g, function( match, shortcode ) {
345                                 if ( shortcode ) {
346                                         return '<p>' + window.decodeURIComponent( shortcode ) + '</p>';
347                                 }
348                                 return ''; // If error, remove the view wrapper
349                         });
350                 }
351         });
352
353         editor.on( 'keydown', function( event ) {
354                 var keyCode = event.keyCode,
355                         body = editor.getBody(),
356                         view, padNode;
357
358                 // If a view isn't selected, let the event go on its merry way.
359                 if ( ! selected ) {
360                         return;
361                 }
362
363                 // Let keypresses that involve the command or control keys through.
364                 // Also, let any of the F# keys through.
365                 if ( event.metaKey || event.ctrlKey || ( keyCode >= 112 && keyCode <= 123 ) ) {
366                         if ( ( event.metaKey || event.ctrlKey ) && keyCode === 88 ) {
367                                 toRemove = selected;
368                         }
369                         return;
370                 }
371
372                 view = getParentView( editor.selection.getNode() );
373
374                 // If the caret is not within the selected view, deselect the
375                 // view and bail.
376                 if ( view !== selected ) {
377                         deselect();
378                         return;
379                 }
380
381                 // Deselect views with the arrow keys
382                 if ( keyCode === VK.LEFT || keyCode === VK.UP ) {
383                         deselect();
384                         // Handle case where two views are stacked on top of one another
385                         if ( isView( view.previousSibling ) ) {
386                                 select( view.previousSibling );
387                         // Handle case where view is the first node
388                         } else if ( ! view.previousSibling ) {
389                                 padNode = createPadNode();
390                                 body.insertBefore( padNode, body.firstChild );
391                                 editor.selection.setCursorLocation( body.firstChild, 0 );
392                         // Handle default case
393                         } else {
394                                 editor.selection.select( view.previousSibling, true );
395                                 editor.selection.collapse();
396                         }
397                 } else if ( keyCode === VK.RIGHT || keyCode === VK.DOWN ) {
398                         deselect();
399                         // Handle case where the next node is another wpview
400                         if ( isView( view.nextSibling ) ) {
401                                 select( view.nextSibling );
402                         // Handle case were the view is that last node
403                         } else if ( ! view.nextSibling ) {
404                                 padNode = createPadNode();
405                                 body.appendChild( padNode );
406                                 editor.selection.setCursorLocation( body.lastChild, 0 );
407                         // Handle default case where the next node is a non-wpview
408                         } else {
409                                 editor.selection.setCursorLocation( view.nextSibling, 0 );
410                         }
411                 } else if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
412                         // If delete or backspace is pressed, delete the view.
413                         editor.dom.remove( selected );
414                 }
415
416                 event.preventDefault();
417         });
418
419         // Select views when arrow keys are used to navigate the content of the editor.
420         editor.on( 'keydown', function( event ) {
421                 var keyCode = event.keyCode,
422                         dom = editor.dom,
423                         range = editor.selection.getRng(),
424                         startNode = range.startContainer,
425                         body = editor.getBody(),
426                         node, container;
427
428                 if ( ! startNode || startNode === body || event.metaKey || event.ctrlKey ) {
429                         return;
430                 }
431
432                 if ( keyCode === VK.UP || keyCode === VK.LEFT ) {
433                         if ( keyCode === VK.LEFT && ( ! range.collapsed || range.startOffset !== 0 ) ) {
434                                 // Not at the beginning of the current range
435                                 return;
436                         }
437
438                         if ( ! ( node = dom.getParent( startNode, dom.isBlock ) ) ) {
439                                 return;
440                         }
441
442                         if ( selectSiblingView( node, 'previous' ) ) {
443                                 event.preventDefault();
444                         }
445                 } else if ( keyCode === VK.DOWN || keyCode === VK.RIGHT ) {
446                         if ( ! ( node = dom.getParent( startNode, dom.isBlock ) ) ) {
447                                 return;
448                         }
449
450                         if ( keyCode === VK.RIGHT ) {
451                                 container = range.endContainer;
452
453                                 if ( ! range.collapsed || ( range.startOffset === 0 && container.length ) ||
454                                         container.nextSibling ||
455                                         ( container.nodeType === 3 && range.startOffset !== container.length ) ) { // Not at the end of the current range
456
457                                         return;
458                                 }
459
460                                 // In a child element
461                                 while ( container && container !== node && container !== body ) {
462                                         if ( container.nextSibling ) {
463                                                 return;
464                                         }
465                                         container = container.parentNode;
466                                 }
467                         }
468
469                         if ( selectSiblingView( node, 'next' ) ) {
470                                 event.preventDefault();
471                         }
472                 }
473         });
474
475         editor.on( 'keyup', function( event ) {
476                 var padNode,
477                         keyCode = event.keyCode,
478                         body = editor.getBody(),
479                         range;
480
481                 if ( toRemove ) {
482                         editor.dom.remove( toRemove );
483                         toRemove = false;
484                 }
485
486                 if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
487                         // Make sure there is padding if the last element is a view
488                         if ( isView( body.lastChild ) ) {
489                                 padNode = createPadNode();
490                                 body.appendChild( padNode );
491
492                                 if ( body.childNodes.length === 2 ) {
493                                         editor.selection.setCursorLocation( padNode, 0 );
494                                 }
495                         }
496
497                         range = editor.selection.getRng();
498
499                         // Allow an initial element in the document to be removed when it is before a view
500                         if ( body.firstChild === range.startContainer && range.collapsed === true &&
501                                         isView( range.startContainer.nextSibling ) && range.startOffset === 0 ) {
502
503                                 editor.dom.remove( range.startContainer );
504                         }
505                 }
506         });
507
508         return {
509                 getViewText: getViewText,
510                 setViewText: setViewText
511         };
512 });