]> scripts.mit.edu Git - autoinstalls/wordpress.git/blob - wp-includes/js/tinymce/plugins/wpview/plugin.js
WordPress 3.9
[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 = '';
336                         } else {
337                                 node.innerText = '';
338                         }
339
340                         // This makes all views into block tags (as we use <div>).
341                         // Can use 'PostProcess' and a regex instead.
342                         dom.replace( dom.create( 'p', null, window.decodeURIComponent( dom.getAttrib( node, 'data-wpview-text' ) ) ), node );
343                 });
344     });
345
346         editor.on( 'keydown', function( event ) {
347                 var keyCode = event.keyCode,
348                         body = editor.getBody(),
349                         view, padNode;
350
351                 // If a view isn't selected, let the event go on its merry way.
352                 if ( ! selected ) {
353                         return;
354                 }
355
356                 // Let keypresses that involve the command or control keys through.
357                 // Also, let any of the F# keys through.
358                 if ( event.metaKey || event.ctrlKey || ( keyCode >= 112 && keyCode <= 123 ) ) {
359                         if ( ( event.metaKey || event.ctrlKey ) && keyCode === 88 ) {
360                                 toRemove = selected;
361                         }
362                         return;
363                 }
364
365                 view = getParentView( editor.selection.getNode() );
366
367                 // If the caret is not within the selected view, deselect the
368                 // view and bail.
369                 if ( view !== selected ) {
370                         deselect();
371                         return;
372                 }
373
374                 // Deselect views with the arrow keys
375                 if ( keyCode === VK.LEFT || keyCode === VK.UP ) {
376                         deselect();
377                         // Handle case where two views are stacked on top of one another
378                         if ( isView( view.previousSibling ) ) {
379                                 select( view.previousSibling );
380                         // Handle case where view is the first node
381                         } else if ( ! view.previousSibling ) {
382                                 padNode = createPadNode();
383                                 body.insertBefore( padNode, body.firstChild );
384                                 editor.selection.setCursorLocation( body.firstChild, 0 );
385                         // Handle default case
386                         } else {
387                                 editor.selection.select( view.previousSibling, true );
388                                 editor.selection.collapse();
389                         }
390                 } else if ( keyCode === VK.RIGHT || keyCode === VK.DOWN ) {
391                         deselect();
392                         // Handle case where the next node is another wpview
393                         if ( isView( view.nextSibling ) ) {
394                                 select( view.nextSibling );
395                         // Handle case were the view is that last node
396                         } else if ( ! view.nextSibling ) {
397                                 padNode = createPadNode();
398                                 body.appendChild( padNode );
399                                 editor.selection.setCursorLocation( body.lastChild, 0 );
400                         // Handle default case where the next node is a non-wpview
401                         } else {
402                                 editor.selection.setCursorLocation( view.nextSibling, 0 );
403                         }
404                 } else if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
405                         // If delete or backspace is pressed, delete the view.
406                         editor.dom.remove( selected );
407                 }
408
409                 event.preventDefault();
410         });
411
412         // Select views when arrow keys are used to navigate the content of the editor.
413         editor.on( 'keydown', function( event ) {
414                 var keyCode = event.keyCode,
415                         dom = editor.dom,
416                         range = editor.selection.getRng(),
417                         startNode = range.startContainer,
418                         body = editor.getBody(),
419                         node, container;
420
421                 if ( ! startNode || startNode === body || event.metaKey || event.ctrlKey ) {
422                         return;
423                 }
424
425                 if ( keyCode === VK.UP || keyCode === VK.LEFT ) {
426                         if ( keyCode === VK.LEFT && ( ! range.collapsed || range.startOffset !== 0 ) ) {
427                                 // Not at the beginning of the current range
428                                 return;
429                         }
430
431                         if ( ! ( node = dom.getParent( startNode, dom.isBlock ) ) ) {
432                                 return;
433                         }
434
435                         if ( selectSiblingView( node, 'previous' ) ) {
436                                 event.preventDefault();
437                         }
438                 } else if ( keyCode === VK.DOWN || keyCode === VK.RIGHT ) {
439                         if ( ! ( node = dom.getParent( startNode, dom.isBlock ) ) ) {
440                                 return;
441                         }
442
443                         if ( keyCode === VK.RIGHT ) {
444                                 container = range.endContainer;
445
446                                 if ( ! range.collapsed || ( range.startOffset === 0 && container.length ) ||
447                                         container.nextSibling ||
448                                         ( container.nodeType === 3 && range.startOffset !== container.length ) ) { // Not at the end of the current range
449
450                                         return;
451                                 }
452
453                                 // In a child element
454                                 while ( container && container !== node && container !== body ) {
455                                         if ( container.nextSibling ) {
456                                                 return;
457                                         }
458                                         container = container.parentNode;
459                                 }
460                         }
461
462                         if ( selectSiblingView( node, 'next' ) ) {
463                                 event.preventDefault();
464                         }
465                 }
466         });
467
468         editor.on( 'keyup', function( event ) {
469                 var padNode,
470                         keyCode = event.keyCode,
471                         body = editor.getBody(),
472                         range;
473
474                 if ( toRemove ) {
475                         editor.dom.remove( toRemove );
476                         toRemove = false;
477                 }
478
479                 if ( keyCode === VK.DELETE || keyCode === VK.BACKSPACE ) {
480                         // Make sure there is padding if the last element is a view
481                         if ( isView( body.lastChild ) ) {
482                                 padNode = createPadNode();
483                                 body.appendChild( padNode );
484
485                                 if ( body.childNodes.length === 2 ) {
486                                         editor.selection.setCursorLocation( padNode, 0 );
487                                 }
488                         }
489
490                         range = editor.selection.getRng();
491
492                         // Allow an initial element in the document to be removed when it is before a view
493                         if ( body.firstChild === range.startContainer && range.collapsed === true &&
494                                         isView( range.startContainer.nextSibling ) && range.startOffset === 0 ) {
495
496                                 editor.dom.remove( range.startContainer );
497                         }
498                 }
499         });
500
501         return {
502                 getViewText: getViewText,
503                 setViewText: setViewText
504         };
505 });