├── .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 '
' . $content . '
';
329 | }
330 |
331 | return $content;
332 | }
333 |
334 | function wp_link_pages( $html ) {
335 | return '' . $html . '
';
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( '