├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .svnignore ├── assets ├── banner-1544x500.png └── icon-256x256.png ├── class-fee.php ├── class-wp-rest-post-autosave-controller.php ├── css ├── fee.css ├── tinymce.core.css └── tinymce.view.css ├── js ├── SelectControl.js ├── blobToBase64.js ├── fee-adminbar.js ├── fee.js ├── filePicker.js ├── insertBlob.js ├── tinymce.image.js └── tinymce.theme.js ├── package.json ├── plugin.php ├── readme.md └── vendor ├── imagetools.js ├── lists.js ├── mce-view.js ├── paste.js ├── tinymce.js ├── wordpress.js ├── wplink.js ├── wptextpattern.js └── wpview.js /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ## Expected behaviour and actual behaviour 5 | 6 | 7 | 8 | ## Steps to reproduce the problem 9 | 10 | 11 | 12 | ## Specifications 13 | 14 | * Browser Name and Version: 15 | * WordPress Version: 16 | * Plugin Version: 17 | * Theme name/URL: 18 | 19 | ## Error messages 20 | 21 | 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .svn 2 | node_modules 3 | readme.txt 4 | -------------------------------------------------------------------------------- /.svnignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | .gitignore 4 | .svnignore 5 | assets 6 | node_modules 7 | package.json 8 | readme.md 9 | -------------------------------------------------------------------------------- /assets/banner-1544x500.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ellatrix/wp-front-end-editor/5f790ef58dc6382ba42e4a5eb9202b22f9d710c4/assets/banner-1544x500.png -------------------------------------------------------------------------------- /assets/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ellatrix/wp-front-end-editor/5f790ef58dc6382ba42e4a5eb9202b22f9d710c4/assets/icon-256x256.png -------------------------------------------------------------------------------- /class-fee.php: -------------------------------------------------------------------------------- 1 | errors[] = $message; 18 | } 19 | 20 | function admin_notices() { 21 | foreach ( $this->errors as $error ) { 22 | echo '

' . $error . '

'; 23 | } 24 | } 25 | 26 | function check_wordpress_version() { 27 | include ABSPATH . WPINC . '/version.php'; 28 | 29 | $wp_version = str_replace( '-src', '', $wp_version ); 30 | 31 | if ( version_compare( $wp_version, self::WORDPRESS_MIN_VERSION, '<' ) ) { 32 | $this->error( sprintf( 33 | /* translators: 1: This plugin 2: WordPress version */ 34 | __( '%1$s requires WordPress version %2$s.', 'wp-front-end-editor' ), 35 | '' . __( 'Front-end Editor', 'wp-front-end-editor' ) . '', 36 | self::WORDPRESS_MIN_VERSION 37 | ) ); 38 | } 39 | } 40 | 41 | function check_rest_api_plugin() { 42 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 43 | 44 | $plugins = get_plugins(); 45 | $uris = wp_list_pluck( $plugins, 'PluginURI' ); 46 | $file = array_search( self::REST_API_PLUGIN_URI, $uris ); 47 | $installed = ! empty( $file ); 48 | 49 | if ( $installed ) { 50 | $data = get_plugin_data( WP_PLUGIN_DIR . '/' . $file ); 51 | 52 | if ( version_compare( $data['Version'], self::REST_API_MIN_VERSION, '<' ) ) { 53 | $link = wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' . $file ), 'upgrade-plugin_' . $file ); 54 | $this->error( sprintf( 55 | /* translators: 1: This plugin 2: REST API version */ 56 | __( '%1$s requires WP REST API version %2$s.', 'wp-front-end-editor' ), 57 | '' . __( 'Front-end Editor', 'wp-front-end-editor' ) . '', 58 | self::REST_API_MIN_VERSION 59 | ) . ' ' . __( 'Update', 'wp-front-end-editor' ) . '' ); 60 | } else if ( ! is_plugin_active( $file ) ) { 61 | $link = wp_nonce_url( self_admin_url( 'plugins.php?action=activate&plugin=' . $file ), 'activate-plugin_' . $file ); 62 | $this->error( sprintf( 63 | /* translators: %s: This plugin */ 64 | __( '%s requires WP REST API to be active.', 'wp-front-end-editor' ), 65 | '' . __( 'Front-end Editor', 'wp-front-end-editor' ) . '' 66 | ) . ' ' . __( 'Activate', 'wp-front-end-editor' ) . '' ); 67 | } 68 | } else { 69 | $link = wp_nonce_url( self_admin_url( 'update.php?action=install-plugin&plugin=' . self::REST_API_PLUGIN_SLUG ), 'install-plugin_' . self::REST_API_PLUGIN_SLUG ); 70 | 71 | $this->error( sprintf( 72 | /* translators: %s: This plugin */ 73 | __( '%s requires WP REST API.', 'wp-front-end-editor' ), 74 | '' . __( 'Front-end Editor', 'wp-front-end-editor' ) . '' 75 | ) . ' ' . sprintf( 76 | /* translators: %s: Plugin name */ 77 | __( 'Install %s now', 'wp-front-end-editor' ), 78 | 'WP REST API' 79 | ) . '' ); 80 | } 81 | } 82 | 83 | function init() { 84 | // Load admin translations. 85 | load_textdomain( 'default', WP_LANG_DIR . '/admin-' . get_locale() . '.mo' ); 86 | // Load plugin translations. 87 | load_plugin_textdomain( 'wp-front-end-editor', FALSE, basename( dirname( __FILE__ ) ) . '/languages' ); 88 | 89 | // Fall back to core translation. 90 | add_filter( 'gettext', array( $this, 'gettext' ), 10, 3 ); 91 | add_filter( 'gettext_with_context', array( $this, 'gettext_with_context' ), 10, 4 ); 92 | 93 | $this->check_wordpress_version(); 94 | $this->check_rest_api_plugin(); 95 | 96 | if ( $this->errors ) { 97 | return add_action( 'admin_notices', array( $this, 'admin_notices' ) ); 98 | } 99 | 100 | add_post_type_support( 'post', 'front-end-editor' ); 101 | add_post_type_support( 'page', 'front-end-editor' ); 102 | 103 | add_action( 'wp_ajax_fee_nonce', array( $this, 'ajax_nonce' ) ); 104 | add_action( 'wp_ajax_fee_thumbnail', array( $this, 'ajax_thumbnail' ) ); 105 | 106 | add_action( 'wp_enqueue_scripts', array( $this, 'register_scripts' ) ); 107 | add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) ); 108 | add_action( 'wp', array( $this, 'wp' ) ); 109 | 110 | add_action( 'rest_api_init', array( $this, 'rest_api_init' ) ); 111 | add_filter( 'rest_pre_dispatch', array( $this, 'rest_reset_content_type' ), 10, 3 ); 112 | add_filter( 'rest_dispatch_request', array( $this, 'rest_revision' ), 10, 3 ); 113 | 114 | add_filter( 'get_edit_post_link', array( $this, 'get_edit_post_link' ), 10, 3 ); 115 | } 116 | 117 | function ajax_nonce() { 118 | echo wp_create_nonce( 'wp_rest' ); 119 | die; 120 | } 121 | 122 | function ajax_thumbnail() { 123 | check_ajax_referer( 'update-post_' . $_POST['post_ID'] ); 124 | 125 | if ( ! current_user_can( 'edit_post', $_POST['post_ID'] ) ) { 126 | wp_send_json_error( array( 'message' => __( 'You are not allowed to edit this item.' ) ) ); 127 | } 128 | 129 | if ( $_POST['thumbnail_ID'] === '-1' ) { 130 | if ( delete_post_thumbnail( $_POST['post_ID'] ) ) { 131 | wp_send_json_success( '' ); 132 | } 133 | } else if ( set_post_thumbnail( $_POST['post_ID'], $_POST['thumbnail_ID'] ) ) { 134 | wp_send_json_success( get_the_post_thumbnail( $_POST['post_ID'], $_POST['size'] ) ); 135 | } 136 | 137 | die; 138 | } 139 | 140 | function api_request( $method = 'GET', $path = '', $query = array() ) { 141 | $request = new WP_REST_Request( $method, '/wp/v2' . $path ); 142 | $request->set_query_params( $query ); 143 | $data = rest_do_request( $request )->get_data(); 144 | 145 | // We need HTML. 146 | if ( isset( $data['content'] ) && isset( $data['content']['raw'] ) ) { 147 | $data['content']['raw'] = wpautop( $data['content']['raw'] ); 148 | } 149 | 150 | return $data; 151 | } 152 | 153 | function register_scripts() { 154 | global $post; 155 | 156 | wp_register_script( 'fee.filePicker', plugins_url( 'js/filePicker.js', __FILE__ ), array( 'jquery' ), self::VERSION, true ); 157 | wp_register_script( 'fee.blobToBase64', plugins_url( 'js/blobToBase64.js', __FILE__ ), array( 'jquery' ), self::VERSION, true ); 158 | wp_register_script( 'fee.insertBlob', plugins_url( 'js/insertBlob.js', __FILE__ ), array( 'fee.blobToBase64' ), self::VERSION, true ); 159 | wp_register_script( 'fee.SelectControl', plugins_url( 'js/SelectControl.js', __FILE__ ), array( 'fee-tinymce' ), self::VERSION, true ); 160 | 161 | wp_register_script( 'fee-tinymce', plugins_url( 'vendor/tinymce.js', __FILE__ ), array(), self::VERSION, true ); 162 | wp_register_script( 'fee-tinymce-lists', plugins_url( 'vendor/lists.js', __FILE__ ), array( 'fee-tinymce' ), self::VERSION, true ); 163 | wp_register_script( 'fee-tinymce-paste', plugins_url( 'vendor/paste.js', __FILE__ ), array( 'fee-tinymce' ), self::VERSION, true ); 164 | wp_register_script( 'fee-tinymce-wordpress', plugins_url( 'vendor/wordpress.js', __FILE__ ), array( 'fee-tinymce' ), self::VERSION, true ); 165 | wp_register_script( 'fee-tinymce-wplink', plugins_url( 'vendor/wplink.js', __FILE__ ), array( 'fee-tinymce' ), self::VERSION, true ); 166 | wp_register_script( 'fee-tinymce-wptextpattern', plugins_url( 'vendor/wptextpattern.js', __FILE__ ), array( 'fee-tinymce' ), self::VERSION, true ); 167 | wp_register_script( 'fee-tinymce-wpview', plugins_url( 'vendor/wpview.js', __FILE__ ), array( 'fee-tinymce', 'fee-mce-view' ), self::VERSION, true ); 168 | wp_register_script( 'fee-mce-view', plugins_url( 'vendor/mce-view.js', __FILE__ ), array( 'shortcode', 'jquery', 'media-views', 'media-audiovideo' ), self::VERSION, true ); 169 | wp_register_script( 'fee-tinymce-image', plugins_url( 'js/tinymce.image.js', __FILE__ ), array( 'fee-tinymce' ), self::VERSION, true ); 170 | wp_register_script( 'fee-tinymce-imagetools', plugins_url( 'vendor/imagetools.js', __FILE__ ), array( 'fee-tinymce' ), self::VERSION, true ); 171 | wp_register_script( 'fee-tinymce-theme', plugins_url( 'js/tinymce.theme.js', __FILE__ ), array( 'fee-tinymce', 'underscore', 'fee.filePicker', 'fee.insertBlob', 'fee.SelectControl' ), self::VERSION, true ); 172 | 173 | $tinymce = array( 174 | 'plugins' => implode( ' ', array_unique( apply_filters( 'fee_tinymce_plugins', array( 175 | 'wordpress', 176 | 'feeImage', 177 | 'wptextpattern', 178 | 'wplink', 179 | 'wpview', 180 | 'paste', 181 | 'lists', 182 | 'imagetools' 183 | ) ) ) ), 184 | 'toolbars' => array( 185 | 'caret' => apply_filters( 'fee_toolbar_caret', array( 186 | 'media', 187 | 'select' 188 | ) ), 189 | 'inline' => apply_filters( 'fee_toolbar_inline', array( 190 | 'bold', 191 | 'italic', 192 | 'strikethrough', 193 | 'link', 194 | 'select' 195 | ) ), 196 | 'block' => apply_filters( 'fee_toolbar_block', array( 197 | 'heading', 198 | 'bullist', 199 | 'numlist', 200 | 'blockquote' 201 | ) ) 202 | ), 203 | 'theme' => 'fee', 204 | 'inline' => true, 205 | 'relative_urls' => false, 206 | 'convert_urls' => false, 207 | 'browser_spellcheck' => true, 208 | 'wpeditimage_html5_captions' => current_theme_supports( 'html5', 'caption' ), 209 | 'end_container_on_empty_block' => true, 210 | 'strings' => array( 211 | 'publish' => __( 'Publish', 'wp-front-end-editor' ), 212 | 'saved' => __( 'Saved', 'wp-front-end-editor' ), 213 | 'saving' => __( 'Saving...', 'wp-front-end-editor' ), 214 | 'error' => __( 'Error', 'wp-front-end-editor' ), 215 | 'paragraph' => __( 'Paragraph', 'wp-front-end-editor' ), 216 | 'heading2' => __( 'Heading 2', 'wp-front-end-editor' ), 217 | 'heading3' => __( 'Heading 3', 'wp-front-end-editor' ), 218 | 'heading4' => __( 'Heading 4', 'wp-front-end-editor' ), 219 | 'heading5' => __( 'Heading 5', 'wp-front-end-editor' ), 220 | 'heading6' => __( 'Heading 6', 'wp-front-end-editor' ), 221 | 'preformatted' => _x( 'Preformatted', 'HTML tag', 'wp-front-end-editor' ) 222 | ) 223 | ); 224 | 225 | if ( $post ) { 226 | wp_register_script( 'fee', plugins_url( '/js/fee.js', __FILE__ ), array( 227 | 'fee-tinymce', 228 | 'fee-tinymce-lists', 229 | 'fee-tinymce-paste', 230 | 'fee-tinymce-wordpress', 231 | 'fee-tinymce-wplink', 232 | 'fee-tinymce-wptextpattern', 233 | 'fee-tinymce-wpview', 234 | 'fee-tinymce-image', 235 | 'fee-tinymce-imagetools', 236 | 'fee-tinymce-theme', 237 | 'media-views', 238 | 'jquery', 239 | 'underscore', 240 | 'backbone' 241 | ), self::VERSION, true ); 242 | 243 | $rest_post = $this->api_request( 'GET', '/' . $this->get_rest_endpoint() . '/' . $post->ID, array( 'context' => 'edit' ) ); 244 | $rest_autosave = $this->api_request( 'GET', '/' . $this->get_rest_endpoint() . '/' . $post->ID . '/autosave' ); 245 | 246 | wp_localize_script( 'fee', 'feeData', array( 247 | 'tinymce' => apply_filters( 'fee_tinymce_config', $tinymce ), 248 | 'post' => $rest_post, 249 | 'autosave' => isset($rest_autosave['modified']) && strtotime($rest_autosave['modified']) > strtotime($rest_post['modified']) ? $rest_autosave : null, 250 | 'titlePlaceholder' => apply_filters( 'enter_title_here', __( 'Enter title here', 'wp-front-end-editor' ), $post ), 251 | 'editURL' => get_edit_post_link(), 252 | 'ajaxURL' => admin_url( 'admin-ajax.php' ), 253 | 'api' => array( 254 | 'endpoint' => $this->get_rest_endpoint(), 255 | 'nonce' => wp_create_nonce( 'wp_rest' ), 256 | 'root' => esc_url_raw( get_rest_url() ) 257 | ) 258 | ) ); 259 | } 260 | 261 | wp_register_script( 'fee-adminbar', plugins_url( '/js/fee-adminbar.js', __FILE__ ), array( 'wp-util' ), self::VERSION, true ); 262 | wp_localize_script( 'fee-adminbar', 'fee_adminbar', array( 263 | 'postTypes' => $this->get_post_types(), 264 | 'adminURL' => admin_url( '/' ), 265 | 'editURL' => is_singular() ? get_edit_post_link() : false, 266 | 'homeURL' => home_url( '/' ), 267 | 'api' => array( 268 | 'nonce' => wp_create_nonce( 'wp_rest' ), 269 | 'root' => esc_url_raw( get_rest_url() ) 270 | ) 271 | ) ); 272 | 273 | wp_register_style( 'fee-tinymce-core' , plugins_url( 'css/tinymce.core.css', __FILE__ ), array(), self::VERSION, 'screen' ); 274 | wp_register_style( 'fee-tinymce-view' , plugins_url( 'css/tinymce.view.css', __FILE__ ), array(), self::VERSION, 'screen' ); 275 | wp_register_style( 'fee' , plugins_url( 'css/fee.css', __FILE__ ), array( 'fee-tinymce-core', 'fee-tinymce-view', 'dashicons' ), self::VERSION, 'screen' ); 276 | 277 | wp_add_inline_style( 'fee', '@media print{.fee-no-print{display:none}}' ); 278 | } 279 | 280 | function enqueue_scripts() { 281 | global $post; 282 | 283 | if ( $this->has_fee() ) { 284 | wp_enqueue_style( 'fee' ); 285 | wp_enqueue_script( 'fee' ); 286 | wp_enqueue_media( array( 'post' => $post ) ); 287 | } 288 | 289 | if ( current_user_can( 'edit_posts' ) ) { 290 | wp_enqueue_script( 'fee-adminbar' ); 291 | } 292 | } 293 | 294 | function wp() { 295 | global $post; 296 | 297 | if ( ! $this->has_fee() ) { 298 | return; 299 | } 300 | 301 | add_filter( 'the_title', array( $this, 'the_title' ), 10, 2 ); 302 | add_filter( 'the_content', array( $this, 'the_content' ), 20 ); 303 | add_filter( 'wp_link_pages', array( $this, 'wp_link_pages' ) ); 304 | add_filter( 'post_thumbnail_html', array( $this, 'post_thumbnail_html' ), 10, 5 ); 305 | add_filter( 'get_post_metadata', array( $this, 'get_post_metadata' ), 10, 4 ); 306 | add_filter( 'private_title_format', array( $this, 'private_title_format' ), 10, 2 ); 307 | add_filter( 'protected_title_format', array( $this, 'private_title_format' ), 10, 2 ); 308 | } 309 | 310 | function the_title( $title, $id ) { 311 | if ( 312 | is_main_query() && 313 | $id === get_queried_object_id() && 314 | $this->did_action( 'wp_head' ) 315 | ) { 316 | $title .= '
'; 317 | } 318 | 319 | return $title; 320 | } 321 | 322 | function the_content( $content ) { 323 | if ( 324 | is_main_query() && 325 | in_the_loop() && 326 | $this->did_action( 'wp_head' ) 327 | ) { 328 | $content = '
' . $content . '
'; 329 | } 330 | 331 | return $content; 332 | } 333 | 334 | function wp_link_pages( $html ) { 335 | return ''; 336 | } 337 | 338 | function post_thumbnail_html( $html, $post_id, $post_thumbnail_id, $size, $attr ) { 339 | if ( 340 | is_main_query() && 341 | in_the_loop() && 342 | get_queried_object_id() === $post_id && 343 | $this->did_action( 'wp_head' ) 344 | ) { 345 | $html = '
' . $html . '
'; 346 | } 347 | 348 | return $html; 349 | } 350 | 351 | // Not sure if this is a good idea, this could have unexpected consequences. But otherwise nothing shows up if the featured image is set in edit mode. 352 | function get_post_metadata( $n, $object_id, $meta_key, $single ) { 353 | static $lock; 354 | 355 | if ( 356 | is_main_query() && 357 | in_the_loop() && 358 | get_queried_object_id() === $object_id && 359 | $this->did_action( 'wp_head' ) && 360 | $meta_key === '_thumbnail_id' && 361 | $single && 362 | empty( $lock ) 363 | ) { 364 | $lock = true; 365 | $thumbnail_id = get_post_thumbnail_id( $object_id ); 366 | $lock = false; 367 | 368 | if ( $thumbnail_id ) { 369 | return $thumbnail_id; 370 | } 371 | 372 | return true; 373 | } 374 | } 375 | 376 | function private_title_format( $title, $post ) { 377 | if ( $post->ID === get_queried_object_id() ) { 378 | $title = '%s'; 379 | } 380 | 381 | return $title; 382 | } 383 | 384 | function supports_fee( $id = null ) { 385 | $post = get_post( $id ); 386 | 387 | if ( ! $post ) return false; 388 | 389 | $post_type_object = get_post_type_object( $post->post_type ); 390 | 391 | if ( 392 | $post->ID !== (int) get_option( 'page_for_posts' ) && 393 | $post_type_object->show_in_rest && 394 | post_type_supports( $post->post_type, 'front-end-editor' ) && 395 | current_user_can( 'edit_post', $post->ID ) 396 | ) { 397 | return apply_filters( 'supports_fee', true, $post ); 398 | } 399 | 400 | return false; 401 | } 402 | 403 | function has_fee() { 404 | return $this->supports_fee() && is_singular(); 405 | } 406 | 407 | function get_post_types() { 408 | $post_types = get_post_types( array( 'show_in_rest' => true ), 'objects' ); 409 | 410 | foreach ( $post_types as $key => $value ) { 411 | $post_types[ $key ] = empty( $post_types[ $key ] ) ? $key : $post_types[ $key ]->rest_base; 412 | } 413 | 414 | return array_intersect_key( $post_types, array_flip( get_post_types_by_support( 'front-end-editor' ) ) ); 415 | } 416 | 417 | function did_action( $tag ) { 418 | return did_action( $tag ) - (int) doing_filter( $tag ); 419 | } 420 | 421 | function rest_api_init() { 422 | if ( ! class_exists( 'WP_REST_Post_Autosave_Controller' ) ) { 423 | require_once 'class-wp-rest-post-autosave-controller.php'; 424 | 425 | foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) { 426 | $autosave_controller = new WP_REST_Post_Autosave_Controller( $post_type->name ); 427 | $autosave_controller->register_routes(); 428 | } 429 | } 430 | } 431 | 432 | function rest_reset_content_type( $result, $server, $request ) { 433 | $content_type = $request->get_content_type(); 434 | 435 | if ( ! empty( $content_type ) && 'text/plain' === $content_type['value'] ) { 436 | $request->set_header( 'content-type', 'application/json' ); 437 | } 438 | } 439 | 440 | function rest_revision( $result, $request ) { 441 | if ( empty( $request['id'] ) || empty( $request['_fee_session'] ) ) { 442 | return; 443 | } 444 | 445 | $session = (int) get_post_meta( $request['id'], '_fee_session', true ); 446 | 447 | if ( $session !== $request['_fee_session'] ) { 448 | wp_save_post_revision( $request['id'] ); 449 | } 450 | 451 | remove_action( 'post_updated', 'wp_save_post_revision', 10 ); 452 | 453 | update_post_meta( $request['id'], '_fee_session', $request['_fee_session'] ); 454 | } 455 | 456 | function get_rest_endpoint( $id = null ) { 457 | $post = get_post( $id ); 458 | 459 | if ( ! $post ) return; 460 | 461 | $object = get_post_type_object( $post->post_type ); 462 | 463 | return empty( $object->rest_base ) ? $object->name : $object->rest_base; 464 | } 465 | 466 | function gettext( $translation, $text, $domain ) { 467 | if ($domain === 'wp-front-end-editor' && $translation === $text) { 468 | $translation = __( $text ); 469 | } 470 | 471 | return $translation; 472 | } 473 | 474 | function gettext_with_context( $translation, $text, $context, $domain ) { 475 | if ($domain === 'wp-front-end-editor' && $translation === $text) { 476 | $translation = _x( $text, $context ); 477 | } 478 | 479 | return $translation; 480 | } 481 | 482 | function get_edit_post_link( $link, $id, $context ) { 483 | $post = get_post( $id ); 484 | 485 | if ( $post ) { 486 | $link = add_query_arg( 'post_type', $post->post_type, $link ); 487 | } 488 | 489 | return $link; 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /class-wp-rest-post-autosave-controller.php: -------------------------------------------------------------------------------- 1 | parent_post_type = $parent_post_type; 11 | $this->parent_controller = new WP_REST_Posts_Controller( $parent_post_type ); 12 | $this->namespace = 'wp/v2'; 13 | $this->rest_base = 'autosave'; 14 | $post_type_object = get_post_type_object( $parent_post_type ); 15 | $this->parent_base = ! empty( $post_type_object->rest_base ) ? $post_type_object->rest_base : $post_type_object->name; 16 | } 17 | 18 | /** 19 | * Register routes for the autosave. 20 | */ 21 | public function register_routes() { 22 | 23 | register_rest_route( $this->namespace, '/' . $this->parent_base . '/(?P[\d]+)/' . $this->rest_base, array( 24 | array( 25 | 'methods' => WP_REST_Server::READABLE, 26 | 'callback' => array( $this, 'get_item' ), 27 | 'permission_callback' => array( $this, 'get_item_permissions_check' ), 28 | 'args' => array( 29 | 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 30 | ), 31 | ), 32 | array( 33 | 'methods' => WP_REST_Server::CREATABLE, 34 | 'callback' => array( $this, 'update_item' ), 35 | 'permission_callback' => array( $this, 'update_item_permissions_check' ), 36 | ), 37 | 38 | 'schema' => array( $this, 'get_public_item_schema' ), 39 | )); 40 | 41 | } 42 | 43 | /** 44 | * Check if a given request has access to get the autosave. 45 | * 46 | * @param WP_REST_Request $request Full data about the request. 47 | * @return WP_Error|boolean 48 | */ 49 | public function get_item_permissions_check( $request ) { 50 | 51 | $parent = get_post( $request['id'] ); 52 | if ( ! $parent ) { 53 | return true; 54 | } 55 | $parent_post_type_obj = get_post_type_object( $parent->post_type ); 56 | if ( ! current_user_can( $parent_post_type_obj->cap->edit_post, $parent->ID ) ) { 57 | return new WP_Error( 'rest_cannot_read', __( 'Sorry, you cannot view autosaves of this post.' ), array( 'status' => rest_authorization_required_code() ) ); 58 | } 59 | 60 | return true; 61 | } 62 | 63 | /** 64 | * Get the autosave for the post. 65 | * 66 | * @param WP_REST_Request $request Full data about the request. 67 | * @return WP_Error|array 68 | */ 69 | public function get_item( $request ) { 70 | 71 | $parent = get_post( $request['id'] ); 72 | if ( ! $parent || $this->parent_post_type !== $parent->post_type ) { 73 | return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post id.' ), array( 'status' => 404 ) ); 74 | } 75 | 76 | // implement getting autosave 77 | $autosave = wp_get_post_autosave( $parent->ID, get_current_user_id() ); 78 | 79 | if ( ! $autosave ) { 80 | return new WP_Error( 'rest_not_found', __( 'No autosave exists for this post for the current user.' ), array( 'status' => 404 ) ); 81 | } 82 | $response = $this->prepare_item_for_response( $autosave, $request ); 83 | return rest_ensure_response( $response ); 84 | } 85 | 86 | public function update_item_permissions_check( $request ) { 87 | $response = $this->get_item_permissions_check( $request ); 88 | if ( ! $response || is_wp_error( $response ) ) { 89 | return $response; 90 | } 91 | 92 | return true; 93 | } 94 | 95 | /** 96 | * Update an autosave for a post. 97 | * 98 | * @param WP_REST_Request $request Full details about the request. 99 | * @return WP_Error|WP_REST_Response 100 | */ 101 | public function update_item( $request ) { 102 | $parent = get_post( $request['id'] ); 103 | $autosave = wp_get_post_autosave( $parent->ID, get_current_user_id() ); 104 | 105 | $post_data = (array) $this->prepare_item_for_database( $request ); 106 | 107 | if ( ! $autosave ) { 108 | $autosave_id = _wp_put_post_revision( $post_data, true ); 109 | } else { 110 | $post_data['ID'] = $autosave->ID; 111 | /** 112 | * Fires before an autosave is stored. 113 | * 114 | * @since 4.1.0 115 | * 116 | * @param array $new_autosave Post array - the autosave that is about to be saved. 117 | */ 118 | do_action( 'wp_creating_autosave', $post_data ); 119 | wp_update_post( $post_data ); 120 | $autosave_id = $autosave->ID; 121 | } 122 | 123 | return $this->prepare_item_for_response( get_post( $autosave_id ), $request ); 124 | } 125 | 126 | /** 127 | * Prepare the autosave for the REST response 128 | * 129 | * @param WP_Post $post Post autosave object. 130 | * @param WP_REST_Request $request Request object. 131 | * @return WP_REST_Response $response 132 | */ 133 | public function prepare_item_for_response( $post, $request ) { 134 | 135 | // Base fields for every post 136 | $data = array( 137 | 'date' => $this->prepare_date_response( $post->post_date_gmt, $post->post_date ), 138 | 'date_gmt' => $this->prepare_date_response( $post->post_date_gmt ), 139 | 'modified' => $this->prepare_date_response( $post->post_modified_gmt, $post->post_modified ), 140 | 'modified_gmt' => $this->prepare_date_response( $post->post_modified_gmt ), 141 | ); 142 | 143 | $schema = $this->get_item_schema(); 144 | 145 | if ( ! empty( $schema['properties']['title'] ) ) { 146 | $data['title'] = $post->post_title; 147 | } 148 | 149 | if ( ! empty( $schema['properties']['content'] ) ) { 150 | $data['content'] = $post->post_content; 151 | } 152 | 153 | if ( ! empty( $schema['properties']['excerpt'] ) ) { 154 | $data['excerpt'] = $post->post_excerpt; 155 | } 156 | 157 | $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; 158 | $data = $this->add_additional_fields_to_object( $data, $request ); 159 | $data = $this->filter_response_by_context( $data, $context ); 160 | $response = rest_ensure_response( $data ); 161 | 162 | /** 163 | * Filter an autosave returned from the API. 164 | * 165 | * Allows modification of the autosave right before it is returned. 166 | * 167 | * @param WP_REST_Response $response The response object. 168 | * @param WP_Post $post The original autosave object. 169 | * @param WP_REST_Request $request Request used to generate the response. 170 | */ 171 | return apply_filters( 'rest_prepare_autosave', $response, $post, $request ); 172 | } 173 | 174 | /** 175 | * Prepare a single post for create or update. 176 | * 177 | * @param WP_REST_Request $request Request object. 178 | * @return WP_Error|object $prepared_post Post object. 179 | */ 180 | protected function prepare_item_for_database( $request ) { 181 | $prepared_post = new stdClass; 182 | 183 | if ( isset( $request['id'] ) ) { 184 | $prepared_post->ID = absint( $request['id'] ); 185 | } 186 | 187 | $schema = $this->get_item_schema(); 188 | 189 | // Post title. 190 | if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) { 191 | if ( is_string( $request['title'] ) ) { 192 | $prepared_post->post_title = wp_filter_post_kses( $request['title'] ); 193 | } elseif ( ! empty( $request['title']['raw'] ) ) { 194 | $prepared_post->post_title = wp_filter_post_kses( $request['title']['raw'] ); 195 | } 196 | } 197 | 198 | // Post content. 199 | if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) { 200 | if ( is_string( $request['content'] ) ) { 201 | $prepared_post->post_content = wp_filter_post_kses( $request['content'] ); 202 | } elseif ( isset( $request['content']['raw'] ) ) { 203 | $prepared_post->post_content = wp_filter_post_kses( $request['content']['raw'] ); 204 | } 205 | } 206 | 207 | // Post excerpt. 208 | if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) { 209 | if ( is_string( $request['excerpt'] ) ) { 210 | $prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt'] ); 211 | } elseif ( isset( $request['excerpt']['raw'] ) ) { 212 | $prepared_post->post_excerpt = wp_filter_post_kses( $request['excerpt']['raw'] ); 213 | } 214 | } 215 | 216 | // Post slug. 217 | if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) { 218 | $prepared_post->post_name = $request['slug']; 219 | } 220 | 221 | // Author 222 | if ( ! empty( $schema['properties']['author'] ) && ! empty( $request['author'] ) ) { 223 | $post_author = (int) $request['author']; 224 | if ( get_current_user_id() !== $post_author ) { 225 | $user_obj = get_userdata( $post_author ); 226 | if ( ! $user_obj ) { 227 | return new WP_Error( 'rest_invalid_author', __( 'Invalid author id.' ), array( 'status' => 400 ) ); 228 | } 229 | } 230 | $prepared_post->post_author = $post_author; 231 | } 232 | 233 | // Post password. 234 | if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) && '' !== $request['password'] ) { 235 | $prepared_post->post_password = $request['password']; 236 | 237 | if ( ! empty( $schema['properties']['sticky'] ) && ! empty( $request['sticky'] ) ) { 238 | return new WP_Error( 'rest_invalid_field', __( 'A post can not be sticky and have a password.' ), array( 'status' => 400 ) ); 239 | } 240 | 241 | if ( ! empty( $prepared_post->ID ) && is_sticky( $prepared_post->ID ) ) { 242 | return new WP_Error( 'rest_invalid_field', __( 'A sticky post can not be password protected.' ), array( 'status' => 400 ) ); 243 | } 244 | } 245 | 246 | // Menu order. 247 | if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) { 248 | $prepared_post->menu_order = (int) $request['menu_order']; 249 | } 250 | 251 | // Comment status. 252 | if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) { 253 | $prepared_post->comment_status = $request['comment_status']; 254 | } 255 | 256 | // Ping status. 257 | if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) { 258 | $prepared_post->ping_status = $request['ping_status']; 259 | } 260 | /** 261 | * Filter the query_vars used in `get_items` for the constructed query. 262 | * 263 | * The dynamic portion of the hook name, $this->post_type, refers to post_type of the post being 264 | * prepared for insertion. 265 | * 266 | * @param object $prepared_post An object representing a single post prepared 267 | * for inserting or updating the database. 268 | * @param WP_REST_Request $request Request object. 269 | */ 270 | return apply_filters( "rest_pre_insert_{$this->parent_post_type}_autosave", $prepared_post, $request ); 271 | 272 | } 273 | 274 | /** 275 | * Check the post_date_gmt or modified_gmt and prepare any post or 276 | * modified date for single post output. 277 | * 278 | * @param string $date_gmt 279 | * @param string|null $date 280 | * @return string|null ISO8601/RFC3339 formatted datetime. 281 | */ 282 | protected function prepare_date_response( $date_gmt, $date = null ) { 283 | if ( '0000-00-00 00:00:00' === $date_gmt ) { 284 | return null; 285 | } 286 | 287 | if ( isset( $date ) ) { 288 | return mysql_to_rfc3339( $date ); 289 | } 290 | 291 | return mysql_to_rfc3339( $date_gmt ); 292 | } 293 | 294 | /** 295 | * Get the autosave's schema, conforming to JSON Schema 296 | * 297 | * @return array 298 | */ 299 | public function get_item_schema() { 300 | $schema = array( 301 | '$schema' => 'http://json-schema.org/draft-04/schema#', 302 | 'title' => "{$this->parent_post_type}-autosave", 303 | 'type' => 'object', 304 | /* 305 | * Base properties for every Autosave 306 | */ 307 | 'properties' => array(), 308 | ); 309 | 310 | $fields = array( 311 | 'post_title' => __( 'Title' ), 312 | 'post_content' => __( 'Content' ), 313 | 'post_excerpt' => __( 'Excerpt' ), 314 | ); 315 | 316 | /** 317 | * Filter the list of fields saved in post revisions. 318 | * 319 | * Included by default: 'post_title', 'post_content' and 'post_excerpt'. 320 | * 321 | * Disallowed fields: 'ID', 'post_name', 'post_parent', 'post_date', 322 | * 'post_date_gmt', 'post_status', 'post_type', 'comment_count', 323 | * and 'post_author'. 324 | * 325 | * @since 2.6.0 326 | * 327 | * @param array $fields List of fields to revision. Contains 'post_title', 328 | * 'post_content', and 'post_excerpt' by default. 329 | */ 330 | $fields = apply_filters( '_wp_post_revision_fields', $fields ); 331 | 332 | foreach ( $fields as $property => $name ) { 333 | 334 | switch ( $property ) { 335 | 336 | case 'post_title': 337 | $schema['properties']['title'] = array( 338 | 'description' => __( 'Title for the object, as it exists in the database.' ), 339 | 'type' => 'string', 340 | 'context' => array( 'view' ), 341 | ); 342 | break; 343 | 344 | case 'post_content': 345 | $schema['properties']['content'] = array( 346 | 'description' => __( 'Content for the object, as it exists in the database.' ), 347 | 'type' => 'string', 348 | 'context' => array( 'view' ), 349 | ); 350 | break; 351 | 352 | case 'post_excerpt': 353 | $schema['properties']['excerpt'] = array( 354 | 'description' => __( 'Excerpt for the object, as it exists in the database.' ), 355 | 'type' => 'string', 356 | 'context' => array( 'view' ), 357 | ); 358 | break; 359 | 360 | } 361 | } 362 | 363 | return $this->add_additional_fields_schema( $schema ); 364 | } 365 | 366 | /** 367 | * Get the query params for collections 368 | * 369 | * @return array 370 | */ 371 | public function get_collection_params() { 372 | return array( 373 | 'context' => $this->get_context_param( array( 'default' => 'view' ) ), 374 | ); 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /css/fee.css: -------------------------------------------------------------------------------- 1 | br.fee-title, 2 | .fee-on .fee-link-pages { 3 | display: none; 4 | } 5 | 6 | .fee-on #wp-admin-bar-edit a { 7 | background: #32373c; 8 | color: #00b9eb; 9 | } 10 | 11 | .fee-on #wp-admin-bar-edit a:before { 12 | color: #00b9eb; 13 | } 14 | 15 | .fee-on .fee-thumbnail, 16 | .fee-on .mce-content-body, 17 | .fee-on .mce-content-body *[data-mce-selected] { 18 | outline: 1px dashed #000; 19 | } 20 | 21 | .fee-on.fee-edit-focus .mce-content-body { 22 | outline: none; 23 | } 24 | 25 | .mce-content-body p { 26 | min-height: 1em; 27 | } 28 | 29 | .mce-content-body[data-placeholder][data-empty]:after { 30 | opacity: 0.5; 31 | content: attr( data-placeholder ); 32 | display: block; 33 | position: absolute; 34 | top: 0; 35 | } 36 | 37 | .mce-content-body[data-placeholder][data-empty]:focus:before { 38 | content: ''; 39 | } 40 | 41 | div.mce-inline-toolbar-grp { 42 | background-color: #f5f5f5; 43 | border: 1px solid #a0a5aa; 44 | -webkit-border-radius: 2px; 45 | border-radius: 2px; 46 | -webkit-box-shadow: 0 1px 3px rgba( 0, 0, 0, 0.15 ); 47 | box-shadow: 0 1px 3px rgba( 0, 0, 0, 0.15 ); 48 | -webkit-box-sizing: border-box; 49 | -moz-box-sizing: border-box; 50 | box-sizing: border-box; 51 | margin-bottom: 8px; 52 | position: absolute; 53 | -moz-user-select: none; 54 | -webkit-user-select: none; 55 | -ms-user-select: none; 56 | user-select: none; 57 | max-width: 98%; 58 | z-index: 99998; /* Under adminbar */ 59 | } 60 | 61 | div.mce-inline-toolbar-grp > div.mce-stack-layout { 62 | padding: 1px; 63 | } 64 | 65 | div.mce-inline-toolbar-grp.mce-arrow-up { 66 | margin-bottom: 0; 67 | margin-top: 8px; 68 | } 69 | 70 | div.mce-inline-toolbar-grp:before, 71 | div.mce-inline-toolbar-grp:after { 72 | position: absolute; 73 | left: 50%; 74 | display: block; 75 | width: 0; 76 | height: 0; 77 | border-style: solid; 78 | border-color: transparent; 79 | content: ''; 80 | } 81 | 82 | div.mce-inline-toolbar-grp.mce-arrow-up:before { 83 | top: -9px; 84 | border-bottom-color: #a0a5aa; 85 | border-width: 0 9px 9px; 86 | margin-left: -9px; 87 | } 88 | 89 | div.mce-inline-toolbar-grp.mce-arrow-down:before { 90 | bottom: -9px; 91 | border-top-color: #a0a5aa; 92 | border-width: 9px 9px 0; 93 | margin-left: -9px; 94 | } 95 | 96 | div.mce-inline-toolbar-grp.mce-arrow-up:after { 97 | top: -8px; 98 | border-bottom-color: #f5f5f5; 99 | border-width: 0 8px 8px; 100 | margin-left: -8px; 101 | } 102 | 103 | div.mce-inline-toolbar-grp.mce-arrow-down:after { 104 | bottom: -8px; 105 | border-top-color: #f5f5f5; 106 | border-width: 8px 8px 0; 107 | margin-left: -8px; 108 | } 109 | 110 | div.mce-inline-toolbar-grp.mce-arrow-left:before, 111 | div.mce-inline-toolbar-grp.mce-arrow-left:after { 112 | margin: 0; 113 | } 114 | 115 | div.mce-inline-toolbar-grp.mce-arrow-left:before { 116 | left: 20px; 117 | } 118 | div.mce-inline-toolbar-grp.mce-arrow-left:after { 119 | left: 21px; 120 | } 121 | 122 | div.mce-inline-toolbar-grp.mce-arrow-right:before, 123 | div.mce-inline-toolbar-grp.mce-arrow-right:after { 124 | left: auto; 125 | margin: 0; 126 | } 127 | 128 | div.mce-inline-toolbar-grp.mce-arrow-right:before { 129 | right: 20px; 130 | } 131 | 132 | div.mce-inline-toolbar-grp.mce-arrow-right:after { 133 | right: 21px; 134 | } 135 | 136 | div.mce-inline-toolbar-grp.mce-arrow-full { 137 | right: 0; 138 | } 139 | 140 | div.mce-inline-toolbar-grp.mce-arrow-full > div { 141 | width: 100%; 142 | overflow-x: auto; 143 | } 144 | 145 | div.mce-inline-toolbar-grp.mce-arrow-left-side:before, 146 | div.mce-inline-toolbar-grp.mce-arrow-left-side:after { 147 | left: auto; 148 | top: 50%; 149 | } 150 | 151 | div.mce-inline-toolbar-grp.mce-arrow-left-side:before { 152 | left: -9px; 153 | border-right-color: #a0a5aa; 154 | border-width: 9px 9px 9px 0; 155 | margin-top: -9px; 156 | } 157 | 158 | div.mce-inline-toolbar-grp.mce-arrow-left-side:after { 159 | left: -8px; 160 | border-right-color: #f5f5f5; 161 | border-width: 8px 8px 8px 0; 162 | margin-top: -8px; 163 | } 164 | 165 | div.wp-link-preview { 166 | float: left; 167 | margin: 5px; 168 | max-width: 694px; 169 | overflow: hidden; 170 | text-overflow: ellipsis; 171 | } 172 | 173 | div.wp-link-preview a { 174 | color: #0073aa; 175 | text-decoration: underline; 176 | -webkit-transition-property: border, background, color; 177 | transition-property: border, background, color; 178 | -webkit-transition-duration: .05s; 179 | transition-duration: .05s; 180 | -webkit-transition-timing-function: ease-in-out; 181 | transition-timing-function: ease-in-out; 182 | cursor: pointer; 183 | } 184 | 185 | div.wp-link-input { 186 | float: left; 187 | margin: 2px; 188 | max-width: 694px; 189 | } 190 | 191 | div.wp-link-input input { 192 | width: 300px; 193 | padding: 3px; 194 | -webkit-box-sizing: border-box; 195 | -moz-box-sizing: border-box; 196 | box-sizing: border-box; 197 | border-width: 1px; 198 | border-style: solid; 199 | background-color: #fff; 200 | color: #333; 201 | border-color: #ddd; 202 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.07); 203 | } 204 | 205 | div.wp-link-input input:focus { 206 | -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.1); 207 | box-shadow: 0 1px 2px rgba(0,0,0,0.1); 208 | border-color: #999; 209 | } 210 | 211 | @media screen and ( max-width: 782px ) { 212 | div.wp-link-preview { 213 | margin: 8px 0 8px 5px; 214 | max-width: 70%; 215 | max-width: -webkit-calc(100% - 86px); 216 | max-width: calc(100% - 86px); 217 | } 218 | } 219 | 220 | .fee-main-toolbar { 221 | transition-duration: 0.6s; 222 | transition-property: transform; 223 | transform: translateY(0); 224 | } 225 | 226 | .fee-main-toolbar.fee-hide { 227 | transform: translateY(48px); 228 | } 229 | 230 | @media screen and ( max-width: 320px ) { 231 | div.mce-inline-toolbar-grp.fee-media-toolbar { 232 | position: fixed !important; 233 | top: 0 !important; 234 | right: 0 !important; 235 | bottom: 0 !important; 236 | left: 0 !important; 237 | z-index: 100000; 238 | max-width: none; 239 | margin: 0; 240 | border: 0; 241 | } 242 | } 243 | 244 | .fee-image-select { 245 | width: 312px; 246 | height: 312px; 247 | overflow-y: scroll; 248 | background: #fff; 249 | border-top: 1px solid #a0a5aa; 250 | margin: 3px -3px -3px; 251 | padding: 2px; 252 | z-index: 1; 253 | position: relative; 254 | } 255 | 256 | .fee-image-select:before, 257 | .fee-image-select:after { 258 | content: ''; 259 | display: table; 260 | } 261 | 262 | .fee-image-select:after { 263 | clear: both; 264 | } 265 | 266 | .fee-image-select .fee-image { 267 | float: left; 268 | width: 100px; 269 | padding: 2px; 270 | height: 100px; 271 | position: relative; 272 | } 273 | 274 | .fee-image-select .fee-image:before { 275 | display: none; 276 | } 277 | 278 | .fee-image-select .fee-image img { 279 | display: block; 280 | width: 100%; 281 | height: auto; 282 | z-index: -1; 283 | position: relative; 284 | } 285 | 286 | .fee-image-select .fee-image.fee-selected img { 287 | opacity: 0.8; 288 | } 289 | 290 | .fee-image-select .fee-image.fee-selected:before { 291 | display: block; 292 | position: absolute; 293 | bottom: 5px; 294 | right: 5px; 295 | background: #0085ba; 296 | border-radius: 11px; 297 | border: 1px solid #fff; 298 | color: #fff; 299 | text-shadow: 0 -1px 1px #006799, 1px 0 1px #006799, 0 1px 1px #006799, -1px 0 1px #006799; 300 | box-shadow: 0 -1px 1px #006799, 1px 0 1px #006799, 0 1px 1px #006799, -1px 0 1px #006799; 301 | } 302 | -------------------------------------------------------------------------------- /css/tinymce.core.css: -------------------------------------------------------------------------------- 1 | .mce-container, 2 | .mce-container *, 3 | .mce-container button, 4 | .mce-container button:focus, 5 | .mce-container button:hover, 6 | .mce-container button:active, 7 | .mce-container input { 8 | -webkit-appearance: none; 9 | background: transparent; 10 | border: 0; 11 | box-sizing: content-box; 12 | -moz-box-sizing: content-box; 13 | -webkit-box-sizing: content-box; 14 | color: inherit; 15 | cursor: inherit; 16 | direction: ltr; 17 | float: none; 18 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen-Sans", "Ubuntu", "Cantarell", "Helvetica Neue", sans-serif; 19 | font-size: 14px; 20 | -webkit-font-smoothing: subpixel-antialiased; 21 | -moz-osx-font-smoothing: auto; 22 | font-weight: normal; 23 | height: auto; 24 | letter-spacing: normal; 25 | line-height: normal; 26 | margin: 0; 27 | max-width: none; 28 | outline: 0; 29 | padding: 0; 30 | position: static; 31 | -webkit-tap-highlight-color: transparent; 32 | text-align: left; 33 | text-decoration: none; 34 | text-shadow: none; 35 | text-transform: none; 36 | transition: none; 37 | -webkit-transition: none; 38 | vertical-align: top; 39 | white-space: nowrap; 40 | width: auto; 41 | } 42 | 43 | .mce-tooltip { 44 | display: none; 45 | } 46 | 47 | .mce-btn button::-moz-focus-inner { 48 | border: 0; 49 | padding: 0; 50 | } 51 | 52 | .mce-inline-toolbar-grp .mce-btn { 53 | border: 1px solid transparent; 54 | position: relative; 55 | display: inline-block; 56 | background: none; 57 | border-radius: 2px; 58 | margin: 2px; 59 | color: #23282d; 60 | } 61 | 62 | .mce-inline-toolbar-grp .mce-btn:hover, 63 | .mce-inline-toolbar-grp .mce-btn:focus { 64 | background-color: #fafafa; 65 | border-color: #23282d; 66 | -webkit-box-shadow: inset 0 1px 0 #fff, 0 1px 0 rgba( 0, 0, 0, 0.08 ); 67 | box-shadow: inset 0 1px 0 #fff, 0 1px 0 rgba( 0, 0, 0, 0.08 ); 68 | } 69 | 70 | .mce-inline-toolbar-grp .mce-btn.mce-active, 71 | .mce-inline-toolbar-grp .mce-btn:active { 72 | background-color: #ebebeb; 73 | border-color: #555d66; 74 | -webkit-box-shadow: inset 0 2px 5px -3px rgba( 0, 0, 0, 0.3 ); 75 | box-shadow: inset 0 2px 5px -3px rgba( 0, 0, 0, 0.3 ); 76 | } 77 | 78 | .mce-inline-toolbar-grp .mce-btn button { 79 | padding: 2px 3px; 80 | display: block; 81 | } 82 | 83 | .mce-inline-toolbar-grp .mce-btn.mce-primary { 84 | background: #0085ba; 85 | border-color: #0073aa #006799 #006799; 86 | -webkit-box-shadow: 0 1px 0 #006799; 87 | box-shadow: 0 1px 0 #006799; 88 | text-decoration: none; 89 | } 90 | 91 | .mce-inline-toolbar-grp .mce-btn.mce-primary .mce-txt, 92 | .mce-inline-toolbar-grp .mce-btn.mce-primary .mce-ico { 93 | color: #fff; 94 | text-shadow: 0 -1px 1px #006799, 95 | 1px 0 1px #006799, 96 | 0 1px 1px #006799, 97 | -1px 0 1px #006799; 98 | } 99 | 100 | .mce-flow-layout-item { 101 | margin: 2px; 102 | } 103 | 104 | .mce-ico { 105 | font-family: 'dashicons'; 106 | font-style: normal; 107 | font-weight: normal; 108 | font-variant: normal; 109 | font-size: 20px; 110 | line-height: 20px; 111 | speak: none; 112 | vertical-align: top; 113 | -webkit-font-smoothing: antialiased; 114 | -moz-osx-font-smoothing: grayscale; 115 | display: inline-block; 116 | width: 20px; 117 | height: 20px; 118 | padding: 0; 119 | display: block; 120 | } 121 | 122 | .mce-i-link:before { 123 | content: '\f103'; 124 | } 125 | 126 | .mce-i-bold:before { 127 | content: '\f200'; 128 | } 129 | 130 | .mce-i-italic:before { 131 | content: '\f201'; 132 | } 133 | 134 | .mce-i-strikethrough:before { 135 | content: '\f224'; 136 | } 137 | 138 | .mce-i-blockquote:before { 139 | content: '\f205'; 140 | } 141 | 142 | .mce-i-hr:before { 143 | content: '\f460'; 144 | } 145 | 146 | .mce-i-bullist:before { 147 | content: '\f203'; 148 | } 149 | 150 | .mce-i-numlist:before { 151 | content: '\f204'; 152 | } 153 | 154 | .mce-i-dashicon.dashicons-editor-textcolor { 155 | background: #23282d; 156 | color: #f5f5f5; 157 | } 158 | 159 | .mce-i-heading .mce-txt { 160 | text-align: center; 161 | font-size: 16px; 162 | } 163 | 164 | .mce-btn select { 165 | width: 26px; 166 | padding: 0; 167 | border: 0; 168 | height: 24px; 169 | opacity: 0; 170 | -webkit-appearance: none; 171 | position: absolute; 172 | top: 0; 173 | } 174 | 175 | .mce-btn .mce-txt { 176 | min-width: 20px; 177 | line-height: 20px; 178 | display: block; 179 | height: 20px; 180 | } 181 | -------------------------------------------------------------------------------- /css/tinymce.view.css: -------------------------------------------------------------------------------- 1 | .wpview, 2 | .mce-content-body hr { 3 | position: relative; 4 | } 5 | 6 | .wpview { 7 | width: 100%; 8 | position: relative; 9 | /*clear: both;*/ 10 | } 11 | 12 | .mce-shim { 13 | position: absolute; 14 | top: 0; 15 | right: 0; 16 | bottom: 0; 17 | left: 0; 18 | } 19 | 20 | .wpview[data-mce-selected="2"] .mce-shim { 21 | display: none; 22 | } 23 | 24 | .wpview .loading-placeholder { 25 | border: 1px dashed #ccc; 26 | padding: 10px; 27 | } 28 | 29 | .wpview[data-mce-selected] .loading-placeholder { 30 | border-color: transparent; 31 | } 32 | 33 | /* A little "loading" animation, not showing in IE < 10 */ 34 | .wpview .wpview-loading { 35 | width: 60px; 36 | height: 5px; 37 | overflow: hidden; 38 | background-color: transparent; 39 | margin: 10px auto 0; 40 | } 41 | 42 | .wpview .wpview-loading ins { 43 | background-color: #333; 44 | margin: 0 0 0 -60px; 45 | width: 36px; 46 | height: 5px; 47 | display: block; 48 | -webkit-animation: wpview-loading 1.3s infinite 1s steps(36); 49 | animation: wpview-loading 1.3s infinite 1s steps(36); 50 | } 51 | 52 | @-webkit-keyframes wpview-loading { 53 | 0% { 54 | margin-left: -60px; 55 | } 56 | 100% { 57 | margin-left: 60px; 58 | } 59 | } 60 | 61 | @keyframes wpview-loading { 62 | 0% { 63 | margin-left: -60px; 64 | } 65 | 100% { 66 | margin-left: 60px; 67 | } 68 | } 69 | 70 | .wpview > iframe { 71 | max-width: 100%; 72 | background: transparent; 73 | } 74 | 75 | .wpview-error { 76 | border: 1px solid #ddd; 77 | padding: 1em 0; 78 | margin: 0; 79 | word-wrap: break-word; 80 | } 81 | 82 | .wpview[data-mce-selected] .wpview-error { 83 | border-color: transparent; 84 | } 85 | 86 | .wpview-error .dashicons, 87 | .loading-placeholder .dashicons { 88 | display: block; 89 | margin: 0 auto; 90 | width: 32px; 91 | height: 32px; 92 | font-size: 32px; 93 | } 94 | 95 | .wpview-error p { 96 | margin: 0; 97 | text-align: center; 98 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen-Sans", "Ubuntu", "Cantarell", "Helvetica Neue", sans-serif; 99 | } 100 | -------------------------------------------------------------------------------- /js/SelectControl.js: -------------------------------------------------------------------------------- 1 | window.fee = window.fee || {} 2 | window.fee.SelectControl = (function (tinymce) { 3 | return tinymce.ui.Widget.extend({ 4 | Defaults: { 5 | classes: 'widget btn', 6 | role: 'button' 7 | }, 8 | 9 | init: function (settings) { 10 | var self = this 11 | var size 12 | 13 | self._super(settings) 14 | settings = self.settings 15 | 16 | size = self.settings.size 17 | 18 | self.on('click mousedown', function (e) { 19 | // e.preventDefault() 20 | }) 21 | 22 | self.on('touchstart', function (e) { 23 | self.fire('click', e) 24 | // e.preventDefault() 25 | }) 26 | 27 | if (settings.subtype) { 28 | self.classes.add(settings.subtype) 29 | } 30 | 31 | if (size) { 32 | self.classes.add('btn-' + size) 33 | } 34 | 35 | if (settings.icon) { 36 | self.icon(settings.icon) 37 | } 38 | }, 39 | 40 | icon: function (icon) { 41 | if (!arguments.length) { 42 | return this.state.get('icon') 43 | } 44 | 45 | this.state.set('icon', icon) 46 | 47 | return this 48 | }, 49 | 50 | repaint: function () { 51 | var btnElm = this.getEl().firstChild 52 | var btnStyle 53 | 54 | if (btnElm) { 55 | btnStyle = btnElm.style 56 | btnStyle.width = btnStyle.height = '100%' 57 | } 58 | 59 | this._super() 60 | }, 61 | 62 | renderHtml: function () { 63 | var self = this 64 | var id = self._id 65 | var prefix = self.classPrefix 66 | var icon = self.state.get('icon') 67 | var image 68 | var text = self.state.get('text') 69 | var textHtml = '' 70 | var optionsHTML = '' 71 | 72 | image = self.settings.image 73 | if (image) { 74 | icon = 'none' 75 | 76 | // Support for [high dpi, low dpi] image sources 77 | if (typeof image !== 'string') { 78 | image = window.getSelection ? image[0] : image[1] 79 | } 80 | 81 | image = ' style="background-image: url(\'' + image + '\')"' 82 | } else { 83 | image = '' 84 | } 85 | 86 | if (text) { 87 | self.classes.add('btn-has-text') 88 | textHtml = '' + self.encode(text) + '' 89 | } 90 | 91 | icon = self.settings.icon ? prefix + 'ico ' + prefix + 'i-' + icon : '' 92 | 93 | tinymce.each(self.settings.options, function (v, k) { 94 | optionsHTML += '' 95 | }) 96 | 97 | return ( 98 | '
' + 99 | '' + 103 | '' + 104 | '
' 105 | ) 106 | }, 107 | 108 | bindStates: function () { 109 | var self = this 110 | var $ = self.$ 111 | var textCls = self.classPrefix + 'txt' 112 | var $select = $('select', self.getEl()) 113 | var options = self.settings.options 114 | var editor = self.settings.editor 115 | 116 | $select.on('change', function () { 117 | self.text(options[ this.value ].icon) 118 | editor.formatter.apply(this.value) 119 | }) 120 | 121 | editor.on('nodechange', function (event) { 122 | var formatter = editor.formatter 123 | 124 | tinymce.each(event.parents, function (node) { 125 | tinymce.each(options, function (value, key) { 126 | if (formatter.matchNode(node, key)) { 127 | $select[0].value = key 128 | self.text(value.icon) 129 | } 130 | }) 131 | }) 132 | }) 133 | 134 | function setButtonText (text) { 135 | var $span = $('span.' + textCls, self.getEl()) 136 | 137 | if (text) { 138 | if (!$span[0]) { 139 | $('button:first', self.getEl()).append('') 140 | $span = $('span.' + textCls, self.getEl()) 141 | } 142 | 143 | $span.html(self.encode(text)) 144 | } else { 145 | $span.remove() 146 | } 147 | 148 | self.classes.toggle('btn-has-text', !!text) 149 | } 150 | 151 | self.state.on('change:text', function (e) { 152 | setButtonText(e.value) 153 | }) 154 | 155 | self.state.on('change:icon', function (e) { 156 | var icon = e.value 157 | var prefix = self.classPrefix 158 | 159 | self.settings.icon = icon 160 | icon = icon ? prefix + 'ico ' + prefix + 'i-' + self.settings.icon : '' 161 | 162 | var btnElm = self.getEl().firstChild 163 | var iconElm = btnElm.getElementsByTagName('i')[0] 164 | 165 | if (icon) { 166 | if (!iconElm || iconElm !== btnElm.firstChild) { 167 | iconElm = document.createElement('i') 168 | btnElm.insertBefore(iconElm, btnElm.firstChild) 169 | } 170 | 171 | iconElm.className = icon 172 | } else if (iconElm) { 173 | btnElm.removeChild(iconElm) 174 | } 175 | 176 | setButtonText(self.state.get('text')) 177 | }) 178 | 179 | return self._super() 180 | } 181 | }) 182 | })(window.tinymce) 183 | -------------------------------------------------------------------------------- /js/blobToBase64.js: -------------------------------------------------------------------------------- 1 | window.fee = window.fee || {} 2 | window.fee.blobToBase64 = (function (Deferred, FileReader) { 3 | return function (blob) { 4 | return Deferred(function (deferred) { 5 | var reader = new FileReader() 6 | 7 | reader.onloadend = function () { 8 | deferred.resolve(reader.result.split(',')[1]) 9 | } 10 | 11 | reader.readAsDataURL(blob) 12 | }) 13 | } 14 | })(window.jQuery.Deferred, window.FileReader) 15 | -------------------------------------------------------------------------------- /js/fee-adminbar.js: -------------------------------------------------------------------------------- 1 | (function ($, settings) { 2 | var typeRegExp = /[?&]post_type=([^&]+)(?:$|&)/ 3 | var idRegExp = /[?&]post=([0-9]+)(?:$|&)/ 4 | 5 | $(function () { 6 | $('a[href^="' + settings.adminURL + 'post-new.php"]').on('click', function (event) { 7 | var type = $(this).attr('href').match(typeRegExp) 8 | 9 | type = type ? type[1] : 'post' 10 | 11 | if (settings.postTypes[ type ]) { 12 | event.preventDefault() 13 | $.post(settings.api.root + 'wp/v2/' + settings.postTypes[ type ], { 14 | _wpnonce: settings.api.nonce, 15 | title: 'Auto Draft' 16 | }).done(function (data) { 17 | if (data.link) { 18 | window.location.href = data.link 19 | } 20 | }) 21 | } 22 | }) 23 | 24 | $('a[href^="' + settings.adminURL + 'post.php"]').on('click', function (event) { 25 | var href = $(this).attr('href') 26 | var id = href.match(idRegExp) 27 | var type = href.match(typeRegExp) 28 | 29 | if (id && type && settings.postTypes[ type[1] ] && href !== settings.editURL) { 30 | event.preventDefault() 31 | window.location.href = settings.homeURL + '?p=' + id[1] + '&edit=post' 32 | } 33 | }) 34 | }) 35 | })(window.jQuery, window.fee_adminbar) 36 | -------------------------------------------------------------------------------- /js/fee.js: -------------------------------------------------------------------------------- 1 | window.fee = (function ( 2 | settings, 3 | $, 4 | media, 5 | tinymce, 6 | _, 7 | Backbone, 8 | location, 9 | history 10 | ) { 11 | var hidden = true 12 | 13 | var BaseModel = Backbone.Model.extend({ 14 | urlRoot: settings.api.root + 'wp/v2/' + settings.api.endpoint, 15 | sync: function (method, model, options) { 16 | var beforeSend = options.beforeSend 17 | 18 | options.beforeSend = function (xhr) { 19 | xhr.setRequestHeader('X-WP-Nonce', settings.api.nonce) 20 | if (beforeSend) return beforeSend.apply(this, arguments) 21 | } 22 | 23 | return Backbone.sync(method, model, _.clone(options)).then(function (data, text, xhr) { 24 | var nonce = xhr.getResponseHeader('X-WP-Nonce') 25 | 26 | if (nonce) { 27 | settings.api.nonce = nonce 28 | } 29 | }, function (data) { 30 | if (data.responseText) { 31 | data = JSON.parse(data.responseText) 32 | 33 | // Nonce expired, so get a new one and try again. 34 | if (data.code === 'rest_cookie_expired_nonce') { 35 | return $.post(settings.ajaxURL, {action: 'fee_nonce'}).then(function (data) { 36 | settings.api.nonce = data 37 | return Backbone.sync(method, model, options) 38 | }) 39 | } 40 | } 41 | }) 42 | } 43 | }) 44 | 45 | var AutosaveModel = BaseModel.extend({ 46 | isNew: function () { 47 | return true 48 | }, 49 | url: function () { 50 | return BaseModel.prototype.url.apply(this, arguments) + '/' + this.get('id') + '/autosave' 51 | } 52 | }) 53 | 54 | var Model = BaseModel.extend({ 55 | save: function (attributes) { 56 | this.trigger('beforesave') 57 | 58 | var publish = attributes && attributes.status === 'publish' 59 | var xhr 60 | 61 | attributes = _.pick(this.toJSON(), ['id', 'title', 'content', '_fee_session']) 62 | 63 | if (publish) { 64 | attributes.status = 'publish' 65 | } 66 | 67 | if (publish || _.some(attributes, function (v, k) { 68 | return !_.isEqual($.trim(v), $.trim(this._fee_last_save[k])) 69 | }, this)) { 70 | // If it's not published, overwrite. 71 | // If the status changes to publish, overwrite. 72 | // Othewise create a copy. 73 | if (this.get('status') !== 'publish' || publish) { 74 | xhr = BaseModel.prototype.save.call(this, attributes, { 75 | patch: true 76 | }) 77 | } else { 78 | this.trigger('request', this, new AutosaveModel(attributes).save(), {}) 79 | } 80 | } 81 | 82 | this._fee_last_save = _.clone(this.attributes) 83 | 84 | return xhr || $.Deferred().resolve().promise() 85 | } 86 | }) 87 | 88 | // Post model to manipulate. 89 | // Needs to represent the state on the server. 90 | var post = new Model() 91 | 92 | // Parse the data we got from the server and fill the model. 93 | post.set(post.parse(settings.post)) 94 | 95 | if (settings.autosave) { 96 | if (settings.autosave.title) { 97 | post.set('title', { 98 | raw: settings.autosave.title, 99 | rendered: post.set('title').rendered 100 | }) 101 | } 102 | 103 | if (settings.autosave.content) { 104 | post.set('content', { 105 | raw: settings.autosave.content, 106 | rendered: post.set('content').rendered 107 | }) 108 | } 109 | } 110 | 111 | post.set('_fee_session', new Date().getTime()) 112 | 113 | post._fee_last_save = _.clone(post.attributes) 114 | 115 | var $document = $(document) 116 | var $body = $(document.body) 117 | var $content = $('.fee-content') 118 | var $titles = $findTitles() 119 | var $title = $findTitle($titles, $content) 120 | var documentTitle = document.title.replace($title.text(), '') 121 | var $thumbnail = $('.fee-thumbnail') 122 | var $hasThumbnail = $('.has-post-thumbnail') 123 | var contentEditor 124 | 125 | $body.addClass('fee fee-off') 126 | $content.removeClass('fee-content') 127 | 128 | var debouncedSave = _.debounce(function () { 129 | post.save() 130 | }, 1000) 131 | 132 | function editor (type, options) { 133 | var setup = options.setup 134 | 135 | return tinymce.init(_.extend(options, { 136 | inline: true, 137 | setup: function (editor) { 138 | var settings = editor.settings 139 | 140 | settings.content_editable = true 141 | 142 | function isEmpty () { 143 | return editor.getContent({ format: 'raw' }).replace(/(?:]*>)?(?:]*>)?(?:<\/p>)?/, '') === '' 144 | } 145 | 146 | editor.on('focus', function () { 147 | $body.addClass('fee-edit-focus') 148 | }) 149 | 150 | editor.on('blur', function () { 151 | $body.removeClass('fee-edit-focus') 152 | }) 153 | 154 | if (settings.placeholder) { 155 | editor.on('init', function () { 156 | editor.getBody().setAttribute('data-placeholder', settings.placeholder) 157 | }) 158 | 159 | editor.on('focus', function () { 160 | editor.getBody().removeAttribute('data-empty') 161 | }) 162 | 163 | editor.on('blur setcontent loadcontent', function () { 164 | if (isEmpty()) { 165 | editor.getBody().setAttribute('data-empty', '') 166 | } else { 167 | editor.getBody().removeAttribute('data-empty') 168 | } 169 | }) 170 | } 171 | 172 | editor.on('init', function () { 173 | editor.on('setcontent', debouncedSave) 174 | }) 175 | 176 | post.on('beforesave', function () { 177 | post.set(type, editor.getContent()) 178 | 179 | editor.undoManager.add() 180 | editor.isNotDirty = true 181 | }) 182 | 183 | setup.call(this, editor) 184 | } 185 | })) 186 | } 187 | 188 | function on () { 189 | if (!hidden) { 190 | return 191 | } 192 | 193 | $body.removeClass('fee-off').addClass('fee-on') 194 | 195 | editor('content', _.extend(settings.tinymce, { 196 | target: $content.get(0), 197 | images_upload_handler: function (blobInfo, success, failure) { 198 | var formData = new window.FormData() 199 | 200 | formData.append('file', blobInfo.blob()) 201 | formData.append('name', blobInfo.filename()) 202 | 203 | $.ajax({ 204 | url: settings.api.root + 'wp/v2/media?_wpnonce=' + settings.api.nonce, 205 | data: formData, 206 | processData: false, 207 | contentType: false, 208 | type: 'POST', 209 | success: function (data) { 210 | success(data.source_url) 211 | } 212 | }) 213 | }, 214 | setup: function (editor) { 215 | editor.load = function (args) { 216 | var elm = this.getElement() 217 | var html 218 | 219 | args = args || {} 220 | args.load = true 221 | args.element = elm 222 | 223 | html = this.setContent(post.get('content').raw, args) 224 | 225 | if (!args.no_events) { 226 | this.fire('LoadContent', args) 227 | } 228 | 229 | args.element = elm = null 230 | 231 | return html 232 | } 233 | 234 | // Remove spaces from empty paragraphs. 235 | editor.on('BeforeSetContent', function (event) { 236 | if (event.content) { 237 | event.content = event.content.replace(/

(?: |\s)+<\/p>/gi, '


') 238 | } 239 | }) 240 | 241 | contentEditor = editor 242 | } 243 | })) 244 | 245 | editor('title', { 246 | target: $title.get(0), 247 | theme: null, 248 | paste_as_text: true, 249 | plugins: 'paste', 250 | placeholder: settings.titlePlaceholder, 251 | entity_encoding: 'raw', 252 | setup: function (editor) { 253 | editor.on('keydown', function (event) { 254 | if (event.keyCode === 13) { 255 | event.preventDefault() 256 | contentEditor.focus() 257 | } 258 | }) 259 | 260 | editor.on('setcontent keyup', function () { 261 | var text = $title.text() 262 | 263 | $titles.text(text) 264 | document.title = documentTitle.replace('', text) 265 | }) 266 | } 267 | }) 268 | 269 | $document.on('keyup.fee-writing', debouncedSave) 270 | 271 | hidden = false 272 | } 273 | 274 | function off () { 275 | if (post.get('status') === 'draft') { 276 | return 277 | } 278 | 279 | post.save().done(function () { 280 | location.reload(true) 281 | }) 282 | } 283 | 284 | if (settings.post.status === 'draft') { 285 | if (settings.post.title.raw === 'Auto Draft' && !settings.post.content.raw) { 286 | $title.empty() 287 | } 288 | 289 | on() 290 | } 291 | 292 | var regExp = /[?&]edit=post(?:$|&)/ 293 | 294 | if (regExp.test(location.href)) { 295 | on() 296 | 297 | if (history.replaceState) { 298 | history.replaceState(null, null, location.href.replace(regExp, '') + location.hash) 299 | } 300 | } 301 | 302 | if (settings.post.featured_media === 0) { 303 | $hasThumbnail.removeClass('has-post-thumbnail') 304 | $thumbnail.hide() 305 | 306 | if (!$thumbnail.siblings().get(0)) { 307 | $thumbnail.parent().hide() 308 | } 309 | } 310 | 311 | _.extend(media.featuredImage, { 312 | set: function (id) { 313 | var settings = media.view.settings 314 | 315 | settings.post.featuredImageId = id 316 | 317 | $hasThumbnail.removeClass('has-post-thumbnail') 318 | $thumbnail.hide() 319 | 320 | if (!$thumbnail.siblings().get(0)) { 321 | $thumbnail.parent().hide() 322 | } 323 | 324 | media.post('fee_thumbnail', { 325 | post_ID: settings.post.id, 326 | thumbnail_ID: settings.post.featuredImageId, 327 | _wpnonce: settings.post.nonce, 328 | size: $thumbnail.data('fee-size') 329 | }).done(function (html) { 330 | if (html) { 331 | $hasThumbnail.addClass('has-post-thumbnail') 332 | $thumbnail.html(html).show().parent().show() 333 | } 334 | }) 335 | } 336 | }) 337 | 338 | // Wait for admin bar to load. 339 | $(function () { 340 | $('a[href="' + settings.editURL + '"]').on('click.fee-link', function (event) { 341 | if (!tinymce.util.VK.modifierPressed(event)) { 342 | event.preventDefault() 343 | hidden ? on() : off() 344 | } 345 | }) 346 | }) 347 | 348 | function $findTitles () { 349 | var $br = $('br.fee-title') 350 | var $titles = $br.parent() 351 | 352 | $br.remove() 353 | 354 | return $titles 355 | } 356 | 357 | function $findTitle ($all, $content) { 358 | var title = false 359 | var $parents = $content.parents() 360 | var index 361 | 362 | $all.each(function () { 363 | var self = this 364 | var i = 0 365 | 366 | $(this).parents().each(function () { 367 | i++ 368 | 369 | if ($.inArray(this, $parents) !== -1) { 370 | if (!index || i < index) { 371 | index = i 372 | title = self 373 | } 374 | 375 | return false 376 | } 377 | }) 378 | }) 379 | 380 | $titles = $all.not(title) 381 | 382 | return $(title) 383 | } 384 | 385 | // Save post data before unloading the page as a last resort. 386 | // This does not work in Opera. 387 | $(window).on('unload', function () { 388 | post.trigger('beforesave') 389 | 390 | var autosave = post.get('status') === 'publish' ? '/autosave' : '' 391 | var url = post.url() + autosave + '?_method=put&_wpnonce=' + settings.api.nonce 392 | var data = JSON.stringify(post.attributes) 393 | 394 | if (!navigator.sendBeacon || !navigator.sendBeacon(url, data)) { 395 | $.post({async: false, data: data, url: url}) 396 | } 397 | }) 398 | 399 | return { 400 | post: post 401 | } 402 | })( 403 | window.feeData, 404 | window.jQuery, 405 | window.wp.media, 406 | window.tinymce, 407 | window._, 408 | window.Backbone, 409 | window.location, 410 | window.history 411 | ) 412 | -------------------------------------------------------------------------------- /js/filePicker.js: -------------------------------------------------------------------------------- 1 | window.fee = window.fee || {} 2 | window.fee.filePicker = (function (Deferred) { 3 | return function () { 4 | return Deferred(function (deferred) { 5 | var input = document.createElement('input') 6 | 7 | input.type = 'file' 8 | input.multiple = true 9 | input.style.position = 'fixed' 10 | input.style.left = 0 11 | input.style.top = 0 12 | input.style.opacity = 0.001 13 | 14 | input.onchange = function (event) { 15 | deferred.resolve(event.target.files) 16 | } 17 | 18 | document.body.appendChild(input) 19 | 20 | input.click() 21 | input.parentNode.removeChild(input) 22 | }) 23 | } 24 | })(window.jQuery.Deferred) 25 | -------------------------------------------------------------------------------- /js/insertBlob.js: -------------------------------------------------------------------------------- 1 | window.fee = window.fee || {} 2 | window.fee.insertBlob = (function (blobToBase64) { 3 | return function (editor, blob) { 4 | var blobCache = editor.editorUpload.blobCache 5 | var blobInfo = blobCache.create(new Date().getTime(), blob, blobToBase64(blob)) 6 | blobCache.add(blobInfo) 7 | editor.insertContent(editor.dom.createHTML('img', {src: blobInfo.blobUri()})) 8 | editor.nodeChanged() 9 | } 10 | })(window.fee.blobToBase64) 11 | -------------------------------------------------------------------------------- /js/tinymce.theme.js: -------------------------------------------------------------------------------- 1 | (function ( 2 | tinymce, 3 | _, 4 | filePicker, 5 | insertBlob, 6 | SelectControl 7 | ) { 8 | tinymce.ThemeManager.add('fee', function (editor) { 9 | tinymce.ui.FEESelect = SelectControl 10 | 11 | this.renderUI = function () { 12 | var settings = editor.settings 13 | var DOM = tinymce.DOM 14 | 15 | editor.on('focus', function () { 16 | if (editor.wp && editor.wp._createToolbar) { 17 | var element 18 | var toolbarInline = editor.wp._createToolbar(settings.toolbars.inline) 19 | var toolbarBlock = editor.wp._createToolbar(settings.toolbars.block) 20 | var toolbarCaret = editor.wp._createToolbar(settings.toolbars.caret) 21 | var toolbarMedia = editor.wp._createToolbar(['media_new', 'media_images', 'media_audio', 'media_video', 'media_insert', 'media_select']) 22 | 23 | toolbarInline.$el.addClass('fee-no-print') 24 | toolbarBlock.$el.addClass('fee-no-print') 25 | toolbarCaret.$el.addClass('fee-no-print mce-arrow-left-side') 26 | toolbarMedia.$el.addClass('fee-no-print mce-arrow-left-side fee-media-toolbar') 27 | 28 | toolbarMedia.blockHide = true 29 | 30 | toolbarCaret.reposition = 31 | toolbarMedia.reposition = function () { 32 | if (!element) return 33 | 34 | var toolbar = this.getEl() 35 | var toolbarRect = toolbar.getBoundingClientRect() 36 | var elementRect = element.getBoundingClientRect() 37 | 38 | DOM.setStyles(toolbar, { 39 | position: 'absolute', 40 | left: elementRect.left + 8 + 'px', 41 | top: elementRect.top + window.pageYOffset + elementRect.height / 2 - toolbarRect.height / 2 + 'px' 42 | }) 43 | 44 | this.show() 45 | } 46 | 47 | editor.on('keyup', _.throttle(function (event) { 48 | if (editor.dom.isEmpty(editor.selection.getNode())) { 49 | editor.nodeChanged() 50 | } else { 51 | toolbarCaret.hide() 52 | } 53 | }, 500)) 54 | 55 | editor.on('blur', function () { 56 | toolbarCaret.hide() 57 | }) 58 | 59 | editor.on('wptoolbar', function (event) { 60 | element = event.element 61 | element.normalize() 62 | 63 | var range = editor.selection.getRng() 64 | var content = editor.selection.getContent() 65 | var block = editor.dom.getParent(range.startContainer, '*[data-mce-selected="block"]') 66 | 67 | if (block) { 68 | event.toolbar = toolbarBlock 69 | event.selection = block 70 | 71 | return 72 | } 73 | 74 | var media = editor.dom.getParent(range.startContainer, '*[data-mce-selected="media"]') 75 | 76 | if (media) { 77 | event.toolbar = toolbarMedia 78 | setTimeout(function() { 79 | var node = toolbarMedia.find( 'toolbar' )[0]; 80 | node && node.focus( true ); 81 | }) 82 | return 83 | } 84 | 85 | // No collapsed selection. 86 | if (range.collapsed) { 87 | if (editor.dom.isEmpty(event.element) && (event.element.nodeName === 'P' || ( 88 | event.element.nodeName === 'BR' && event.element.parentNode.nodeName === 'P' 89 | ))) { 90 | event.toolbar = toolbarCaret 91 | } 92 | 93 | return 94 | } 95 | 96 | // No non editable elements. 97 | if ( 98 | element.getAttribute('contenteditable') === 'false' || 99 | element.getAttribute('data-mce-bogus') === 'all' 100 | ) { 101 | return 102 | } 103 | 104 | // No images. 105 | if (element.nodeName === 'IMG') { 106 | return 107 | } 108 | 109 | // No horizontal rules. 110 | if (element.nodeName === 'HR') { 111 | return 112 | } 113 | 114 | // No links. 115 | if (element.nodeName === 'A') { 116 | return 117 | } 118 | 119 | // No empty selection. 120 | if (!content.replace(/<[^>]+>/g, '').replace(/(?:\s| )/g, '')) { 121 | return 122 | } 123 | 124 | event.toolbar = toolbarInline 125 | event.selection = range 126 | }) 127 | } 128 | }) 129 | 130 | editor.addButton('heading', { 131 | editor: editor, 132 | type: 'FEESelect', 133 | text: 'H', 134 | classes: 'widget btn i-heading', 135 | stateSelector: 'h2,h3,h4,h5,h6', 136 | options: { 137 | p: { 138 | text: settings.strings.paragraph, 139 | icon: 'H' 140 | }, 141 | h2: { 142 | text: settings.strings.heading2, 143 | icon: 'H2' 144 | }, 145 | h3: { 146 | text: settings.strings.heading3, 147 | icon: 'H3' 148 | }, 149 | h4: { 150 | text: settings.strings.heading4, 151 | icon: 'H4' 152 | }, 153 | h5: { 154 | text: settings.strings.heading5, 155 | icon: 'H5' 156 | }, 157 | h6: { 158 | text: settings.strings.heading6, 159 | icon: 'H6' 160 | } 161 | } 162 | }) 163 | 164 | editor.addButton('save', { 165 | text: settings.strings.saved, 166 | onclick: function () { 167 | window.fee.post.save() 168 | }, 169 | onPostRender: function () { 170 | var button = this 171 | 172 | window.fee.post.on('request', function (model, xhr) { 173 | button.$el.find('.mce-txt').text(settings.strings.saving) 174 | button.active(true) 175 | button.disabled(true) 176 | 177 | xhr.done(function () { 178 | button.$el.find('.mce-txt').text(settings.strings.saved) 179 | }).fail(function () { 180 | button.$el.find('.mce-txt').text(settings.strings.error) 181 | }).always(function () { 182 | button.active(false) 183 | button.disabled(true) 184 | }) 185 | }) 186 | } 187 | }) 188 | 189 | editor.addButton('publish', { 190 | text: settings.strings.publish, 191 | classes: 'widget btn primary', 192 | onclick: function () { 193 | window.fee.post.save({status: 'publish'}).done(function () { 194 | window.location.reload(true) 195 | }) 196 | } 197 | }) 198 | 199 | editor.on('preinit', function () { 200 | if (editor.wp && editor.wp._createToolbar) { 201 | var toolbar = editor.wp._createToolbar(['save', 'publish']).show() 202 | 203 | toolbar.$el.addClass('fee-no-print fee-main-toolbar') 204 | 205 | toolbar._visible = true 206 | 207 | toolbar.reposition = function () { 208 | var element = editor.getBody() 209 | var toolbar = this.getEl() 210 | var elementRect = element.getBoundingClientRect() 211 | var toolbarRect = toolbar.getBoundingClientRect() 212 | 213 | DOM.setStyles(toolbar, { 214 | 'position': 'fixed', 215 | 'left': elementRect.left + (elementRect.width / 2) - (toolbarRect.width / 2), 216 | 'bottom': 0 217 | }) 218 | } 219 | 220 | toolbar.show = function () { 221 | if (!this._visible) { 222 | this.$el.removeClass('fee-hide') 223 | this._visible = true 224 | } 225 | } 226 | 227 | toolbar.hide = function () { 228 | if (this._visible) { 229 | this.$el.addClass('fee-hide') 230 | this._visible = false 231 | } 232 | } 233 | 234 | editor.on('keydown', function (event) { 235 | if (!tinymce.util.VK.modifierPressed(event) && window.pageYOffset > 0) { 236 | toolbar.hide() 237 | } 238 | }) 239 | 240 | DOM.bind(editor.getWin(), 'scroll', function () { 241 | toolbar.show() 242 | }) 243 | 244 | toolbar.reposition() 245 | } 246 | }) 247 | 248 | editor.addButton('add_featured_image', { 249 | icon: 'dashicon dashicons-edit', 250 | onclick: function () { 251 | window.wp.media.featuredImage.frame().open() 252 | } 253 | }) 254 | 255 | editor.addButton('remove_featured_image', { 256 | icon: 'dashicon dashicons-no', 257 | onclick: function () { 258 | window.wp.media.featuredImage.remove() 259 | } 260 | }) 261 | 262 | editor.on('preinit', function () { 263 | if (editor.wp && editor.wp._createToolbar) { 264 | var element = tinymce.$('.fee-thumbnail')[0] 265 | 266 | if (!element) return 267 | 268 | var toolbar = editor.wp._createToolbar([ 'add_featured_image', 'remove_featured_image' ]) 269 | 270 | toolbar.$el.addClass('fee-no-print mce-arrow-down') 271 | 272 | toolbar.reposition = function () { 273 | var toolbar = this.getEl() 274 | var elementRect = element.getBoundingClientRect() 275 | var toolbarRect = toolbar.getBoundingClientRect() 276 | 277 | DOM.setStyles(toolbar, { 278 | 'position': 'absolute', 279 | 'left': elementRect.left + (elementRect.width / 2) - (toolbarRect.width / 2), 280 | 'top': elementRect.top + window.pageYOffset - toolbarRect.height - 8 281 | }) 282 | } 283 | 284 | toolbar.reposition() 285 | 286 | DOM.bind(window, 'click', function (event) { 287 | if (event.target === element) { 288 | toolbar.show() 289 | } else { 290 | toolbar.hide() 291 | } 292 | }) 293 | } 294 | }) 295 | 296 | editor.addButton('media', { 297 | icon: 'dashicon dashicons-admin-media', 298 | onclick: function () { 299 | var range = editor.selection.getRng() 300 | var $start = editor.$(editor.dom.getParent(range.startContainer, editor.dom.isBlock)) 301 | 302 | $start.attr('data-mce-selected', 'media') 303 | editor.nodeChanged() 304 | 305 | editor.once('click keydown nodechange', function (event) { 306 | if (tinymce.util.VK.modifierPressed(event)) { 307 | return; 308 | } 309 | 310 | editor.$('*[data-mce-selected="media"]').removeAttr('data-mce-selected') 311 | editor.nodeChanged() 312 | }) 313 | } 314 | }) 315 | 316 | editor.addButton('select', { 317 | icon: 'dashicon dashicons-editor-textcolor', 318 | onclick: function () { 319 | var range = editor.selection.getRng() 320 | var $start = editor.$(editor.dom.getParent(range.startContainer, editor.dom.isBlock)) 321 | var $end = editor.$(editor.dom.getParent(range.endContainer, editor.dom.isBlock)) 322 | 323 | $start.add($start.nextUntil($end)).add($end).attr('data-mce-selected', 'block') 324 | editor.nodeChanged() 325 | 326 | editor.once('click keydown', function () { 327 | editor.$('*[data-mce-selected="block"]').removeAttr('data-mce-selected') 328 | editor.nodeChanged() 329 | }) 330 | } 331 | }) 332 | 333 | editor.addButton('media_new', { 334 | icon: 'dashicon dashicons-plus-alt', 335 | onclick: function () { 336 | filePicker().done(function (fileList) { 337 | _.each(fileList, function (file) { 338 | insertBlob(editor, file) 339 | }) 340 | 341 | editor.editorUpload.uploadImages() 342 | }) 343 | } 344 | }) 345 | 346 | editor.addButton('media_images', { 347 | icon: 'dashicon dashicons-format-image', 348 | active: true, 349 | onclick: function () {} 350 | }) 351 | 352 | editor.addButton('media_audio', { 353 | icon: 'dashicon dashicons-format-audio', 354 | onclick: function () { 355 | window.wp.media.editor.open(editor.id) 356 | } 357 | }) 358 | 359 | editor.addButton('media_video', { 360 | icon: 'dashicon dashicons-video-alt2', 361 | onclick: function () { 362 | window.wp.media.editor.open(editor.id) 363 | } 364 | }) 365 | 366 | var Collection = window.Backbone.Collection.extend({ 367 | url: window.feeData.api.root + 'wp/v2/media?per_page=20&media_type=image&context=edit&_wpnonce=' + window.feeData.api.nonce 368 | }) 369 | 370 | var collection = new Collection() 371 | 372 | tinymce.ui.FEEImageSelect = tinymce.ui.Control.extend({ 373 | renderHtml: function () { 374 | return ( 375 | '
' 376 | ) 377 | }, 378 | load: function () { 379 | var self = this 380 | 381 | this.$el.on('click', function (event) { 382 | tinymce.$(event.target).toggleClass('fee-selected') 383 | }) 384 | 385 | collection.fetch().done(function (data) { 386 | var string = '' 387 | 388 | _.each(data, function (image) { 389 | if (image.media_details.sizes.thumbnail) { 390 | string += '
' 391 | } 392 | }) 393 | 394 | self.$el.append(string) 395 | }) 396 | } 397 | }) 398 | 399 | editor.addButton('media_select', { 400 | type: 'FEEImageSelect', 401 | onPostRender: function () { 402 | this.load() 403 | } 404 | }) 405 | 406 | editor.addButton('media_insert', { 407 | text: 'Insert', 408 | classes: 'widget btn primary', 409 | onclick: function () { 410 | tinymce.$('.fee-image-select .fee-selected img').each(function () { 411 | var image = collection.get(tinymce.$(this).attr('data-id')) 412 | editor.insertContent(editor.dom.createHTML('img', {src: image.get('source_url')})) 413 | editor.nodeChanged() 414 | editor.focus() 415 | }) 416 | } 417 | }) 418 | 419 | return {} 420 | } 421 | }) 422 | })( 423 | window.tinymce, 424 | window._, 425 | window.fee.filePicker, 426 | window.fee.insertBlob, 427 | window.fee.SelectControl 428 | ) 429 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "imagemin-cli": "latest", 4 | "standard": "latest" 5 | }, 6 | "scripts": { 7 | "readme": "cat readme.md > readme.txt", 8 | "imagemin": "imagemin assets/* --out-dir=assets", 9 | "svn-add": "svn add --force .", 10 | "svn-assets": "svn copy ", 11 | "svn-checkout": "svn checkout https://plugins.svn.wordpress.org/wp-front-end-editor/trunk svn && rm -rf .svn && mv svn/.svn .svn && rm -rf svn", 12 | "svn-checkout-assets": "svn checkout https://plugins.svn.wordpress.org/wp-front-end-editor/assets svn && rm -rf assets/.svn && mv svn/.svn assets/.svn && rm -rf svn", 13 | "svn-ignore": "svn propset svn:ignore -F .svnignore .", 14 | "svn-remove": "svn st | grep ^! | awk '{print $2}' | xargs svn rm", 15 | "svn-resolve": "svn resolved -R .", 16 | "svn-tag": "svn copy https://plugins.svn.wordpress.org/wp-front-end-editor/trunk https://plugins.svn.wordpress.org/wp-front-end-editor/tags/$1 -m $1", 17 | "test": "standard js/*" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /plugin.php: -------------------------------------------------------------------------------- 1 | ) and horizontal rule (---). 22 | * Automatically embed media from [this list](https://codex.wordpress.org/Embeds). Just paste the URL. 23 | * You can also link text by just pasting the URL over it. 24 | * Add a featured image, if your theme supports it. 25 | 26 | ### Configure and extend 27 | 28 | This plugin is designed to be “plug and play”, but also configurable and extensible. 29 | 30 | #### Toolbars and buttons 31 | 32 | You can add more buttons to any of the toolbars with the following filters: 33 | 34 | * `fee_toolbar_caret` for the caret, 35 | * `fee_toolbar_inline` for normal selections, 36 | * `fee_toolbar_block` for block selections. 37 | 38 | E.g. 39 | 40 | add_filter('fee_toolbar_inline', function($buttons){ 41 | return array_merge($buttons, array('subscript')); 42 | }); 43 | 44 | You may need to provide extra CSS and JS. See the [Codex page](https://codex.wordpress.org/TinyMCE_Custom_Buttons) and [TinyMCE docs](https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols) for more information about adding toolbar buttons with TinyMCE. 45 | 46 | #### Linking to the editor 47 | 48 | You can link to the editor from anywhere on the website with the normal edit link to the admin, and it will be picked up by the plugin. Use `edit_post_link` or similar. 49 | 50 | #### Custom Post Types Support 51 | 52 | add_post_type_support( 'page', 'front-end-editor' ); 53 | 54 | Please make sure you also support the [REST API](http://v2.wp-api.org/extending/custom-content-types/). 55 | 56 | #### Disable 57 | 58 | If you’d like to disable the editor for certain posts, you can use the `supports_fee` filter. 59 | 60 | // Disable for the post with ID 1. 61 | add_filter('supports_fee', function($supports, $post) { 62 | return $post->ID !== 1; 63 | }, 10, 2); 64 | -------------------------------------------------------------------------------- /vendor/lists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * plugin.js 3 | * 4 | * Released under LGPL License. 5 | * Copyright (c) 1999-2015 Ephox Corp. All rights reserved 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 isChildOfBody(elm) { 18 | return editor.$.contains(editor.getBody(), elm); 19 | } 20 | 21 | function isBr(node) { 22 | return node && node.nodeName == 'BR'; 23 | } 24 | 25 | function isListNode(node) { 26 | return node && (/^(OL|UL|DL)$/).test(node.nodeName) && isChildOfBody(node); 27 | } 28 | 29 | function isFirstChild(node) { 30 | return node.parentNode.firstChild == node; 31 | } 32 | 33 | function isLastChild(node) { 34 | return node.parentNode.lastChild == node; 35 | } 36 | 37 | function isTextBlock(node) { 38 | return node && !!editor.schema.getTextBlockElements()[node.nodeName]; 39 | } 40 | 41 | function isEditorBody(elm) { 42 | return elm === editor.getBody(); 43 | } 44 | 45 | editor.on('init', function() { 46 | var dom = editor.dom, selection = editor.selection; 47 | 48 | function isEmpty(elm, keepBookmarks) { 49 | var empty = dom.isEmpty(elm); 50 | 51 | if (keepBookmarks && dom.select('span[data-mce-type=bookmark]').length > 0) { 52 | return false; 53 | } 54 | 55 | return empty; 56 | } 57 | 58 | /** 59 | * Returns a range bookmark. This will convert indexed bookmarks into temporary span elements with 60 | * index 0 so that they can be restored properly after the DOM has been modified. Text bookmarks will not have spans 61 | * added to them since they can be restored after a dom operation. 62 | * 63 | * So this:

||

64 | * becomes:

||

65 | * 66 | * @param {DOMRange} rng DOM Range to get bookmark on. 67 | * @return {Object} Bookmark object. 68 | */ 69 | function createBookmark(rng) { 70 | var bookmark = {}; 71 | 72 | function setupEndPoint(start) { 73 | var offsetNode, container, offset; 74 | 75 | container = rng[start ? 'startContainer' : 'endContainer']; 76 | offset = rng[start ? 'startOffset' : 'endOffset']; 77 | 78 | if (container.nodeType == 1) { 79 | offsetNode = dom.create('span', {'data-mce-type': 'bookmark'}); 80 | 81 | if (container.hasChildNodes()) { 82 | offset = Math.min(offset, container.childNodes.length - 1); 83 | 84 | if (start) { 85 | container.insertBefore(offsetNode, container.childNodes[offset]); 86 | } else { 87 | dom.insertAfter(offsetNode, container.childNodes[offset]); 88 | } 89 | } else { 90 | container.appendChild(offsetNode); 91 | } 92 | 93 | container = offsetNode; 94 | offset = 0; 95 | } 96 | 97 | bookmark[start ? 'startContainer' : 'endContainer'] = container; 98 | bookmark[start ? 'startOffset' : 'endOffset'] = offset; 99 | } 100 | 101 | setupEndPoint(true); 102 | 103 | if (!rng.collapsed) { 104 | setupEndPoint(); 105 | } 106 | 107 | return bookmark; 108 | } 109 | 110 | /** 111 | * Moves the selection to the current bookmark and removes any selection container wrappers. 112 | * 113 | * @param {Object} bookmark Bookmark object to move selection to. 114 | */ 115 | function moveToBookmark(bookmark) { 116 | function restoreEndPoint(start) { 117 | var container, offset, node; 118 | 119 | function nodeIndex(container) { 120 | var node = container.parentNode.firstChild, idx = 0; 121 | 122 | while (node) { 123 | if (node == container) { 124 | return idx; 125 | } 126 | 127 | // Skip data-mce-type=bookmark nodes 128 | if (node.nodeType != 1 || node.getAttribute('data-mce-type') != 'bookmark') { 129 | idx++; 130 | } 131 | 132 | node = node.nextSibling; 133 | } 134 | 135 | return -1; 136 | } 137 | 138 | container = node = bookmark[start ? 'startContainer' : 'endContainer']; 139 | offset = bookmark[start ? 'startOffset' : 'endOffset']; 140 | 141 | if (!container) { 142 | return; 143 | } 144 | 145 | if (container.nodeType == 1) { 146 | offset = nodeIndex(container); 147 | container = container.parentNode; 148 | dom.remove(node); 149 | } 150 | 151 | bookmark[start ? 'startContainer' : 'endContainer'] = container; 152 | bookmark[start ? 'startOffset' : 'endOffset'] = offset; 153 | } 154 | 155 | restoreEndPoint(true); 156 | restoreEndPoint(); 157 | 158 | var rng = dom.createRng(); 159 | 160 | rng.setStart(bookmark.startContainer, bookmark.startOffset); 161 | 162 | if (bookmark.endContainer) { 163 | rng.setEnd(bookmark.endContainer, bookmark.endOffset); 164 | } 165 | 166 | selection.setRng(rng); 167 | } 168 | 169 | function createNewTextBlock(contentNode, blockName) { 170 | var node, textBlock, fragment = dom.createFragment(), hasContentNode; 171 | var blockElements = editor.schema.getBlockElements(); 172 | 173 | if (editor.settings.forced_root_block) { 174 | blockName = blockName || editor.settings.forced_root_block; 175 | } 176 | 177 | if (blockName) { 178 | textBlock = dom.create(blockName); 179 | 180 | if (textBlock.tagName === editor.settings.forced_root_block) { 181 | dom.setAttribs(textBlock, editor.settings.forced_root_block_attrs); 182 | } 183 | 184 | fragment.appendChild(textBlock); 185 | } 186 | 187 | if (contentNode) { 188 | while ((node = contentNode.firstChild)) { 189 | var nodeName = node.nodeName; 190 | 191 | if (!hasContentNode && (nodeName != 'SPAN' || node.getAttribute('data-mce-type') != 'bookmark')) { 192 | hasContentNode = true; 193 | } 194 | 195 | if (blockElements[nodeName]) { 196 | fragment.appendChild(node); 197 | textBlock = null; 198 | } else { 199 | if (blockName) { 200 | if (!textBlock) { 201 | textBlock = dom.create(blockName); 202 | fragment.appendChild(textBlock); 203 | } 204 | 205 | textBlock.appendChild(node); 206 | } else { 207 | fragment.appendChild(node); 208 | } 209 | } 210 | } 211 | } 212 | 213 | if (!editor.settings.forced_root_block) { 214 | fragment.appendChild(dom.create('br')); 215 | } else { 216 | // BR is needed in empty blocks on non IE browsers 217 | if (!hasContentNode && (!tinymce.Env.ie || tinymce.Env.ie > 10)) { 218 | textBlock.appendChild(dom.create('br', {'data-mce-bogus': '1'})); 219 | } 220 | } 221 | 222 | return fragment; 223 | } 224 | 225 | function getSelectedListItems() { 226 | return tinymce.grep(selection.getSelectedBlocks(), function(block) { 227 | return /^(LI|DT|DD)$/.test(block.nodeName); 228 | }); 229 | } 230 | 231 | function splitList(ul, li, newBlock) { 232 | var tmpRng, fragment, bookmarks, node; 233 | 234 | function removeAndKeepBookmarks(targetNode) { 235 | tinymce.each(bookmarks, function(node) { 236 | targetNode.parentNode.insertBefore(node, li.parentNode); 237 | }); 238 | 239 | dom.remove(targetNode); 240 | } 241 | 242 | bookmarks = dom.select('span[data-mce-type="bookmark"]', ul); 243 | newBlock = newBlock || createNewTextBlock(li); 244 | tmpRng = dom.createRng(); 245 | tmpRng.setStartAfter(li); 246 | tmpRng.setEndAfter(ul); 247 | fragment = tmpRng.extractContents(); 248 | 249 | for (node = fragment.firstChild; node; node = node.firstChild) { 250 | if (node.nodeName == 'LI' && dom.isEmpty(node)) { 251 | dom.remove(node); 252 | break; 253 | } 254 | } 255 | 256 | if (!dom.isEmpty(fragment)) { 257 | dom.insertAfter(fragment, ul); 258 | } 259 | 260 | dom.insertAfter(newBlock, ul); 261 | 262 | if (isEmpty(li.parentNode)) { 263 | removeAndKeepBookmarks(li.parentNode); 264 | } 265 | 266 | dom.remove(li); 267 | 268 | if (isEmpty(ul)) { 269 | dom.remove(ul); 270 | } 271 | } 272 | 273 | var shouldMerge = function (listBlock, sibling) { 274 | var targetStyle = editor.dom.getStyle(listBlock, 'list-style-type', true); 275 | var style = editor.dom.getStyle(sibling, 'list-style-type', true); 276 | return targetStyle === style; 277 | }; 278 | 279 | function mergeWithAdjacentLists(listBlock) { 280 | var sibling, node; 281 | 282 | sibling = listBlock.nextSibling; 283 | if (sibling && isListNode(sibling) && sibling.nodeName == listBlock.nodeName && shouldMerge(listBlock, sibling)) { 284 | while ((node = sibling.firstChild)) { 285 | listBlock.appendChild(node); 286 | } 287 | 288 | dom.remove(sibling); 289 | } 290 | 291 | sibling = listBlock.previousSibling; 292 | if (sibling && isListNode(sibling) && sibling.nodeName == listBlock.nodeName && shouldMerge(listBlock, sibling)) { 293 | while ((node = sibling.firstChild)) { 294 | listBlock.insertBefore(node, listBlock.firstChild); 295 | } 296 | 297 | dom.remove(sibling); 298 | } 299 | } 300 | 301 | /** 302 | * Normalizes the all lists in the specified element. 303 | */ 304 | function normalizeList(element) { 305 | tinymce.each(tinymce.grep(dom.select('ol,ul', element)), function(ul) { 306 | var sibling, parentNode = ul.parentNode; 307 | 308 | // Move UL/OL to previous LI if it's the only child of a LI 309 | if (parentNode.nodeName == 'LI' && parentNode.firstChild == ul) { 310 | sibling = parentNode.previousSibling; 311 | if (sibling && sibling.nodeName == 'LI') { 312 | sibling.appendChild(ul); 313 | 314 | if (isEmpty(parentNode)) { 315 | dom.remove(parentNode); 316 | } 317 | } 318 | } 319 | 320 | // Append OL/UL to previous LI if it's in a parent OL/UL i.e. old HTML4 321 | if (isListNode(parentNode)) { 322 | sibling = parentNode.previousSibling; 323 | if (sibling && sibling.nodeName == 'LI') { 324 | sibling.appendChild(ul); 325 | } 326 | } 327 | }); 328 | } 329 | 330 | function outdent(li) { 331 | var ul = li.parentNode, ulParent = ul.parentNode, newBlock; 332 | 333 | function removeEmptyLi(li) { 334 | if (isEmpty(li)) { 335 | dom.remove(li); 336 | } 337 | } 338 | 339 | if (isEditorBody(ul)) { 340 | return true; 341 | } 342 | 343 | if (li.nodeName == 'DD') { 344 | dom.rename(li, 'DT'); 345 | return true; 346 | } 347 | 348 | if (isFirstChild(li) && isLastChild(li)) { 349 | if (ulParent.nodeName == "LI") { 350 | dom.insertAfter(li, ulParent); 351 | removeEmptyLi(ulParent); 352 | dom.remove(ul); 353 | } else if (isListNode(ulParent)) { 354 | dom.remove(ul, true); 355 | } else { 356 | ulParent.insertBefore(createNewTextBlock(li), ul); 357 | dom.remove(ul); 358 | } 359 | 360 | return true; 361 | } else if (isFirstChild(li)) { 362 | if (ulParent.nodeName == "LI") { 363 | dom.insertAfter(li, ulParent); 364 | li.appendChild(ul); 365 | removeEmptyLi(ulParent); 366 | } else if (isListNode(ulParent)) { 367 | ulParent.insertBefore(li, ul); 368 | } else { 369 | ulParent.insertBefore(createNewTextBlock(li), ul); 370 | dom.remove(li); 371 | } 372 | 373 | return true; 374 | } else if (isLastChild(li)) { 375 | if (ulParent.nodeName == "LI") { 376 | dom.insertAfter(li, ulParent); 377 | } else if (isListNode(ulParent)) { 378 | dom.insertAfter(li, ul); 379 | } else { 380 | dom.insertAfter(createNewTextBlock(li), ul); 381 | dom.remove(li); 382 | } 383 | 384 | return true; 385 | } 386 | 387 | if (ulParent.nodeName == 'LI') { 388 | ul = ulParent; 389 | newBlock = createNewTextBlock(li, 'LI'); 390 | } else if (isListNode(ulParent)) { 391 | newBlock = createNewTextBlock(li, 'LI'); 392 | } else { 393 | newBlock = createNewTextBlock(li); 394 | } 395 | 396 | splitList(ul, li, newBlock); 397 | normalizeList(ul.parentNode); 398 | 399 | return true; 400 | } 401 | 402 | function indent(li) { 403 | var sibling, newList, listStyle; 404 | 405 | function mergeLists(from, to) { 406 | var node; 407 | 408 | if (isListNode(from)) { 409 | while ((node = li.lastChild.firstChild)) { 410 | to.appendChild(node); 411 | } 412 | 413 | dom.remove(from); 414 | } 415 | } 416 | 417 | if (li.nodeName == 'DT') { 418 | dom.rename(li, 'DD'); 419 | return true; 420 | } 421 | 422 | sibling = li.previousSibling; 423 | 424 | if (sibling && isListNode(sibling)) { 425 | sibling.appendChild(li); 426 | return true; 427 | } 428 | 429 | if (sibling && sibling.nodeName == 'LI' && isListNode(sibling.lastChild)) { 430 | sibling.lastChild.appendChild(li); 431 | mergeLists(li.lastChild, sibling.lastChild); 432 | return true; 433 | } 434 | 435 | sibling = li.nextSibling; 436 | 437 | if (sibling && isListNode(sibling)) { 438 | sibling.insertBefore(li, sibling.firstChild); 439 | return true; 440 | } 441 | 442 | /*if (sibling && sibling.nodeName == 'LI' && isListNode(li.lastChild)) { 443 | return false; 444 | }*/ 445 | 446 | sibling = li.previousSibling; 447 | if (sibling && sibling.nodeName == 'LI') { 448 | newList = dom.create(li.parentNode.nodeName); 449 | listStyle = dom.getStyle(li.parentNode, 'listStyleType'); 450 | if (listStyle) { 451 | dom.setStyle(newList, 'listStyleType', listStyle); 452 | } 453 | sibling.appendChild(newList); 454 | newList.appendChild(li); 455 | mergeLists(li.lastChild, newList); 456 | return true; 457 | } 458 | 459 | return false; 460 | } 461 | 462 | function indentSelection() { 463 | var listElements = getSelectedListItems(); 464 | 465 | if (listElements.length) { 466 | var bookmark = createBookmark(selection.getRng(true)); 467 | 468 | for (var i = 0; i < listElements.length; i++) { 469 | if (!indent(listElements[i]) && i === 0) { 470 | break; 471 | } 472 | } 473 | 474 | moveToBookmark(bookmark); 475 | editor.nodeChanged(); 476 | 477 | return true; 478 | } 479 | } 480 | 481 | function outdentSelection() { 482 | var listElements = getSelectedListItems(); 483 | 484 | if (listElements.length) { 485 | var bookmark = createBookmark(selection.getRng(true)); 486 | var i, y, root = editor.getBody(); 487 | 488 | i = listElements.length; 489 | while (i--) { 490 | var node = listElements[i].parentNode; 491 | 492 | while (node && node != root) { 493 | y = listElements.length; 494 | while (y--) { 495 | if (listElements[y] === node) { 496 | listElements.splice(i, 1); 497 | break; 498 | } 499 | } 500 | 501 | node = node.parentNode; 502 | } 503 | } 504 | 505 | for (i = 0; i < listElements.length; i++) { 506 | if (!outdent(listElements[i]) && i === 0) { 507 | break; 508 | } 509 | } 510 | 511 | moveToBookmark(bookmark); 512 | editor.nodeChanged(); 513 | 514 | return true; 515 | } 516 | } 517 | 518 | function applyList(listName, detail) { 519 | var rng = selection.getRng(true), bookmark, listItemName = 'LI'; 520 | 521 | if (dom.getContentEditable(selection.getNode()) === "false") { 522 | return; 523 | } 524 | 525 | listName = listName.toUpperCase(); 526 | 527 | if (listName == 'DL') { 528 | listItemName = 'DT'; 529 | } 530 | 531 | function getSelectedTextBlocks() { 532 | var textBlocks = [], root = editor.getBody(); 533 | 534 | function getEndPointNode(start) { 535 | var container, offset; 536 | 537 | container = rng[start ? 'startContainer' : 'endContainer']; 538 | offset = rng[start ? 'startOffset' : 'endOffset']; 539 | 540 | // Resolve node index 541 | if (container.nodeType == 1) { 542 | container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; 543 | } 544 | 545 | while (container.parentNode != root) { 546 | if (isTextBlock(container)) { 547 | return container; 548 | } 549 | 550 | if (/^(TD|TH)$/.test(container.parentNode.nodeName)) { 551 | return container; 552 | } 553 | 554 | container = container.parentNode; 555 | } 556 | 557 | return container; 558 | } 559 | 560 | var startNode = getEndPointNode(true); 561 | var endNode = getEndPointNode(); 562 | var block, siblings = []; 563 | 564 | for (var node = startNode; node; node = node.nextSibling) { 565 | siblings.push(node); 566 | 567 | if (node == endNode) { 568 | break; 569 | } 570 | } 571 | 572 | tinymce.each(siblings, function(node) { 573 | if (isTextBlock(node)) { 574 | textBlocks.push(node); 575 | block = null; 576 | return; 577 | } 578 | 579 | if (dom.isBlock(node) || isBr(node)) { 580 | if (isBr(node)) { 581 | dom.remove(node); 582 | } 583 | 584 | block = null; 585 | return; 586 | } 587 | 588 | var nextSibling = node.nextSibling; 589 | if (tinymce.dom.BookmarkManager.isBookmarkNode(node)) { 590 | if (isTextBlock(nextSibling) || (!nextSibling && node.parentNode == root)) { 591 | block = null; 592 | return; 593 | } 594 | } 595 | 596 | if (!block) { 597 | block = dom.create('p'); 598 | node.parentNode.insertBefore(block, node); 599 | textBlocks.push(block); 600 | } 601 | 602 | block.appendChild(node); 603 | }); 604 | 605 | return textBlocks; 606 | } 607 | 608 | bookmark = createBookmark(rng); 609 | 610 | tinymce.each(getSelectedTextBlocks(), function(block) { 611 | var listBlock, sibling; 612 | 613 | var hasCompatibleStyle = function (sib) { 614 | var sibStyle = dom.getStyle(sib, 'list-style-type'); 615 | var detailStyle = detail ? detail['list-style-type'] : ''; 616 | 617 | detailStyle = detailStyle === null ? '' : detailStyle; 618 | 619 | return sibStyle === detailStyle; 620 | }; 621 | 622 | sibling = block.previousSibling; 623 | if (sibling && isListNode(sibling) && sibling.nodeName == listName && hasCompatibleStyle(sibling)) { 624 | listBlock = sibling; 625 | block = dom.rename(block, listItemName); 626 | sibling.appendChild(block); 627 | } else { 628 | listBlock = dom.create(listName); 629 | block.parentNode.insertBefore(listBlock, block); 630 | listBlock.appendChild(block); 631 | block = dom.rename(block, listItemName); 632 | } 633 | 634 | updateListStyle(listBlock, detail); 635 | mergeWithAdjacentLists(listBlock); 636 | }); 637 | 638 | moveToBookmark(bookmark); 639 | } 640 | 641 | var updateListStyle = function (el, detail) { 642 | dom.setStyle(el, 'list-style-type', detail ? detail['list-style-type'] : null); 643 | }; 644 | 645 | function removeList() { 646 | var bookmark = createBookmark(selection.getRng(true)), root = editor.getBody(); 647 | 648 | tinymce.each(getSelectedListItems(), function(li) { 649 | var node, rootList; 650 | 651 | if (isEditorBody(li.parentNode)) { 652 | return; 653 | } 654 | 655 | if (isEmpty(li)) { 656 | outdent(li); 657 | return; 658 | } 659 | 660 | for (node = li; node && node != root; node = node.parentNode) { 661 | if (isListNode(node)) { 662 | rootList = node; 663 | } 664 | } 665 | 666 | splitList(rootList, li); 667 | }); 668 | 669 | moveToBookmark(bookmark); 670 | } 671 | 672 | function toggleList(listName, detail) { 673 | var parentList = dom.getParent(selection.getStart(), 'OL,UL,DL'); 674 | 675 | if (isEditorBody(parentList)) { 676 | return; 677 | } 678 | 679 | if (parentList) { 680 | if (parentList.nodeName == listName) { 681 | removeList(listName); 682 | } else { 683 | var bookmark = createBookmark(selection.getRng(true)); 684 | updateListStyle(parentList, detail); 685 | mergeWithAdjacentLists(dom.rename(parentList, listName)); 686 | 687 | moveToBookmark(bookmark); 688 | } 689 | } else { 690 | applyList(listName, detail); 691 | } 692 | } 693 | 694 | function queryListCommandState(listName) { 695 | return function() { 696 | var parentList = dom.getParent(editor.selection.getStart(), 'UL,OL,DL'); 697 | 698 | return parentList && parentList.nodeName == listName; 699 | }; 700 | } 701 | 702 | function isBogusBr(node) { 703 | if (!isBr(node)) { 704 | return false; 705 | } 706 | 707 | if (dom.isBlock(node.nextSibling) && !isBr(node.previousSibling)) { 708 | return true; 709 | } 710 | 711 | return false; 712 | } 713 | 714 | self.backspaceDelete = function(isForward) { 715 | function findNextCaretContainer(rng, isForward) { 716 | var node = rng.startContainer, offset = rng.startOffset; 717 | var nonEmptyBlocks, walker; 718 | 719 | if (node.nodeType == 3 && (isForward ? offset < node.data.length : offset > 0)) { 720 | return node; 721 | } 722 | 723 | nonEmptyBlocks = editor.schema.getNonEmptyElements(); 724 | if (node.nodeType == 1) { 725 | node = tinymce.dom.RangeUtils.getNode(node, offset); 726 | } 727 | 728 | walker = new tinymce.dom.TreeWalker(node, editor.getBody()); 729 | 730 | // Delete at
  • |
  • then jump over the bogus br 731 | if (isForward) { 732 | if (isBogusBr(node)) { 733 | walker.next(); 734 | } 735 | } 736 | 737 | while ((node = walker[isForward ? 'next' : 'prev2']())) { 738 | if (node.nodeName == 'LI' && !node.hasChildNodes()) { 739 | return node; 740 | } 741 | 742 | if (nonEmptyBlocks[node.nodeName]) { 743 | return node; 744 | } 745 | 746 | if (node.nodeType == 3 && node.data.length > 0) { 747 | return node; 748 | } 749 | } 750 | } 751 | 752 | function mergeLiElements(fromElm, toElm) { 753 | var node, listNode, ul = fromElm.parentNode; 754 | 755 | if (!isChildOfBody(fromElm) || !isChildOfBody(toElm)) { 756 | return; 757 | } 758 | 759 | if (isListNode(toElm.lastChild)) { 760 | listNode = toElm.lastChild; 761 | } 762 | 763 | if (ul == toElm.lastChild) { 764 | if (isBr(ul.previousSibling)) { 765 | dom.remove(ul.previousSibling); 766 | } 767 | } 768 | 769 | node = toElm.lastChild; 770 | if (node && isBr(node) && fromElm.hasChildNodes()) { 771 | dom.remove(node); 772 | } 773 | 774 | if (isEmpty(toElm, true)) { 775 | dom.$(toElm).empty(); 776 | } 777 | 778 | if (!isEmpty(fromElm, true)) { 779 | while ((node = fromElm.firstChild)) { 780 | toElm.appendChild(node); 781 | } 782 | } 783 | 784 | if (listNode) { 785 | toElm.appendChild(listNode); 786 | } 787 | 788 | dom.remove(fromElm); 789 | 790 | if (isEmpty(ul) && !isEditorBody(ul)) { 791 | dom.remove(ul); 792 | } 793 | } 794 | 795 | if (selection.isCollapsed()) { 796 | var li = dom.getParent(selection.getStart(), 'LI'), ul, rng, otherLi; 797 | 798 | if (li) { 799 | ul = li.parentNode; 800 | if (isEditorBody(ul) && dom.isEmpty(ul)) { 801 | return true; 802 | } 803 | 804 | rng = selection.getRng(true); 805 | otherLi = dom.getParent(findNextCaretContainer(rng, isForward), 'LI'); 806 | 807 | if (otherLi && otherLi != li) { 808 | var bookmark = createBookmark(rng); 809 | 810 | if (isForward) { 811 | mergeLiElements(otherLi, li); 812 | } else { 813 | mergeLiElements(li, otherLi); 814 | } 815 | 816 | moveToBookmark(bookmark); 817 | 818 | return true; 819 | } else if (!otherLi) { 820 | if (!isForward && removeList(ul.nodeName)) { 821 | return true; 822 | } 823 | } 824 | } 825 | } 826 | }; 827 | 828 | editor.on('BeforeExecCommand', function(e) { 829 | var cmd = e.command.toLowerCase(), isHandled; 830 | 831 | if (cmd == "indent") { 832 | if (indentSelection()) { 833 | isHandled = true; 834 | } 835 | } else if (cmd == "outdent") { 836 | if (outdentSelection()) { 837 | isHandled = true; 838 | } 839 | } 840 | 841 | if (isHandled) { 842 | editor.fire('ExecCommand', {command: e.command}); 843 | e.preventDefault(); 844 | return true; 845 | } 846 | }); 847 | 848 | editor.addCommand('InsertUnorderedList', function(ui, detail) { 849 | toggleList('UL', detail); 850 | }); 851 | 852 | editor.addCommand('InsertOrderedList', function(ui, detail) { 853 | toggleList('OL', detail); 854 | }); 855 | 856 | editor.addCommand('InsertDefinitionList', function(ui, detail) { 857 | toggleList('DL', detail); 858 | }); 859 | 860 | editor.addQueryStateHandler('InsertUnorderedList', queryListCommandState('UL')); 861 | editor.addQueryStateHandler('InsertOrderedList', queryListCommandState('OL')); 862 | editor.addQueryStateHandler('InsertDefinitionList', queryListCommandState('DL')); 863 | 864 | editor.on('keydown', function(e) { 865 | // Check for tab but not ctrl/cmd+tab since it switches browser tabs 866 | if (e.keyCode != 9 || tinymce.util.VK.metaKeyPressed(e)) { 867 | return; 868 | } 869 | 870 | if (editor.dom.getParent(editor.selection.getStart(), 'LI,DT,DD')) { 871 | e.preventDefault(); 872 | 873 | if (e.shiftKey) { 874 | outdentSelection(); 875 | } else { 876 | indentSelection(); 877 | } 878 | } 879 | }); 880 | }); 881 | 882 | editor.addButton('indent', { 883 | icon: 'indent', 884 | title: 'Increase indent', 885 | cmd: 'Indent', 886 | onPostRender: function() { 887 | var ctrl = this; 888 | 889 | editor.on('nodechange', function() { 890 | var blocks = editor.selection.getSelectedBlocks(); 891 | var disable = false; 892 | 893 | for (var i = 0, l = blocks.length; !disable && i < l; i++) { 894 | var tag = blocks[i].nodeName; 895 | 896 | disable = (tag == 'LI' && isFirstChild(blocks[i]) || tag == 'UL' || tag == 'OL' || tag == 'DD'); 897 | } 898 | 899 | ctrl.disabled(disable); 900 | }); 901 | } 902 | }); 903 | 904 | editor.on('keydown', function(e) { 905 | if (e.keyCode == tinymce.util.VK.BACKSPACE) { 906 | if (self.backspaceDelete()) { 907 | e.preventDefault(); 908 | } 909 | } else if (e.keyCode == tinymce.util.VK.DELETE) { 910 | if (self.backspaceDelete(true)) { 911 | e.preventDefault(); 912 | } 913 | } 914 | }); 915 | }); 916 | -------------------------------------------------------------------------------- /vendor/mce-view.js: -------------------------------------------------------------------------------- 1 | /* global tinymce */ 2 | 3 | /* 4 | * The TinyMCE view API. 5 | * 6 | * Note: this API is "experimental" meaning that it will probably change 7 | * in the next few releases based on feedback from 3.9.0. 8 | * If you decide to use it, please follow the development closely. 9 | * 10 | * Diagram 11 | * 12 | * |- registered view constructor (type) 13 | * | |- view instance (unique text) 14 | * | | |- editor 1 15 | * | | | |- view node 16 | * | | | |- view node 17 | * | | | |- ... 18 | * | | |- editor 2 19 | * | | | |- ... 20 | * | |- view instance 21 | * | | |- ... 22 | * |- registered view 23 | * | |- ... 24 | */ 25 | ( function( window, wp, shortcode, $ ) { 26 | 'use strict'; 27 | 28 | var views = {}, 29 | instances = {}; 30 | 31 | wp.mce = wp.mce || {}; 32 | 33 | /** 34 | * wp.mce.views 35 | * 36 | * A set of utilities that simplifies adding custom UI within a TinyMCE editor. 37 | * At its core, it serves as a series of converters, transforming text to a 38 | * custom UI, and back again. 39 | */ 40 | wp.mce.views = { 41 | 42 | /** 43 | * Registers a new view type. 44 | * 45 | * @param {String} type The view type. 46 | * @param {Object} extend An object to extend wp.mce.View.prototype with. 47 | */ 48 | register: function( type, extend ) { 49 | views[ type ] = wp.mce.View.extend( _.extend( extend, { type: type } ) ); 50 | }, 51 | 52 | /** 53 | * Unregisters a view type. 54 | * 55 | * @param {String} type The view type. 56 | */ 57 | unregister: function( type ) { 58 | delete views[ type ]; 59 | }, 60 | 61 | /** 62 | * Returns the settings of a view type. 63 | * 64 | * @param {String} type The view type. 65 | * 66 | * @return {Function} The view constructor. 67 | */ 68 | get: function( type ) { 69 | return views[ type ]; 70 | }, 71 | 72 | /** 73 | * Unbinds all view nodes. 74 | * Runs before removing all view nodes from the DOM. 75 | */ 76 | unbind: function() { 77 | _.each( instances, function( instance ) { 78 | instance.unbind(); 79 | } ); 80 | }, 81 | 82 | /** 83 | * Scans a given string for each view's pattern, 84 | * replacing any matches with markers, 85 | * and creates a new instance for every match. 86 | * 87 | * @param {String} content The string to scan. 88 | * 89 | * @return {String} The string with markers. 90 | */ 91 | setMarkers: function( content ) { 92 | var pieces = [ { content: content } ], 93 | self = this, 94 | instance, current; 95 | 96 | _.each( views, function( view, type ) { 97 | current = pieces.slice(); 98 | pieces = []; 99 | 100 | _.each( current, function( piece ) { 101 | var remaining = piece.content, 102 | result, text; 103 | 104 | // Ignore processed pieces, but retain their location. 105 | if ( piece.processed ) { 106 | pieces.push( piece ); 107 | return; 108 | } 109 | 110 | // Iterate through the string progressively matching views 111 | // and slicing the string as we go. 112 | while ( remaining && ( result = view.prototype.match( remaining ) ) ) { 113 | // Any text before the match becomes an unprocessed piece. 114 | if ( result.index ) { 115 | pieces.push( { content: remaining.substring( 0, result.index ) } ); 116 | } 117 | 118 | instance = self.createInstance( type, result.content, result.options ); 119 | text = instance.loader ? '.' : instance.text; 120 | 121 | // Add the processed piece for the match. 122 | pieces.push( { 123 | content: instance.ignore ? text : '

    ' + text + '

    ', 124 | processed: true 125 | } ); 126 | 127 | // Update the remaining content. 128 | remaining = remaining.slice( result.index + result.content.length ); 129 | } 130 | 131 | // There are no additional matches. 132 | // If any content remains, add it as an unprocessed piece. 133 | if ( remaining ) { 134 | pieces.push( { content: remaining } ); 135 | } 136 | } ); 137 | } ); 138 | 139 | content = _.pluck( pieces, 'content' ).join( '' ); 140 | return content.replace( /

    \s*

    ' ); 141 | }, 142 | 143 | /** 144 | * Create a view instance. 145 | * 146 | * @param {String} type The view type. 147 | * @param {String} text The textual representation of the view. 148 | * @param {Object} options Options. 149 | * @param {Boolean} force Recreate the instance. Optional. 150 | * 151 | * @return {wp.mce.View} The view instance. 152 | */ 153 | createInstance: function( type, text, options, force ) { 154 | var View = this.get( type ), 155 | encodedText, 156 | instance; 157 | 158 | text = tinymce.DOM.decode( text ); 159 | 160 | if ( ! force ) { 161 | instance = this.getInstance( text ); 162 | 163 | if ( instance ) { 164 | return instance; 165 | } 166 | } 167 | 168 | encodedText = encodeURIComponent( text ); 169 | 170 | options = _.extend( options || {}, { 171 | text: text, 172 | encodedText: encodedText 173 | } ); 174 | 175 | return instances[ encodedText ] = new View( options ); 176 | }, 177 | 178 | /** 179 | * Get a view instance. 180 | * 181 | * @param {(String|HTMLElement)} object The textual representation of the view or the view node. 182 | * 183 | * @return {wp.mce.View} The view instance or undefined. 184 | */ 185 | getInstance: function( object ) { 186 | if ( typeof object === 'string' ) { 187 | return instances[ encodeURIComponent( object ) ]; 188 | } 189 | 190 | return instances[ $( object ).attr( 'data-wpview-text' ) ]; 191 | }, 192 | 193 | /** 194 | * Given a view node, get the view's text. 195 | * 196 | * @param {HTMLElement} node The view node. 197 | * 198 | * @return {String} The textual representation of the view. 199 | */ 200 | getText: function( node ) { 201 | return decodeURIComponent( $( node ).attr( 'data-wpview-text' ) || '' ); 202 | }, 203 | 204 | /** 205 | * Renders all view nodes that are not yet rendered. 206 | * 207 | * @param {Boolean} force Rerender all view nodes. 208 | */ 209 | render: function( force ) { 210 | _.each( instances, function( instance ) { 211 | instance.render( force ); 212 | } ); 213 | }, 214 | 215 | /** 216 | * Update the text of a given view node. 217 | * 218 | * @param {String} text The new text. 219 | * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in. 220 | * @param {HTMLElement} node The view node to update. 221 | * @param {Boolean} force Recreate the instance. Optional. 222 | */ 223 | update: function( text, editor, node, force ) { 224 | var instance = this.getInstance( node ); 225 | 226 | if ( instance ) { 227 | instance.update( text, editor, node, force ); 228 | } 229 | }, 230 | 231 | /** 232 | * Renders any editing interface based on the view type. 233 | * 234 | * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in. 235 | * @param {HTMLElement} node The view node to edit. 236 | */ 237 | edit: function( editor, node ) { 238 | var instance = this.getInstance( node ); 239 | 240 | if ( instance && instance.edit ) { 241 | instance.edit( instance.text, function( text, force ) { 242 | instance.update( text, editor, node, force ); 243 | } ); 244 | } 245 | }, 246 | 247 | /** 248 | * Remove a given view node from the DOM. 249 | * 250 | * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in. 251 | * @param {HTMLElement} node The view node to remove. 252 | */ 253 | remove: function( editor, node ) { 254 | var instance = this.getInstance( node ); 255 | 256 | if ( instance ) { 257 | instance.remove( editor, node ); 258 | } 259 | } 260 | }; 261 | 262 | /** 263 | * A Backbone-like View constructor intended for use when rendering a TinyMCE View. 264 | * The main difference is that the TinyMCE View is not tied to a particular DOM node. 265 | * 266 | * @param {Object} options Options. 267 | */ 268 | wp.mce.View = function( options ) { 269 | _.extend( this, options ); 270 | this.initialize(); 271 | }; 272 | 273 | wp.mce.View.extend = Backbone.View.extend; 274 | 275 | _.extend( wp.mce.View.prototype, { 276 | 277 | /** 278 | * The content. 279 | * 280 | * @type {*} 281 | */ 282 | content: null, 283 | 284 | /** 285 | * Whether or not to display a loader. 286 | * 287 | * @type {Boolean} 288 | */ 289 | loader: true, 290 | 291 | /** 292 | * Runs after the view instance is created. 293 | */ 294 | initialize: function() {}, 295 | 296 | /** 297 | * Retuns the content to render in the view node. 298 | * 299 | * @return {*} 300 | */ 301 | getContent: function() { 302 | return this.content; 303 | }, 304 | 305 | /** 306 | * Renders all view nodes tied to this view instance that are not yet rendered. 307 | * 308 | * @param {String} content The content to render. Optional. 309 | * @param {Boolean} force Rerender all view nodes tied to this view instance. Optional. 310 | */ 311 | render: function( content, force ) { 312 | if ( content != null ) { 313 | this.content = content; 314 | } 315 | 316 | content = this.getContent(); 317 | 318 | // If there's nothing to render an no loader needs to be shown, stop. 319 | if ( ! this.loader && ! content ) { 320 | return; 321 | } 322 | 323 | // We're about to rerender all views of this instance, so unbind rendered views. 324 | force && this.unbind(); 325 | 326 | // Replace any left over markers. 327 | this.replaceMarkers(); 328 | 329 | if ( content ) { 330 | this.setContent( content, function( editor, node ) { 331 | $( node ).data( 'rendered', true ); 332 | this.bindNode.call( this, editor, node ); 333 | }, force ? null : false ); 334 | } else { 335 | this.setLoader(); 336 | } 337 | }, 338 | 339 | /** 340 | * Binds a given node after its content is added to the DOM. 341 | */ 342 | bindNode: function() {}, 343 | 344 | /** 345 | * Unbinds a given node before its content is removed from the DOM. 346 | */ 347 | unbindNode: function() {}, 348 | 349 | /** 350 | * Unbinds all view nodes tied to this view instance. 351 | * Runs before their content is removed from the DOM. 352 | */ 353 | unbind: function() { 354 | this.getNodes( function( editor, node ) { 355 | this.unbindNode.call( this, editor, node ); 356 | }, true ); 357 | }, 358 | 359 | /** 360 | * Gets all the TinyMCE editor instances that support views. 361 | * 362 | * @param {Function} callback A callback. 363 | */ 364 | getEditors: function( callback ) { 365 | _.each( tinymce.editors, function( editor ) { 366 | if ( editor.plugins.wpview ) { 367 | callback.call( this, editor ); 368 | } 369 | }, this ); 370 | }, 371 | 372 | /** 373 | * Gets all view nodes tied to this view instance. 374 | * 375 | * @param {Function} callback A callback. 376 | * @param {Boolean} rendered Get (un)rendered view nodes. Optional. 377 | */ 378 | getNodes: function( callback, rendered ) { 379 | this.getEditors( function( editor ) { 380 | var self = this; 381 | 382 | $( editor.getBody() ) 383 | .find( '[data-wpview-text="' + self.encodedText + '"]' ) 384 | .filter( function() { 385 | var data; 386 | 387 | if ( rendered == null ) { 388 | return true; 389 | } 390 | 391 | data = $( this ).data( 'rendered' ) === true; 392 | 393 | return rendered ? data : ! data; 394 | } ) 395 | .each( function() { 396 | callback.call( self, editor, this, this /* back compat */ ); 397 | } ); 398 | } ); 399 | }, 400 | 401 | /** 402 | * Gets all marker nodes tied to this view instance. 403 | * 404 | * @param {Function} callback A callback. 405 | */ 406 | getMarkers: function( callback ) { 407 | this.getEditors( function( editor ) { 408 | var self = this; 409 | 410 | $( editor.getBody() ) 411 | .find( '[data-wpview-marker="' + this.encodedText + '"]' ) 412 | .each( function() { 413 | callback.call( self, editor, this ); 414 | } ); 415 | } ); 416 | }, 417 | 418 | /** 419 | * Replaces all marker nodes tied to this view instance. 420 | */ 421 | replaceMarkers: function() { 422 | this.getMarkers( function( editor, node ) { 423 | var $viewNode; 424 | 425 | if ( ! this.loader && $( node ).text() !== this.text ) { 426 | editor.dom.setAttrib( node, 'data-wpview-marker', null ); 427 | return; 428 | } 429 | 430 | $viewNode = editor.$( 431 | '

    ' 432 | ); 433 | 434 | editor.$( node ).replaceWith( $viewNode ); 435 | } ); 436 | }, 437 | 438 | /** 439 | * Removes all marker nodes tied to this view instance. 440 | */ 441 | removeMarkers: function() { 442 | this.getMarkers( function( editor, node ) { 443 | editor.dom.setAttrib( node, 'data-wpview-marker', null ); 444 | } ); 445 | }, 446 | 447 | /** 448 | * Sets the content for all view nodes tied to this view instance. 449 | * 450 | * @param {*} content The content to set. 451 | * @param {Function} callback A callback. Optional. 452 | * @param {Boolean} rendered Only set for (un)rendered nodes. Optional. 453 | */ 454 | setContent: function( content, callback, rendered ) { 455 | if ( _.isObject( content ) && content.body.indexOf( ' Visual. 516 | setTimeout( function() { 517 | var iframe, iframeWin, iframeDoc, MutationObserver, observer, i, block; 518 | 519 | editor.undoManager.transact( function() { 520 | node.innerHTML = ''; 521 | 522 | iframe = dom.add( node, 'iframe', { 523 | /* jshint scripturl: true */ 524 | src: tinymce.Env.ie ? 'javascript:""' : '', 525 | frameBorder: '0', 526 | allowTransparency: 'true', 527 | scrolling: 'no', 528 | 'class': 'wpview-sandbox', 529 | style: { 530 | width: '100%', 531 | display: 'block' 532 | }, 533 | height: self.iframeHeight 534 | } ); 535 | 536 | dom.add( node, 'span', { 'class': 'mce-shim' } ); 537 | dom.add( node, 'span', { 'class': 'wpview-end' } ); 538 | } ); 539 | 540 | // Bail if the iframe node is not attached to the DOM. 541 | // Happens when the view is dragged in the editor. 542 | // There is a browser restriction when iframes are moved in the DOM. They get emptied. 543 | // The iframe will be rerendered after dropping the view node at the new location. 544 | if ( ! iframe.contentWindow ) { 545 | return; 546 | } 547 | 548 | iframeWin = iframe.contentWindow; 549 | iframeDoc = iframeWin.document; 550 | iframeDoc.open(); 551 | 552 | iframeDoc.write( 553 | '' + 554 | '' + 555 | '' + 556 | '' + 557 | head + 558 | styles + 559 | '' + 576 | '' + 577 | '' + 578 | body + 579 | '' + 580 | '' 581 | ); 582 | 583 | iframeDoc.close(); 584 | 585 | function resize() { 586 | var $iframe; 587 | 588 | if ( block ) { 589 | return; 590 | } 591 | 592 | // Make sure the iframe still exists. 593 | if ( iframe.contentWindow ) { 594 | $iframe = $( iframe ); 595 | self.iframeHeight = $( iframeDoc.body ).height(); 596 | 597 | if ( $iframe.height() !== self.iframeHeight ) { 598 | $iframe.height( self.iframeHeight ); 599 | editor.nodeChanged(); 600 | } 601 | } 602 | } 603 | 604 | if ( self.iframeHeight ) { 605 | block = true; 606 | 607 | setTimeout( function() { 608 | block = false; 609 | resize(); 610 | }, 3000 ); 611 | } 612 | 613 | $( iframeWin ).on( 'load', resize ); 614 | 615 | MutationObserver = iframeWin.MutationObserver || iframeWin.WebKitMutationObserver || iframeWin.MozMutationObserver; 616 | 617 | if ( MutationObserver ) { 618 | observer = new MutationObserver( _.debounce( resize, 100 ) ); 619 | 620 | observer.observe( iframeDoc.body, { 621 | attributes: true, 622 | childList: true, 623 | subtree: true 624 | } ); 625 | } else { 626 | for ( i = 1; i < 6; i++ ) { 627 | setTimeout( resize, i * 700 ); 628 | } 629 | } 630 | 631 | callback && callback.call( self, editor, node ); 632 | }, 50 ); 633 | }, rendered ); 634 | }, 635 | 636 | /** 637 | * Sets a loader for all view nodes tied to this view instance. 638 | */ 639 | setLoader: function() { 640 | this.setContent( 641 | '
    ' + 642 | '
    ' + 643 | '
    ' + 644 | '
    ' 645 | ); 646 | }, 647 | 648 | /** 649 | * Sets an error for all view nodes tied to this view instance. 650 | * 651 | * @param {String} message The error message to set. 652 | * @param {String} dashicon A dashicon ID. Optional. {@link https://developer.wordpress.org/resource/dashicons/} 653 | */ 654 | setError: function( message, dashicon ) { 655 | this.setContent( 656 | '
    ' + 657 | '
    ' + 658 | '

    ' + message + '

    ' + 659 | '
    ' 660 | ); 661 | }, 662 | 663 | /** 664 | * Tries to find a text match in a given string. 665 | * 666 | * @param {String} content The string to scan. 667 | * 668 | * @return {Object} 669 | */ 670 | match: function( content ) { 671 | var match = shortcode.next( this.type, content ); 672 | 673 | if ( match ) { 674 | return { 675 | index: match.index, 676 | content: match.content, 677 | options: { 678 | shortcode: match.shortcode 679 | } 680 | }; 681 | } 682 | }, 683 | 684 | /** 685 | * Update the text of a given view node. 686 | * 687 | * @param {String} text The new text. 688 | * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in. 689 | * @param {HTMLElement} node The view node to update. 690 | * @param {Boolean} force Recreate the instance. Optional. 691 | */ 692 | update: function( text, editor, node, force ) { 693 | _.find( views, function( view, type ) { 694 | var match = view.prototype.match( text ); 695 | 696 | if ( match ) { 697 | $( node ).data( 'rendered', false ); 698 | editor.dom.setAttrib( node, 'data-wpview-text', encodeURIComponent( text ) ); 699 | wp.mce.views.createInstance( type, text, match.options, force ).render(); 700 | editor.focus(); 701 | 702 | return true; 703 | } 704 | } ); 705 | }, 706 | 707 | /** 708 | * Remove a given view node from the DOM. 709 | * 710 | * @param {tinymce.Editor} editor The TinyMCE editor instance the view node is in. 711 | * @param {HTMLElement} node The view node to remove. 712 | */ 713 | remove: function( editor, node ) { 714 | this.unbindNode.call( this, editor, node ); 715 | editor.dom.remove( node ); 716 | editor.focus(); 717 | } 718 | } ); 719 | } )( window, window.wp, window.wp.shortcode, window.jQuery ); 720 | 721 | /* 722 | * The WordPress core TinyMCE views. 723 | * Views for the gallery, audio, video, playlist and embed shortcodes, 724 | * and a view for embeddable URLs. 725 | */ 726 | ( function( window, views, media, $ ) { 727 | var base, gallery, av, embed, 728 | schema, parser, serializer; 729 | 730 | function verifyHTML( string ) { 731 | var settings = {}; 732 | 733 | if ( ! window.tinymce ) { 734 | return string.replace( /<[^>]+>/g, '' ); 735 | } 736 | 737 | if ( ! string || ( string.indexOf( '<' ) === -1 && string.indexOf( '>' ) === -1 ) ) { 738 | return string; 739 | } 740 | 741 | schema = schema || new window.tinymce.html.Schema( settings ); 742 | parser = parser || new window.tinymce.html.DomParser( settings, schema ); 743 | serializer = serializer || new window.tinymce.html.Serializer( settings, schema ); 744 | 745 | return serializer.serialize( parser.parse( string, { forced_root_block: false } ) ); 746 | } 747 | 748 | base = { 749 | state: [], 750 | 751 | edit: function( text, update ) { 752 | var type = this.type, 753 | frame = media[ type ].edit( text ); 754 | 755 | this.pausePlayers && this.pausePlayers(); 756 | 757 | _.each( this.state, function( state ) { 758 | frame.state( state ).on( 'update', function( selection ) { 759 | update( media[ type ].shortcode( selection ).string(), type === 'gallery' ); 760 | } ); 761 | } ); 762 | 763 | frame.on( 'close', function() { 764 | frame.detach(); 765 | } ); 766 | 767 | frame.open(); 768 | } 769 | }; 770 | 771 | gallery = _.extend( {}, base, { 772 | state: [ 'gallery-edit' ], 773 | template: media.template( 'editor-gallery' ), 774 | 775 | initialize: function() { 776 | var attachments = media.gallery.attachments( this.shortcode, media.view.settings.post.id ), 777 | attrs = this.shortcode.attrs.named, 778 | self = this; 779 | 780 | attachments.more() 781 | .done( function() { 782 | attachments = attachments.toJSON(); 783 | 784 | _.each( attachments, function( attachment ) { 785 | if ( attachment.sizes ) { 786 | if ( attrs.size && attachment.sizes[ attrs.size ] ) { 787 | attachment.thumbnail = attachment.sizes[ attrs.size ]; 788 | } else if ( attachment.sizes.thumbnail ) { 789 | attachment.thumbnail = attachment.sizes.thumbnail; 790 | } else if ( attachment.sizes.full ) { 791 | attachment.thumbnail = attachment.sizes.full; 792 | } 793 | } 794 | } ); 795 | 796 | self.render( self.template( { 797 | verifyHTML: verifyHTML, 798 | attachments: attachments, 799 | columns: attrs.columns ? parseInt( attrs.columns, 10 ) : media.galleryDefaults.columns 800 | } ) ); 801 | } ) 802 | .fail( function( jqXHR, textStatus ) { 803 | self.setError( textStatus ); 804 | } ); 805 | } 806 | } ); 807 | 808 | av = _.extend( {}, base, { 809 | action: 'parse-media-shortcode', 810 | 811 | initialize: function() { 812 | var self = this; 813 | 814 | if ( this.url ) { 815 | this.loader = false; 816 | this.shortcode = media.embed.shortcode( { 817 | url: this.text 818 | } ); 819 | } 820 | 821 | wp.ajax.post( this.action, { 822 | post_ID: media.view.settings.post.id, 823 | type: this.shortcode.tag, 824 | shortcode: this.shortcode.string() 825 | } ) 826 | .done( function( response ) { 827 | self.render( response ); 828 | } ) 829 | .fail( function( response ) { 830 | if ( self.url ) { 831 | self.ignore = true; 832 | self.removeMarkers(); 833 | } else { 834 | self.setError( response.message || response.statusText, 'admin-media' ); 835 | } 836 | } ); 837 | 838 | this.getEditors( function( editor ) { 839 | editor.on( 'wpview-selected', function() { 840 | self.pausePlayers(); 841 | } ); 842 | } ); 843 | }, 844 | 845 | pausePlayers: function() { 846 | this.getNodes( function( editor, node, content ) { 847 | var win = $( 'iframe.wpview-sandbox', content ).get( 0 ); 848 | 849 | if ( win && ( win = win.contentWindow ) && win.mejs ) { 850 | _.each( win.mejs.players, function( player ) { 851 | try { 852 | player.pause(); 853 | } catch ( e ) {} 854 | } ); 855 | } 856 | } ); 857 | } 858 | } ); 859 | 860 | embed = _.extend( {}, av, { 861 | action: 'parse-embed', 862 | 863 | edit: function( text, update ) { 864 | var frame = media.embed.edit( text, this.url ), 865 | self = this; 866 | 867 | this.pausePlayers(); 868 | 869 | frame.state( 'embed' ).props.on( 'change:url', function( model, url ) { 870 | if ( url && model.get( 'url' ) ) { 871 | frame.state( 'embed' ).metadata = model.toJSON(); 872 | } 873 | } ); 874 | 875 | frame.state( 'embed' ).on( 'select', function() { 876 | var data = frame.state( 'embed' ).metadata; 877 | 878 | if ( self.url ) { 879 | update( data.url ); 880 | } else { 881 | update( media.embed.shortcode( data ).string() ); 882 | } 883 | } ); 884 | 885 | frame.on( 'close', function() { 886 | frame.detach(); 887 | } ); 888 | 889 | frame.open(); 890 | } 891 | } ); 892 | 893 | views.register( 'gallery', _.extend( {}, gallery ) ); 894 | 895 | views.register( 'audio', _.extend( {}, av, { 896 | state: [ 'audio-details' ] 897 | } ) ); 898 | 899 | views.register( 'video', _.extend( {}, av, { 900 | state: [ 'video-details' ] 901 | } ) ); 902 | 903 | views.register( 'playlist', _.extend( {}, av, { 904 | state: [ 'playlist-edit', 'video-playlist-edit' ] 905 | } ) ); 906 | 907 | views.register( 'embed', _.extend( {}, embed ) ); 908 | 909 | views.register( 'embedURL', _.extend( {}, embed, { 910 | match: function( content ) { 911 | var re = /(^|

    )(https?:\/\/[^\s"]+?)(<\/p>\s*|$)/gi, 912 | match = re.exec( content ); 913 | 914 | if ( match ) { 915 | return { 916 | index: match.index + match[1].length, 917 | content: match[2], 918 | options: { 919 | url: true 920 | } 921 | }; 922 | } 923 | } 924 | } ) ); 925 | } )( window, window.wp.mce.views, window.wp.media, window.jQuery ); 926 | -------------------------------------------------------------------------------- /vendor/wordpress.js: -------------------------------------------------------------------------------- 1 | /* global getUserSetting, setUserSetting */ 2 | ( function( tinymce ) { 3 | // Set the minimum value for the modals z-index higher than #wpadminbar (100000) 4 | tinymce.ui.FloatPanel.zIndex = 100100; 5 | 6 | tinymce.PluginManager.add( 'wordpress', function( editor ) { 7 | var wpAdvButton, style, 8 | DOM = tinymce.DOM, 9 | each = tinymce.each, 10 | __ = editor.editorManager.i18n.translate, 11 | $ = window.jQuery, 12 | wp = window.wp, 13 | hasWpautop = ( wp && wp.editor && wp.editor.autop && editor.getParam( 'wpautop', true ) ); 14 | 15 | if ( $ ) { 16 | $( document ).triggerHandler( 'tinymce-editor-setup', [ editor ] ); 17 | } 18 | 19 | function toggleToolbars( state ) { 20 | var iframe, initial, toolbars, 21 | pixels = 0; 22 | 23 | initial = ( state === 'hide' ); 24 | 25 | if ( editor.theme.panel ) { 26 | toolbars = editor.theme.panel.find('.toolbar:not(.menubar)'); 27 | } 28 | 29 | if ( ! toolbars || toolbars.length < 2 || ( state === 'hide' && ! toolbars[1].visible() ) ) { 30 | return; 31 | } 32 | 33 | if ( ! state && toolbars[1].visible() ) { 34 | state = 'hide'; 35 | } 36 | 37 | each( toolbars, function( toolbar, i ) { 38 | if ( i > 0 ) { 39 | if ( state === 'hide' ) { 40 | toolbar.hide(); 41 | pixels += 30; 42 | } else { 43 | toolbar.show(); 44 | pixels -= 30; 45 | } 46 | } 47 | }); 48 | 49 | if ( pixels && ! initial ) { 50 | // Resize iframe, not needed in iOS 51 | if ( ! tinymce.Env.iOS ) { 52 | iframe = editor.getContentAreaContainer().firstChild; 53 | DOM.setStyle( iframe, 'height', iframe.clientHeight + pixels ); 54 | } 55 | 56 | if ( state === 'hide' ) { 57 | setUserSetting('hidetb', '0'); 58 | wpAdvButton && wpAdvButton.active( false ); 59 | } else { 60 | setUserSetting('hidetb', '1'); 61 | wpAdvButton && wpAdvButton.active( true ); 62 | } 63 | } 64 | 65 | editor.fire( 'wp-toolbar-toggle' ); 66 | } 67 | 68 | // Add the kitchen sink button :) 69 | editor.addButton( 'wp_adv', { 70 | tooltip: 'Toolbar Toggle', 71 | cmd: 'WP_Adv', 72 | onPostRender: function() { 73 | wpAdvButton = this; 74 | wpAdvButton.active( getUserSetting( 'hidetb' ) === '1' ? true : false ); 75 | } 76 | }); 77 | 78 | // Hide the toolbars after loading 79 | editor.on( 'PostRender', function() { 80 | if ( editor.getParam( 'wordpress_adv_hidden', true ) && getUserSetting( 'hidetb', '0' ) === '0' ) { 81 | toggleToolbars( 'hide' ); 82 | } 83 | }); 84 | 85 | editor.addCommand( 'WP_Adv', function() { 86 | toggleToolbars(); 87 | }); 88 | 89 | editor.on( 'focus', function() { 90 | window.wpActiveEditor = editor.id; 91 | }); 92 | 93 | editor.on( 'BeforeSetContent', function( event ) { 94 | var title; 95 | 96 | if ( event.content ) { 97 | if ( event.content.indexOf( '/g, function( match, moretext ) { 101 | return ''; 103 | }); 104 | } 105 | 106 | if ( event.content.indexOf( '' ) !== -1 ) { 107 | title = __( 'Page break' ); 108 | 109 | event.content = event.content.replace( //g, 110 | '' ); 112 | } 113 | 114 | if ( event.load && event.format !== 'raw' && hasWpautop ) { 115 | event.content = wp.editor.autop( event.content ); 116 | } 117 | 118 | if ( event.content.indexOf( ']*>[\s\S]*?<\/\1>/g, function( match, tag ) { 120 | return ''; 130 | } ); 131 | } 132 | 133 | // Remove spaces from empty paragraphs. 134 | // Avoid backtracking, can freeze the editor. See #35890. 135 | // (This is also quite faster than using only one regex.) 136 | event.content = event.content.replace( /

    ([^<>]+)<\/p>/gi, function( tag, text ) { 137 | if ( /^( |\s|\u00a0|\ufeff)+$/i.test( text ) ) { 138 | return '


    '; 139 | } 140 | 141 | return tag; 142 | }); 143 | } 144 | }); 145 | 146 | editor.on( 'PostProcess', function( event ) { 147 | if ( event.get ) { 148 | event.content = event.content.replace(/]+>/g, function( image ) { 149 | var match, 150 | string, 151 | moretext = ''; 152 | 153 | if ( image.indexOf( 'data-wp-more="more"' ) !== -1 ) { 154 | if ( match = image.match( /data-wp-more-text="([^"]+)"/ ) ) { 155 | moretext = match[1]; 156 | } 157 | 158 | string = ''; 159 | } else if ( image.indexOf( 'data-wp-more="nextpage"' ) !== -1 ) { 160 | string = ''; 161 | } else if ( image.indexOf( 'data-wp-preserve' ) !== -1 ) { 162 | if ( match = image.match( / data-wp-preserve="([^"]+)"/ ) ) { 163 | string = decodeURIComponent( match[1] ); 164 | } 165 | } 166 | 167 | return string || image; 168 | }); 169 | } 170 | }); 171 | 172 | // Display the tag name instead of img in element path 173 | editor.on( 'ResolveName', function( event ) { 174 | var attr; 175 | 176 | if ( event.target.nodeName === 'IMG' && ( attr = editor.dom.getAttrib( event.target, 'data-wp-more' ) ) ) { 177 | event.name = attr; 178 | } 179 | }); 180 | 181 | // Register commands 182 | editor.addCommand( 'WP_More', function( tag ) { 183 | var parent, html, title, 184 | classname = 'wp-more-tag', 185 | dom = editor.dom, 186 | node = editor.selection.getNode(); 187 | 188 | tag = tag || 'more'; 189 | classname += ' mce-wp-' + tag; 190 | title = tag === 'more' ? 'Read more...' : 'Next page'; 191 | title = __( title ); 192 | html = ''; 194 | 195 | // Most common case 196 | if ( node.nodeName === 'BODY' || ( node.nodeName === 'P' && node.parentNode.nodeName === 'BODY' ) ) { 197 | editor.insertContent( html ); 198 | return; 199 | } 200 | 201 | // Get the top level parent node 202 | parent = dom.getParent( node, function( found ) { 203 | if ( found.parentNode && found.parentNode.nodeName === 'BODY' ) { 204 | return true; 205 | } 206 | 207 | return false; 208 | }, editor.getBody() ); 209 | 210 | if ( parent ) { 211 | if ( parent.nodeName === 'P' ) { 212 | parent.appendChild( dom.create( 'p', null, html ).firstChild ); 213 | } else { 214 | dom.insertAfter( dom.create( 'p', null, html ), parent ); 215 | } 216 | 217 | editor.nodeChanged(); 218 | } 219 | }); 220 | 221 | editor.addCommand( 'WP_Code', function() { 222 | editor.formatter.toggle('code'); 223 | }); 224 | 225 | editor.addCommand( 'WP_Page', function() { 226 | editor.execCommand( 'WP_More', 'nextpage' ); 227 | }); 228 | 229 | editor.addCommand( 'WP_Help', function() { 230 | var access = tinymce.Env.mac ? __( 'Ctrl + Alt + letter:' ) : __( 'Shift + Alt + letter:' ), 231 | meta = tinymce.Env.mac ? __( 'Cmd + letter:' ) : __( 'Ctrl + letter:' ), 232 | table1 = [], 233 | table2 = [], 234 | header, html, dialog, $wrap; 235 | 236 | each( [ 237 | { c: 'Copy', x: 'Cut' }, 238 | { v: 'Paste', a: 'Select all' }, 239 | { z: 'Undo', y: 'Redo' }, 240 | { b: 'Bold', i: 'Italic' }, 241 | { u: 'Underline', k: 'Insert/edit link' } 242 | ], function( row ) { 243 | table1.push( tr( row ) ); 244 | } ); 245 | 246 | each( [ 247 | { 1: 'Heading 1', 2: 'Heading 2' }, 248 | { 3: 'Heading 3', 4: 'Heading 4' }, 249 | { 5: 'Heading 5', 6: 'Heading 6' }, 250 | { l: 'Align left', c: 'Align center' }, 251 | { r: 'Align right', j: 'Justify' }, 252 | { d: 'Strikethrough', q: 'Blockquote' }, 253 | { u: 'Bullet list', o: 'Numbered list' }, 254 | { a: 'Insert/edit link', s: 'Remove link' }, 255 | { m: 'Insert/edit image', t: 'Insert Read More tag' }, 256 | { h: 'Keyboard Shortcuts', x: 'Code' }, 257 | { p: 'Insert Page Break tag', w: 'Distraction-free writing mode' } 258 | ], function( row ) { 259 | table2.push( tr( row ) ); 260 | } ); 261 | 262 | function tr( row ) { 263 | var out = ''; 264 | 265 | each( row, function( text, key ) { 266 | if ( ! text ) { 267 | out += ''; 268 | } else { 269 | out += '' + key + '' + __( text ) + ''; 270 | } 271 | }); 272 | 273 | return out + ''; 274 | } 275 | 276 | header = [ __( 'Letter' ), __( 'Action' ), __( 'Letter' ), __( 'Action' ) ]; 277 | header = '' + header.join( '' ) + ''; 278 | 279 | html = '
    '; 280 | 281 | // Main section, default and additional shortcuts 282 | html = html + 283 | '

    ' + __( 'Default shortcuts,' ) + ' ' + meta + '

    ' + 284 | '' + 285 | header + 286 | table1.join('') + 287 | '
    ' + 288 | '

    ' + __( 'Additional shortcuts,' ) + ' ' + access + '

    ' + 289 | '' + 290 | header + 291 | table2.join('') + 292 | '
    '; 293 | 294 | if ( editor.plugins.wptextpattern && ( ! tinymce.Env.ie || tinymce.Env.ie > 8 ) ) { 295 | // Text pattern section 296 | html = html + 297 | '

    ' + __( 'When starting a new paragraph with one of these formatting shortcuts followed by a space, the formatting will be applied automatically. Press Backspace or Escape to undo.' ) + '

    ' + 298 | '' + 299 | tr({ '*': 'Bullet list', '1.': 'Numbered list' }) + 300 | tr({ '-': 'Bullet list', '1)': 'Numbered list' }) + 301 | '
    '; 302 | 303 | html = html + 304 | '

    ' + __( 'The following formatting shortcuts are replaced when pressing Enter. Press Escape or the Undo button to undo.' ) + '

    ' + 305 | '' + 306 | tr({ '>': 'Blockquote' }) + 307 | tr({ '##': 'Heading 2' }) + 308 | tr({ '###': 'Heading 3' }) + 309 | tr({ '####': 'Heading 4' }) + 310 | tr({ '#####': 'Heading 5' }) + 311 | tr({ '######': 'Heading 6' }) + 312 | tr({ '---': 'Horizontal line' }) + 313 | '
    '; 314 | } 315 | 316 | // Focus management section 317 | html = html + 318 | '

    ' + __( 'Focus shortcuts:' ) + '

    ' + 319 | '' + 320 | tr({ 'Alt + F8': 'Inline toolbar (when an image, link or preview is selected)' }) + 321 | tr({ 'Alt + F9': 'Editor menu (when enabled)' }) + 322 | tr({ 'Alt + F10': 'Editor toolbar' }) + 323 | tr({ 'Alt + F11': 'Elements path' }) + 324 | '
    ' + 325 | '

    ' + __( 'To move focus to other buttons use Tab or the arrow keys. To return focus to the editor press Escape or use one of the buttons.' ) + '

    '; 326 | 327 | html += '
    '; 328 | 329 | dialog = editor.windowManager.open( { 330 | title: 'Keyboard Shortcuts', 331 | items: { 332 | type: 'container', 333 | classes: 'wp-help', 334 | html: html 335 | }, 336 | buttons: { 337 | text: 'Close', 338 | onclick: 'close' 339 | } 340 | } ); 341 | 342 | if ( dialog.$el ) { 343 | dialog.$el.find( 'div[role="application"]' ).attr( 'role', 'document' ); 344 | $wrap = dialog.$el.find( '.mce-wp-help' ); 345 | 346 | if ( $wrap[0] ) { 347 | $wrap.attr( 'tabindex', '0' ); 348 | $wrap[0].focus(); 349 | $wrap.on( 'keydown', function( event ) { 350 | // Prevent use of: page up, page down, end, home, left arrow, up arrow, right arrow, down arrow 351 | // in the dialog keydown handler. 352 | if ( event.keyCode >= 33 && event.keyCode <= 40 ) { 353 | event.stopPropagation(); 354 | } 355 | }); 356 | } 357 | } 358 | } ); 359 | 360 | editor.addCommand( 'WP_Medialib', function() { 361 | if ( wp && wp.media && wp.media.editor ) { 362 | wp.media.editor.open( editor.id ); 363 | } 364 | }); 365 | 366 | // Register buttons 367 | editor.addButton( 'wp_more', { 368 | tooltip: 'Insert Read More tag', 369 | onclick: function() { 370 | editor.execCommand( 'WP_More', 'more' ); 371 | } 372 | }); 373 | 374 | editor.addButton( 'wp_page', { 375 | tooltip: 'Page break', 376 | onclick: function() { 377 | editor.execCommand( 'WP_More', 'nextpage' ); 378 | } 379 | }); 380 | 381 | editor.addButton( 'wp_help', { 382 | tooltip: 'Keyboard Shortcuts', 383 | cmd: 'WP_Help' 384 | }); 385 | 386 | editor.addButton( 'wp_code', { 387 | tooltip: 'Code', 388 | cmd: 'WP_Code', 389 | stateSelector: 'code' 390 | }); 391 | 392 | // Menubar 393 | // Insert->Add Media 394 | if ( wp && wp.media && wp.media.editor ) { 395 | editor.addMenuItem( 'add_media', { 396 | text: 'Add Media', 397 | icon: 'wp-media-library', 398 | context: 'insert', 399 | cmd: 'WP_Medialib' 400 | }); 401 | } 402 | 403 | // Insert "Read More..." 404 | editor.addMenuItem( 'wp_more', { 405 | text: 'Insert Read More tag', 406 | icon: 'wp_more', 407 | context: 'insert', 408 | onclick: function() { 409 | editor.execCommand( 'WP_More', 'more' ); 410 | } 411 | }); 412 | 413 | // Insert "Next Page" 414 | editor.addMenuItem( 'wp_page', { 415 | text: 'Page break', 416 | icon: 'wp_page', 417 | context: 'insert', 418 | onclick: function() { 419 | editor.execCommand( 'WP_More', 'nextpage' ); 420 | } 421 | }); 422 | 423 | editor.on( 'BeforeExecCommand', function(e) { 424 | if ( tinymce.Env.webkit && ( e.command === 'InsertUnorderedList' || e.command === 'InsertOrderedList' ) ) { 425 | if ( ! style ) { 426 | style = editor.dom.create( 'style', {'type': 'text/css'}, 427 | '#tinymce,#tinymce span,#tinymce li,#tinymce li>span,#tinymce p,#tinymce p>span{font:medium sans-serif;color:#000;line-height:normal;}'); 428 | } 429 | 430 | editor.getDoc().head.appendChild( style ); 431 | } 432 | }); 433 | 434 | editor.on( 'ExecCommand', function( e ) { 435 | if ( tinymce.Env.webkit && style && 436 | ( 'InsertUnorderedList' === e.command || 'InsertOrderedList' === e.command ) ) { 437 | 438 | editor.dom.remove( style ); 439 | } 440 | }); 441 | 442 | editor.on( 'init', function() { 443 | var env = tinymce.Env, 444 | bodyClass = ['mceContentBody'], // back-compat for themes that use this in editor-style.css... 445 | doc = editor.getDoc(), 446 | dom = editor.dom; 447 | 448 | if ( env.iOS ) { 449 | dom.addClass( doc.documentElement, 'ios' ); 450 | } 451 | 452 | if ( editor.getParam( 'directionality' ) === 'rtl' ) { 453 | bodyClass.push('rtl'); 454 | dom.setAttrib( doc.documentElement, 'dir', 'rtl' ); 455 | } 456 | 457 | dom.setAttrib( doc.documentElement, 'lang', editor.getParam( 'wp_lang_attr' ) ); 458 | 459 | if ( env.ie ) { 460 | if ( parseInt( env.ie, 10 ) === 9 ) { 461 | bodyClass.push('ie9'); 462 | } else if ( parseInt( env.ie, 10 ) === 8 ) { 463 | bodyClass.push('ie8'); 464 | } else if ( env.ie < 8 ) { 465 | bodyClass.push('ie7'); 466 | } 467 | } else if ( env.webkit ) { 468 | bodyClass.push('webkit'); 469 | } 470 | 471 | bodyClass.push('wp-editor'); 472 | 473 | each( bodyClass, function( cls ) { 474 | if ( cls ) { 475 | dom.addClass( doc.body, cls ); 476 | } 477 | }); 478 | 479 | // Remove invalid parent paragraphs when inserting HTML 480 | editor.on( 'BeforeSetContent', function( event ) { 481 | if ( event.content ) { 482 | event.content = event.content.replace( /

    \s*<(p|div|ul|ol|dl|table|blockquote|h[1-6]|fieldset|pre)( [^>]*)?>/gi, '<$1$2>' ) 483 | .replace( /<\/(p|div|ul|ol|dl|table|blockquote|h[1-6]|fieldset|pre)>\s*<\/p>/gi, '' ); 484 | } 485 | }); 486 | 487 | if ( $ ) { 488 | $( document ).triggerHandler( 'tinymce-editor-init', [editor] ); 489 | } 490 | 491 | if ( window.tinyMCEPreInit && window.tinyMCEPreInit.dragDropUpload ) { 492 | dom.bind( doc, 'dragstart dragend dragover drop', function( event ) { 493 | if ( $ ) { 494 | // Trigger the jQuery handlers. 495 | $( document ).trigger( new $.Event( event ) ); 496 | } 497 | }); 498 | } 499 | 500 | if ( editor.getParam( 'wp_paste_filters', true ) ) { 501 | editor.on( 'PastePreProcess', function( event ) { 502 | // Remove trailing
    added by WebKit browsers to the clipboard 503 | event.content = event.content.replace( /
    /gi, '' ); 504 | 505 | // In WebKit this is handled by removeWebKitStyles() 506 | if ( ! tinymce.Env.webkit ) { 507 | // Remove all inline styles 508 | event.content = event.content.replace( /(<[^>]+) style="[^"]*"([^>]*>)/gi, '$1$2' ); 509 | 510 | // Put back the internal styles 511 | event.content = event.content.replace(/(<[^>]+) data-mce-style=([^>]+>)/gi, '$1 style=$2' ); 512 | } 513 | }); 514 | 515 | editor.on( 'PastePostProcess', function( event ) { 516 | // Remove empty paragraphs 517 | each( dom.select( 'p', event.node ), function( node ) { 518 | if ( dom.isEmpty( node ) ) { 519 | dom.remove( node ); 520 | } 521 | }); 522 | }); 523 | } 524 | }); 525 | 526 | editor.on( 'SaveContent', function( event ) { 527 | // If editor is hidden, we just want the textarea's value to be saved 528 | if ( ! editor.inline && editor.isHidden() ) { 529 | event.content = event.element.value; 530 | return; 531 | } 532 | 533 | // Keep empty paragraphs :( 534 | event.content = event.content.replace( /

    (?:
    |\u00a0|\uFEFF| )*<\/p>/g, '

     

    ' ); 535 | 536 | if ( hasWpautop ) { 537 | event.content = wp.editor.removep( event.content ); 538 | } 539 | }); 540 | 541 | editor.on( 'preInit', function() { 542 | var validElementsSetting = '@[id|accesskey|class|dir|lang|style|tabindex|' + 543 | 'title|contenteditable|draggable|dropzone|hidden|spellcheck|translate],' + // Global attributes. 544 | 'i,' + // Don't replace with and with and don't remove them when empty. 545 | 'b,' + 546 | 'script[src|async|defer|type|charset|crossorigin|integrity]'; // Add support for