Changeset 1598924
- Timestamp:
- 02/19/2017 02:44:57 AM (9 years ago)
- Location:
- text-styler/trunk
- Files:
-
- 5 edited
-
TXST/MCEForm.php (modified) (6 diffs)
-
readme.txt (modified) (3 diffs)
-
scripts/pop-up-text-styler.php (modified) (5 diffs)
-
scripts/tinymce-text-styler.js (modified) (3 diffs)
-
text-styler.php (modified) (9 diffs)
Legend:
- Unmodified
- Added
- Removed
-
text-styler/trunk/TXST/MCEForm.php
r1456088 r1598924 121 121 122 122 protected function _getValue($db_fieldname, $default = '') { 123 $value = ($this->_data->$db_fieldname) ? $this->_data->$db_fieldname : $default; 124 123 $value = (isset($this->_data->$db_fieldname)) ? $this->_data->$db_fieldname : $default; 125 124 return $value; 126 125 } … … 252 251 } 253 252 } 254 253 255 254 return $text; 256 255 } 257 256 258 257 public static function unSwapChars($text) { 259 258 $chars1 = '12345678907!@3#6$%'; 260 259 $chars2 = 'praywithoutceasing'; 261 260 262 261 for ($i = 0; $i < strlen($text); $i++) { 263 262 $pos = strpos($chars1, $text[$i]); … … 266 265 } 267 266 } 268 267 269 268 return $text; 270 } 271 269 } 270 272 271 public function _showColorPicker($fieldname, $args = array()) { 273 $options = array_merge(array('label' => $fieldname), $args); 274 $db_fieldname = str_replace('-', '_', $fieldname); 275 $value = ($this->_data->$db_fieldname) ? $this->_data->$db_fieldname : (isset($args['default']) ? $args['default'] : ''); 276 277 $html = '<div class="std-form-line field-' . $fieldname . '"> 278 <label>' . $options['label'] . '</label> 279 <div class="grp"> 280 <input class="wp-color-picker std-input ' . (isset($options['classes']) ? $options['classes'] : '') . '" type="text" ' . (($options['placeholder']) ? 'placeholder="' . $options['placeholder'] . '"' : '') . ' id="' . ((isset($args['id']) ? $args['id'] : $fieldname)) . '" name="data[' . $fieldname . ']" style="height: ' . $options['height'] . '" value="' . $value . '" /> 272 $options = array_merge(array('label' => $fieldname, 'placeholder' => '', 'note1' => ''), $args); 273 274 $db_fieldname = str_replace('_showColorPicker-', '_', $fieldname); 275 $value = (isset($this->_data->$db_fieldname)) ? $this->_data->$db_fieldname : (isset($args['default']) ? $args['default'] : ''); 276 277 $html = '<div class="std-form-line field-' . $fieldname . '"> 278 <label>' . $options['label'] . '</label> 279 <div class="grp"> 280 <input class="wp-color-picker std-input ' . (isset($options['classes']) ? $options['classes'] : '') . '" type="text" ' . (($options['placeholder']) ? 'placeholder="' . $options['placeholder'] . '"' : '') . ' id="' . ((isset($args['id']) ? $args['id'] : $fieldname)) . '" name="data[' . $fieldname . ']" value="' . $value . '" /> 281 281 ' . (($options['note1']) ? '<span class="important-note">' . $options['note1'] . '</span>' : '') . ' 282 282 <span class="error"></span> … … 291 291 $options = array_merge(array('label' => $fieldname), $args); 292 292 $db_fieldname = str_replace('-', '_', $fieldname); 293 $value = ( $this->_data->$db_fieldname) ? $this->_data->$db_fieldname : (isset($args['default']) ? $args['default'] : '');293 $value = (isset($this->_data->$db_fieldname)) ? $this->_data->$db_fieldname : (isset($args['default']) ? $args['default'] : ''); 294 294 295 295 $opt = '<select name="data[' . $fieldname . ']" data-name="' . $fieldname . '">'; … … 314 314 protected function _showStandardRadioButtonField($fieldname, $args = array()) { 315 315 $options = array_merge(array('label' => $fieldname), $args); 316 317 $hide_class = ( !$this->isShowField($this->_conditions[$fieldname])) ? true : false;318 319 $db_fieldname = str_replace('-', '_', $fieldname); 320 $value = ( $this->_data->$db_fieldname) ? $this->_data->$db_fieldname : (isset($args['default']) ? $args['default'] : '');316 $cond = (isset($this->_conditions[$fieldname])) ? $this->_conditions[$fieldname] : false; 317 $hide_class = ($cond && !$this->isShowField($this->_conditions[$fieldname])) ? true : false; 318 319 $db_fieldname = str_replace('-', '_', $fieldname); 320 $value = (isset($this->_data->$db_fieldname)) ? $this->_data->$db_fieldname : (isset($args['default']) ? $args['default'] : ''); 321 321 $opt = ''; 322 322 … … 368 368 <label>' . $options['label'] . '</label> 369 369 <div class="grp"> 370 <input class="std-input ' . (isset($options['classes']) ? $options['classes'] : '') . '" type="text" ' . (($options['placeholder']) ? 'placeholder="' . $options['placeholder'] . '"' : '') . ' id="' . ((isset($args['id']) ? $args['id'] : $fieldname)) . '" name="data[' . $fieldname . ']" style="height: ' . $options['height'] . '"value="' . $value . '" />370 <input class="std-input ' . (isset($options['classes']) ? $options['classes'] : '') . '" type="text" ' . (($options['placeholder']) ? 'placeholder="' . $options['placeholder'] . '"' : '') . ' id="' . ((isset($args['id']) ? $args['id'] : $fieldname)) . '" name="data[' . $fieldname . ']" value="' . $value . '" /> 371 371 ' . (($options['note1']) ? '<span class="important-note">' . $options['note1'] . '</span>' : '') . ' 372 372 <span class="error"></span> -
text-styler/trunk/readme.txt
r1456088 r1598924 4 4 Requires at least: 4.5.3 5 5 Tested up to: 4.5.3 6 Stable tag: 1.0. 66 Stable tag: 1.0.7 7 7 Tags: wp text styler, text styler, tinymce button, tinymce add-on, wordpress editor add-on, tinymce, advance tinymce 8 8 License: GPLv2 or later … … 78 78 = 1.0.3 = 79 79 * Added more CSS/styling options 80 * Fix "unable to add new element" issue.80 * Fixed "unable to add new element" issue. 81 81 82 82 = 1.0.4 = 83 83 * Added more CSS/styling options (UL and OL) 84 84 * Added link to "How to Use Text Styler" video. 85 * Ma ke changing text styles easier85 * Made changing text styles easier 86 86 87 87 = 1.0.5 = … … 91 91 = 1.0.6 = 92 92 * Add more css options (font weight, font style) 93 * Improve UI of tinymce pop up window 93 * Improved UI of tinymce pop up window 94 95 = 1.0.7 = 96 * Fixed coding issues 97 * (Coming soon To work with my own builder) -
text-styler/trunk/scripts/pop-up-text-styler.php
r1456088 r1598924 1 1 <?php 2 include_once('../../../../wp-load.php'); 2 3 include_once('../TXST/MCEForm.php'); 3 4 include_once('../TXST/MCEForm/StyleOptions.php'); 5 6 //$styles = TextStyler::getBuilderStyles($_GET['editor_id']); 4 7 ?> 5 8 <!DOCTYPE html> … … 11 14 <meta http-equiv="pragma" content="no-cache" /> 12 15 13 <link rel='stylesheet' href='<?php echo $_GET['wp_url']?>/wp-admin/load-styles.php?c=1&dir=ltr&load%5B%5D=wp-color-picker' type='text/css' media='all' />16 <link rel='stylesheet' href='<?php bloginfo('wpurl') ?>/wp-admin/load-styles.php?c=1&dir=ltr&load%5B%5D=wp-color-picker' type='text/css' media='all' /> 14 17 <link rel='stylesheet' href='../styles/tinymce-color-picker.css' type='text/css' media='all' /> 15 18 <link rel='stylesheet' href='../styles/tinymce-text-styler.css' type='text/css' media='all' /> 16 19 <style type='text/css'> 17 20 18 21 </style> 22 <script type="text/javascript"> 23 var $ajax_url = '<?php echo admin_url('admin-ajax.php') ?>'; 24 </script> 19 25 </head> 20 26 <body> … … 55 61 $field_types['font-weight'] = 'radio'; 56 62 63 /*$(top.tinymce.activeEditor.dom.select('head'), jq_context).append('<style type="text/css" id="custom-text-styles">' + 64 '<?php //echo $styles?>' + 65 '</style>');*/ 57 66 58 67 $('#continue-step2', jq_context).live('click', function(){ 59 68 $id = $('input[name="element"]:checked', jq_context).val(); 69 60 70 if ($('input[name="element"]:checked', jq_context).hasClass('UL') || 61 71 $('input[name="element"]:checked', jq_context).hasClass('OL')) { 62 72 $('#css-list', jq_context).css('display', 'block'); 63 73 } 74 75 if (passed_arguments.type == 'builder') { 76 $builder_id = passed_arguments.builder_id; 77 78 $params = '&type=' + passed_arguments.type + '&builder_id=' + $builder_id; 79 } 80 else if (passed_arguments.type == 'post') { 81 $params = '&type=' + passed_arguments.type + '&post_id=' + passed_arguments.post_id; 82 } 83 64 84 $.ajax({ 65 url: passed_arguments.ajax_url, //AJAX file path – admin_url("admin-ajax.php")85 url: $ajax_url, //AJAX file path – admin_url("admin-ajax.php") 66 86 type: "POST", 67 data: 'action=get_styles&id=' + $id + '&post_id=' + passed_arguments.post_id,87 data: 'action=get_styles&id=' + $id + $params, 68 88 dataType: "json", 69 89 success: function($data){ … … 96 116 } 97 117 } 118 $('.step1', jq_context).css('display','none'); 119 $('.step2', jq_context).css('display','block'); 98 120 } 99 121 } 100 }, jq_context); 101 102 $('.step1', jq_context).css('display','none'); 103 $('.step2', jq_context).css('display','block'); 122 }, jq_context); 104 123 }); 105 124 … … 117 136 passed_arguments.editor.selection.setContent($text); 118 137 } 138 139 if (passed_arguments.type == 'builder') { 140 $builder_id = passed_arguments.builder_id; 141 142 $params = '&' + $form + '&type=' + passed_arguments.type + '&builder_id=' + $builder_id; 143 } 144 else if (passed_arguments.type == 'post') { 145 $params = '&' + $form + '&type=' + passed_arguments.type + '&post_id=' + passed_arguments.post_id; 146 } 119 147 120 148 $.ajax({ 121 url: passed_arguments.ajax_url, //AJAX file path – admin_url("admin-ajax.php")149 url: $ajax_url, //AJAX file path – admin_url("admin-ajax.php") 122 150 type: "POST", 123 data: 'action=save_styles&id=' + $id + '&post_id=' + passed_arguments.post_id + '&' + $form,151 data: 'action=save_styles&id=' + $id + $params, 124 152 dataType: "json", 125 153 success: function($data){ -
text-styler/trunk/scripts/tinymce-text-styler.js
r1456088 r1598924 1 1 (function($) { 2 var $passed = text_styler_data; 3 var $wp_url = $passed.wp_url; 4 var $plugin_url = $passed.plugin_url; 5 var is_style_added = false; 2 $plugin_url = text_styler_data.plugin_url; 3 $post_id = text_styler_data.post_id; 4 $styles = text_styler_data.styles; 5 $type = text_styler_data.type; 6 $ajax_url = text_styler_data.ajax_url; 6 7 7 8 tinymce.create('tinymce.plugins.text_styler_plugin', { 8 init: function(editor, url) { 9 init: function(editor, url) { 9 10 title = 'Text Styler'; 11 12 // loads the custom styles of the current post to the tinymce editor 13 if ($type == 'post') { 14 $params = '&type=' + $type + '&post_id=' + $post_id; 15 } 16 else if ($type == 'builder') { 17 $editor_id = editor.id; 18 $builder_id = $editor_id.substring(3); 19 $params = '&type=' + $type + '&builder_id=' + $builder_id; 20 } 21 22 $.ajax({ 23 url: $ajax_url, //AJAX file path – admin_url("admin-ajax.php") 24 type: "POST", 25 data: 'action=init_styles' + $params, 26 dataType: "json", 27 success: function($data){ 28 if ($data.success) { 29 $(editor.dom.select('head')).append('<style type="text/css" id="custom-text-styles">' + $data.styles + '</style>'); 30 } 31 } 32 }); 33 34 is_style_added = true; 35 10 36 editor.addButton('text_styler_button', { 11 37 title: title, … … 40 66 } 41 67 42 if (!is_style_added) { 43 // loads the custom styles of the current post to the tinymce editor 44 $(tinyMCE.activeEditor.dom.select('head')).append('<style type="text/css" id="custom-text-styles">' + text_styler_data.styles + '</style>'); 45 is_style_added = true; 46 } 68 47 69 48 70 }); … … 78 100 if(id_split[0] == 'st') { 79 101 $value = $(node).html(); 80 showWindow(editor, $value, null, $opt, text_styler_data.post_id, url, text_styler_data.ajax_url, $wp_url);102 showWindow(editor, $value, null, $opt, $post_id, url); 81 103 } 82 else { 104 else { 83 105 $value = editor.selection.getContent(); 84 showWindow(editor, $value, $p, $opt, text_styler_data.post_id, url, text_styler_data.ajax_url, $wp_url);106 showWindow(editor, $value, $p, $opt, $post_id, url); 85 107 } 86 }); 108 }); 87 109 } 88 110 }); 89 111 90 112 tinymce.PluginManager.add('text_styler_plugin', tinymce.plugins.text_styler_plugin); 91 92 function showWindow(editor, $value, $p,$opt, $post_id, $url, $ajax_url, $wp_url) { 93 editor.windowManager.open( 94 // Properties of the window. 95 { 96 title: title, 97 file: $url + '/pop-up-text-styler.php?wp_url=' + $wp_url, 98 width: 600, 99 height: 500, 100 inline: 1 101 }, 102 { 103 editor: editor, 104 jquery: $, 105 value: $value, 106 parents : $p, 107 options: $opt, 108 ajax_url: $ajax_url, 109 post_id: $post_id 110 } 111 ); 112 } 113 114 function showWindow(editor, $value, $p,$opt, $post_id, $url) { 115 if ($type === 'builder'){ 116 $builder_id = $('input[name="builder-id"]').val(); 117 118 editor.windowManager.open( 119 // Properties of the window. 120 { 121 title: title, 122 file: $url + '/pop-up-text-styler.php?editor_id=' + editor.id, 123 width: 600, 124 height: 500, 125 inline: 1 126 }, 127 { 128 editor: editor, 129 jquery: $, 130 value: $value, 131 parents : $p, 132 options: $opt, 133 type: $type, 134 builder_id: $builder_id 135 } 136 ); 137 } 138 else if ($type === 'post'){ 139 $builder_id = 'n/a'; 140 editor.windowManager.open( 141 // Properties of the window. 142 { 143 title: title, 144 file: $url + '/pop-up-text-styler.php?editor_id=' + editor.id, 145 width: 600, 146 height: 500, 147 inline: 1 148 }, 149 { 150 editor: editor, 151 jquery: $, 152 value: $value, 153 parents : $p, 154 options: $opt, 155 type: $type, 156 post_id: $post_id 157 } 158 ); 159 } 160 } 113 161 })(jQuery); -
text-styler/trunk/text-styler.php
r1456093 r1598924 4 4 * Description: This plugin will allow a user to style text/phrase of a post or page. He can set background color, text color, and padding, border, etc. Easily adds styles to all tags such as header,paragraph, list, list item, span, strong, etc. 5 5 * Author: EdesaC 6 * Version: 1.0. 66 * Version: 1.0.7 7 7 **/ 8 8 … … 31 31 add_action('wp_footer', array($this, 'load_website_scripts_css')); 32 32 33 add_action( 'admin_head', array($this, 'saveTinymceData')); 34 add_filter( 'mce_buttons', array($this, 'registerTinymceButton')); 35 add_filter( 'mce_external_plugins', array($this, 'registerTinymcePlugin')); 33 add_action('admin_head', array($this, 'initTinymceData')); 34 add_action('init', array($this, 'tinymce')); 36 35 37 36 add_action('wp_ajax_get_styles', array($this, 'ajaxGetStyles')); … … 40 39 add_action('wp_ajax_save_styles', array($this, 'ajaxSaveStyles')); 41 40 add_action('wp_ajax_nopriv_save_styles', array($this, 'ajaxSaveStyles')); 42 41 42 add_action('wp_ajax_init_styles', array($this, 'ajaxInitStyles')); 43 add_action('wp_ajax_nopriv_init_styles', array($this, 'ajaxInitStyles')); 44 43 45 add_filter('the_content', array($this, 'filterContent')); 44 45 46 } 46 47 … … 51 52 $data = $_POST; 52 53 53 $post_id = $data['post_id']; 54 $css_id = $data['id']; 55 $post_styles = get_post_meta($post_id, 'text_styles', true); 56 57 $post_styles[$css_id] = $data['data']; 58 update_post_meta($post_id, 'text_styles', $post_styles); 59 60 $new_styles = $this->getStyles($post_id); 61 $ret['styles'] = $new_styles; 62 $ret['post_id'] = $data['post_id']; 54 if ($_POST['type'] == 'builder') { 55 $builder_id = $_POST['builder_id']; 56 $builder_styles = get_option('text_styles_' . $builder_id); 57 $css_id = $data['id']; 58 $builder_styles[$css_id] = $data['data']; 59 update_option('text_styles_' . $builder_id, $builder_styles); 60 61 $new_styles = $this->getBuilderStyles($builder_id); 62 } 63 else if ($_POST['type'] == 'post'){ 64 $post_id = $data['post_id']; 65 $css_id = $data['id']; 66 $post_styles = get_post_meta($post_id, 'text_styles', true); 67 68 $post_styles[$css_id] = $data['data']; 69 update_post_meta($post_id, 'text_styles', $post_styles); 70 71 $new_styles = $this->getPostStyles($post_id); 72 $ret['post_id'] = $data['post_id']; 73 } 74 75 $ret['styles'] = $new_styles; 63 76 $ret['success'] = true; 64 77 65 print_r( json_encode($ret)); 78 print_r( json_encode($ret)); 66 79 exit; 67 80 } … … 76 89 77 90 $defaults = array('color' => '', 'background-color' => '', 'padding' => ''); 78 $post_styles = get_post_meta($post_id, 'text_styles', true); 91 92 if ($_POST['type'] == 'post') { 93 $post_styles = get_post_meta($post_id, 'text_styles', true); 94 $ret['post_id'] = $data['post_id']; 95 } 96 else if ($_POST['type'] == 'builder') { 97 $post_styles = get_option('text_styles_' . $data['builder_id'], true); 98 } 79 99 80 100 if (is_array($post_styles) && isset($post_styles[$css_id])) { … … 85 105 } 86 106 87 $ret['styles'] = $final_styles; 88 $ret['post_id'] = $data['post_id']; 107 $ret['styles'] = $final_styles; 89 108 $ret['success'] = true; 90 109 91 110 print_r( json_encode($ret)); 92 111 exit; 112 } 113 114 public function ajaxInitStyles() { 115 if ($_POST['type'] == 'post') { 116 $ret['styles'] = $this->getPostStyles($_POST['post_id']); 117 } 118 else if ($_POST['type'] == 'builder') { 119 $ret['styles'] = $this->getBuilderStyles($_POST['builder_id']); 120 } 121 122 $ret['success'] = true; 123 124 print_r( json_encode($ret)); 125 exit; 93 126 } 94 127 … … 98 131 * @return string e.g. #id1 {color: red} #id2 {background: green} 99 132 */ 100 public function getStyles($post_id) { 101 $post_styles = get_post_meta($post_id, 'text_styles', true); 133 public static function __getStyles($styles_data) { 102 134 $allowed_styles = array('padding', 'margin', 'color', 'background-color', 'font-family', 'font-size', 'font-weight', 'font-style', 'line-height', 'border-style', 'border-width', 'border-color', 'border-radius', 'list-style-position', 'list-style-type', 'font-style', 'font-weight'); 103 104 if (is_array($ post_styles)) {135 $s = ''; 136 if (is_array($styles_data)) { 105 137 $s = '.ts ul, .ts ol {padding: 0; margin: 0;}'; 106 foreach ($ post_stylesas $id => $styles) {138 foreach ($styles_data as $id => $styles) { 107 139 if (strpos($id, 'st-') === 0) { 108 140 $s .= "#" . $id . " { "; … … 123 155 } 124 156 } 125 157 126 158 return $s; 127 159 } 128 160 129 public function registerTinymceButton( $button_array ) { 130 global $current_screen; 131 $type = $current_screen->post_type; 132 133 if( is_admin() && ( $type == 'post' || $type == 'page' ) ) { 134 array_push( $button_array, 'text_styler_button' ); 135 } 136 161 public function getPostStyles($post_id) { 162 $post_styles = get_post_meta($post_id, 'text_styles', true); 163 return TextStyler::__getStyles($post_styles); 164 } 165 166 public function getBuilderStyles($builder_id) { 167 $builder_styles = get_option('text_styles_' . $builder_id); 168 return TextStyler::__getStyles($builder_styles); 169 } 170 171 public function filterTinymcePlugin($plugins) { 172 $plugins []= 'text_styler_plugin'; 173 return $plugins; 174 } 175 176 public function tinymce() { 177 add_filter( 'mce_external_plugins', array($this, 'registerTinymcePlugin')); 178 add_filter( 'mce_buttons', array($this, 'registerTinymceButton')); 179 } 180 181 public function registerTinymcePlugin($plugin_array) { 182 $plugin_array['text_styler_plugin'] = $this->_plugin_url . 'scripts/tinymce-text-styler.js'; 183 return $plugin_array; 184 } 185 186 public function registerTinymceButton($button_array) { 187 array_push($button_array, 'text_styler_button'); 137 188 return $button_array; 138 189 } 139 190 140 public function registerTinymcePlugin( $plugin_array ) { 141 global $current_screen; 142 $type = $current_screen->post_type; 143 144 if( is_admin() && ( $type == 'post' || $type == 'page' ) ) { 145 $plugin_array['text_styler_plugin'] = $this->_plugin_url . 'scripts/tinymce-text-styler.js'; 146 } 147 148 return $plugin_array; 149 } 150 151 public function saveTinymceData() { 191 public function initTinymceData() { 152 192 global $post; 153 193 if (isset($_GET['post'])) { 154 194 $post_id = $post->ID; 155 $post_styles = $this->get Styles($post_id);195 $post_styles = $this->getPostStyles($post_id); 156 196 ?> 157 197 <script type='text/javascript'> 158 198 var text_styler_data = { 199 'ajax_url': '<?php echo admin_url('admin-ajax.php') ?>', 159 200 'plugin_url': '<?php echo $this->_plugin_url; ?>', 160 'wp_url': '<?php echo get_bloginfo('wpurl'); ?>',161 'ajax_url': '<?php echo admin_url('admin-ajax.php'); ?>',162 201 'post_id': <?php echo $post->ID ?>, 163 'styles': '<?php echo $post_styles ?>' 202 'styles': '<?php echo $post_styles ?>', 203 'type': 'post' 164 204 }; 165 205 </script> 166 206 <?php 167 207 } 168 } 169 208 else { 209 ?> 210 <script type='text/javascript'> 211 var text_styler_data = { 212 'ajax_url': '<?php echo admin_url('admin-ajax.php') ?>', 213 'plugin_url': '<?php echo $this->_plugin_url; ?>', 214 'post_id': '', 215 'styles': '', 216 'type': 'builder' 217 }; 218 </script> 219 <?php 220 } 221 } 222 170 223 public function filterContent($content){ 171 224 return '<div ciass="ts">' . $content . '</div>'; … … 173 226 174 227 public function load_website_scripts_css() { 175 wp_enqueue_script($this->_plugin_folder . '-wp-scripts', $this->_plugin_url . '/scripts/wp-scripts.js');228 //wp_enqueue_script($this->_plugin_folder . '-wp-scripts', $this->_plugin_url . '/scripts/wp-scripts.js'); 176 229 wp_enqueue_style($this->_plugin_folder . '-wp-styles', $this->_plugin_url . '/styles/wp-styles.css'); 177 230 178 231 global $post; 179 $post_id = $post->ID; 180 $s = $this->getStyles($post_id); 181 182 // load the custom styles for the current post 183 $styles = "<style type=\"text/css\">" . $s . "</style>"; 184 echo $styles; 232 if ($post->ID) { 233 $post_id = $post->ID; 234 $s = $this->getPostStyles($post_id); 235 236 // load the custom styles for the current post 237 $styles = "<style type=\"text/css\">" . $s . "</style>"; 238 echo $styles; 239 } 185 240 } 186 241
Note: See TracChangeset
for help on using the changeset viewer.