1<?php 2/** 3 * DokuWiki Plugin prosemirror (Action Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Andreas Gohr <gohr@cosmocode.de> 7 */ 8 9// must be run within Dokuwiki 10use dokuwiki\Form\ButtonElement; 11 12if (!defined('DOKU_INC')) { 13 die(); 14} 15 16class action_plugin_prosemirror_editor extends DokuWiki_Action_Plugin 17{ 18 /** 19 * Registers a callback function for a given event 20 * 21 * @param Doku_Event_Handler $controller DokuWiki's event controller object 22 * 23 * @return void 24 */ 25 public function register(Doku_Event_Handler $controller) 26 { 27 $controller->register_hook('ACTION_HEADERS_SEND', 'BEFORE', $this, 'forceWYSIWYG'); 28 $controller->register_hook('ACTION_HEADERS_SEND', 'AFTER', $this, 'addJSINFO'); 29 $controller->register_hook('HTML_EDITFORM_OUTPUT', 'BEFORE', $this, 'addDataAndToggleButton'); 30 $controller->register_hook('FORM_EDIT_OUTPUT', 'BEFORE', $this, 'addDataAndToggleButton'); 31 $controller->register_hook('TPL_ACT_RENDER', 'AFTER', $this, 'addAddtionalForms'); 32 } 33 34 /** 35 * If the current user is forced to use the WYSIWYG editor, set the cookie accordingly 36 * 37 * Triggered by event: ACTION_HEADERS_SEND 38 * 39 * @param Doku_Event $event 40 * @param $param 41 */ 42 public function forceWYSIWYG(Doku_Event $event, $param) 43 { 44 if ($this->isForceWYSIWYG()) { 45 set_doku_pref('plugin_prosemirror_useWYSIWYG', true); 46 } 47 } 48 49 /** 50 * Add the editor toggle button and, if using the WYSIWYG editor, the instructions rendered to json 51 * 52 * Triggered by event: HTML_EDITFORM_OUTPUT 53 * 54 * @param Doku_Event $event event object 55 * @param mixed $param [the parameters passed as fifth argument to register_hook() when this 56 * handler was registered] 57 * 58 * @return void 59 */ 60 public function addDataAndToggleButton(Doku_Event $event, $param) 61 { 62 if (!$this->allowWYSIWYG()) { 63 return; 64 } 65 66 /** @var Doku_Form|\dokuwiki\Form\Form $form */ 67 $form = $event->data; 68 69 // return early if content is not editable 70 if ($this->isReadOnly($form)) return; 71 72 73 $useWYSIWYG = get_doku_pref('plugin_prosemirror_useWYSIWYG', false); 74 75 $prosemirrorJSON = ''; 76 if ($useWYSIWYG) { 77 global $TEXT; 78 $instructions = p_get_instructions($TEXT); 79 try { 80 $prosemirrorJSON = p_render('prosemirror', $instructions, $info); 81 } catch (Throwable $e) { 82 $errorMsg = 'Rendering the page\'s syntax for the WYSIWYG editor failed: ' . $e->getMessage(); 83 84 /** @var \helper_plugin_prosemirror $helper */ 85 $helper = plugin_load('helper', 'prosemirror'); 86 if ($helper->tryToLogErrorToSentry($e, ['text' => $TEXT])) { 87 $errorMsg .= ' -- The error has been logged to Sentry.'; 88 } 89 90 msg($errorMsg, -1); 91 return; 92 } 93 } 94 95 if (is_a($form, \dokuwiki\Form\Form::class)) { 96 $form->addElement($this->buildToggleButton(), 0); 97 $form->setHiddenField('prosemirror_json',$prosemirrorJSON); 98 $form->addHTML('<div class="prosemirror_wrapper" id="prosemirror__editor"></div>', 1); 99 } else { 100 // todo remove when old stable is no longer supported 101 $form->insertElement(0, $this->buildOldToggleButton()); 102 $form->addHidden('prosemirror_json',$prosemirrorJSON); 103 $form->insertElement(1, '<div class="prosemirror_wrapper" id="prosemirror__editor"></div>'); 104 } 105 } 106 107 /** 108 * Create the button to toggle the WYSIWYG editor 109 * 110 * Creates it as hidden if forcing WYSIWYG 111 * 112 * @deprecated use buildToggleButton instead 113 * @return array the pseudo-tag expected by \Doku_Form::insertElement 114 */ 115 protected function buildOldToggleButton() 116 { 117 dbg_deprecated('buildToggleButton'); 118 $attr = [ 119 'class' => 'button plugin_prosemirror_useWYSIWYG' 120 ]; 121 if ($this->isForceWYSIWYG()) { 122 $attr['style'] = 'display: none;'; 123 } 124 return form_makeButton('button', '', $this->getLang('switch_editors'), $attr); 125 } 126 127 /** 128 * Create the button to toggle the WYSIWYG editor 129 * 130 * Creates it as hidden if forcing WYSIWYG 131 * 132 * @return ButtonElement 133 */ 134 protected function buildToggleButton() 135 { 136 $button = new ButtonElement('prosemirror', $this->getLang('switch_editors')); 137 $button->attr('type', 'button'); 138 $button->addClass('button plugin_prosemirror_useWYSIWYG'); 139 if ($this->isForceWYSIWYG()) { 140 $button->attr('style', 'display: none;'); 141 } 142 return $button; 143 } 144 145 /** 146 * Determine if the current user is forced to use the WYSIWYG editor 147 * 148 * @return bool 149 */ 150 protected function isForceWYSIWYG() 151 { 152 return $this->getConf('forceWYSIWYG') && !auth_ismanager(); 153 } 154 155 /** 156 * Forbid using WYSIWYG editor when editing anything else then sections or the entire page 157 * 158 * This would be the case for the edittable editor or the editor of the data plugin 159 * 160 * @return bool 161 */ 162 protected function allowWYSIWYG() 163 { 164 global $INPUT; 165 return !$INPUT->has('target') || $INPUT->str('target') === 'section'; 166 } 167 168 public function addAddtionalForms(Doku_Event $event) 169 { 170 if (!$this->allowWYSIWYG()) { 171 return; 172 } 173 174 if (!in_array($event->data, ['edit', 'preview'])) { 175 return; 176 } 177 178 $linkForm = new dokuwiki\Form\Form([ 179 'class' => 'plugin_prosemirror_linkform', 180 'id' => 'prosemirror-linkform', 181 'style' => 'display: none;', 182 ]); 183 $linkForm->addFieldsetOpen('Links')->addClass('js-link-fieldset');; 184 $iwOptions = array_keys(getInterwiki()); 185 $linkForm->addDropdown('iwshortcut', $iwOptions, 'InterWiki')->attr('required', 'required'); 186 187 $linkForm->addButtonHTML('linkwiz', inlineSVG(DOKU_PLUGIN . 'prosemirror/images/link.svg'))->attrs([ 188 'type' => 'button', 189 'class' => 'js-open-linkwiz linkform_linkwiz' 190 ]); 191 $linkForm->addTextInput('linktarget', $this->getLang('link target'))->attrs( 192 [ 193 'required'=> 'required', 194 'autofocus' => 'autofocus', 195 ] 196 ); 197 198 $linkForm->addTagOpen('div')->addClass('radio-wrapper'); 199 $linkForm->addTagOpen('fieldset'); 200 $linkForm->addTagOpen('legend'); 201 $linkForm->addHTML('Link Type'); 202 $linkForm->addTagClose('legend'); 203 $linkForm->addRadioButton('linktype', $this->getLang('type:wiki page'))->val('internallink'); 204 $linkForm->addRadioButton('linktype', $this->getLang('type:interwiki'))->val('interwikilink'); 205 $linkForm->addRadioButton('linktype', $this->getLang('type:email'))->val('emaillink'); 206 $linkForm->addRadioButton('linktype', $this->getLang('type:external'))->val('externallink')->attr('checked', 'checked'); 207 $linkForm->addRadioButton('linktype', $this->getLang('type:other'))->val('other'); 208 $linkForm->addTagClose('fieldset'); 209 $linkForm->addTagClose('div'); 210 211 $linkForm->addTagOpen('div')->addClass('radio-wrapper'); 212 $linkForm->addTagOpen('fieldset'); 213 $linkForm->addTagOpen('legend'); 214 $linkForm->addHTML('Link Name Type'); 215 $linkForm->addTagClose('legend'); 216 $linkForm->addRadioButton('nametype', $this->getLang('type:automatic title'))->val('automatic')->attr('checked', 'checked'); 217 $linkForm->addRadioButton('nametype', $this->getLang('type:custom title'))->val('custom'); 218 $linkForm->addRadioButton('nametype', $this->getLang('type:image'))->val('image'); 219 $linkForm->addTextInput('linkname', 'Link name')->attr('placeholder', $this->getLang('placeholder:link name')); 220 $linkForm->addTagOpen('div')->addClass('js-media-wrapper'); 221 $linkForm->addTagClose('div'); 222 $linkForm->addTagClose('fieldset'); 223 $linkForm->addTagClose('div'); 224 225 226 $linkForm->addFieldsetClose(); 227 $linkForm->addButton('ok-button', 'OK')->attr('type', 'submit'); 228 $linkForm->addButton('cancel-button', $this->getLang('cancel'))->attr('type', 'button'); 229 230 echo $linkForm->toHTML(); 231 232 $mediaForm = new dokuwiki\Form\Form([ 233 'class' => 'plugin_prosemirror_mediaform', 234 'id' => 'prosemirror-mediaform', 235 'style' => 'display: none;', 236 ]); 237 $mediaForm->addFieldsetOpen($this->getLang('legend:media'))->addClass('js-media-fieldset'); 238 $mediaForm->addButtonHTML('mediamanager', inlineSVG(DOKU_PLUGIN . 'prosemirror/images/file-image-outline.svg'))->attrs([ 239 'type' => 'button', 240 'class' => 'js-open-mediamanager mediaform_mediamanager' 241 ]); 242 $mediaForm->addTextInput('mediatarget', $this->getLang('media target'))->attrs( 243 [ 244 'required'=> 'required', 245 'autofocus' => 'autofocus', 246 ] 247 ); 248 $mediaForm->addTextInput('mediacaption', $this->getLang('label:caption')); 249 250 $mediaForm->addTagOpen('div')->addClass('image-properties'); 251 $mediaForm->addTagOpen('p'); 252 $mediaForm->addHTML($this->getLang('label:image_properties')); 253 $mediaForm->addTagClose('p'); 254 255 $mediaForm->addTagOpen('div')->addClass('input-wrapper'); 256 $mediaForm->addTagOpen('fieldset'); 257 $mediaForm->addTagOpen('legend'); 258 $mediaForm->addHTML($this->getLang('legend:size')); 259 $mediaForm->addTagClose('legend'); 260 $mediaForm->addTextInput('width', $this->getLang('label:width'))->attr('type', 'number'); 261 $mediaForm->addTextInput('height', $this->getLang('label:height'))->attr('type', 'number'); 262 $mediaForm->addTagClose('fieldset'); 263 $mediaForm->addTagClose('div'); 264 265 $mediaForm->addTagOpen('div')->addClass('input-wrapper'); 266 $mediaForm->addTagOpen('fieldset'); 267 $mediaForm->addTagOpen('legend'); 268 $mediaForm->addHTML($this->getLang('legend:alignment')); 269 $mediaForm->addTagClose('legend'); 270 $mediaForm->addRadioButton('alignment', $this->getLang('label:default alignment'))->val('')->attr('checked', 'checked'); 271 $mediaForm->addRadioButton('alignment', $this->getLang('label:float left'))->val('left'); 272 $mediaForm->addRadioButton('alignment', $this->getLang('label:center alignment'))->val('center'); 273 $mediaForm->addRadioButton('alignment', $this->getLang('label:float right'))->val('right'); 274 $mediaForm->addTagClose('fieldset'); 275 $mediaForm->addTagClose('div'); 276 277 $mediaForm->addTagOpen('div')->addClass('input-wrapper'); 278 $mediaForm->addTagOpen('fieldset'); 279 $mediaForm->addTagOpen('legend'); 280 $mediaForm->addHTML($this->getLang('legend:linking')); 281 $mediaForm->addTagClose('legend'); 282 $mediaForm->addRadioButton('linking', $this->getLang('label:default linking'))->val('details')->attr('checked', 'checked'); 283 $mediaForm->addRadioButton('linking', $this->getLang('label:direct linking'))->val('direct'); 284 $mediaForm->addRadioButton('linking', $this->getLang('label:nolink'))->val('nolink'); 285 $mediaForm->addRadioButton('linking', $this->getLang('label:linkonly'))->val('linkonly'); 286 $mediaForm->addTagClose('fieldset'); 287 $mediaForm->addTagClose('div'); 288 289 $mediaForm->addTagOpen('div')->addClass('input-wrapper'); 290 $mediaForm->addTagOpen('fieldset'); 291 $mediaForm->addTagOpen('legend'); 292 $mediaForm->addHTML($this->getLang('legend:caching')); 293 $mediaForm->addTagClose('legend'); 294 $mediaForm->addRadioButton('caching', $this->getLang('label:default caching'))->val('')->attr('checked', 'checked'); 295 $mediaForm->addRadioButton('caching', $this->getLang('label:recache'))->val('recache'); 296 $mediaForm->addRadioButton('caching', $this->getLang('label:nocache'))->val('nocache'); 297 $mediaForm->addTagClose('fieldset'); 298 $mediaForm->addTagClose('div'); 299 300 $mediaForm->addTagClose('div'); // end of image-properties 301 302 $mediaForm->addFieldsetClose(); 303 $mediaForm->addButton('ok-button', 'OK')->attr('type', 'submit'); 304 $mediaForm->addButton('cancel-button', $this->getLang('cancel'))->attr('type', 'button'); 305 306 // dynamic image hack? https://www.dokuwiki.org/images#dynamic_images 307 308 echo $mediaForm->toHTML(); 309 310 // phpcs:disable 311 $languages = explode(' ', '4cs 6502acme 6502kickass 6502tasm 68000devpac abap actionscript3 actionscript ada aimms algol68 apache applescript apt_sources arm asm asp asymptote autoconf autohotkey autoit avisynth awk bascomavr bash basic4gl batch bf biblatex bibtex blitzbasic bnf boo caddcl cadlisp ceylon cfdg cfm chaiscript chapel cil c_loadrunner clojure c_mac cmake cobol coffeescript c cpp cpp-qt cpp-winapi csharp css cuesheet c_winapi dart dcl dcpu16 dcs delphi diff div dos dot d ecmascript eiffel email epc e erlang euphoria ezt f1 falcon fo fortran freebasic freeswitch fsharp gambas gdb genero genie gettext glsl gml gnuplot go groovy gwbasic haskell haxe hicest hq9plus html html4strict html5 icon idl ini inno intercal io ispfpanel java5 java javascript jcl j jquery julia kixtart klonec klonecpp kotlin latex lb ldif lisp llvm locobasic logtalk lolcode lotusformulas lotusscript lscript lsl2 lua m68k magiksf make mapbasic mathematica matlab mercury metapost mirc mk-61 mmix modula2 modula3 mpasm mxml mysql nagios netrexx newlisp nginx nimrod nsis oberon2 objc objeck ocaml-brief ocaml octave oobas oorexx oracle11 oracle8 oxygene oz parasail parigp pascal pcre perl6 perl per pf phix php-brief php pic16 pike pixelbender pli plsql postgresql postscript povray powerbuilder powershell proftpd progress prolog properties providex purebasic pycon pys60 python qbasic qml q racket rails rbs rebol reg rexx robots rpmspec rsplus ruby rust sas sass scala scheme scilab scl sdlbasic smalltalk smarty spark sparql sql standardml stonescript swift systemverilog tclegg tcl teraterm texgraph text thinbasic tsql twig typoscript unicon upc urbi uscript vala vbnet vb vbscript vedit verilog vhdl vim visualfoxpro visualprolog whitespace whois winbatch xbasic xml xojo xorg_conf xpp yaml z80 zxbasic'); 312 // phpcs:enable 313 $datalistHTML = '<datalist id="codelanguages">'; 314 foreach ($languages as $language) { 315 $datalistHTML .= "<option value=\"$language\">"; 316 } 317 $datalistHTML .= '</datalist>'; 318 echo $datalistHTML; 319 } 320 321 /** 322 * Provide the current smiley configuration to Javascript 323 */ 324 public function addJSINFO() 325 { 326 global $JSINFO; 327 $JSINFO['SMILEY_CONF'] = getSmileys(); 328 } 329 330 /** 331 * Returns true if the current content is read only 332 * 333 * @todo remove Doku_Form case when the class is removed 334 * 335 * @param $form 336 * @return bool 337 */ 338 protected function isReadOnly($form) 339 { 340 if (is_a($form, \dokuwiki\Form\Form::class)) { 341 $textareaPos = $form->findPositionByType('textarea'); 342 $readonly = $textareaPos !== false && !empty($form->getElementAt($textareaPos)->attr('readonly')); 343 } else { 344 /** @var Doku_Form $form */ 345 $textareaPos = $form->findElementByType('wikitext'); 346 $readonly = $textareaPos !== false && !empty($form->getElementAt($textareaPos)['readonly']); 347 } 348 return $readonly; 349 } 350} 351 352// vim:ts=4:sw=4:et: 353