commit 9a18131bcf026a4c5860fbd2772ca0720f598c39 Author: bachy Date: Sat Oct 27 15:02:05 2012 +0200 first import 2.1 Signed-off-by: bachy diff --git a/CHANGELOG.txt b/CHANGELOG.txt new file mode 100644 index 00000000..2b79f35b --- /dev/null +++ b/CHANGELOG.txt @@ -0,0 +1,326 @@ + +Wysiwyg 7.x-2.x, xxxx-xx-xx +--------------------------- + + +Wysiwyg 7.x-2.1, 2011-06-19 +--------------------------- +#679056 by TwoD: Fixed patch for pressing enter in autocomplete, new jQuery API. +#524126 by sun: Re-added #wysiwyg property to enforce no editor via code. +#1153458 by Deciphered: Fixed TinyMCE 'Verify HTML' setting being ignored. +#1079694 by TwoD: Fixed Whizzywig not restoring textarea styles when detached. +#1132142 by tacituseu, TwoD, sun: Fixed nicEdit not removing its submit handler. +#1143104 by EugenMayer: Fixed CKEditor 3.5.4 version detection. +#1009880 by AndyF: Fixed another CKEditor selection handling issue. +#1048556 by cousin_itt, TwoD: Fixed TinyMCE insertdatetime plugin setting. +#1036900 by mattyoung: Minor code clean-up in wysiwyg_tinymce_version(). +#1026088 by sun: Fixed installation instructions in README.txt. +by sun: Merged in module baseline to facilitate testing. +#1034476 by quartsize, sun: Changed Wysiwyg profiles into entities. + + +Wysiwyg 7.x-2.0, 2010-01-06 +--------------------------- +#950216 by TwoD, sun: Fixed missing editor for a single text format. +#1007630 by aspilicious: Removed files[] declarations from .info file. +#612954 by sun: Reverted 'buttons' change in profile configuration form. +#975546 by TwoD, sun: Fixed markItUp CSS loaded with wrong weight. +#659428 by TwoD, sun: Fixed editor is attached to disabled text format widgets. +#975490 by Gábor Hojtsy: Updated for 'group' API change in drupal_add_js(). +#974604 by CrookedNumber, Gábor Hojtsy: Fixed theme CSS not loaded in editors. +#931374 by TwoD, ksenzee: Updated for text format schema change. +#826914 by catch, sun: Added database cache for wysiwyg_profile_load(). +#941230 by sun: Fixed missing Configuration link on Modules page. +#739558 by sun, TwoD: Updated for new #type text_format. +#612954 by TwoD: Fixed broken editor settings. +#585932 by sun: Ported to Drupal 7. + + +Wysiwyg 6.x-2.3, 2011-01-30 +--------------------------- +#1025296 by TwoD: Updated CKEditor to support iFrame button. +#737318 by TwoD: Fixed CKEditor default skin array not being reindexed. +by sun: Fixed coding style in wysiwyg_schema(). +#964978 by sun, TwoD: Added hook_wysiwyg_editor_settings_alter() documentation. +#775972 by TwoD, Agileware, sun: Fixed broken user default status preferences. +#1007066 by TwoD, penguin25: Fixed CKEditor ignores resizable option. +#613944 by TwoD, sun: Fixed data.node not always present in CKEditor. +#1009880 by TwoD: Fixed selection handling broken in CKEditor. + + +Wysiwyg 6.x-2.2, 2010-12-20 +--------------------------- +#613944 by TwoD, sun: Fixed data.node not available in CKEditor. +#748888 by TwoD, sun: Fixed isNode() not called in CKEditor. +#767550 by TwoD, sun, ungeek: Fixed invalid API docs and logic for + $plugin['filename']. +#988200 by sun: Changed static language list to ISO 639 defaults of Drupal core. +#973808 by David_Rothstein: Fixed CKEditor incorrectly formatting the
tag. +#773856 by Roi Danton: Added CSS path and file documentation. +#735186 by TwoD, torbs: Fixed missing Norwegian language code. +#678580 by TwoD, sun: Fixed Drupal.wysiwygAttachToggleLink breaks click events. +#497654 by TwoD: Fixed Drupal plugins disabled in FCKeditor/WebKit browsers. +#735624 by sun: Fixed enabling one button removes default editor toolbar. +#755610 by sun, TwoD, BrightBold: Fixed white-space in block formats setting + breaks editors. +#713942 by TwoD, sun: Fixed jQuery closure breaks OpenWYSIWYG. +#679056 by sun, TwoD: Fixed pressing enter in autocomplete detaches editors. +#80170 by sun: Changed dialog/plugin API for Inline API compatibility. +#803466 by hotspoons: Fixed TinyMCE image map support in advimage plugin. +#922436 by TwoD: Fixed Whizzywig Uncaught TypeError in Chrome. +#922520 by TwoD: Fixed Whizzywig is not detached properly. +#907186 by TwoD: Fixed Whizzywig v60+ compatibility. +#765292 by TwoD: Added TinyMCE WordCount plugin. +#768726 by TwoD: Added TinyMCE AutoResize plugin. +#781086 by TwoD: Fixed TinyMCE plugin options merged wrongly. +#767628 by TwoD: Fixed 'The version of markItUp could not be detected' error. +#651490 by TwoD: Fixed Whizzywig width. +#715228 by TwoD: Fixed TinyMCE image popups not launching for existing images. +#606952 by TwoD: Fixed inserting content in fullscreen TinyMCE. +#593008 by TwoD: Fixed third-party scripts breaking Wysiwyg. +#695398 by RichieB, Cl1mh4224rd, mcpuddin: Fixed TinyMCE 3.3.9.1 detection. +#737318 by dboune: Fixed CKEditor default skin depends on filesystem order. +#775608 by TwoD: Fixed FCKEditor crashes IE on save. +#824710 by TwoD: CKEditor not disabled upon enabling. +#752516 by nquocbao, sun: Fixed openwysiwyg version callback. +#753536 by TwoD: Fixed version detection for Whizzywig. +#752516 by nquocbao, sun: Fixed file stream warnings in version callbacks. + + +Wysiwyg 6.x-2.1, 2010-03-08 +--------------------------- +#628110 by quicksketch, sun, markus_petrux: Added editor settings alter hook. +#689218 by wwalc, TwoD, sun: Improved support for CKEditor. +#695398 by TwoD: Updated support for TinyMCE 3.3. +#613096 by Scott Reynolds: Fixed no editor appearing for user signature field. +#696040 by Dave Reid: Fixed missing Cancel link on profile form. +#594322 by TwoD: Added insert method for NicEdit. +#659200 by TwoD: Fixed YUI Editor content lost in IE. +#594928 by ericbellot, TwoD, sun: Fixed 'attribs' button missing in TinyMCE. +#557090 by TwoD: Fixed Whizzywig 56 instance not removed on detach(). +#667848 by TwoD, kaakuu: Fixed FCKeditor is not properly detached in IE. +#695768 by sun: Fixed #resizable removed when no editor profiles are loaded. +#631494 by TwoD: Fixed multi-site libraries directory failure for WYMeditor. +#660080 by TwoD: Fixed Notice: Undefined offset. +#613922 by TwoD, sun: Fixed PHP warning when saving profiles. +#582298 by dereine: Added auto-paste from Word detection for FCKeditor. +#597852 by sun: Fixed missing Turkish in language list. +#620176 by sun: Fixed missing Ukrainian in language list. +#613480 by TwoD, Dave Reid: Fixed PHP 5.3 compatibility. +#462146 by TwoD: Cleaned up CKEditor implementation. +#380586 by SimonEast: Updated YUI editor: Version detection not working. +#610132 by TwoD: Updated CKEditor 3.0.1, stylesheets and version check. +#620858 by quicksketch: Fixed focus event not firing for CKeditor. +#585932 by sun: Synced various clean-ups from 7.x. +#489156 by sun: Removed orphan global 'showToggle' JS setting. +#462146 by sun, Niels Hackius: Fixed version detection for CKeditor. +#545210 by sun: Fixed default value for editor toggle link. +#372826 by Roi Danton, sun: Added Wysiwyg API developer documentation. +by sun: Fixed PHP notice. +#514912 by Likeless, sun: Added plugin/button handling for WYMeditor. +#538996 by darktygur: Fixed 404 errors for non-existing theme CSS files. +#509570 by Rob Loach, sun: Added forced detaching of editor upon form submit. +#526644 by Darren Oh: Fixed broken editor theme validation. +#490914 by sun: Fixed JS/CSS not updated after update with caching enabled. +#522440 by authentictech, sun: UX: Fixed user interface for Wysiwyg profiles. +#507608 by jfh: Added WYMeditor instance API methods. +by sun: Fixed form_build_id not removed from serialized profile settings. +#496744 by TwoD: Fixed FCKeditor: "Apply source formatting" not working. +#462146 by TwoD, et al: Added support for CKeditor. +#490270 by sun: Fixed openWYSIWYG displays no buttons by default. +#490266 by sun: Fixed JS error when wysiwyg profile contains no buttons. +#400482 by sun: Fixed editor.instance.prepareContent() breaks editor's native + markup handling. Drupal plugin authors should add the CSS class + 'drupal-content' to prevent the editor selection to activate internal editor + buttons. +#394068 by kswan: Fixed missing button icons in markItUp. + + +Wysiwyg 6.x-2.0, 2009-06-10 +--------------------------- +#474908 by TwoD: Fixed Teaser break causing error in IE8. +by sun: Major code clean-up. +#331089 by wwalc: Fixed FCKeditor toolbar buttons do not wrap. +#407014 by sun: Fixed/removed migration from other editor integration modules. +#485264 by sun: Changed installation instructions to be more concise. +#479514 by sun: Fixed native plugin loading for TinyMCE (follow-up). +#434590 by sun: Fixed path admin/settings/wysiwyg not found. +#479514 by TwoD, sun: Added native plugin support for FCKeditor. +#341054 by sun: Fixed toggle link configuration setting not working. +by sun: Fixed markItUp button icons are not displayed. +by sun: Added openWYSIWYG editor support. +#362137 by jfh, sun: Fixed WYMeditor broken when JS/CSS aggregation is enabled. +#328252 by sun: Added TinyMCE plugin BBCode for 3.x. +#429926 by TwoD, sun: Fixed TinyMCE broken due to renamed Flash/Media plugin. +#342864 by davexoxide, sun: Added YUI editor support. +#332139 by sun: Fixed editor must not be changed when profile is configured. +#362137 by jfh: Added WYMeditor support. +#470928 by jfh, sun: Fixed Drupal.wysiwyg.clone turns arrays into objects. +#445826 by TwoD: Fixed FCKeditor: Drupal.wysiwyg.activeId not updated. +#478324 by jeffschuler: Fixed typo in profile configuration form. +#373542 by sun: Fixed encoding of HTML entities for certain languages. +#320562 by sun: Changed location for external editor libraries. +#449134 by sun: Fixed stylesheets of theme missing in node form previews. + + +Wysiwyg 6.x-2.0-ALPHA1, 2009-05-17 +---------------------------------- +#403728 by jfduchesneau: Fixed none.js breaks if textarea.js is not loaded. +#454992 by sun: Fixed drupal_get_js() query string 'q' breaks plugin loading. +#419696 by sun: Fixed native plugins plugins are not loaded for all profiles. +#414768 by sun: Fixed Wysiwyg API not working in Konqueror. +#293803 by sun: Fixed "Show summary in full view" checkbox not displayed. +#416742 by sun: Fixed type casting of $profile in profile configuration form. +#404532 by TwoD: Fixed Teaser break comment stripped in IE. +#380698 by TwoD: Added Drupal plugin support for FCKeditor. +#380698 by TwoD, sun: Added Drupal plugin support for FCKeditor (part I). +#398848 by sun: Added support for TinyMCE 3.1. +#394068 by StephaneC: markItUp: Fixed icons not displayed due to #385736. +#385974 by sun: Fixed form element description for CSS path (for Define CSS). +#390460 by sun: Fixed broken plugin loading due to #359626. +#385736 by sun: Fixed markItUp: Wrong editor library location. +#308912 by sun: Fixed TinyMCE: Buttons do not wrap in IE/Chrome. +#380586 by sun, hass: YUI editor: Fixed version detection. +#390224 by hass: Fixed JS error YAHOO.widget.Editor is not a constructor. +#359626 by sun: Fixed external/Drupal plugins are not loaded for all profiles. +#369115 by sun: Fixed TinyMCE's URL conversion magic breaks some input filters. +#376400 by TwoD: Fixed bad grammar in help text on profile overview page. +#367632 by sun: Fixed $this and i JavaScript variables defined in global scope. +#319363 by sun: Fixed missing spacer.gif for Teaser break plugin. +#373672 by chawl: Added (native) xhtmlextras plugin for TinyMCE 3. +#287025 by sun: Fixed native editor plugin options for TinyMCE and FCKeditor. +#373542 by sun: Fixed TinyMCE: entity_encoding 'raw' removes HTML entities. +#372806 by sun: Fixed block format configuration form element description. +#370277 by sun: Fixed "Uncaught SyntaxError: Unexpected token" in IE/Chrome. +#367632 by sun: Fixed "Unexpected identifier, string or number" error in IE. +#367632 by sun: Fixed invalid JavaScript syntax. +#319363 by sun: Follow-up: Synced 1.x with 2.x where possible. +#319363 by sun, quicksketch: Rewrote editor plugin API. The new plugin API + allows Drupal modules to expose editor plugins for ANY editor without + implementing editor-specific code. Major milestone for better content-editing + in Drupal. +#364782 by sun: Fixed theme stylesheets not properly loaded. +#352938 by sun: Fixed JS error (blank page) in IE when plugins are loaded. +#331089 by wwalc, sun: Added custom toolbar configuration support for FCKeditor. +#331089 by sun: Fixed PHP notice for 'user_choose'; FCKeditor clean-up. +#344230 by wwalc: Fixed wrong editor base path setting for FCKeditor. +#361289 by sun: Fixed CSS files do not need to use media 'screen'. +#360696 by sun: Fixed IE does not trigger onChange event when selecting an input + format. +#342376 by sun: Extended API to allow "preprocess" JavaScript option for some + editors. +#352295 by sun: Added markItUp editor support. +#352703 by sun: Fixed wrong default configuration options for TinyMCE 3.2.1+. +#348317 by sun: Fixed TinyMCE's extended_valid_elements for advlink/advimage + plugin. +#348986 by sun: Added CSS class for toggle link container. +#342864 by davexoxide, sun: Added YUI editor support. +#343217 by sun: Fixed improperly capitalized library script name for nicEdit. +#341267 by sun: Fixed improper test for internal editor plugins. +#341996 by sun: Fixed editor cannot be re-enabled with one input format only. +#341267 by sun: Added support for extensions that do not need to be loaded. + + +Wysiwyg 6.x-0.5, 2008-12-01 +--------------------------- +#340758 by sun: Changed installation instructions to be displayed permanently. +#322657 by sun: Fixed "Enabled by default" option does not work when disabled. +#328052 by sun: Fixed switching input formats leads to wrong editor/state. +#337569 by sun: Fixed different profiles for same editor are not respected. +#340195 by sun: Fixed #after_build function not invoked on all forms. +#333521 by sun: Fixed TinyMCE version detection to look at the actual script. +#329657 by svendecabooter, sun: Added Whizzywig support. +#333521 by sun: Fixed TinyMCE version detection docs. +#327100 by sun: Changed access permission for settings page to 'administer + filters' to prevent incomplete updates. +#322731 by sun: Fixed improper use of t() in module install file. +#329410 by sun: Fixed editor not loaded if there is only one input format. +#324366 by sun: Fixed "Illegal offset type" error on custom content-types. +#328948 by sun: Fixed PHP notices when editors are assigned, but not configured. +#327710 by sun: Fixed nicEdit version could not be detected. +#328116 by sun: Added Safari plugin for TinyMCE 3. +#327710 by sun: Added nicEdit support. +#323855 by sun: Increased supported version of jWYSIWYG to 0.5. +#323671 by sun: Fixed TinyMCE editor not resized when browser is resized. +#327152 by sun: Fixed breadcrumbs for profile configuration pages. +#323855 by Rob Loach, sun: Added jWYSIWYG support. +#327100 by sun: Associate editors/profiles with input formats. Major milestone. +#325980 by markus_petrux: Added Spanish/Catalan translation for Break plugin. +#323795 by sun: Removed obsolete Wysiwyg Editor module files. +#308912 by sun: Fixed alignment of editor buttons in TinyMCE 3. +#316507 by sun: Fixed TinyMCE 3 not detached properly from AJAX contents. +#320559 by markus_petrux, sun: Added confirmation form to delete profiles. + + +Wysiwyg 6.x-0.4, 2008-10-14 +--------------------------- +#321216 by sun: Replaced Wysiwyg Editor module with Wysiwyg module. +#321086 by sun: Fixed (old-style) Teaser break plugin breaks TinyMCE 3. +#316507 by sun: Code clean-up; editor settings should be cloned for init, too. +#282717 by sun: Fixed FCKeditor default settings while FCKeditor maintainers get + up and running. +#319363 by sun: Changed JS settings namespace 'wysiwygEditor' to 'wysiwyg'. +#319363 by sun: Code clean-up; fixed missing namespace change in tinymce-3.js. +#273408 by quicksketch: Added blockquote button for TinyMCE 3. +#319363 by sun: Changed JavaScript namespaces and centralized namespace + initialization. +#270780 by sun: Fixed TinyMCE 3 support for external plugins. +#309832 by sun: Fixed README.txt. +#253600 by sun: Changed editor integration so that client-side editors attach to + input formats instead of textareas and are invoked for input format enabled + textareas only. +#282717 by sun: Added (basic) FCKeditor support. +#316507 by sun: Added Drupal.wysiwyg function stacks to execute editor library + specific actions upon initializing, attaching, detaching, and toggling an + editor. Editor specific JavaScript resides in separate files now, as specified + and returned by implementations of hook_editor(). + Wysiwyg is a real API finally, supporting multiple editors and editor versions. +#316507 by sun: Rewrote Wysiwyg API's internal architecture to support multiple + editors. + + +Wysiwyg 6.x-0.3, 2008-09-12 +--------------------------- +#125267 by sun: Removed Safari browser warning configuration option. +#304243 by sun: Fixed profile configuration improperly passed to JavaScript. +#304243 by sun: Code clean-up for wysiwyg_editor_profile_overview(). +#289218 by gustav: Fixed E^ALL notice if node has no body field. +#304243 by sun: Code clean-up for wysiwyg_editor_user_status(). +#299108 by toniw: Added setting for TinyMCE's auto-cleanup paste feature. +#293916 by sun: Clarified TinyMCE compatibility in README.txt. +#293425 by sun: Fixed foreach warning during upgrade from TinyMCE module. +#292517 by sun: Fixed SQL error during upgrade from TinyMCE module. +#286470 by chayner, sun: Fixed wrong editorBasePath in editor configuration. +#227687 by sun: Fixed improperly capitalized package name. +#288028 by Matthew Davidson: Fixed outdated check for PHP input filter. +#280727 by sun: Removed gzip compressor from installation instructions. + + +Wysiwyg 5.x-0.2, 2008-07-16 +--------------------------- +by sun: Fixed JavaScript errors when JS aggregation/compression is enabled. +#268562 by sun: Code clean-up; changed format for custom defined CSS classes + and removed error-prone auto-layout of buttons in favor of aligning them in + one row with a stylesheet; may break existing profiles. +#270730 by hass, sun: Added German translation for Teaser break plugin. +#268838 by sun: Fixed PHP warning if no buttons are enabled for a profile. +#268838 by sun: Ported to Drupal 6.x. +#152046 by sun: Added hook_wysiwyg_plugin(). +#268562 by sun: Code clean-up. +#60667 by sun: Fixed wrong editor profile is loaded when user is granted access + to more than one profile. +#264739 by sun: Fixed missing t() around some profile settings options. + + +Wysiwyg 5.x-0.1, 2008-06-07 +--------------------------- +#264739 by sun: Improved output strings. +#264739 by hass, sun: Fixed potx error due to wrong t() string. +#264411 by sun: Cleaned coding-style using coder_format script. +#264411 by sun: Moved admin functions into separate include file. +#264411 by sun: Added TinyMCE data import upon installation. +#264411 by sun: Renamed module to Wysiwyg Editor. +#118747 by nedjo, sun: Upgraded code for jQuery. +Initial fork of TinyMCE module (2008-05-30). + + diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..2c095c8d --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,274 @@ +GNU GENERAL PUBLIC LICENSE + + Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. 675 Mass Ave, +Cambridge, MA 02139, USA. Everyone is permitted to copy and distribute +verbatim copies of this license document, but changing it is not allowed. + + Preamble + +The licenses for most software are designed to take away your freedom to +share and change it. By contrast, the GNU General Public License is +intended to guarantee your freedom to share and change free software--to +make sure the software is free for all its users. This General Public License +applies to most of the Free Software Foundation's software and to any other +program whose authors commit to using it. (Some other Free Software +Foundation software is covered by the GNU Library General Public License +instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our +General Public Licenses are designed to make sure that you have the +freedom to distribute copies of free software (and charge for this service if +you wish), that you receive source code or can get it if you want it, that you +can change the software or use pieces of it in new free programs; and that +you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to +deny you these rights or to ask you to surrender the rights. These restrictions +translate to certain responsibilities for you if you distribute copies of the +software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for +a fee, you must give the recipients all the rights that you have. You must make +sure that they, too, receive or can get the source code. And you must show +them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) +offer you this license which gives you legal permission to copy, distribute +and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that +everyone understands that there is no warranty for this free software. If the +software is modified by someone else and passed on, we want its recipients +to know that what they have is not the original, so that any problems +introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We +wish to avoid the danger that redistributors of a free program will individually +obtain patent licenses, in effect making the program proprietary. To prevent +this, we have made it clear that any patent must be licensed for everyone's +free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification +follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND + MODIFICATION + +0. This License applies to any program or other work which contains a notice +placed by the copyright holder saying it may be distributed under the terms +of this General Public License. The "Program", below, refers to any such +program or work, and a "work based on the Program" means either the +Program or any derivative work under copyright law: that is to say, a work +containing the Program or a portion of it, either verbatim or with +modifications and/or translated into another language. (Hereinafter, translation +is included without limitation in the term "modification".) Each licensee is +addressed as "you". + +Activities other than copying, distribution and modification are not covered +by this License; they are outside its scope. The act of running the Program is +not restricted, and the output from the Program is covered only if its contents +constitute a work based on the Program (independent of having been made +by running the Program). Whether that is true depends on what the Program +does. + +1. You may copy and distribute verbatim copies of the Program's source +code as you receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice and +disclaimer of warranty; keep intact all the notices that refer to this License +and to the absence of any warranty; and give any other recipients of the +Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you +may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, +thus forming a work based on the Program, and copy and distribute such +modifications or work under the terms of Section 1 above, provided that you +also meet all of these conditions: + +a) You must cause the modified files to carry prominent notices stating that +you changed the files and the date of any change. + +b) You must cause any work that you distribute or publish, that in whole or in +part contains or is derived from the Program or any part thereof, to be +licensed as a whole at no charge to all third parties under the terms of this +License. + +c) If the modified program normally reads commands interactively when run, +you must cause it, when started running for such interactive use in the most +ordinary way, to print or display an announcement including an appropriate +copyright notice and a notice that there is no warranty (or else, saying that +you provide a warranty) and that users may redistribute the program under +these conditions, and telling the user how to view a copy of this License. +(Exception: if the Program itself is interactive but does not normally print such +an announcement, your work based on the Program is not required to print +an announcement.) + +These requirements apply to the modified work as a whole. If identifiable +sections of that work are not derived from the Program, and can be +reasonably considered independent and separate works in themselves, then +this License, and its terms, do not apply to those sections when you distribute +them as separate works. But when you distribute the same sections as part +of a whole which is a work based on the Program, the distribution of the +whole must be on the terms of this License, whose permissions for other +licensees extend to the entire whole, and thus to each and every part +regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to +work written entirely by you; rather, the intent is to exercise the right to +control the distribution of derivative or collective works based on the +Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of a +storage or distribution medium does not bring the other work under the scope +of this License. + +3. You may copy and distribute the Program (or a work based on it, under +Section 2) in object code or executable form under the terms of Sections 1 +and 2 above provided that you also do one of the following: + +a) Accompany it with the complete corresponding machine-readable source +code, which must be distributed under the terms of Sections 1 and 2 above +on a medium customarily used for software interchange; or, + +b) Accompany it with a written offer, valid for at least three years, to give +any third party, for a charge no more than your cost of physically performing +source distribution, a complete machine-readable copy of the corresponding +source code, to be distributed under the terms of Sections 1 and 2 above on +a medium customarily used for software interchange; or, + +c) Accompany it with the information you received as to the offer to distribute +corresponding source code. (This alternative is allowed only for +noncommercial distribution and only if you received the program in object +code or executable form with such an offer, in accord with Subsection b +above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source code +means all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation and +installation of the executable. However, as a special exception, the source +code distributed need not include anything that is normally distributed (in +either source or binary form) with the major components (compiler, kernel, +and so on) of the operating system on which the executable runs, unless that +component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to +copy from a designated place, then offering equivalent access to copy the +source code from the same place counts as distribution of the source code, +even though third parties are not compelled to copy the source along with the +object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as +expressly provided under this License. Any attempt otherwise to copy, +modify, sublicense or distribute the Program is void, and will automatically +terminate your rights under this License. However, parties who have received +copies, or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. +However, nothing else grants you permission to modify or distribute the +Program or its derivative works. These actions are prohibited by law if you +do not accept this License. Therefore, by modifying or distributing the +Program (or any work based on the Program), you indicate your acceptance +of this License to do so, and all its terms and conditions for copying, +distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the original +licensor to copy, distribute or modify the Program subject to these terms and +conditions. You may not impose any further restrictions on the recipients' +exercise of the rights granted herein. You are not responsible for enforcing +compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), conditions +are imposed on you (whether by court order, agreement or otherwise) that +contradict the conditions of this License, they do not excuse you from the +conditions of this License. If you cannot distribute so as to satisfy +simultaneously your obligations under this License and any other pertinent +obligations, then as a consequence you may not distribute the Program at all. +For example, if a patent license would not permit royalty-free redistribution +of the Program by all those who receive copies directly or indirectly through +you, then the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply and +the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or +other property right claims or to contest validity of any such claims; this +section has the sole purpose of protecting the integrity of the free software +distribution system, which is implemented by public license practices. Many +people have made generous contributions to the wide range of software +distributed through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing to +distribute software through any other system and a licensee cannot impose +that choice. + +This section is intended to make thoroughly clear what is believed to be a +consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain +countries either by patents or by copyrighted interfaces, the original copyright +holder who places the Program under this License may add an explicit +geographical distribution limitation excluding those countries, so that +distribution is permitted only in or among countries not thus excluded. In such +case, this License incorporates the limitation as if written in the body of this +License. + +9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will be +similar in spirit to the present version, but may differ in detail to address new +problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies +a version number of this License which applies to it and "any later version", +you have the option of following the terms and conditions either of that +version or of any later version published by the Free Software Foundation. If +the Program does not specify a version number of this License, you may +choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs +whose distribution conditions are different, write to the author to ask for +permission. For software which is copyrighted by the Free Software +Foundation, write to the Free Software Foundation; we sometimes make +exceptions for this. Our decision will be guided by the two goals of +preserving the free status of all derivatives of our free software and of +promoting the sharing and reuse of software generally. + + NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT +PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE +STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT +WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, +INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND +PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL +NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR +AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR +ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE +LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, +SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OR INABILITY TO USE THE +PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA +OR DATA BEING RENDERED INACCURATE OR LOSSES +SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE +PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN +IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF +THE POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS diff --git a/README.txt b/README.txt new file mode 100644 index 00000000..cc81447a --- /dev/null +++ b/README.txt @@ -0,0 +1,53 @@ + +-- SUMMARY -- + +Wysiwyg API allows to users of your site to use WYSIWYG/rich-text, and other +client-side editors for editing contents. This module depends on third-party +editor libraries, most often based on JavaScript. + +For a full description of the module, visit the project page: + http://drupal.org/project/wysiwyg +To submit bug reports and feature suggestions, or to track changes: + http://drupal.org/project/issues/wysiwyg + + +-- REQUIREMENTS -- + +* None. + + +-- INSTALLATION -- + +* Install as usual, see http://drupal.org/node/70151 for further information. + +* Go to Administration » Configuration » Content authoring » Wysiwyg, + and follow the displayed installation instructions to download and install one + of the supported editors. + + +-- CONFIGURATION -- + +* Go to Administration » Configuration » Content authoring » Text formats, and + + - either configure the Full HTML format, assign it to trusted roles, and + disable "HTML filter", "Line break converter", and (optionally) "URL filter". + + - or add a new text format, assign it to trusted roles, and ensure that above + mentioned input filters are disabled. + +* Setup editor profiles in Administration » Configuration » Content authoring + » Wysiwyg. + + +-- CONTACT -- + +Current maintainers: +* Daniel F. Kudwien (sun) - http://drupal.org/user/54136 +* Henrik Danielsson (TwoD) - http://drupal.org/user/244227 + +This project has been sponsored by: +* UNLEASHED MIND + Specialized in consulting and planning of Drupal powered sites, UNLEASHED + MIND offers installation, development, theming, customization, and hosting + to get you started. Visit http://www.unleashedmind.com for more information. + diff --git a/editors/ckeditor.inc b/editors/ckeditor.inc new file mode 100644 index 00000000..0a56fe08 --- /dev/null +++ b/editors/ckeditor.inc @@ -0,0 +1,333 @@ + 'CKEditor', + 'vendor url' => 'http://ckeditor.com', + 'download url' => 'http://ckeditor.com/download', + 'libraries' => array( + '' => array( + 'title' => 'Default', + 'files' => array( + 'ckeditor.js' => array('preprocess' => FALSE), + ), + ), + 'src' => array( + 'title' => 'Source', + 'files' => array( + 'ckeditor_source.js' => array('preprocess' => FALSE), + ), + ), + ), + 'version callback' => 'wysiwyg_ckeditor_version', + 'themes callback' => 'wysiwyg_ckeditor_themes', + 'settings callback' => 'wysiwyg_ckeditor_settings', + 'plugin callback' => 'wysiwyg_ckeditor_plugins', + 'plugin settings callback' => 'wysiwyg_ckeditor_plugin_settings', + 'proxy plugin' => array( + 'drupal' => array( + 'load' => TRUE, + 'proxy' => TRUE, + ), + ), + 'proxy plugin settings callback' => 'wysiwyg_ckeditor_proxy_plugin_settings', + 'versions' => array( + '3.0.0.3665' => array( + 'js files' => array('ckeditor-3.0.js'), + ), + ), + ); + return $editor; +} + +/** + * Detect editor version. + * + * @param $editor + * An array containing editor properties as returned from hook_editor(). + * + * @return + * The installed editor version. + */ +function wysiwyg_ckeditor_version($editor) { + $library = $editor['library path'] . '/ckeditor.js'; + if (!file_exists($library)) { + return; + } + $library = fopen($library, 'r'); + $max_lines = 8; + while ($max_lines && $line = fgets($library, 500)) { + // version:'CKEditor 3.0 SVN',revision:'3665' + // version:'3.0 RC',revision:'3753' + // version:'3.0.1',revision:'4391' + if (preg_match('@version:\'(?:CKEditor )?([\d\.]+)(?:.+revision:\'([\d]+))?@', $line, $version)) { + fclose($library); + // Version numbers need to have three parts since 3.0.1. + $version[1] = preg_replace('/^(\d+)\.(\d+)$/', '${1}.${2}.0', $version[1]); + return $version[1] . '.' . $version[2]; + } + $max_lines--; + } + fclose($library); +} + +/** + * Determine available editor themes or check/reset a given one. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $profile + * A wysiwyg editor profile. + * + * @return + * An array of theme names. The first returned name should be the default + * theme name. + */ +function wysiwyg_ckeditor_themes($editor, $profile) { + // @todo Skins are not themes but this will do for now. + $path = $editor['library path'] . '/skins/'; + if (file_exists($path) && ($dir_handle = opendir($path))) { + $themes = array(); + while ($file = readdir($dir_handle)) { + if (is_dir($path . $file) && substr($file, 0, 1) != '.' && $file != 'CVS') { + $themes[] = $file; + } + } + closedir($dir_handle); + natcasesort($themes); + $themes = array_values($themes); + return !empty($themes) ? $themes : array('default'); + } + else { + return array('default'); + } +} + +/** + * Return runtime editor settings for a given wysiwyg profile. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $config + * An array containing wysiwyg editor profile settings. + * @param $theme + * The name of a theme/GUI/skin to use. + * + * @return + * A settings array to be populated in + * Drupal.settings.wysiwyg.configs.{editor} + */ +function wysiwyg_ckeditor_settings($editor, $config, $theme) { + $settings = array( + 'baseHref' => $GLOBALS['base_url'] . '/', + 'width' => '100%', + // For better compatibility with smaller textareas. + 'resize_minWidth' => 450, + 'height' => 420, + // @todo Do not use skins as themes and add separate skin handling. + 'theme' => 'default', + 'skin' => !empty($theme) ? $theme : 'kama', + // By default, CKEditor converts most characters into HTML entities. Since + // it does not support a custom definition, but Drupal supports Unicode, we + // disable at least the additional character sets. CKEditor always converts + // XML default characters '&', '<', '>'. + // @todo Check whether completely disabling ProcessHTMLEntities is an option. + 'entities_latin' => FALSE, + 'entities_greek' => FALSE, + ); + + // Add HTML block format settings; common block formats are already predefined + // by CKEditor. + if (isset($config['block_formats'])) { + $block_formats = explode(',', drupal_strtolower($config['block_formats'])); + $predefined_formats = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'address', 'div'); + foreach (array_diff($block_formats, $predefined_formats) as $tag) { + $tag = trim($tag); + $settings["format_$tag"] = array('element' => $tag); + } + $settings['format_tags'] = implode(';', $block_formats); + } + + if (isset($config['apply_source_formatting'])) { + $settings['apply_source_formatting'] = $config['apply_source_formatting']; + } + + if (isset($config['css_setting'])) { + // Versions below 3.0.1 could only handle one stylesheet. + if (version_compare($editor['installed version'], '3.0.1.4391', '<')) { + if ($config['css_setting'] == 'theme') { + $settings['contentsCss'] = reset(wysiwyg_get_css()); + } + elseif ($config['css_setting'] == 'self' && isset($config['css_path'])) { + $settings['contentsCss'] = strtr($config['css_path'], array('%b' => base_path(), '%t' => path_to_theme())); + } + } + else { + if ($config['css_setting'] == 'theme') { + $settings['contentsCss'] = wysiwyg_get_css(); + } + elseif ($config['css_setting'] == 'self' && isset($config['css_path'])) { + $settings['contentsCss'] = explode(',', strtr($config['css_path'], array('%b' => base_path(), '%t' => path_to_theme()))); + } + } + } + + if (isset($config['language'])) { + $settings['language'] = $config['language']; + } + if (isset($config['resizing'])) { + // CKEditor tests "!== false", so ensure it is a Boolean. + $settings['resize_enabled'] = (bool) $config['resizing']; + } + if (isset($config['toolbar_loc'])) { + $settings['toolbarLocation'] = $config['toolbar_loc']; + } + + $settings['toolbar'] = array(); + if (!empty($config['buttons'])) { + $extra_plugins = array(); + $plugins = wysiwyg_get_plugins($editor['name']); + foreach ($config['buttons'] as $plugin => $buttons) { + foreach ($buttons as $button => $enabled) { + // Iterate separately over buttons and extensions properties. + foreach (array('buttons', 'extensions') as $type) { + // Skip unavailable plugins. + if (!isset($plugins[$plugin][$type][$button])) { + continue; + } + // Add buttons. + if ($type == 'buttons') { + $settings['toolbar'][] = $button; + } + // Add external Drupal plugins to the list of extensions. + if ($type == 'buttons' && !empty($plugins[$plugin]['proxy'])) { + $extra_plugins[] = $button; + } + // Add external plugins to the list of extensions. + elseif ($type == 'buttons' && empty($plugins[$plugin]['internal'])) { + $extra_plugins[] = $plugin; + } + // Add internal buttons that also need to be loaded as extension. + elseif ($type == 'buttons' && !empty($plugins[$plugin]['load'])) { + $extra_plugins[] = $plugin; + } + // Add plain extensions. + elseif ($type == 'extensions' && !empty($plugins[$plugin]['load'])) { + $extra_plugins[] = $plugin; + } + // Allow plugins to add or override global configuration settings. + if (!empty($plugins[$plugin]['options'])) { + $settings = array_merge($settings, $plugins[$plugin]['options']); + } + } + } + } + if (!empty($extra_plugins)) { + $settings['extraPlugins'] = implode(',', $extra_plugins); + } + } + // For now, all buttons are placed into one row. + $settings['toolbar'] = array($settings['toolbar']); + + return $settings; +} + +/** + * Build a JS settings array of native external plugins that need to be loaded separately. + */ +function wysiwyg_ckeditor_plugin_settings($editor, $profile, $plugins) { + $settings = array(); + foreach ($plugins as $name => $plugin) { + // Register all plugins that need to be loaded. + if (!empty($plugin['load'])) { + $settings[$name] = array(); + // Add path for native external plugins. + if (empty($plugin['internal']) && isset($plugin['path'])) { + $settings[$name]['path'] = base_path() . $plugin['path'] . '/'; + } + // Force native internal plugins to use the standard path. + else { + $settings[$name]['path'] = base_path() . $editor['library path'] . '/plugins/' . $name . '/'; + } + // CKEditor defaults to 'plugin.js' on its own when filename is not set. + if (!empty($plugin['filename'])) { + $settings[$name]['fileName'] = $plugin['filename']; + } + } + } + return $settings; +} + +/** + * Build a JS settings array for Drupal plugins loaded via the proxy plugin. + */ +function wysiwyg_ckeditor_proxy_plugin_settings($editor, $profile, $plugins) { + $settings = array(); + foreach ($plugins as $name => $plugin) { + // Populate required plugin settings. + $settings[$name] = $plugin['dialog settings'] + array( + 'title' => $plugin['title'], + 'icon' => base_path() . $plugin['icon path'] . '/' . $plugin['icon file'], + 'iconTitle' => $plugin['icon title'], + // @todo These should only be set if the plugin defined them. + 'css' => base_path() . $plugin['css path'] . '/' . $plugin['css file'], + ); + } + return $settings; +} + +/** + * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin(). + */ +function wysiwyg_ckeditor_plugins($editor) { + $plugins = array( + 'default' => array( + 'buttons' => array( + 'Bold' => t('Bold'), 'Italic' => t('Italic'), 'Underline' => t('Underline'), + 'Strike' => t('Strike-through'), + 'JustifyLeft' => t('Align left'), 'JustifyCenter' => t('Align center'), 'JustifyRight' => t('Align right'), 'JustifyBlock' => t('Justify'), + 'BulletedList' => t('Bullet list'), 'NumberedList' => t('Numbered list'), + 'Outdent' => t('Outdent'), 'Indent' => t('Indent'), + 'Undo' => t('Undo'), 'Redo' => t('Redo'), + 'Link' => t('Link'), 'Unlink' => t('Unlink'), 'Anchor' => t('Anchor'), + 'Image' => t('Image'), + 'TextColor' => t('Forecolor'), 'BGColor' => t('Backcolor'), + 'Superscript' => t('Superscript'), 'Subscript' => t('Subscript'), + 'Blockquote' => t('Blockquote'), 'Source' => t('Source code'), + 'HorizontalRule' => t('Horizontal rule'), + 'Cut' => t('Cut'), 'Copy' => t('Copy'), 'Paste' => t('Paste'), + 'PasteText' => t('Paste Text'), 'PasteFromWord' => t('Paste from Word'), + 'ShowBlocks' => t('Show blocks'), + 'RemoveFormat' => t('Remove format'), + 'SpecialChar' => t('Character map'), + 'Format' => t('HTML block format'), 'Font' => t('Font'), 'FontSize' => t('Font size'), 'Styles' => t('Font style'), + 'Table' => t('Table'), + 'SelectAll' => t('Select all'), 'Find' => t('Search'), 'Replace' => t('Replace'), + 'Flash' => t('Flash'), 'Smiley' => t('Smiley'), + 'CreateDiv' => t('Div container'), + 'Iframe' => t('iFrame'), + 'Maximize' => t('Maximize'), + 'SpellChecker' => t('Check spelling'), 'Scayt' => t('Check spelling as you type'), + 'About' => t('About'), + ), + 'internal' => TRUE, + ), + ); + + if (version_compare($editor['installed version'], '3.1.0.4885', '<')) { + unset($plugins['default']['buttons']['CreateDiv']); + } + if (version_compare($editor['installed version'], '3.5.0.6260', '<')) { + unset($plugins['default']['buttons']['Iframe']); + } + return $plugins; +} + diff --git a/editors/css/openwysiwyg.css b/editors/css/openwysiwyg.css new file mode 100644 index 00000000..2fea7fb3 --- /dev/null +++ b/editors/css/openwysiwyg.css @@ -0,0 +1,11 @@ + +/** + * openWYSIWYG. + */ +table.tableTextareaEditor, table.tableTextareaEditor table { + margin: 0; + border-collapse: separate; +} +table.tableTextareaEditor td { + padding: 0; +} diff --git a/editors/css/tinymce-2.css b/editors/css/tinymce-2.css new file mode 100644 index 00000000..4aa201d4 --- /dev/null +++ b/editors/css/tinymce-2.css @@ -0,0 +1,27 @@ + +/** + * TinyMCE 2.x + */ +table.mceEditor { + clear: left; +} + +/** + * Align all buttons and separators in a single row, so they wrap into multiple + * rows if required. + */ +.mceToolbarTop a, .mceToolbarBottom a { + float: left; +} +.mceSeparatorLine { + float: left; + margin-top: 3px; +} +.mceSelectList { + float: left; + margin-bottom: 1px; +} +/* Place table plugin buttons into new row */ +#mce_editor_0_table, #mce_editor_1_table { + clear: left; +} diff --git a/editors/css/tinymce-3.css b/editors/css/tinymce-3.css new file mode 100644 index 00000000..5d0ebe94 --- /dev/null +++ b/editors/css/tinymce-3.css @@ -0,0 +1,24 @@ + +/** + * TinyMCE 3.x + */ +table.mceLayout { + clear: left; +} + +/** + * Align all buttons and separators in a single row, so they wrap into multiple + * rows if required. + */ +.mceToolbar td { + display: inline; +} +.mceToolbar a, +.mceSeparator { + float: left; +} +.mceListBox, +.mceSplitButton { + float: left; + margin-bottom: 1px; +} diff --git a/editors/fckeditor.inc b/editors/fckeditor.inc new file mode 100644 index 00000000..70954da6 --- /dev/null +++ b/editors/fckeditor.inc @@ -0,0 +1,292 @@ + 'FCKeditor', + 'vendor url' => 'http://www.fckeditor.net', + 'download url' => 'http://www.fckeditor.net/download', + 'libraries' => array( + '' => array( + 'title' => 'Default', + 'files' => array('fckeditor.js'), + ), + ), + 'version callback' => 'wysiwyg_fckeditor_version', + 'themes callback' => 'wysiwyg_fckeditor_themes', + 'settings callback' => 'wysiwyg_fckeditor_settings', + 'plugin callback' => 'wysiwyg_fckeditor_plugins', + 'plugin settings callback' => 'wysiwyg_fckeditor_plugin_settings', + 'proxy plugin' => array( + 'drupal' => array( + 'load' => TRUE, + 'proxy' => TRUE, + ), + ), + 'proxy plugin settings callback' => 'wysiwyg_fckeditor_proxy_plugin_settings', + 'versions' => array( + '2.6' => array( + 'js files' => array('fckeditor-2.6.js'), + ), + ), + ); + return $editor; +} + +/** + * Detect editor version. + * + * @param $editor + * An array containing editor properties as returned from hook_editor(). + * + * @return + * The installed editor version. + */ +function wysiwyg_fckeditor_version($editor) { + $library = $editor['library path'] . '/fckeditor.js'; + if (!file_exists($library)) { + return; + } + $library = fopen($library, 'r'); + $max_lines = 100; + while ($max_lines && $line = fgets($library, 60)) { + if (preg_match('@^FCKeditor.prototype.Version\s*= \'([\d\.]+)@', $line, $version)) { + fclose($library); + return $version[1]; + } + $max_lines--; + } + fclose($library); +} + +/** + * Determine available editor themes or check/reset a given one. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $profile + * A wysiwyg editor profile. + * + * @return + * An array of theme names. The first returned name should be the default + * theme name. + */ +function wysiwyg_fckeditor_themes($editor, $profile) { + return array('default', 'office2003', 'silver'); +} + +/** + * Return runtime editor settings for a given wysiwyg profile. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $config + * An array containing wysiwyg editor profile settings. + * @param $theme + * The name of a theme/GUI/skin to use. + * + * @return + * A settings array to be populated in + * Drupal.settings.wysiwyg.configs.{editor} + */ +function wysiwyg_fckeditor_settings($editor, $config, $theme) { + $settings = array( + 'EditorPath' => base_path() . $editor['library path'] . '/', + 'SkinPath' => base_path() . $editor['library path'] . '/editor/skins/' . $theme . '/', + 'CustomConfigurationsPath' => base_path() . drupal_get_path('module', 'wysiwyg') . '/editors/js/fckeditor.config.js', + 'Width' => '100%', + 'Height' => 420, + 'LinkBrowser' => FALSE, + 'LinkUpload' => FALSE, + 'ImageBrowser' => FALSE, + 'ImageUpload' => FALSE, + 'FlashBrowser' => FALSE, + 'FlashUpload' => FALSE, + // By default, FCKeditor converts most characters into HTML entities. Since + // it does not support a custom definition, but Drupal supports Unicode, we + // disable at least the additional character sets. FCKeditor always converts + // XML default characters '&', '<', '>'. + // @todo Check whether completely disabling ProcessHTMLEntities is an option. + 'IncludeLatinEntities' => FALSE, + 'IncludeGreekEntities' => FALSE, + ); + if (isset($config['block_formats'])) { + $settings['FontFormats'] = strtr($config['block_formats'], array(',' => ';')); + } + if (isset($config['apply_source_formatting'])) { + $settings['FormatOutput'] = $settings['FormatSource'] = $config['apply_source_formatting']; + } + if (isset($config['paste_auto_cleanup_on_paste'])) { + $settings['AutoDetectPasteFromWord'] = $config['paste_auto_cleanup_on_paste']; + } + + if (isset($config['css_setting'])) { + if ($config['css_setting'] == 'theme') { + $settings['EditorAreaCSS'] = implode(',', wysiwyg_get_css()); + } + else if ($config['css_setting'] == 'self' && isset($config['css_path'])) { + $settings['EditorAreaCSS'] = strtr($config['css_path'], array('%b' => base_path(), '%t' => path_to_theme())); + } + } + + // Use our custom toolbar set. + $settings['ToolbarSet'] = 'Wysiwyg'; + // Populate our custom toolbar set for fckeditor.config.js. + $settings['buttons'] = array(); + if (!empty($config['buttons'])) { + $plugins = wysiwyg_get_plugins($editor['name']); + foreach ($config['buttons'] as $plugin => $buttons) { + foreach ($buttons as $button => $enabled) { + // Iterate separately over buttons and extensions properties. + foreach (array('buttons', 'extensions') as $type) { + // Skip unavailable plugins. + if (!isset($plugins[$plugin][$type][$button])) { + continue; + } + // Add buttons. + if ($type == 'buttons') { + $settings['buttons'][] = $button; + } + // Allow plugins to add or override global configuration settings. + if (!empty($plugins[$plugin]['options'])) { + $settings = array_merge($settings, $plugins[$plugin]['options']); + } + } + } + } + } + // For now, all buttons are placed into one row. + $settings['buttons'] = array($settings['buttons']); + + return $settings; +} + +/** + * Build a JS settings array of native external plugins that need to be loaded separately. + */ +function wysiwyg_fckeditor_plugin_settings($editor, $profile, $plugins) { + $settings = array(); + foreach ($plugins as $name => $plugin) { + // Register all plugins that need to be loaded. + if (!empty($plugin['load'])) { + $settings[$name] = array(); + // Add path for native external plugins; internal ones do not need a path. + if (empty($plugin['internal']) && isset($plugin['path'])) { + // All native FCKeditor plugins use the filename fckplugin.js. + $settings[$name]['path'] = base_path() . $plugin['path'] . '/'; + } + if (!empty($plugin['languages'])) { + $settings[$name]['languages'] = $plugin['languages']; + } + } + } + return $settings; +} + +/** + * Build a JS settings array for Drupal plugins loaded via the proxy plugin. + */ +function wysiwyg_fckeditor_proxy_plugin_settings($editor, $profile, $plugins) { + $settings = array(); + foreach ($plugins as $name => $plugin) { + // Populate required plugin settings. + $settings[$name] = $plugin['dialog settings'] + array( + 'title' => $plugin['title'], + 'icon' => base_path() . $plugin['icon path'] . '/' . $plugin['icon file'], + 'iconTitle' => $plugin['icon title'], + // @todo These should only be set if the plugin defined them. + 'css' => base_path() . $plugin['css path'] . '/' . $plugin['css file'], + ); + } + return $settings; +} + +/** + * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin(). + */ +function wysiwyg_fckeditor_plugins($editor) { + $plugins = array( + 'default' => array( + 'buttons' => array( + 'Bold' => t('Bold'), 'Italic' => t('Italic'), 'Underline' => t('Underline'), + 'StrikeThrough' => t('Strike-through'), + 'JustifyLeft' => t('Align left'), 'JustifyCenter' => t('Align center'), 'JustifyRight' => t('Align right'), 'JustifyFull' => t('Justify'), + 'UnorderedList' => t('Bullet list'), 'OrderedList' => t('Numbered list'), + 'Outdent' => t('Outdent'), 'Indent' => t('Indent'), + 'Undo' => t('Undo'), 'Redo' => t('Redo'), + 'Link' => t('Link'), 'Unlink' => t('Unlink'), 'Anchor' => t('Anchor'), + 'Image' => t('Image'), + 'TextColor' => t('Forecolor'), 'BGColor' => t('Backcolor'), + 'Superscript' => t('Superscript'), 'Subscript' => t('Subscript'), + 'Blockquote' => t('Blockquote'), 'Source' => t('Source code'), + 'Rule' => t('Horizontal rule'), + 'Cut' => t('Cut'), 'Copy' => t('Copy'), 'Paste' => t('Paste'), + 'PasteText' => t('Paste Text'), 'PasteWord' => t('Paste from Word'), + 'ShowBlocks' => t('Show blocks'), + 'RemoveFormat' => t('Remove format'), + 'SpecialChar' => t('Character map'), + 'About' => t('About'), + 'FontFormat' => t('HTML block format'), 'FontName' => t('Font'), 'FontSize' => t('Font size'), 'Style' => t('Font style'), + 'Table' => t('Table'), + 'Find' => t('Search'), 'Replace' => t('Replace'), 'SelectAll' => t('Select all'), + 'CreateDiv' => t('Create DIV container'), + 'Flash' => t('Flash'), 'Smiley' => t('Smiley'), + 'FitWindow' => t('FitWindow'), + 'SpellCheck' => t('Check spelling'), + ), + 'internal' => TRUE, + ), + 'autogrow' => array( + 'path' => $editor['library path'] . '/editor/plugins', + 'extensions' => array( + 'autogrow' => t('Autogrow'), + ), + 'options' => array( + 'AutoGrowMax' => 800, + ), + 'internal' => TRUE, + 'load' => TRUE, + ), + 'bbcode' => array( + 'path' => $editor['library path'] . '/editor/plugins', + 'extensions' => array( + 'bbcode' => t('BBCode'), + ), + 'internal' => TRUE, + 'load' => TRUE, + ), + 'dragresizetable' => array( + 'path' => $editor['library path'] . '/editor/plugins', + 'extensions' => array( + 'dragresizetable' => t('Table drag/resize'), + ), + 'internal' => TRUE, + 'load' => TRUE, + ), + 'tablecommands' => array( + 'path' => $editor['library path'] . '/editor/plugins', + 'buttons' => array( + 'TableCellProp' => t('Table: Cell properties'), + 'TableInsertRowAfter' => t('Table: Insert row after'), + 'TableInsertColumnAfter' => t('Table: Insert column after'), + 'TableInsertCellAfter' => t('Table: Insert cell after'), + 'TableDeleteRows' => t('Table: Delete rows'), + 'TableDeleteColumns' => t('Table: Delete columns'), + 'TableDeleteCells' => t('Table: Delete cells'), + 'TableMergeCells' => t('Table: Merge cells'), + 'TableHorizontalSplitCell' => t('Table: Horizontal split cell'), + ), + 'internal' => TRUE, + 'load' => TRUE, + ), + ); + return $plugins; +} + diff --git a/editors/js/ckeditor-3.0.js b/editors/js/ckeditor-3.0.js new file mode 100644 index 00000000..d2cf3005 --- /dev/null +++ b/editors/js/ckeditor-3.0.js @@ -0,0 +1,217 @@ +(function($) { + +Drupal.wysiwyg.editor.init.ckeditor = function(settings) { + // Plugins must only be loaded once. Only the settings from the first format + // will be used but they're identical anyway. + var registeredPlugins = {}; + for (var format in settings) { + if (Drupal.settings.wysiwyg.plugins[format]) { + // Register native external plugins. + // Array syntax required; 'native' is a predefined token in JavaScript. + for (var pluginName in Drupal.settings.wysiwyg.plugins[format]['native']) { + if (!registeredPlugins[pluginName]) { + var plugin = Drupal.settings.wysiwyg.plugins[format]['native'][pluginName]; + CKEDITOR.plugins.addExternal(pluginName, plugin.path, plugin.fileName); + registeredPlugins[pluginName] = true; + } + } + // Register Drupal plugins. + for (var pluginName in Drupal.settings.wysiwyg.plugins[format].drupal) { + if (!registeredPlugins[pluginName]) { + Drupal.wysiwyg.editor.instance.ckeditor.addPlugin(pluginName, Drupal.settings.wysiwyg.plugins[format].drupal[pluginName], Drupal.settings.wysiwyg.plugins.drupal[pluginName]); + registeredPlugins[pluginName] = true; + } + } + } + } +}; + + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.ckeditor = function(context, params, settings) { + // Apply editor instance settings. + CKEDITOR.config.customConfig = ''; + + settings.on = { + instanceReady: function(ev) { + var editor = ev.editor; + // Get a list of block, list and table tags from CKEditor's XHTML DTD. + // @see http://docs.cksource.com/CKEditor_3.x/Developers_Guide/Output_Formatting. + var dtd = CKEDITOR.dtd; + var tags = CKEDITOR.tools.extend({}, dtd.$block, dtd.$listItem, dtd.$tableContent); + // Set source formatting rules for each listed tag except
.
+      // Linebreaks can be inserted before or after opening and closing tags.
+      if (settings.apply_source_formatting) {
+        // Mimic FCKeditor output, by breaking lines between tags.
+        for (var tag in tags) {
+          if (tag == 'pre') {
+            continue;
+          }
+          this.dataProcessor.writer.setRules(tag, {
+            indent: true,
+            breakBeforeOpen: true,
+            breakAfterOpen: false,
+            breakBeforeClose: false,
+            breakAfterClose: true
+          });
+        }
+      }
+      else {
+        // CKEditor adds default formatting to 
, so we want to remove that + // here too. + tags.br = 1; + // No indents or linebreaks; + for (var tag in tags) { + if (tag == 'pre') { + continue; + } + this.dataProcessor.writer.setRules(tag, { + indent: false, + breakBeforeOpen: false, + breakAfterOpen: false, + breakBeforeClose: false, + breakAfterClose: false + }); + } + } + }, + + pluginsLoaded: function(ev) { + // Override the conversion methods to let Drupal plugins modify the data. + var editor = ev.editor; + if (editor.dataProcessor && Drupal.settings.wysiwyg.plugins[params.format]) { + editor.dataProcessor.toHtml = CKEDITOR.tools.override(editor.dataProcessor.toHtml, function(originalToHtml) { + // Convert raw data for display in WYSIWYG mode. + return function(data, fixForBody) { + for (var plugin in Drupal.settings.wysiwyg.plugins[params.format].drupal) { + if (typeof Drupal.wysiwyg.plugins[plugin].attach == 'function') { + data = Drupal.wysiwyg.plugins[plugin].attach(data, Drupal.settings.wysiwyg.plugins.drupal[plugin], editor.name); + data = Drupal.wysiwyg.instances[params.field].prepareContent(data); + } + } + return originalToHtml.call(this, data, fixForBody); + }; + }); + editor.dataProcessor.toDataFormat = CKEDITOR.tools.override(editor.dataProcessor.toDataFormat, function(originalToDataFormat) { + // Convert WYSIWYG mode content to raw data. + return function(data, fixForBody) { + data = originalToDataFormat.call(this, data, fixForBody); + for (var plugin in Drupal.settings.wysiwyg.plugins[params.format].drupal) { + if (typeof Drupal.wysiwyg.plugins[plugin].detach == 'function') { + data = Drupal.wysiwyg.plugins[plugin].detach(data, Drupal.settings.wysiwyg.plugins.drupal[plugin], editor.name); + } + } + return data; + }; + }); + } + }, + + selectionChange: function (event) { + var pluginSettings = Drupal.settings.wysiwyg.plugins[params.format]; + if (pluginSettings && pluginSettings.drupal) { + $.each(pluginSettings.drupal, function (name) { + var plugin = Drupal.wysiwyg.plugins[name]; + if ($.isFunction(plugin.isNode)) { + var node = event.data.selection.getSelectedElement(); + var state = plugin.isNode(node ? node.$ : null) ? CKEDITOR.TRISTATE_ON : CKEDITOR.TRISTATE_OFF; + event.editor.getCommand(name).setState(state); + } + }); + } + }, + + focus: function(ev) { + Drupal.wysiwyg.activeId = ev.editor.name; + } + }; + + // Attach editor. + CKEDITOR.replace(params.field, settings); +}; + +/** + * Detach a single or all editors. + * + * @todo 3.x: editor.prototype.getInstances() should always return an array + * containing all instances or the passed in params.field instance, but + * always return an array to simplify all detach functions. + */ +Drupal.wysiwyg.editor.detach.ckeditor = function(context, params) { + if (typeof params != 'undefined') { + var instance = CKEDITOR.instances[params.field]; + if (instance) { + instance.destroy(); + } + } + else { + for (var instanceName in CKEDITOR.instances) { + CKEDITOR.instances[instanceName].destroy(); + } + } +}; + +Drupal.wysiwyg.editor.instance.ckeditor = { + addPlugin: function(pluginName, settings, pluginSettings) { + CKEDITOR.plugins.add(pluginName, { + // Wrap Drupal plugin in a proxy pluygin. + init: function(editor) { + if (settings.css) { + editor.on('mode', function(ev) { + if (ev.editor.mode == 'wysiwyg') { + // Inject CSS files directly into the editing area head tag. + $('head', $('#cke_contents_' + ev.editor.name + ' iframe').eq(0).contents()).append(''); + } + }); + } + if (typeof Drupal.wysiwyg.plugins[pluginName].invoke == 'function') { + var pluginCommand = { + exec: function (editor) { + var data = { format: 'html', node: null, content: '' }; + var selection = editor.getSelection(); + if (selection) { + data.node = selection.getSelectedElement(); + if (data.node) { + data.node = data.node.$; + } + if (selection.getType() == CKEDITOR.SELECTION_TEXT) { + if (CKEDITOR.env.ie) { + data.content = selection.getNative().createRange().text; + } + else { + data.content = selection.getNative().toString(); + } + } + else if (data.node) { + // content is supposed to contain the "outerHTML". + data.content = data.node.parentNode.innerHTML; + } + } + Drupal.wysiwyg.plugins[pluginName].invoke(data, pluginSettings, editor.name); + } + }; + editor.addCommand(pluginName, pluginCommand); + } + editor.ui.addButton(pluginName, { + label: settings.iconTitle, + command: pluginName, + icon: settings.icon + }); + + // @todo Add button state handling. + } + }); + }, + prepareContent: function(content) { + // @todo Don't know if we need this yet. + return content; + }, + insert: function(content) { + content = this.prepareContent(content); + CKEDITOR.instances[this.field].insertHtml(content); + } +}; + +})(jQuery); diff --git a/editors/js/fckeditor-2.6.js b/editors/js/fckeditor-2.6.js new file mode 100644 index 00000000..4ee2cff7 --- /dev/null +++ b/editors/js/fckeditor-2.6.js @@ -0,0 +1,181 @@ +(function($) { + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.fckeditor = function(context, params, settings) { + var FCKinstance = new FCKeditor(params.field, settings.Width, settings.Height, settings.ToolbarSet); + // Apply editor instance settings. + FCKinstance.BasePath = settings.EditorPath; + FCKinstance.Config.wysiwygFormat = params.format; + FCKinstance.Config.CustomConfigurationsPath = settings.CustomConfigurationsPath; + + // Load Drupal plugins and apply format specific settings. + // @see fckeditor.config.js + // @see Drupal.wysiwyg.editor.instance.fckeditor.init() + + // Attach editor. + FCKinstance.ReplaceTextarea(); +}; + +/** + * Detach a single or all editors. + */ +Drupal.wysiwyg.editor.detach.fckeditor = function(context, params) { + var instances = []; + if (typeof params != 'undefined' && typeof FCKeditorAPI != 'undefined') { + var instance = FCKeditorAPI.GetInstance(params.field); + if (instance) { + instances[params.field] = instance; + } + } + else { + instances = FCKeditorAPI.__Instances; + } + + for (var instanceName in instances) { + var instance = instances[instanceName]; + instance.UpdateLinkedField(); + // Since we already detach the editor and update the textarea, the submit + // event handler needs to be removed to prevent data loss (in IE). + // FCKeditor uses 2 nested iFrames; instance.EditingArea.Window is the + // deepest. Its parent is the iFrame containing the editor. + var instanceScope = instance.EditingArea.Window.parent; + instanceScope.FCKTools.RemoveEventListener(instance.GetParentForm(), 'submit', instance.UpdateLinkedField); + // Run cleanups before forcing an unload of the iFrames or IE crashes. + // This also deletes the instance from the FCKeditorAPI.__Instances array. + instanceScope.FCKTools.RemoveEventListener(instanceScope, 'unload', instanceScope.FCKeditorAPI_Cleanup); + instanceScope.FCKTools.RemoveEventListener(instanceScope, 'beforeunload', instanceScope.FCKeditorAPI_ConfirmCleanup); + if (jQuery.isFunction(instanceScope.FCKIECleanup_Cleanup)) { + instanceScope.FCKIECleanup_Cleanup(); + } + instanceScope.FCKeditorAPI_ConfirmCleanup(); + instanceScope.FCKeditorAPI_Cleanup(); + // Remove the editor elements. + $('#' + instanceName + '___Config').remove(); + $('#' + instanceName + '___Frame').remove(); + $('#' + instanceName).show(); + } +}; + +Drupal.wysiwyg.editor.instance.fckeditor = { + init: function(instance) { + // Track which editor instance is active. + instance.FCK.Events.AttachEvent('OnFocus', function(editorInstance) { + Drupal.wysiwyg.activeId = editorInstance.Name; + }); + + // Create a custom data processor to wrap the default one and allow Drupal + // plugins modify the editor contents. + var wysiwygDataProcessor = function() {}; + wysiwygDataProcessor.prototype = new instance.FCKDataProcessor(); + // Attach: Convert text into HTML. + wysiwygDataProcessor.prototype.ConvertToHtml = function(data) { + // Called from SetData() with stripped comments/scripts, revert those + // manipulations and attach Drupal plugins. + var data = instance.FCKConfig.ProtectedSource.Revert(data); + if (Drupal.settings.wysiwyg.plugins[instance.wysiwygFormat] && Drupal.settings.wysiwyg.plugins[instance.wysiwygFormat].drupal) { + for (var plugin in Drupal.settings.wysiwyg.plugins[instance.wysiwygFormat].drupal) { + if (typeof Drupal.wysiwyg.plugins[plugin].attach == 'function') { + data = Drupal.wysiwyg.plugins[plugin].attach(data, Drupal.settings.wysiwyg.plugins.drupal[plugin], instance.FCK.Name); + data = Drupal.wysiwyg.editor.instance.fckeditor.prepareContent(data); + } + } + } + // Re-protect the source and use the original data processor to convert it + // into XHTML. + data = instance.FCKConfig.ProtectedSource.Protect(data); + return instance.FCKDataProcessor.prototype.ConvertToHtml.call(this, data); + }; + // Detach: Convert HTML into text. + wysiwygDataProcessor.prototype.ConvertToDataFormat = function(rootNode, excludeRoot, ignoreIfEmptyParagraph, format) { + // Called from GetData(), convert the content's DOM into a XHTML string + // using the original data processor and detach Drupal plugins. + var data = instance.FCKDataProcessor.prototype.ConvertToDataFormat.call(this, rootNode, excludeRoot, ignoreIfEmptyParagraph, format); + if (Drupal.settings.wysiwyg.plugins[instance.wysiwygFormat] && Drupal.settings.wysiwyg.plugins[instance.wysiwygFormat].drupal) { + for (var plugin in Drupal.settings.wysiwyg.plugins[instance.wysiwygFormat].drupal) { + if (typeof Drupal.wysiwyg.plugins[plugin].detach == 'function') { + data = Drupal.wysiwyg.plugins[plugin].detach(data, Drupal.settings.wysiwyg.plugins.drupal[plugin], instance.FCK.Name); + } + } + } + return data; + }; + instance.FCK.DataProcessor = new wysiwygDataProcessor(); + }, + + addPlugin: function(plugin, settings, pluginSettings, instance) { + if (typeof Drupal.wysiwyg.plugins[plugin] != 'object') { + return; + } + + if (Drupal.settings.wysiwyg.plugins[instance.wysiwygFormat].drupal[plugin].css) { + instance.FCKConfig.EditorAreaCSS += ',' + Drupal.settings.wysiwyg.plugins[instance.wysiwygFormat].drupal[plugin].css; + } + + // @see fckcommands.js, fck_othercommands.js, fckpastewordcommand.js + instance.FCKCommands.RegisterCommand(plugin, { + // Invoke the plugin's button. + Execute: function () { + if (typeof Drupal.wysiwyg.plugins[plugin].invoke == 'function') { + var data = { format: 'html', node: instance.FCKSelection.GetParentElement() }; + // @todo This is NOT the same as data.node. + data.content = data.node.innerHTML; + Drupal.wysiwyg.plugins[plugin].invoke(data, pluginSettings, instance.FCK.Name); + } + }, + + // isNode: Return whether the plugin button should be enabled for the + // current selection. + // @see FCKUnlinkCommand.prototype.GetState() + GetState: function () { + // Always disabled if not in WYSIWYG mode. + if (instance.FCK.EditMode != FCK_EDITMODE_WYSIWYG) { + return FCK_TRISTATE_DISABLED; + } + var state = instance.FCK.GetNamedCommandState(this.Name); + // FCKeditor sets the wrong state in WebKit browsers. + if (!$.support.queryCommandEnabled && state == FCK_TRISTATE_DISABLED) { + state = FCK_TRISTATE_OFF; + } + if (state == FCK_TRISTATE_OFF && instance.FCK.EditMode == FCK_EDITMODE_WYSIWYG) { + if (typeof Drupal.wysiwyg.plugins[plugin].isNode == 'function') { + var node = instance.FCKSelection.GetSelectedElement(); + state = Drupal.wysiwyg.plugins[plugin].isNode(node) ? FCK_TRISTATE_ON : FCK_TRISTATE_OFF; + } + } + return state; + }, + + /** + * Return information about the plugin as a name/value array. + */ + Name: plugin + }); + + // Register the plugin button. + // Arguments: commandName, label, tooltip, style, sourceView, contextSensitive, icon. + instance.FCKToolbarItems.RegisterItem(plugin, new instance.FCKToolbarButton(plugin, settings.iconTitle, settings.iconTitle, null, false, true, settings.icon)); + }, + + openDialog: function(dialog, params) { + // @todo Implement open dialog. + }, + + closeDialog: function(dialog) { + // @todo Implement close dialog. + }, + + prepareContent: function(content) { + // @todo Not needed for FCKeditor? + return content; + }, + + insert: function(content) { + var instance = FCKeditorAPI.GetInstance(this.field); + // @see FCK.InsertHtml(), FCK.InsertElement() + instance.InsertHtml(content); + } +}; + +})(jQuery); diff --git a/editors/js/fckeditor.config.js b/editors/js/fckeditor.config.js new file mode 100644 index 00000000..d3fbc2fc --- /dev/null +++ b/editors/js/fckeditor.config.js @@ -0,0 +1,73 @@ + +Drupal = window.parent.Drupal; + +/** + * Fetch and provide original editor settings as local variable. + * + * FCKeditor does not support to pass complex variable types to the editor. + * Instance settings passed to FCKinstance.Config are temporarily stored in + * FCKConfig.PageConfig. + */ +var wysiwygFormat = FCKConfig.PageConfig.wysiwygFormat; +var wysiwygSettings = Drupal.settings.wysiwyg.configs.fckeditor[wysiwygFormat]; +var pluginSettings = (Drupal.settings.wysiwyg.plugins[wysiwygFormat] ? Drupal.settings.wysiwyg.plugins[wysiwygFormat] : { 'native': {}, 'drupal': {} }); + +/** + * Apply format-specific settings. + */ +for (var setting in wysiwygSettings) { + if (setting == 'buttons') { + // Apply custom Wysiwyg toolbar for this format. + // FCKConfig.ToolbarSets['Wysiwyg'] = wysiwygSettings.buttons; + + // Temporarily stack buttons into multiple button groups and remove + // separators until #277954 is solved. + FCKConfig.ToolbarSets['Wysiwyg'] = []; + for (var i = 0; i < wysiwygSettings.buttons[0].length; i++) { + FCKConfig.ToolbarSets['Wysiwyg'].push([wysiwygSettings.buttons[0][i]]); + } + FCKTools.AppendStyleSheet(document, '#xToolbar .TB_Start { display:none; }'); + // Set valid height of select element in silver and office2003 skins. + if (FCKConfig.SkinPath.match(/\/office2003\/$/)) { + FCKTools.AppendStyleSheet(document, '#xToolbar .SC_FieldCaption { height: 24px; } #xToolbar .TB_End { display: none; }'); + } + else if (FCKConfig.SkinPath.match(/\/silver\/$/)) { + FCKTools.AppendStyleSheet(document, '#xToolbar .SC_FieldCaption { height: 27px; }'); + } + } + else { + FCKConfig[setting] = wysiwygSettings[setting]; + } +} + +/** + * Initialize this editor instance. + */ +Drupal.wysiwyg.editor.instance.fckeditor.init(window); + +/** + * Register native plugins for this input format. + * + * Parameters to Plugins.Add are: + * - Plugin name. + * - Languages the plugin is available in. + * - Location of the plugin folder; /fckplugin.js is appended. + */ +for (var plugin in pluginSettings['native']) { + // Languages and path may be undefined for internal plugins. + FCKConfig.Plugins.Add(plugin, pluginSettings['native'][plugin].languages, pluginSettings['native'][plugin].path); +} + +/** + * Register Drupal plugins for this input format. + * + * Parameters to addPlugin() are: + * - Plugin name. + * - Format specific plugin settings. + * - General plugin settings. + * - A reference to this window so the plugin setup can access FCKConfig. + */ +for (var plugin in pluginSettings.drupal) { + Drupal.wysiwyg.editor.instance.fckeditor.addPlugin(plugin, pluginSettings.drupal[plugin], Drupal.settings.wysiwyg.plugins.drupal[plugin], window); +} + diff --git a/editors/js/jwysiwyg.js b/editors/js/jwysiwyg.js new file mode 100644 index 00000000..ae478532 --- /dev/null +++ b/editors/js/jwysiwyg.js @@ -0,0 +1,25 @@ +(function($) { + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.jwysiwyg = function(context, params, settings) { + // Attach editor. + $('#' + params.field).wysiwyg(); +}; + +/** + * Detach a single or all editors. + */ +Drupal.wysiwyg.editor.detach.jwysiwyg = function(context, params) { + var $field = $('#' + params.field); + var editor = $field.data('wysiwyg'); + if (typeof editor != 'undefined') { + editor.saveContent(); + editor.element.remove(); + } + $field.removeData('wysiwyg'); + $field.show(); +}; + +})(jQuery); diff --git a/editors/js/markitup.js b/editors/js/markitup.js new file mode 100644 index 00000000..66918114 --- /dev/null +++ b/editors/js/markitup.js @@ -0,0 +1,29 @@ +(function($) { + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.markitup = function(context, params, settings) { + $('#' + params.field, context).markItUp(settings); + + // Adjust CSS for editor buttons. + $.each(settings.markupSet, function (button) { + $('.' + settings.nameSpace + ' .' + this.className + ' a') + .css({ backgroundImage: 'url(' + settings.root + 'sets/default/images/' + button + '.png' + ')' }) + .parents('li').css({ backgroundImage: 'none' }); + }); +}; + +/** + * Detach a single or all editors. + */ +Drupal.wysiwyg.editor.detach.markitup = function(context, params) { + if (typeof params != 'undefined') { + $('#' + params.field, context).markItUpRemove(); + } + else { + $('.markItUpEditor', context).markItUpRemove(); + } +}; + +})(jQuery); diff --git a/editors/js/nicedit.js b/editors/js/nicedit.js new file mode 100644 index 00000000..d5d97957 --- /dev/null +++ b/editors/js/nicedit.js @@ -0,0 +1,95 @@ +(function($) { + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.nicedit = function(context, params, settings) { + // Intercept and ignore submit handlers or they will revert changes made + // since the instance was removed. The handlers are anonymous and hidden out + // of scope in a closure so we can't unbind them. The same operations are + // performed when the instance is detached anyway. + var oldAddEvent = bkLib.addEvent; + bkLib.addEvent = function(obj, type, fn) { + if (type != 'submit') { + oldAddEvent(obj, type, fn); + } + } + // Attach editor. + var editor = new nicEditor(settings); + editor.panelInstance(params.field); + // The old addEvent() must be restored after creating a new instance, as + // plugins with dialogs use it to bind submit handlers to their forms. + bkLib.addEvent = oldAddEvent; + editor.addEvent('focus', function () { + Drupal.wysiwyg.activeId = params.field; + }); +}; + +/** + * Detach a single or all editors. + * + * See Drupal.wysiwyg.editor.detach.none() for a full description of this hook. + */ +Drupal.wysiwyg.editor.detach.nicedit = function(context, params) { + if (typeof params != 'undefined') { + var instance = nicEditors.findEditor(params.field); + if (instance) { + instance.ne.removeInstance(params.field); + instance.ne.removePanel(); + } + } + else { + for (var e in nicEditors.editors) { + // Save contents of all editors back into textareas. + var instances = nicEditors.editors[e].nicInstances; + for (var i = 0; i < instances.length; i++) { + instances[i].remove(); + } + // Remove all editor instances. + nicEditors.editors[e].nicInstances = []; + } + } +}; + +/** + * Instance methods for nicEdit. + */ +Drupal.wysiwyg.editor.instance.nicedit = { + insert: function (content) { + var instance = nicEditors.findEditor(this.field); + var editingArea = instance.getElm(); + var sel = instance.getSel(); + // IE. + if (document.selection) { + editingArea.focus(); + sel.createRange().text = content; + } + else { + // Convert selection to a range. + var range; + // W3C compatible. + if (sel.getRangeAt) { + range = sel.getRangeAt(0); + } + // Safari. + else { + range = editingArea.ownerDocument.createRange(); + range.setStart(sel.anchorNode, sel.anchorOffset); + range.setEnd(sel.focusNode, userSeletion.focusOffset); + } + // The code below doesn't work in IE, but it never gets here. + var fragment = editingArea.ownerDocument.createDocumentFragment(); + // Fragments don't support innerHTML. + var wrapper = editingArea.ownerDocument.createElement('div'); + wrapper.innerHTML = content; + while (wrapper.firstChild) { + fragment.appendChild(wrapper.firstChild); + } + range.deleteContents(); + // Only fragment children are inserted. + range.insertNode(fragment); + } + } +}; + +})(jQuery); diff --git a/editors/js/none.js b/editors/js/none.js new file mode 100644 index 00000000..34020240 --- /dev/null +++ b/editors/js/none.js @@ -0,0 +1,71 @@ +(function($) { + +/** + * Attach this editor to a target element. + * + * @param context + * A DOM element, supplied by Drupal.attachBehaviors(). + * @param params + * An object containing input format parameters. Default parameters are: + * - editor: The internal editor name. + * - theme: The name/key of the editor theme/profile to use. + * - field: The CSS id of the target element. + * @param settings + * An object containing editor settings for all enabled editor themes. + */ +Drupal.wysiwyg.editor.attach.none = function(context, params, settings) { + if (params.resizable) { + var $wrapper = $('#' + params.field).parents('.form-textarea-wrapper:first'); + $wrapper.addClass('resizable'); + if (Drupal.behaviors.textarea.attach) { + Drupal.behaviors.textarea.attach(); + } + } +}; + +/** + * Detach a single or all editors. + * + * @param context + * A DOM element, supplied by Drupal.attachBehaviors(). + * @param params + * (optional) An object containing input format parameters. If defined, + * only the editor instance in params.field should be detached. Otherwise, + * all editors should be detached and saved, so they can be submitted in + * AJAX/AHAH applications. + */ +Drupal.wysiwyg.editor.detach.none = function(context, params) { + if (typeof params != 'undefined') { + var $wrapper = $('#' + params.field).parents('.form-textarea-wrapper:first'); + $wrapper.removeOnce('textarea').removeClass('.resizable-textarea') + .find('.grippie').remove(); + } +}; + +/** + * Instance methods for plain text areas. + */ +Drupal.wysiwyg.editor.instance.none = { + insert: function(content) { + var editor = document.getElementById(this.field); + + // IE support. + if (document.selection) { + editor.focus(); + var sel = document.selection.createRange(); + sel.text = content; + } + // Mozilla/Firefox/Netscape 7+ support. + else if (editor.selectionStart || editor.selectionStart == '0') { + var startPos = editor.selectionStart; + var endPos = editor.selectionEnd; + editor.value = editor.value.substring(0, startPos) + content + editor.value.substring(endPos, editor.value.length); + } + // Fallback, just add to the end of the content. + else { + editor.value += content; + } + } +}; + +})(jQuery); diff --git a/editors/js/openwysiwyg.js b/editors/js/openwysiwyg.js new file mode 100644 index 00000000..89a5337e --- /dev/null +++ b/editors/js/openwysiwyg.js @@ -0,0 +1,68 @@ + +// Backup $ and reset it to jQuery. +Drupal.wysiwyg._openwysiwyg = $; +$ = jQuery; + +// Wrap openWYSIWYG's methods to temporarily use its version of $. +jQuery.each(WYSIWYG, function (key, value) { + if (jQuery.isFunction(value)) { + WYSIWYG[key] = function () { + var old$ = $; + $ = Drupal.wysiwyg._openwysiwyg; + var result = value.apply(this, arguments); + $ = old$; + return result; + }; + } +}); + +// Override editor functions. +WYSIWYG.getEditor = function (n) { + return Drupal.wysiwyg._openwysiwyg("wysiwyg" + n); +}; + +(function($) { + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.openwysiwyg = function(context, params, settings) { + // Initialize settings. + settings.ImagesDir = settings.path + 'images/'; + settings.PopupsDir = settings.path + 'popups/'; + settings.CSSFile = settings.path + 'styles/wysiwyg.css'; + //settings.DropDowns = []; + var config = new WYSIWYG.Settings(); + for (var setting in settings) { + config[setting] = settings[setting]; + } + // Attach editor. + WYSIWYG.setSettings(params.field, config); + WYSIWYG_Core.includeCSS(WYSIWYG.config[params.field].CSSFile); + WYSIWYG._generate(params.field, config); +}; + +/** + * Detach a single or all editors. + */ +Drupal.wysiwyg.editor.detach.openwysiwyg = function(context, params) { + if (typeof params != 'undefined') { + var instance = WYSIWYG.config[params.field]; + if (typeof instance != 'undefined') { + WYSIWYG.updateTextArea(params.field); + jQuery('#wysiwyg_div_' + params.field).remove(); + delete instance; + } + jQuery('#' + params.field).show(); + } + else { + jQuery.each(WYSIWYG.config, function(field) { + WYSIWYG.updateTextArea(field); + jQuery('#wysiwyg_div_' + field).remove(); + delete this; + jQuery('#' + field).show(); + }); + } +}; + +})(jQuery); diff --git a/editors/js/tinymce-2.js b/editors/js/tinymce-2.js new file mode 100644 index 00000000..088021f7 --- /dev/null +++ b/editors/js/tinymce-2.js @@ -0,0 +1,213 @@ +(function($) { + +/** + * Initialize editor instances. + * + * This function needs to be called before the page is fully loaded, as + * calling tinyMCE.init() after the page is loaded breaks IE6. + * + * @param editorSettings + * An object containing editor settings for each input format. + */ +Drupal.wysiwyg.editor.init.tinymce = function(settings) { + // If JS compression is enabled, TinyMCE is unable to autodetect its global + // settinge, hence we need to define them manually. + // @todo Move global library settings somewhere else. + tinyMCE.baseURL = settings.global.editorBasePath; + tinyMCE.srcMode = (settings.global.execMode == 'src' ? '_src' : ''); + tinyMCE.gzipMode = (settings.global.execMode == 'gzip'); + + // Initialize editor configurations. + for (var format in settings) { + if (format == 'global') { + continue; + } + tinyMCE.init(settings[format]); + if (Drupal.settings.wysiwyg.plugins[format]) { + // Load native external plugins. + // Array syntax required; 'native' is a predefined token in JavaScript. + for (var plugin in Drupal.settings.wysiwyg.plugins[format]['native']) { + tinyMCE.loadPlugin(plugin, Drupal.settings.wysiwyg.plugins[format]['native'][plugin]); + } + // Load Drupal plugins. + for (var plugin in Drupal.settings.wysiwyg.plugins[format].drupal) { + Drupal.wysiwyg.editor.instance.tinymce.addPlugin(plugin, Drupal.settings.wysiwyg.plugins[format].drupal[plugin], Drupal.settings.wysiwyg.plugins.drupal[plugin]); + } + } + } +}; + +/** + * Attach this editor to a target element. + * + * See Drupal.wysiwyg.editor.attach.none() for a full desciption of this hook. + */ +Drupal.wysiwyg.editor.attach.tinymce = function(context, params, settings) { + // Configure editor settings for this input format. + for (var setting in settings) { + tinyMCE.settings[setting] = settings[setting]; + } + + // Remove TinyMCE's internal mceItem class, which was incorrectly added to + // submitted content by Wysiwyg <2.1. TinyMCE only temporarily adds the class + // for placeholder elements. If preemptively set, the class prevents (native) + // editor plugins from gaining an active state, so we have to manually remove + // it prior to attaching the editor. This is done on the client-side instead + // of the server-side, as Wysiwyg has no way to figure out where content is + // stored, and the class only affects editing. + $field = $('#' + params.field); + $field.val($field.val().replace(/(<.+?\s+class=['"][\w\s]*?)\bmceItem\b([\w\s]*?['"].*?>)/ig, '$1$2')); + + // Attach editor. + tinyMCE.execCommand('mceAddControl', true, params.field); +}; + +/** + * Detach a single or all editors. + * + * See Drupal.wysiwyg.editor.detach.none() for a full desciption of this hook. + */ +Drupal.wysiwyg.editor.detach.tinymce = function(context, params) { + if (typeof params != 'undefined') { + tinyMCE.removeMCEControl(tinyMCE.getEditorId(params.field)); + $('#' + params.field).removeAttr('style'); + } +// else if (tinyMCE.activeEditor) { +// tinyMCE.triggerSave(); +// tinyMCE.activeEditor.remove(); +// } +}; + +Drupal.wysiwyg.editor.instance.tinymce = { + addPlugin: function(plugin, settings, pluginSettings) { + if (typeof Drupal.wysiwyg.plugins[plugin] != 'object') { + return; + } + tinyMCE.addPlugin(plugin, { + + // Register an editor command for this plugin, invoked by the plugin's button. + execCommand: function(editor_id, element, command, user_interface, value) { + switch (command) { + case plugin: + if (typeof Drupal.wysiwyg.plugins[plugin].invoke == 'function') { + var ed = tinyMCE.getInstanceById(editor_id); + var data = { format: 'html', node: ed.getFocusElement(), content: ed.getFocusElement() }; + Drupal.wysiwyg.plugins[plugin].invoke(data, pluginSettings, ed.formTargetElementId); + return true; + } + } + // Pass to next handler in chain. + return false; + }, + + // Register the plugin button. + getControlHTML: function(control_name) { + switch (control_name) { + case plugin: + return tinyMCE.getButtonHTML(control_name, settings.iconTitle, settings.icon, plugin); + } + return ''; + }, + + // Load custom CSS for editor contents on startup. + initInstance: function(ed) { + if (settings.css) { + tinyMCE.importCSS(ed.getDoc(), settings.css); + } + }, + + cleanup: function(type, content) { + switch (type) { + case 'insert_to_editor': + // Attach: Replace plain text with HTML representations. + if (typeof Drupal.wysiwyg.plugins[plugin].attach == 'function') { + content = Drupal.wysiwyg.plugins[plugin].attach(content, pluginSettings, tinyMCE.selectedInstance.editorId); + content = Drupal.wysiwyg.editor.instance.tinymce.prepareContent(content); + } + break; + + case 'get_from_editor': + // Detach: Replace HTML representations with plain text. + if (typeof Drupal.wysiwyg.plugins[plugin].detach == 'function') { + content = Drupal.wysiwyg.plugins[plugin].detach(content, pluginSettings, tinyMCE.selectedInstance.editorId); + } + break; + } + // Pass through to next handler in chain + return content; + }, + + // isNode: Return whether the plugin button should be enabled for the + // current selection. + handleNodeChange: function(editor_id, node, undo_index, undo_levels, visual_aid, any_selection) { + if (node === null) { + return; + } + if (typeof Drupal.wysiwyg.plugins[plugin].isNode == 'function') { + if (Drupal.wysiwyg.plugins[plugin].isNode(node)) { + tinyMCE.switchClass(editor_id + '_' + plugin, 'mceButtonSelected'); + return true; + } + } + tinyMCE.switchClass(editor_id + '_' + plugin, 'mceButtonNormal'); + return true; + }, + + /** + * Return information about the plugin as a name/value array. + */ + getInfo: function() { + return { + longname: settings.title + }; + } + }); + }, + + openDialog: function(dialog, params) { + var editor = tinyMCE.getInstanceById(this.field); + tinyMCE.openWindow({ + file: dialog.url + '/' + this.field, + width: dialog.width, + height: dialog.height, + inline: 1 + }, params); + }, + + closeDialog: function(dialog) { + var editor = tinyMCE.getInstanceById(this.field); + tinyMCEPopup.close(); + }, + + prepareContent: function(content) { + // Certain content elements need to have additional DOM properties applied + // to prevent this editor from highlighting an internal button in addition + // to the button of a Drupal plugin. + var specialProperties = { + img: { 'name': 'mce_drupal' } + }; + var $content = $('
' + content + '
'); // No .outerHTML() in jQuery :( + jQuery.each(specialProperties, function(element, properties) { + $content.find(element).each(function() { + for (var property in properties) { + if (property == 'class') { + $(this).addClass(properties[property]); + } + else { + $(this).attr(property, properties[property]); + } + } + }); + }); + return $content.html(); + }, + + insert: function(content) { + content = this.prepareContent(content); + var editor = tinyMCE.getInstanceById(this.field); + editor.execCommand('mceInsertContent', false, content); + editor.repaint(); + } +}; + +})(jQuery); diff --git a/editors/js/tinymce-3.js b/editors/js/tinymce-3.js new file mode 100644 index 00000000..b38f5238 --- /dev/null +++ b/editors/js/tinymce-3.js @@ -0,0 +1,235 @@ +(function($) { + +/** + * Initialize editor instances. + * + * @todo Is the following note still valid for 3.x? + * This function needs to be called before the page is fully loaded, as + * calling tinyMCE.init() after the page is loaded breaks IE6. + * + * @param editorSettings + * An object containing editor settings for each input format. + */ +Drupal.wysiwyg.editor.init.tinymce = function(settings) { + // If JS compression is enabled, TinyMCE is unable to autodetect its global + // settinge, hence we need to define them manually. + // @todo Move global library settings somewhere else. + tinyMCE.baseURL = settings.global.editorBasePath; + tinyMCE.srcMode = (settings.global.execMode == 'src' ? '_src' : ''); + tinyMCE.gzipMode = (settings.global.execMode == 'gzip'); + + // Initialize editor configurations. + for (var format in settings) { + if (format == 'global') { + continue; + }; + tinyMCE.init(settings[format]); + if (Drupal.settings.wysiwyg.plugins[format]) { + // Load native external plugins. + // Array syntax required; 'native' is a predefined token in JavaScript. + for (var plugin in Drupal.settings.wysiwyg.plugins[format]['native']) { + tinymce.PluginManager.load(plugin, Drupal.settings.wysiwyg.plugins[format]['native'][plugin]); + } + // Load Drupal plugins. + for (var plugin in Drupal.settings.wysiwyg.plugins[format].drupal) { + Drupal.wysiwyg.editor.instance.tinymce.addPlugin(plugin, Drupal.settings.wysiwyg.plugins[format].drupal[plugin], Drupal.settings.wysiwyg.plugins.drupal[plugin]); + } + } + } +}; + +/** + * Attach this editor to a target element. + * + * See Drupal.wysiwyg.editor.attach.none() for a full desciption of this hook. + */ +Drupal.wysiwyg.editor.attach.tinymce = function(context, params, settings) { + // Configure editor settings for this input format. + var ed = new tinymce.Editor(params.field, settings); + // Reset active instance id on any event. + ed.onEvent.add(function(ed, e) { + Drupal.wysiwyg.activeId = ed.id; + }); + // Make toolbar buttons wrappable (required for IE). + ed.onPostRender.add(function (ed) { + var $toolbar = $('
'); + $('#' + ed.editorContainer + ' table.mceToolbar > tbody > tr > td').each(function () { + $('
').addClass(this.className).append($(this).children()).appendTo($toolbar); + }); + $('#' + ed.editorContainer + ' table.mceLayout td.mceToolbar').append($toolbar); + $('#' + ed.editorContainer + ' table.mceToolbar').remove(); + }); + + // Remove TinyMCE's internal mceItem class, which was incorrectly added to + // submitted content by Wysiwyg <2.1. TinyMCE only temporarily adds the class + // for placeholder elements. If preemptively set, the class prevents (native) + // editor plugins from gaining an active state, so we have to manually remove + // it prior to attaching the editor. This is done on the client-side instead + // of the server-side, as Wysiwyg has no way to figure out where content is + // stored, and the class only affects editing. + $field = $('#' + params.field); + $field.val($field.val().replace(/(<.+?\s+class=['"][\w\s]*?)\bmceItem\b([\w\s]*?['"].*?>)/ig, '$1$2')); + + // Attach editor. + ed.render(); +}; + +/** + * Detach a single or all editors. + * + * See Drupal.wysiwyg.editor.detach.none() for a full desciption of this hook. + */ +Drupal.wysiwyg.editor.detach.tinymce = function(context, params) { + if (typeof params != 'undefined') { + var instance = tinyMCE.get(params.field); + if (instance) { + instance.save(); + instance.remove(); + } + } + else { + // Save contents of all editors back into textareas. + tinyMCE.triggerSave(); + // Remove all editor instances. + for (var instance in tinyMCE.editors) { + tinyMCE.editors[instance].remove(); + } + } +}; + +Drupal.wysiwyg.editor.instance.tinymce = { + addPlugin: function(plugin, settings, pluginSettings) { + if (typeof Drupal.wysiwyg.plugins[plugin] != 'object') { + return; + } + tinymce.create('tinymce.plugins.' + plugin, { + /** + * Initialize the plugin, executed after the plugin has been created. + * + * @param ed + * The tinymce.Editor instance the plugin is initialized in. + * @param url + * The absolute URL of the plugin location. + */ + init: function(ed, url) { + // Register an editor command for this plugin, invoked by the plugin's button. + ed.addCommand(plugin, function() { + if (typeof Drupal.wysiwyg.plugins[plugin].invoke == 'function') { + var data = { format: 'html', node: ed.selection.getNode(), content: ed.selection.getContent() }; + // TinyMCE creates a completely new instance for fullscreen mode. + var instanceId = ed.id == 'mce_fullscreen' ? ed.getParam('fullscreen_editor_id') : ed.id; + Drupal.wysiwyg.plugins[plugin].invoke(data, pluginSettings, instanceId); + } + }); + + // Register the plugin button. + ed.addButton(plugin, { + title : settings.iconTitle, + cmd : plugin, + image : settings.icon + }); + + // Load custom CSS for editor contents on startup. + ed.onInit.add(function() { + if (settings.css) { + ed.dom.loadCSS(settings.css); + } + }); + + // Attach: Replace plain text with HTML representations. + ed.onBeforeSetContent.add(function(ed, data) { + if (typeof Drupal.wysiwyg.plugins[plugin].attach == 'function') { + data.content = Drupal.wysiwyg.plugins[plugin].attach(data.content, pluginSettings, ed.id); + data.content = Drupal.wysiwyg.editor.instance.tinymce.prepareContent(data.content); + } + }); + + // Detach: Replace HTML representations with plain text. + ed.onGetContent.add(function(ed, data) { + if (typeof Drupal.wysiwyg.plugins[plugin].detach == 'function') { + data.content = Drupal.wysiwyg.plugins[plugin].detach(data.content, pluginSettings, ed.id); + } + }); + + // isNode: Return whether the plugin button should be enabled for the + // current selection. + ed.onNodeChange.add(function(ed, command, node) { + if (typeof Drupal.wysiwyg.plugins[plugin].isNode == 'function') { + command.setActive(plugin, Drupal.wysiwyg.plugins[plugin].isNode(node)); + } + }); + }, + + /** + * Return information about the plugin as a name/value array. + */ + getInfo: function() { + return { + longname: settings.title + }; + } + }); + + // Register plugin. + tinymce.PluginManager.add(plugin, tinymce.plugins[plugin]); + }, + + openDialog: function(dialog, params) { + var instanceId = this.isFullscreen() ? 'mce_fullscreen' : this.field; + var editor = tinyMCE.get(instanceId); + editor.windowManager.open({ + file: dialog.url + '/' + instanceId, + width: dialog.width, + height: dialog.height, + inline: 1 + }, params); + }, + + closeDialog: function(dialog) { + var instanceId = this.isFullscreen() ? 'mce_fullscreen' : this.field; + var editor = tinyMCE.get(instanceId); + editor.windowManager.close(dialog); + }, + + prepareContent: function(content) { + // Certain content elements need to have additional DOM properties applied + // to prevent this editor from highlighting an internal button in addition + // to the button of a Drupal plugin. + var specialProperties = { + img: { 'class': 'mceItem' } + }; + var $content = $('
' + content + '
'); // No .outerHTML() in jQuery :( + // Find all placeholder/replacement content of Drupal plugins. + $content.find('.drupal-content').each(function() { + // Recursively process DOM elements below this element to apply special + // properties. + var $drupalContent = $(this); + $.each(specialProperties, function(element, properties) { + $drupalContent.find(element).andSelf().each(function() { + for (var property in properties) { + if (property == 'class') { + $(this).addClass(properties[property]); + } + else { + $(this).attr(property, properties[property]); + } + } + }); + }); + }); + return $content.html(); + }, + + insert: function(content) { + content = this.prepareContent(content); + var instanceId = this.isFullscreen() ? 'mce_fullscreen' : this.field; + tinyMCE.execInstanceCommand(instanceId, 'mceInsertContent', false, content); + }, + + isFullscreen: function() { + // TinyMCE creates a completely new instance for fullscreen mode. + return tinyMCE.activeEditor.id == 'mce_fullscreen' && tinyMCE.activeEditor.getParam('fullscreen_editor_id') == this.field; + } +}; + +})(jQuery); diff --git a/editors/js/whizzywig-56.js b/editors/js/whizzywig-56.js new file mode 100644 index 00000000..229a70b2 --- /dev/null +++ b/editors/js/whizzywig-56.js @@ -0,0 +1,133 @@ + +var wysiwygWhizzywig = { currentField: null, fields: {} }; +var buttonPath = null; + +/** + * Override Whizzywig's document.write() function. + * + * Whizzywig uses document.write() by default, which leads to a blank page when + * invoked in jQuery.ready(). Luckily, Whizzywig developers implemented a + * shorthand w() substitute function that we can override to redirect the output + * into the global wysiwygWhizzywig variable. + * + * @see o() + */ +var w = function (string) { + if (string) { + wysiwygWhizzywig.fields[wysiwygWhizzywig.currentField] += string; + } + return wysiwygWhizzywig.fields[wysiwygWhizzywig.currentField]; +}; + +/** + * Override Whizzywig's document.getElementById() function. + * + * Since we redirect the output of w() into a temporary string upon attaching + * an editor, we also have to override the o() shorthand substitute function + * for document.getElementById() to search in the document or our container. + * This override function also inserts the editor instance when Whizzywig + * tries to access its IFRAME, so it has access to the full/regular window + * object. + * + * @see w() + */ +var o = function (id) { + // Upon first access to "whizzy" + id, Whizzywig tries to access its IFRAME, + // so we need to insert the editor into the DOM. + if (id == 'whizzy' + wysiwygWhizzywig.currentField && wysiwygWhizzywig.fields[wysiwygWhizzywig.currentField]) { + jQuery('#' + wysiwygWhizzywig.currentField).after('
'); + // Iframe's .contentWindow becomes null in Webkit if inserted via .after(). + jQuery('#' + wysiwygWhizzywig.currentField + '-whizzywig').html(w()); + // Prevent subsequent invocations from inserting the editor multiple times. + wysiwygWhizzywig.fields[wysiwygWhizzywig.currentField] = ''; + } + // If id exists in the regular window.document, return it. + if (jQuery('#' + id).size()) { + return jQuery('#' + id).get(0); + } + // Otherwise return id from our container. + return jQuery('#' + id, w()).get(0); +}; + +(function($) { + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.whizzywig = function(context, params, settings) { + // Previous versions used per-button images found in this location, + // now it is only used for custom buttons. + if (settings.buttonPath) { + window.buttonPath = settings.buttonPath; + } + // Assign the toolbar image path used for native buttons, if available. + if (settings.toolbarImagePath) { + btn._f = settings.toolbarImagePath; + } + // Fall back to text labels for all buttons. + else { + window.buttonPath = 'textbuttons'; + } + // Create Whizzywig container. + wysiwygWhizzywig.currentField = params.field; + wysiwygWhizzywig.fields[wysiwygWhizzywig.currentField] = ''; + // Whizzywig needs to have the width set 'inline'. + $field = $('#' + params.field); + var originalValues = Drupal.wysiwyg.instances[params.field]; + originalValues.originalStyle = $field.attr('style'); + $field.css('width', $field.width() + 'px'); + + // Attach editor. + makeWhizzyWig(params.field, (settings.buttons ? settings.buttons : 'all')); + // Whizzywig fails to detect and set initial textarea contents. + var instance = $('#whizzy' + params.field).get(0); + if (instance) { + instance.contentWindow.document.body.innerHTML = tidyD($field.val()); + } +}; + +/** + * Detach a single or all editors. + */ +Drupal.wysiwyg.editor.detach.whizzywig = function(context, params) { + var detach = function (index) { + var id = whizzies[index]; + var instance = $('#whizzy' + id).get(0); + if (!instance) { + return; + } + var editingArea = instance.contentWindow.document; + var $field = $('#' + id); + // Whizzywig shows the original textarea in source mode. + if ($field.css('display') == 'block') { + editingArea.body.innerHTML = $field.val(); + } + + // Save contents of editor back into textarea. + $field.val(tidyH(editingArea)); + // Remove editor instance. + $('#' + id + '-whizzywig').remove(); + whizzies.splice(index, 1); + + // Restore original textarea styling. + var originalValues = Drupal.wysiwyg.instances[id]; + $field.removeAttr('style'); + $field.attr('style', originalValues.originalStyle); + }; + + if (typeof params != 'undefined') { + for (var i = 0; i < whizzies.length; i++) { + if (whizzies[i] == params.field) { + detach(i); + break; + } + } + } + else { + while (whizzies.length > 0) { + detach(0); + } + } +}; + +})(jQuery); diff --git a/editors/js/whizzywig-60.js b/editors/js/whizzywig-60.js new file mode 100644 index 00000000..dc995f6c --- /dev/null +++ b/editors/js/whizzywig-60.js @@ -0,0 +1,85 @@ + +var buttonPath = null; + +(function($) { + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.whizzywig = function(context, params, settings) { + // Previous versions used per-button images found in this location, + // now it is only used for custom buttons. + if (settings.buttonPath) { + window.buttonPath = settings.buttonPath; + } + // Assign the toolbar image path used for native buttons, if available. + if (settings.toolbarImagePath) { + btn._f = settings.toolbarImagePath; + } + // Fall back to text labels for all buttons. + else { + window.buttonPath = 'textbuttons'; + } + // Whizzywig needs to have the width set 'inline'. + $field = $('#' + params.field); + var originalValues = Drupal.wysiwyg.instances[params.field]; + originalValues.originalStyle = $field.attr('style'); + $field.css('width', $field.width() + 'px'); + + // Attach editor. + makeWhizzyWig(params.field, (settings.buttons ? settings.buttons : 'all')); + // Whizzywig fails to detect and set initial textarea contents. + var instance = $('#whizzy' + params.field).get(0); + if (instance) { + instance.contentWindow.document.body.innerHTML = tidyD($field.val()); + } +}; + +/** + * Detach a single or all editors. + */ +Drupal.wysiwyg.editor.detach.whizzywig = function(context, params) { + var detach = function (index) { + var id = whizzies[index]; + var instance = $('#whizzy' + id).get(0); + if (!instance) { + return; + } + var editingArea = instance.contentWindow.document; + var $field = $('#' + id); + // Whizzywig shows the original textarea in source mode. + if ($field.css('display') == 'block') { + editingArea.body.innerHTML = $field.val(); + } + + // Save contents of editor back into textarea. + $field.val(tidyH(editingArea)); + // Move original textarea back to its previous location. + $container = $('#CONTAINER' + id); + $field.insertBefore($container); + // Remove editor instance. + $container.remove(); + whizzies.splice(index, 1); + + // Restore original textarea styling. + var originalValues = Drupal.wysiwyg.instances[id]; + $field.removeAttr('style'); + $field.attr('style', originalValues.originalStyle); + } + + if (typeof params != 'undefined') { + for (var i = 0; i < whizzies.length; i++) { + if (whizzies[i] == params.field) { + detach(i); + break; + } + } + } + else { + while (whizzies.length > 0) { + detach(0); + } + } +}; + +})(jQuery); diff --git a/editors/js/whizzywig.js b/editors/js/whizzywig.js new file mode 100644 index 00000000..e98bc4da --- /dev/null +++ b/editors/js/whizzywig.js @@ -0,0 +1,126 @@ + +var wysiwygWhizzywig = { currentField: null, fields: {} }; +var buttonPath = null; + +/** + * Override Whizzywig's document.write() function. + * + * Whizzywig uses document.write() by default, which leads to a blank page when + * invoked in jQuery.ready(). Luckily, Whizzywig developers implemented a + * shorthand w() substitute function that we can override to redirect the output + * into the global wysiwygWhizzywig variable. + * + * @see o() + */ +var w = function (string) { + if (string) { + wysiwygWhizzywig.fields[wysiwygWhizzywig.currentField] += string; + } + return wysiwygWhizzywig.fields[wysiwygWhizzywig.currentField]; +}; + +/** + * Override Whizzywig's document.getElementById() function. + * + * Since we redirect the output of w() into a temporary string upon attaching + * an editor, we also have to override the o() shorthand substitute function + * for document.getElementById() to search in the document or our container. + * This override function also inserts the editor instance when Whizzywig + * tries to access its IFRAME, so it has access to the full/regular window + * object. + * + * @see w() + */ +var o = function (id) { + // Upon first access to "whizzy" + id, Whizzywig tries to access its IFRAME, + // so we need to insert the editor into the DOM. + if (id == 'whizzy' + wysiwygWhizzywig.currentField && wysiwygWhizzywig.fields[wysiwygWhizzywig.currentField]) { + jQuery('#' + wysiwygWhizzywig.currentField).after('
'); + // Iframe's .contentWindow becomes null in Webkit if inserted via .after(). + jQuery('#' + wysiwygWhizzywig.currentField + '-whizzywig').html(w()); + // Prevent subsequent invocations from inserting the editor multiple times. + wysiwygWhizzywig.fields[wysiwygWhizzywig.currentField] = ''; + } + // If id exists in the regular window.document, return it. + if (jQuery('#' + id).size()) { + return jQuery('#' + id).get(0); + } + // Otherwise return id from our container. + return jQuery('#' + id, w()).get(0); +}; + +(function($) { + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.whizzywig = function(context, params, settings) { + // Assign button images path, if available. + if (settings.buttonPath) { + window.buttonPath = settings.buttonPath; + } + // Create Whizzywig container. + wysiwygWhizzywig.currentField = params.field; + wysiwygWhizzywig.fields[wysiwygWhizzywig.currentField] = ''; + // Whizzywig needs to have the width set 'inline'. + $field = $('#' + params.field); + var originalValues = Drupal.wysiwyg.instances[params.field]; + originalValues.originalStyle = $field.attr('style'); + $field.css('width', $field.width() + 'px'); + + // Attach editor. + makeWhizzyWig(params.field, (settings.buttons ? settings.buttons : 'all')); + // Whizzywig fails to detect and set initial textarea contents. + var instance = $('#whizzy' + params.field).get(0); + if (instance) { + instance.contentWindow.document.body.innerHTML = tidyD($field.val()); + } +}; + +/** + * Detach a single or all editors. + */ +Drupal.wysiwyg.editor.detach.whizzywig = function(context, params) { + var detach = function (index) { + var id = whizzies[index]; + var instance = $('#whizzy' + id).get(0); + if (!instance) { + return; + } + var body = instance.contentWindow.document.body; + var $field = $('#' + id); + // Whizzywig shows the original textarea in source mode. + if ($field.css('display') == 'block') { + body.innerHTML = $field.val(); + } + body.innerHTML = tidyH(body.innerHTML); + + // Save contents of editor back into textarea. + $field.val(window.get_xhtml ? get_xhtml(body) : body.innerHTML); + $field.val($field.val().replace(location.href + '#', '#')); + // Remove editor instance. + $('#' + id + '-whizzywig').remove(); + whizzies.splice(index, 1); + + // Restore original textarea styling. + var originalValues = Drupal.wysiwyg.instances[id]; + $field.removeAttr('style'); + $field.attr('style', originalValues.originalStyle); + }; + + if (typeof params != 'undefined') { + for (var i = 0; i < whizzies.length; i++) { + if (whizzies[i] == params.field) { + detach(i); + break; + } + } + } + else { + while (whizzies.length > 0) { + detach(0); + } + } +}; + +})(jQuery); diff --git a/editors/js/wymeditor.js b/editors/js/wymeditor.js new file mode 100644 index 00000000..ed667848 --- /dev/null +++ b/editors/js/wymeditor.js @@ -0,0 +1,56 @@ +(function($) { + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.wymeditor = function (context, params, settings) { + // Prepend basePath to wymPath. + settings.wymPath = settings.basePath + settings.wymPath; + // Update activeId on focus. + settings.postInit = function (instance) { + $(instance._doc).focus(function () { + Drupal.wysiwyg.activeId = params.field; + }); + }; + // Attach editor. + $('#' + params.field).wymeditor(settings); +}; + +/** + * Detach a single or all editors. + */ +Drupal.wysiwyg.editor.detach.wymeditor = function (context, params) { + if (typeof params != 'undefined') { + var $field = $('#' + params.field); + var index = $field.data(WYMeditor.WYM_INDEX); + if (typeof index != 'undefined') { + var instance = WYMeditor.INSTANCES[index]; + instance.update(); + $(instance._box).remove(); + $(instance._element).show(); + delete instance; + } + $field.show(); + } + else { + jQuery.each(WYMeditor.INSTANCES, function () { + this.update(); + $(this._box).remove(); + $(this._element).show(); + delete this; + }); + } +}; + +Drupal.wysiwyg.editor.instance.wymeditor = { + insert: function (content) { + var $field = $('#' + this.field); + var index = $field.data(WYMeditor.WYM_INDEX); + if (typeof index != 'undefined') { + var instance = WYMeditor.INSTANCES[index]; + instance.insert(content); + } + } +}; + +})(jQuery); diff --git a/editors/js/yui.js b/editors/js/yui.js new file mode 100644 index 00000000..ad8be368 --- /dev/null +++ b/editors/js/yui.js @@ -0,0 +1,35 @@ +(function($) { + +/** + * Attach this editor to a target element. + */ +Drupal.wysiwyg.editor.attach.yui = function(context, params, settings) { + // Apply theme. + $('#' + params.field).parent().addClass('yui-skin-' + settings.theme); + // Attach editor. + var editor = new YAHOO.widget.Editor(params.field, settings); + editor.render(); +}; + +/** + * Detach a single or all editors. + * + * See Drupal.wysiwyg.editor.detach.none() for a full desciption of this hook. + */ +Drupal.wysiwyg.editor.detach.yui = function(context, params) { + if (typeof params != 'undefined') { + var instance = YAHOO.widget.EditorInfo.getEditorById(params.field); + if (instance) { + instance.destroy(); + } + } + else { + for (var e in YAHOO.widget.EditorInfo._instances) { + // Save contents of all editors back into textareas. + var instance = YAHOO.widget.EditorInfo._instances[e]; + instance.destroy(); + } + } +}; + +})(jQuery); diff --git a/editors/jwysiwyg.inc b/editors/jwysiwyg.inc new file mode 100644 index 00000000..fa65b741 --- /dev/null +++ b/editors/jwysiwyg.inc @@ -0,0 +1,62 @@ + 'jWYSIWYG', + 'vendor url' => 'http://code.google.com/p/jwysiwyg/', + 'download url' => 'http://code.google.com/p/jwysiwyg/downloads/list', + 'libraries' => array( + '' => array( + 'title' => 'Source', + 'files' => array('jquery.wysiwyg.js'), + ), + 'pack' => array( + 'title' => 'Packed', + 'files' => array('jquery.wysiwyg.pack.js'), + ), + ), + 'version callback' => 'wysiwyg_jwysiwyg_version', + // @todo Wrong property; add separate properties for editor requisites. + 'css path' => wysiwyg_get_path('jwysiwyg'), + 'versions' => array( + '0.5' => array( + 'js files' => array('jwysiwyg.js'), + 'css files' => array('jquery.wysiwyg.css'), + ), + ), + ); + return $editor; +} + +/** + * Detect editor version. + * + * @param $editor + * An array containing editor properties as returned from hook_editor(). + * + * @return + * The installed editor version. + */ +function wysiwyg_jwysiwyg_version($editor) { + $script = $editor['library path'] . '/jquery.wysiwyg.js'; + if (!file_exists($script)) { + return; + } + $script = fopen($script, 'r'); + fgets($script); + $line = fgets($script); + if (preg_match('@([0-9\.]+)$@', $line, $version)) { + fclose($script); + return $version[1]; + } + fclose($script); +} + diff --git a/editors/markitup.inc b/editors/markitup.inc new file mode 100644 index 00000000..57a37e83 --- /dev/null +++ b/editors/markitup.inc @@ -0,0 +1,189 @@ + 'markItUp', + 'vendor url' => 'http://markitup.jaysalvat.com', + 'download url' => 'http://markitup.jaysalvat.com/downloads', + 'library path' => wysiwyg_get_path('markitup'), + 'libraries' => array( + '' => array( + 'title' => 'Source', + 'files' => array('markitup/jquery.markitup.js'), + ), + 'pack' => array( + 'title' => 'Packed', + 'files' => array('markitup/jquery.markitup.pack.js'), + ), + ), + 'version callback' => 'wysiwyg_markitup_version', + 'themes callback' => 'wysiwyg_markitup_themes', + 'settings callback' => 'wysiwyg_markitup_settings', + 'plugin callback' => 'wysiwyg_markitup_plugins', + 'versions' => array( + '1.1.5' => array( + 'js files' => array('markitup.js'), + ), + ), + ); + return $editor; +} + +/** + * Detect editor version. + * + * @param $editor + * An array containing editor properties as returned from hook_editor(). + * + * @return + * The installed editor version. + */ +function wysiwyg_markitup_version($editor) { + // Changelog was in markitup/markitup/readme.txt <= 1.1.5. + $changelog = $editor['library path'] . '/markitup/readme.txt'; + if (!file_exists($changelog)) { + // Changelog was moved up to markitup/CHANGELOG.md after 1.1.5. + $changelog = $editor['library path'] . '/CHANGELOG.md'; + if (!file_exists($changelog)) { + return; + } + } + $changelog = fopen($changelog, 'r'); + $line = fgets($changelog); + if (preg_match('@([0-9\.]+)@', $line, $version)) { + fclose($changelog); + return $version[1]; + } + fclose($changelog); +} + +/** + * Determine available editor themes or check/reset a given one. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $profile + * A wysiwyg editor profile. + * + * @return + * An array of theme names. The first returned name should be the default + * theme name. + */ +function wysiwyg_markitup_themes($editor, $profile) { + return array('simple', 'markitup'); +} + +/** + * Return runtime editor settings for a given wysiwyg profile. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $config + * An array containing wysiwyg editor profile settings. + * @param $theme + * The name of a theme/GUI/skin to use. + * + * @return + * A settings array to be populated in + * Drupal.settings.wysiwyg.configs.{editor} + */ +function wysiwyg_markitup_settings($editor, $config, $theme) { + drupal_add_css($editor['library path'] . '/markitup/skins/' . $theme . '/style.css', array( + // Specify an alternate basename; otherwise, style.css would override a + // commonly used style.css file of the theme. + 'basename' => 'markitup.' . $theme . '.style.css', + 'group' => CSS_THEME, + )); + + $settings = array( + 'root' => base_path() . $editor['library path'] . '/markitup/', + 'nameSpace' => $theme, + 'markupSet' => array(), + ); + + // Add configured buttons or all available. + $default_buttons = array( + 'bold' => array( + 'name' => t('Bold'), + 'className' => 'markitup-bold', + 'key' => 'B', + 'openWith' => '(!(|!|)!)', + 'closeWith' => '(!(|!|)!)', + ), + 'italic' => array( + 'name' => t('Italic'), + 'className' => 'markitup-italic', + 'key' => 'I', + 'openWith' => '(!(|!|)!)', + 'closeWith' => '(!(|!|)!)', + ), + 'stroke' => array( + 'name' => t('Strike-through'), + 'className' => 'markitup-stroke', + 'key' => 'S', + 'openWith' => '', + 'closeWith' => '', + ), + 'image' => array( + 'name' => t('Image'), + 'className' => 'markitup-image', + 'key' => 'P', + 'replaceWith' => '[![Alternative text]!]', + ), + 'link' => array( + 'name' => t('Link'), + 'className' => 'markitup-link', + 'key' => 'K', + 'openWith' => '', + 'closeWith' => '', + 'placeHolder' => 'Your text to link...', + ), + // @todo + // 'cleanup' => array('name' => t('Clean-up'), 'className' => 'markitup-cleanup', 'replaceWith' => 'function(markitup) { return markitup.selection.replace(/<(.*?)>/g, "") }'), + 'preview' => array( + 'name' => t('Preview'), + 'className' => 'markitup-preview', + 'call' => 'preview', + ), + ); + $settings['markupSet'] = array(); + if (!empty($config['buttons'])) { + foreach ($config['buttons'] as $plugin) { + foreach ($plugin as $button => $enabled) { + if (isset($default_buttons[$button])) { + $settings['markupSet'][$button] = $default_buttons[$button]; + } + } + } + } + + return $settings; +} + +/** + * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin(). + */ +function wysiwyg_markitup_plugins($editor) { + return array( + 'default' => array( + 'buttons' => array( + 'bold' => t('Bold'), 'italic' => t('Italic'), + 'stroke' => t('Strike-through'), + 'link' => t('Link'), + 'image' => t('Image'), + // 'cleanup' => t('Clean-up'), + 'preview' => t('Preview'), + ), + 'internal' => TRUE, + ), + ); +} + diff --git a/editors/nicedit.inc b/editors/nicedit.inc new file mode 100644 index 00000000..779660c9 --- /dev/null +++ b/editors/nicedit.inc @@ -0,0 +1,119 @@ + 'NicEdit', + 'vendor url' => 'http://nicedit.com', + 'download url' => 'http://nicedit.com/download.php', + 'libraries' => array( + '' => array( + 'title' => 'Source', + 'files' => array('nicEdit.js'), + ), + ), + 'version callback' => 'wysiwyg_nicedit_version', + 'settings callback' => 'wysiwyg_nicedit_settings', + 'plugin callback' => 'wysiwyg_nicedit_plugins', + 'versions' => array( + '0.9' => array( + 'js files' => array('nicedit.js'), + ), + ), + ); + return $editor; +} + +/** + * Detect editor version. + * + * @param $editor + * An array containing editor properties as returned from hook_editor(). + * + * @return + * The installed editor version. + */ +function wysiwyg_nicedit_version($editor) { + // @see http://nicedit.com/forums/viewtopic.php?t=425 + return '0.9'; +} + +/** + * Return runtime editor settings for a given wysiwyg profile. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $config + * An array containing wysiwyg editor profile settings. + * @param $theme + * The name of a theme/GUI/skin to use. + * + * @return + * A settings array to be populated in + * Drupal.settings.wysiwyg.configs.{editor} + */ +function wysiwyg_nicedit_settings($editor, $config, $theme) { + $settings = array( + 'iconsPath' => base_path() . $editor['library path'] . '/nicEditorIcons.gif', + ); + + // Add configured buttons or all available. + $settings['buttonList'] = array(); + if (!empty($config['buttons'])) { + $buttons = array(); + foreach ($config['buttons'] as $plugin) { + $buttons = array_merge($buttons, $plugin); + } + $settings['buttonList'] = array_keys($buttons); + } + + // Add editor content stylesheet. + if (isset($config['css_setting'])) { + if ($config['css_setting'] == 'theme') { + $css = path_to_theme() . '/style.css'; + if (file_exists($css)) { + $settings['externalCSS'] = base_path() . $css; + } + } + else if ($config['css_setting'] == 'self' && isset($config['css_path'])) { + $settings['externalCSS'] = strtr($config['css_path'], array('%b' => base_path(), '%t' => path_to_theme())); + } + } + + return $settings; +} + +/** + * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin(). + */ +function wysiwyg_nicedit_plugins($editor) { + return array( + 'default' => array( + 'buttons' => array( + 'bold' => t('Bold'), 'italic' => t('Italic'), 'underline' => t('Underline'), + 'strikethrough' => t('Strike-through'), + 'left' => t('Align left'), 'center' => t('Align center'), 'right' => t('Align right'), + 'ul' => t('Bullet list'), 'ol' => t('Numbered list'), + 'outdent' => t('Outdent'), 'indent' => t('Indent'), + 'image' => t('Image'), + 'forecolor' => t('Forecolor'), 'bgcolor' => t('Backcolor'), + 'superscript' => t('Superscript'), 'subscript' => t('Subscript'), + 'hr' => t('Horizontal rule'), + // @todo New challenge: Optional internal plugins packaged into editor + // library. + 'link' => t('Link'), 'unlink' => t('Unlink'), + 'fontFormat' => t('HTML block format'), 'fontFamily' => t('Font'), 'fontSize' => t('Font size'), + 'xhtml' => t('Source code'), + ), + 'internal' => TRUE, + ), + ); +} + diff --git a/editors/openwysiwyg.inc b/editors/openwysiwyg.inc new file mode 100644 index 00000000..f521da56 --- /dev/null +++ b/editors/openwysiwyg.inc @@ -0,0 +1,173 @@ + 'openWYSIWYG', + 'vendor url' => 'http://www.openwebware.com', + 'download url' => 'http://www.openwebware.com/download.shtml', + 'library path' => wysiwyg_get_path('openwysiwyg') . '/scripts', + 'libraries' => array( + 'src' => array( + 'title' => 'Source', + 'files' => array('wysiwyg.js'), + ), + ), + 'version callback' => 'wysiwyg_openwysiwyg_version', + 'themes callback' => 'wysiwyg_openwysiwyg_themes', + 'settings callback' => 'wysiwyg_openwysiwyg_settings', + 'plugin callback' => 'wysiwyg_openwysiwyg_plugins', + 'versions' => array( + '1.4.7' => array( + 'js files' => array('openwysiwyg.js'), + 'css files' => array('openwysiwyg.css'), + ), + ), + ); + return $editor; +} + +/** + * Detect editor version. + * + * @param $editor + * An array containing editor properties as returned from hook_editor(). + * + * @return + * The installed editor version. + */ +function wysiwyg_openwysiwyg_version($editor) { + // 'library path' has '/scripts' appended already. + $changelog = $editor['editor path'] . '/changelog'; + if (!file_exists($changelog)) { + return; + } + $changelog = fopen($changelog, 'r'); + $line = fgets($changelog, 20); + if (preg_match('@v([\d\.]+)@', $line, $version)) { + fclose($changelog); + return $version[1]; + } + fclose($changelog); +} + +/** + * Determine available editor themes or check/reset a given one. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $profile + * A wysiwyg editor profile. + * + * @return + * An array of theme names. The first returned name should be the default + * theme name. + */ +function wysiwyg_openwysiwyg_themes($editor, $profile) { + return array('default'); +} + +/** + * Return runtime editor settings for a given wysiwyg profile. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $config + * An array containing wysiwyg editor profile settings. + * @param $theme + * The name of a theme/GUI/skin to use. + * + * @return + * A settings array to be populated in + * Drupal.settings.wysiwyg.configs.{editor} + */ +function wysiwyg_openwysiwyg_settings($editor, $config, $theme) { + $settings = array( + 'path' => base_path() . $editor['editor path'] . '/', + 'Width' => '100%', + ); + + if (isset($config['path_loc']) && $config['path_loc'] == 'none') { + $settings['StatusBarEnabled'] = FALSE; + } + + if (isset($config['css_setting'])) { + if ($config['css_setting'] == 'theme') { + $settings['CSSFile'] = reset(wysiwyg_get_css()); + } + else if ($config['css_setting'] == 'self' && isset($config['css_path'])) { + $settings['CSSFile'] = strtr($config['css_path'], array('%b' => base_path(), '%t' => path_to_theme())); + } + } + + $settings['Toolbar'] = array(); + if (!empty($config['buttons'])) { + $plugins = wysiwyg_get_plugins($editor['name']); + foreach ($config['buttons'] as $plugin => $buttons) { + foreach ($buttons as $button => $enabled) { + foreach (array('buttons', 'extensions') as $type) { + // Skip unavailable plugins. + if (!isset($plugins[$plugin][$type][$button])) { + continue; + } + // Add buttons. + if ($type == 'buttons') { + $settings['Toolbar'][0][] = $button; + } + } + } + } + } + + // @todo +// if (isset($config['block_formats'])) { +// $settings['DropDowns']['headings']['elements'] = explode(',', $config['block_formats']); +// } + + return $settings; +} + +/** + * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin(). + */ +function wysiwyg_openwysiwyg_plugins($editor) { + $plugins = array( + 'default' => array( + 'buttons' => array( + 'bold' => t('Bold'), 'italic' => t('Italic'), 'underline' => t('Underline'), + 'strikethrough' => t('Strike-through'), + 'justifyleft' => t('Align left'), 'justifycenter' => t('Align center'), 'justifyright' => t('Align right'), 'justifyfull' => t('Justify'), + 'unorderedlist' => t('Bullet list'), 'orderedlist' => t('Numbered list'), + 'outdent' => t('Outdent'), 'indent' => t('Indent'), + 'undo' => t('Undo'), 'redo' => t('Redo'), + 'createlink' => t('Link'), + 'insertimage' => t('Image'), + 'cleanup' => t('Clean-up'), + 'forecolor' => t('Forecolor'), 'backcolor' => t('Backcolor'), + 'superscript' => t('Sup'), 'subscript' => t('Sub'), + 'blockquote' => t('Blockquote'), 'viewSource' => t('Source code'), + 'hr' => t('Horizontal rule'), + 'cut' => t('Cut'), 'copy' => t('Copy'), 'paste' => t('Paste'), + 'visualaid' => t('Visual aid'), + 'removeformat' => t('Remove format'), + 'charmap' => t('Character map'), + 'headings' => t('HTML block format'), 'font' => t('Font'), 'fontsize' => t('Font size'), + 'maximize' => t('Fullscreen'), + 'preview' => t('Preview'), + 'print' => t('Print'), + 'inserttable' => t('Table'), + 'help' => t('Help'), + ), + 'internal' => TRUE, + ), + ); + return $plugins; +} + diff --git a/editors/tinymce.inc b/editors/tinymce.inc new file mode 100644 index 00000000..a7250f35 --- /dev/null +++ b/editors/tinymce.inc @@ -0,0 +1,608 @@ +_alter() to add/inject optional libraries like gzip. + */ +function wysiwyg_tinymce_editor() { + $editor['tinymce'] = array( + 'title' => 'TinyMCE', + 'vendor url' => 'http://tinymce.moxiecode.com', + 'download url' => 'http://tinymce.moxiecode.com/download.php', + 'library path' => wysiwyg_get_path('tinymce') . '/jscripts/tiny_mce', + 'libraries' => array( + '' => array( + 'title' => 'Minified', + 'files' => array('tiny_mce.js'), + ), + 'src' => array( + 'title' => 'Source', + 'files' => array('tiny_mce_src.js'), + ), + ), + 'version callback' => 'wysiwyg_tinymce_version', + 'themes callback' => 'wysiwyg_tinymce_themes', + 'settings callback' => 'wysiwyg_tinymce_settings', + 'plugin callback' => 'wysiwyg_tinymce_plugins', + 'plugin settings callback' => 'wysiwyg_tinymce_plugin_settings', + 'proxy plugin' => array( + 'drupal' => array( + 'load' => TRUE, + 'proxy' => TRUE, + ), + ), + 'proxy plugin settings callback' => 'wysiwyg_tinymce_proxy_plugin_settings', + 'versions' => array( + '2.1' => array( + 'js files' => array('tinymce-2.js'), + 'css files' => array('tinymce-2.css'), + 'download url' => 'http://sourceforge.net/project/showfiles.php?group_id=103281&package_id=111430&release_id=557383', + ), + // @todo Starting from 3.3, tiny_mce.js may support JS aggregation. + '3.1' => array( + 'js files' => array('tinymce-3.js'), + 'css files' => array('tinymce-3.css'), + 'libraries' => array( + '' => array( + 'title' => 'Minified', + 'files' => array( + 'tiny_mce.js' => array('preprocess' => FALSE), + ), + ), + 'jquery' => array( + 'title' => 'jQuery', + 'files' => array('tiny_mce_jquery.js'), + ), + 'src' => array( + 'title' => 'Source', + 'files' => array('tiny_mce_src.js'), + ), + ), + ), + ), + ); + return $editor; +} + +/** + * Detect editor version. + * + * @param $editor + * An array containing editor properties as returned from hook_editor(). + * + * @return + * The installed editor version. + */ +function wysiwyg_tinymce_version($editor) { + $script = $editor['library path'] . '/tiny_mce.js'; + if (!file_exists($script)) { + return; + } + $script = fopen($script, 'r'); + // Version is contained in the first 200 chars. + $line = fgets($script, 200); + fclose($script); + // 2.x: this.majorVersion="2";this.minorVersion="1.3" + // 3.x: majorVersion:'3',minorVersion:'2.0.1' + if (preg_match('@majorVersion[=:]["\'](\d).+?minorVersion[=:]["\']([\d\.]+)@', $line, $version)) { + return $version[1] . '.' . $version[2]; + } +} + +/** + * Determine available editor themes or check/reset a given one. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $profile + * A wysiwyg editor profile. + * + * @return + * An array of theme names. The first returned name should be the default + * theme name. + */ +function wysiwyg_tinymce_themes($editor, $profile) { + /* + $themes = array(); + $dir = $editor['library path'] . '/themes/'; + if (is_dir($dir) && $dh = opendir($dir)) { + while (($file = readdir($dh)) !== FALSE) { + if (!in_array($file, array('.', '..', 'CVS', '.svn')) && is_dir($dir . $file)) { + $themes[$file] = $file; + } + } + closedir($dh); + asort($themes); + } + return $themes; + */ + return array('advanced', 'simple'); +} + +/** + * Return runtime editor settings for a given wysiwyg profile. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $config + * An array containing wysiwyg editor profile settings. + * @param $theme + * The name of a theme/GUI/skin to use. + * + * @return + * A settings array to be populated in + * Drupal.settings.wysiwyg.configs.{editor} + */ +function wysiwyg_tinymce_settings($editor, $config, $theme) { + $settings = array( + 'button_tile_map' => TRUE, // @todo Add a setting for this. + 'document_base_url' => base_path(), + 'mode' => 'none', + 'plugins' => array(), + 'theme' => $theme, + 'width' => '100%', + // Strict loading mode must be enabled; otherwise TinyMCE would use + // document.write() in IE and Chrome. + 'strict_loading_mode' => TRUE, + // TinyMCE's URL conversion magic breaks Drupal modules that use a special + // syntax for paths. This makes 'relative_urls' obsolete. + 'convert_urls' => FALSE, + // The default entity_encoding ('named') converts too many characters in + // languages (like Greek). Since Drupal supports Unicode, we only convert + // HTML control characters and invisible characters. TinyMCE always converts + // XML default characters '&', '<', '>'. + 'entities' => '160,nbsp,173,shy,8194,ensp,8195,emsp,8201,thinsp,8204,zwnj,8205,zwj,8206,lrm,8207,rlm', + ); + if (isset($config['apply_source_formatting'])) { + $settings['apply_source_formatting'] = $config['apply_source_formatting']; + } + if (isset($config['convert_fonts_to_spans'])) { + $settings['convert_fonts_to_spans'] = $config['convert_fonts_to_spans']; + } + if (isset($config['language'])) { + $settings['language'] = $config['language']; + } + if (isset($config['paste_auto_cleanup_on_paste'])) { + $settings['paste_auto_cleanup_on_paste'] = $config['paste_auto_cleanup_on_paste']; + } + if (isset($config['preformatted'])) { + $settings['preformatted'] = $config['preformatted']; + } + if (isset($config['remove_linebreaks'])) { + $settings['remove_linebreaks'] = $config['remove_linebreaks']; + } + if (isset($config['verify_html'])) { + $settings['verify_html'] = (bool) $config['verify_html']; + } + + if (!empty($config['css_classes'])) { + $settings['theme_advanced_styles'] = implode(';', array_filter(explode("\n", str_replace("\r", '', $config['css_classes'])))); + } + + if (isset($config['css_setting'])) { + if ($config['css_setting'] == 'theme') { + $settings['content_css'] = implode(',', wysiwyg_get_css()); + } + else if ($config['css_setting'] == 'self' && isset($config['css_path'])) { + $settings['content_css'] = strtr($config['css_path'], array('%b' => base_path(), '%t' => path_to_theme())); + } + } + + // Find the enabled buttons and the button row they belong on. + // Also map the plugin metadata for each button. + // @todo What follows is a pain; needs a rewrite. + // $settings['buttons'] are stacked into $settings['theme_advanced_buttons1'] + // later. + $settings['buttons'] = array(); + if (!empty($config['buttons']) && is_array($config['buttons'])) { + // Only array keys in $settings['extensions'] matter; added to + // $settings['plugins'] later. + $settings['extensions'] = array(); + // $settings['extended_valid_elements'] are just stacked, unique'd later, + // and transformed into a comma-separated string in + // wysiwyg_add_editor_settings(). + // @todo Needs a complete plugin API redesign using arrays for + // tag => attributes definitions and array_merge_recursive(). + $settings['extended_valid_elements'] = array(); + + $plugins = wysiwyg_get_plugins($editor['name']); + foreach ($config['buttons'] as $plugin => $buttons) { + foreach ($buttons as $button => $enabled) { + // Iterate separately over buttons and extensions properties. + foreach (array('buttons', 'extensions') as $type) { + // Skip unavailable plugins. + if (!isset($plugins[$plugin][$type][$button])) { + continue; + } + // Add buttons. + if ($type == 'buttons') { + $settings['buttons'][] = $button; + } + // Add external Drupal plugins to the list of extensions. + if ($type == 'buttons' && !empty($plugins[$plugin]['proxy'])) { + $settings['extensions'][_wysiwyg_tinymce_plugin_name('add', $button)] = 1; + } + // Add external plugins to the list of extensions. + else if ($type == 'buttons' && empty($plugins[$plugin]['internal'])) { + $settings['extensions'][_wysiwyg_tinymce_plugin_name('add', $plugin)] = 1; + } + // Add internal buttons that also need to be loaded as extension. + else if ($type == 'buttons' && !empty($plugins[$plugin]['load'])) { + $settings['extensions'][$plugin] = 1; + } + // Add plain extensions. + else if ($type == 'extensions' && !empty($plugins[$plugin]['load'])) { + $settings['extensions'][$plugin] = 1; + } + // Allow plugins to add valid HTML elements. + if (!empty($plugins[$plugin]['extended_valid_elements'])) { + $settings['extended_valid_elements'] = array_merge($settings['extended_valid_elements'], $plugins[$plugin]['extended_valid_elements']); + } + // Allow plugins to add or override global configuration settings. + if (!empty($plugins[$plugin]['options'])) { + $settings = array_merge($settings, $plugins[$plugin]['options']); + } + } + } + } + // Clean-up. + $settings['extended_valid_elements'] = array_unique($settings['extended_valid_elements']); + if ($settings['extensions']) { + $settings['plugins'] = array_keys($settings['extensions']); + } + unset($settings['extensions']); + } + + // Add theme-specific settings. + switch ($theme) { + case 'advanced': + $settings += array( + 'theme_advanced_resize_horizontal' => FALSE, + 'theme_advanced_resizing_use_cookie' => FALSE, + 'theme_advanced_path_location' => isset($config['path_loc']) ? $config['path_loc'] : 'bottom', + 'theme_advanced_resizing' => isset($config['resizing']) ? $config['resizing'] : 1, + 'theme_advanced_toolbar_location' => isset($config['toolbar_loc']) ? $config['toolbar_loc'] : 'top', + 'theme_advanced_toolbar_align' => isset($config['toolbar_align']) ? $config['toolbar_align'] : 'left', + ); + if (isset($config['block_formats'])) { + $settings['theme_advanced_blockformats'] = $config['block_formats']; + } + if (isset($settings['buttons'])) { + // These rows explicitly need to be set to be empty, otherwise TinyMCE + // loads its default buttons of the advanced theme for each row. + $settings += array( + 'theme_advanced_buttons1' => array(), + 'theme_advanced_buttons2' => array(), + 'theme_advanced_buttons3' => array(), + ); + // @todo Allow to sort/arrange editor buttons. + for ($i = 0; $i < count($settings['buttons']); $i++) { + $settings['theme_advanced_buttons1'][] = $settings['buttons'][$i]; + } + } + break; + } + unset($settings['buttons']); + + // Convert the config values into the form expected by TinyMCE. + $csv_settings = array('plugins', 'extended_valid_elements', 'theme_advanced_buttons1', 'theme_advanced_buttons2', 'theme_advanced_buttons3'); + foreach ($csv_settings as $key) { + if (isset($settings[$key]) && is_array($settings[$key])) { + $settings[$key] = implode(',', $settings[$key]); + } + } + + return $settings; +} + +/** + * Build a JS settings array of native external plugins that need to be loaded separately. + * + * TinyMCE requires that external plugins (i.e. not residing in the editor's + * directory) are loaded (once) upon initializing the editor. + */ +function wysiwyg_tinymce_plugin_settings($editor, $profile, $plugins) { + $settings = array(); + foreach ($plugins as $name => $plugin) { + if (!empty($plugin['load'])) { + // Add path for native external plugins; internal ones are loaded + // automatically. + if (empty($plugin['internal']) && isset($plugin['filename'])) { + $settings[$name] = base_path() . $plugin['path'] . '/' . $plugin['filename']; + } + } + } + return $settings; +} + +/** + * Build a JS settings array for Drupal plugins loaded via the proxy plugin. + */ +function wysiwyg_tinymce_proxy_plugin_settings($editor, $profile, $plugins) { + $settings = array(); + foreach ($plugins as $name => $plugin) { + // Populate required plugin settings. + $settings[$name] = $plugin['dialog settings'] + array( + 'title' => $plugin['title'], + 'icon' => base_path() . $plugin['icon path'] . '/' . $plugin['icon file'], + 'iconTitle' => $plugin['icon title'], + ); + if (isset($plugin['css file'])) { + $settings[$name]['css'] = base_path() . $plugin['css path'] . '/' . $plugin['css file']; + } + } + return $settings; +} + +/** + * Add or remove leading hiven to/of external plugin names. + * + * TinyMCE requires that external plugins, which should not be loaded from + * its own plugin repository are prefixed with a hiven in the name. + * + * @param string $op + * Operation to perform, 'add' or 'remove' (hiven). + * @param string $name + * A plugin name. + */ +function _wysiwyg_tinymce_plugin_name($op, $name) { + if ($op == 'add') { + if (strpos($name, '-') !== 0) { + return '-' . $name; + } + return $name; + } + else if ($op == 'remove') { + if (strpos($name, '-') === 0) { + return substr($name, 1); + } + return $name; + } +} + +/** + * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin(). + */ +function wysiwyg_tinymce_plugins($editor) { + $plugins = array( + 'default' => array( + 'path' => $editor['library path'] . '/themes/advanced', + 'buttons' => array( + 'bold' => t('Bold'), 'italic' => t('Italic'), 'underline' => t('Underline'), + 'strikethrough' => t('Strike-through'), + 'justifyleft' => t('Align left'), 'justifycenter' => t('Align center'), 'justifyright' => t('Align right'), 'justifyfull' => t('Justify'), + 'bullist' => t('Bullet list'), 'numlist' => t('Numbered list'), + 'outdent' => t('Outdent'), 'indent' => t('Indent'), + 'undo' => t('Undo'), 'redo' => t('Redo'), + 'link' => t('Link'), 'unlink' => t('Unlink'), 'anchor' => t('Anchor'), + 'image' => t('Image'), + 'cleanup' => t('Clean-up'), + 'forecolor' => t('Forecolor'), 'backcolor' => t('Backcolor'), + 'sup' => t('Superscript'), 'sub' => t('Subscript'), + 'blockquote' => t('Blockquote'), 'code' => t('Source code'), + 'hr' => t('Horizontal rule'), + 'cut' => t('Cut'), 'copy' => t('Copy'), 'paste' => t('Paste'), + 'visualaid' => t('Visual aid'), + 'removeformat' => t('Remove format'), + 'charmap' => t('Character map'), + 'help' => t('Help'), + ), + 'internal' => TRUE, + ), + 'advhr' => array( + 'path' => $editor['library path'] . '/plugins/advhr', + 'buttons' => array('advhr' => t('Advanced horizontal rule')), + 'extended_valid_elements' => array('hr[class|width|size|noshade]'), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/advhr', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'advimage' => array( + 'path' => $editor['library path'] . '/plugins/advimage', + 'extensions' => array('advimage' => t('Advanced image')), + 'extended_valid_elements' => array('img[src|alt|title|align|width|height|usemap|hspace|vspace|border|style|class|onmouseover|onmouseout|id|name]'), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/advimage', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'advlink' => array( + 'path' => $editor['library path'] . '/plugins/advlink', + 'extensions' => array('advlink' => t('Advanced link')), + 'extended_valid_elements' => array('a[name|href|target|title|class|onfocus|onblur|onclick|ondlbclick|onmousedown|onmouseup|onmouseover|onmouseout|onkeypress|onkeydown|onkeyup|id|style|rel]'), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/advlink', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'autosave' => array( + 'path' => $editor['library path'] . '/plugins/autosave', + 'extensions' => array('autosave' => t('Auto save')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/autosave', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'contextmenu' => array( + 'path' => $editor['library path'] . '/plugins/contextmenu', + 'extensions' => array('contextmenu' => t('Context menu')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/contextmenu', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'directionality' => array( + 'path' => $editor['library path'] . '/plugins/directionality', + 'buttons' => array('ltr' => t('Left-to-right'), 'rtl' => t('Right-to-left')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/directionality', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'emotions' => array( + 'path' => $editor['library path'] . '/plugins/emotions', + 'buttons' => array('emotions' => t('Emotions')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/emotions', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'font' => array( + 'path' => $editor['library path'] . '/plugins/font', + 'buttons' => array('formatselect' => t('HTML block format'), 'fontselect' => t('Font'), 'fontsizeselect' => t('Font size'), 'styleselect' => t('Font style')), + 'extended_valid_elements' => array('font[face|size|color|style],span[class|align|style]'), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/font', + 'internal' => TRUE, + ), + 'fullscreen' => array( + 'path' => $editor['library path'] . '/plugins/fullscreen', + 'buttons' => array('fullscreen' => t('Fullscreen')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/fullscreen', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'inlinepopups' => array( + 'path' => $editor['library path'] . '/plugins/inlinepopups', + 'extensions' => array('inlinepopups' => t('Inline popups')), + 'options' => array( + 'dialog_type' => array('modal'), + ), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/inlinepopups', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'insertdatetime' => array( + 'path' => $editor['library path'] . '/plugins/insertdatetime', + 'buttons' => array('insertdate' => t('Insert date'), 'inserttime' => t('Insert time')), + 'options' => array( + 'plugin_insertdate_dateFormat' => '%Y-%m-%d', + 'plugin_insertdate_timeFormat' => '%H:%M:%S', + ), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/insertdatetime', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'layer' => array( + 'path' => $editor['library path'] . '/plugins/layer', + 'buttons' => array('insertlayer' => t('Insert layer'), 'moveforward' => t('Move forward'), 'movebackward' => t('Move backward'), 'absolute' => t('Absolute')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/layer', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'paste' => array( + 'path' => $editor['library path'] . '/plugins/paste', + 'buttons' => array('pastetext' => t('Paste text'), 'pasteword' => t('Paste from Word'), 'selectall' => t('Select all')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/paste', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'preview' => array( + 'path' => $editor['library path'] . '/plugins/preview', + 'buttons' => array('preview' => t('Preview')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/preview', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'print' => array( + 'path' => $editor['library path'] . '/plugins/print', + 'buttons' => array('print' => t('Print')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/print', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'searchreplace' => array( + 'path' => $editor['library path'] . '/plugins/searchreplace', + 'buttons' => array('search' => t('Search'), 'replace' => t('Replace')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/searchreplace', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'style' => array( + 'path' => $editor['library path'] . '/plugins/style', + 'buttons' => array('styleprops' => t('Style properties')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/style', + 'internal' => TRUE, + 'load' => TRUE, + ), + 'table' => array( + 'path' => $editor['library path'] . '/plugins/table', + 'buttons' => array('tablecontrols' => t('Table')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/table', + 'internal' => TRUE, + 'load' => TRUE, + ), + ); + if (version_compare($editor['installed version'], '3', '<')) { + $plugins['flash'] = array( + 'path' => $editor['library path'] . '/plugins/flash', + 'buttons' => array('flash' => t('Flash')), + 'extended_valid_elements' => array('img[class|src|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name|obj|param|embed]'), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/flash', + 'internal' => TRUE, + 'load' => TRUE, + ); + } + if (version_compare($editor['installed version'], '2.0.6', '>')) { + $plugins['media'] = array( + 'path' => $editor['library path'] . '/plugins/media', + 'buttons' => array('media' => t('Media')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/media', + 'internal' => TRUE, + 'load' => TRUE, + ); + $plugins['xhtmlxtras'] = array( + 'path' => $editor['library path'] . '/plugins/xhtmlxtras', + 'buttons' => array('cite' => t('Citation'), 'del' => t('Deleted'), 'abbr' => t('Abbreviation'), 'acronym' => t('Acronym'), 'ins' => t('Inserted'), 'attribs' => t('HTML attributes')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/xhtmlxtras', + 'internal' => TRUE, + 'load' => TRUE, + ); + } + if (version_compare($editor['installed version'], '3', '>')) { + $plugins['bbcode'] = array( + 'path' => $editor['library path'] . '/plugins/bbcode', + 'extensions' => array('bbcode' => t('BBCode')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/bbcode', + 'internal' => TRUE, + 'load' => TRUE, + ); + if (version_compare($editor['installed version'], '3.3', '<')) { + $plugins['safari'] = array( + 'path' => $editor['library path'] . '/plugins/safari', + 'extensions' => array('safari' => t('Safari compatibility')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/safari', + 'internal' => TRUE, + 'load' => TRUE, + ); + } + } + if (version_compare($editor['installed version'], '3.2.5', '>=')) { + $plugins['autoresize'] = array( + 'path' => $editor['library path'] . '/plugins/autoresize', + 'extensions' => array('autoresize' => t('Auto resize')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/autoresize', + 'internal' => TRUE, + 'load' => TRUE, + ); + } + if (version_compare($editor['installed version'], '3.3', '>=')) { + $plugins['advlist'] = array( + 'path' => $editor['library path'] . '/plugins/advlist', + 'extensions' => array('advlist' => t('Advanced list')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/advlist', + 'internal' => TRUE, + 'load' => TRUE, + ); + } + if (version_compare($editor['installed version'], '3.2.6', '>=')) { + $plugins['wordcount'] = array( + 'path' => $editor['library path'] . '/plugins/wordcount', + 'extensions' => array('wordcount' => t('Word count')), + 'url' => 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/wordcount', + 'internal' => TRUE, + 'load' => TRUE, + ); + } + return $plugins; +} + diff --git a/editors/whizzywig.inc b/editors/whizzywig.inc new file mode 100644 index 00000000..d82cc0fe --- /dev/null +++ b/editors/whizzywig.inc @@ -0,0 +1,147 @@ + 'Whizzywig', + 'vendor url' => 'http://www.unverse.net', + 'download url' => 'http://www.unverse.net/whizzywig-download.html', + 'libraries' => array( + '' => array( + 'title' => 'Default', + 'files' => array('whizzywig.js', 'xhtml.js'), + ), + ), + 'version callback' => 'wysiwyg_whizzywig_version', + 'settings callback' => 'wysiwyg_whizzywig_settings', + 'plugin callback' => 'wysiwyg_whizzywig_plugins', + 'versions' => array( + '55' => array( + 'js files' => array('whizzywig.js'), + ), + '56' => array( + 'js files' => array('whizzywig-56.js'), + ), + '60' => array( + 'js files' => array('whizzywig-60.js'), + ), + ), + ); + return $editor; +} + +/** + * Detect editor version. + * + * @param $editor + * An array containing editor properties as returned from hook_editor(). + * + * @return + * The installed editor version. + */ +function wysiwyg_whizzywig_version($editor) { + $script = $editor['library path'] . '/whizzywig.js'; + if (!file_exists($script)) { + return; + } + $script = fopen($script, 'r'); + $line = fgets($script, 43); + // 55: Whizzywig v55i + // 60: Whizzywig 60 + if (preg_match('@Whizzywig v?([0-9]+)@', $line, $version)) { + fclose($script); + return $version[1]; + } + fclose($script); +} + +/** + * Return runtime editor settings for a given wysiwyg profile. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $config + * An array containing wysiwyg editor profile settings. + * @param $theme + * The name of a theme/GUI/skin to use. + * + * @return + * A settings array to be populated in + * Drupal.settings.wysiwyg.configs.{editor} + */ +function wysiwyg_whizzywig_settings($editor, $config, $theme) { + $settings = array(); + + // Add path to button images, if available. + if (is_dir($editor['library path'] . '/btn')) { + $settings['buttonPath'] = base_path() . $editor['library path'] . '/btn/'; + } + if (file_exists($editor['library path'] . '/WhizzywigToolbar.png')) { + $settings['toolbarImagePath'] = base_path() . $editor['library path'] . '/WhizzywigToolbar.png'; + } + // Filename changed in version 60. + elseif (file_exists($editor['library path'] . '/icons.png')) { + $settings['toolbarImagePath'] = base_path() . $editor['library path'] . '/icons.png'; + } + + // Add configured buttons or all available. + $settings['buttons'] = array(); + if (!empty($config['buttons'])) { + $buttons = array(); + foreach ($config['buttons'] as $plugin) { + $buttons = array_merge($buttons, $plugin); + } + $settings['buttons'] = implode(' ', array_keys($buttons)); + } + + // Add editor content stylesheet. + if (isset($config['css_setting'])) { + if ($config['css_setting'] == 'theme') { + $css = path_to_theme() . '/style.css'; + if (file_exists($css)) { + $settings['externalCSS'] = base_path() . $css; + } + } + else if ($config['css_setting'] == 'self' && isset($config['css_path'])) { + $settings['externalCSS'] = strtr($config['css_path'], array('%b' => base_path(), '%t' => path_to_theme())); + } + } + + return $settings; +} + +/** + * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin(). + */ +function wysiwyg_whizzywig_plugins($editor) { + return array( + 'default' => array( + 'buttons' => array( + 'formatblock' => t('HTML block format'), 'fontname' => t('Font'), 'fontsize' => t('Font size'), + 'bold' => t('Bold'), 'italic' => t('Italic'), 'underline' => t('Underline'), + 'left' => t('Align left'), 'center' => t('Align center'), 'right' => t('Align right'), + 'bullet' => t('Bullet list'), 'number' => t('Numbered list'), + 'outdent' => t('Outdent'), 'indent' => t('Indent'), + 'undo' => t('Undo'), 'redo' => t('Redo'), + 'image' => t('Image'), + 'color' => t('Forecolor'), 'hilite' => t('Backcolor'), + 'rule' => t('Horizontal rule'), + 'link' => t('Link'), + 'image' => t('Image'), + 'table' => t('Table'), + 'clean' => t('Clean-up'), + 'html' => t('Source code'), + 'spellcheck' => t('Spell check'), + ), + 'internal' => TRUE, + ), + ); +} + diff --git a/editors/wymeditor.inc b/editors/wymeditor.inc new file mode 100644 index 00000000..3e8ffd24 --- /dev/null +++ b/editors/wymeditor.inc @@ -0,0 +1,233 @@ + 'WYMeditor', + 'vendor url' => 'http://www.wymeditor.org/', + 'download url' => 'http://www.wymeditor.org/download/', + 'library path' => wysiwyg_get_path('wymeditor') . '/wymeditor', + 'libraries' => array( + 'min' => array( + 'title' => 'Minified', + 'files' => array('jquery.wymeditor.min.js'), + ), + 'pack' => array( + 'title' => 'Packed', + 'files' => array('jquery.wymeditor.pack.js'), + ), + 'src' => array( + 'title' => 'Source', + 'files' => array('jquery.wymeditor.js'), + ), + ), + 'version callback' => 'wysiwyg_wymeditor_version', + 'themes callback' => 'wysiwyg_wymeditor_themes', + 'settings callback' => 'wysiwyg_wymeditor_settings', + 'plugin callback' => 'wysiwyg_wymeditor_plugins', + 'versions' => array( + '0.5-rc1' => array( + 'js files' => array('wymeditor.js'), + ), + ), + ); + return $editor; +} + +/** + * Detect editor version. + * + * @param $editor + * An array containing editor properties as returned from hook_editor(). + * + * @return + * The installed editor version. + */ +function wysiwyg_wymeditor_version($editor) { + $script = $editor['library path'] . '/jquery.wymeditor.js'; + if (!file_exists($script)) { + return; + } + $script = fopen($script, 'r'); + fgets($script); + $line = fgets($script); + if (preg_match('@version\s+([0-9a-z\.-]+)@', $line, $version)) { + fclose($script); + return $version[1]; + } + fclose($script); +} + +/** + * Determine available editor themes or check/reset a given one. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $profile + * A wysiwyg editor profile. + * + * @return + * An array of theme names. The first returned name should be the default + * theme name. + */ +function wysiwyg_wymeditor_themes($editor, $profile) { + return array('compact', 'default', 'minimal', 'silver', 'twopanels'); +} + +/** + * Return runtime editor settings for a given wysiwyg profile. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $config + * An array containing wysiwyg editor profile settings. + * @param $theme + * The name of a theme/GUI/skin to use. + * + * @return + * A settings array to be populated in + * Drupal.settings.wysiwyg.configs.{editor} + */ +function wysiwyg_wymeditor_settings($editor, $config, $theme) { + // @todo Setup $library in wysiwyg_load_editor() already. + $library = (isset($editor['library']) ? $editor['library'] : key($editor['libraries'])); + $settings = array( + 'basePath' => base_path() . $editor['library path'] . '/', + 'wymPath' => $editor['libraries'][$library]['files'][0], + // @todo Does not work in Drupal; jQuery can live anywhere. + 'jQueryPath' => base_path() . 'misc/jquery.js', + 'updateSelector' => '.form-submit', + 'skin' => $theme, + ); + + if (isset($config['language'])) { + $settings['lang'] = $config['language']; + } + + // Add configured buttons. + $settings['toolsItems'] = array(); + if (!empty($config['buttons'])) { + $buttoninfo = _wysiwyg_wymeditor_button_info(); + $plugins = wysiwyg_get_plugins($editor['name']); + foreach ($config['buttons'] as $plugin => $buttons) { + foreach ($buttons as $button => $enabled) { + // Iterate separately over buttons and extensions properties. + foreach (array('buttons', 'extensions') as $type) { + // Skip unavailable plugins. + if (!isset($plugins[$plugin][$type][$button])) { + continue; + } + // Add buttons. + if ($type == 'buttons') { + // Merge meta-data for internal default buttons. + if (isset($buttoninfo[$button])) { + $buttoninfo[$button] += array('name' => $button); + $settings['toolsItems'][] = $buttoninfo[$button]; + } + // For custom buttons, try to provide a valid button definition. + else { + $settings['toolsItems'][] = array( + 'name' => $button, + 'title' => $plugins[$plugin][$type][$button], + 'css' => 'wym_tools_' . $button, + ); + } + } + } + } + } + } + + if (!empty($config['block_formats'])) { + $containers = array( + 'p' => 'Paragraph', + 'h1' => 'Heading_1', + 'h2' => 'Heading_2', + 'h3' => 'Heading_3', + 'h4' => 'Heading_4', + 'h5' => 'Heading_5', + 'h6' => 'Heading_6', + 'pre' => 'Preformatted', + 'blockquote' => 'Blockquote', + 'th' => 'Table_Header', + ); + foreach (explode(',', $config['block_formats']) as $tag) { + if (isset($containers[$tag])) { + $settings['containersItems'][] = array( + 'name' => strtoupper($tag), + 'title' => $containers[$tag], + 'css' => 'wym_containers_' . $tag, + ); + } + } + } + + if (isset($config['css_setting'])) { + if ($config['css_setting'] == 'theme') { + // WYMeditor only supports one CSS file currently. + $settings['stylesheet'] = reset(wysiwyg_get_css()); + } + else if ($config['css_setting'] == 'self' && isset($config['css_path'])) { + $settings['stylesheet'] = strtr($config['css_path'], array('%b' => base_path(), '%t' => path_to_theme())); + } + } + + return $settings; +} + +/** + * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin(). + */ +function wysiwyg_wymeditor_plugins($editor) { + $plugins = array( + 'default' => array( + 'buttons' => array( + 'Bold' => t('Bold'), 'Italic' => t('Italic'), + 'InsertOrderedList' => t('Bullet list'), 'InsertUnorderedList' => t('Numbered list'), + 'Outdent' => t('Outdent'), 'Indent' => t('Indent'), + 'Undo' => t('Undo'), 'Redo' => t('Redo'), + 'CreateLink' => t('Link'), 'Unlink' => t('Unlink'), + 'InsertImage' => t('Image'), + 'Superscript' => t('Superscript'), 'Subscript' => t('Subscript'), + 'ToggleHtml' => t('Source code'), + 'Paste' => t('Paste'), + 'InsertTable' => t('Table'), + 'Preview' => t('Preview'), + ), + 'internal' => TRUE, + ), + ); + return $plugins; +} + +/** + * Helper function to provide additional meta-data for internal default buttons. + */ +function _wysiwyg_wymeditor_button_info() { + return array( + 'Bold' => array('title'=> 'Strong', 'css'=> 'wym_tools_strong'), + 'Italic' => array('title'=> 'Emphasis', 'css'=> 'wym_tools_emphasis'), + 'Superscript' => array('title'=> 'Superscript', 'css'=> 'wym_tools_superscript'), + 'Subscript' => array('title'=> 'Subscript', 'css'=> 'wym_tools_subscript'), + 'InsertOrderedList' => array('title'=> 'Ordered_List', 'css'=> 'wym_tools_ordered_list'), + 'InsertUnorderedList' => array('title'=> 'Unordered_List', 'css'=> 'wym_tools_unordered_list'), + 'Indent' => array('title'=> 'Indent', 'css'=> 'wym_tools_indent'), + 'Outdent' => array('title'=> 'Outdent', 'css'=> 'wym_tools_outdent'), + 'Undo' => array('title'=> 'Undo', 'css'=> 'wym_tools_undo'), + 'Redo' => array('title'=> 'Redo', 'css'=> 'wym_tools_redo'), + 'CreateLink' => array('title'=> 'Link', 'css'=> 'wym_tools_link'), + 'Unlink' => array('title'=> 'Unlink', 'css'=> 'wym_tools_unlink'), + 'InsertImage' => array('title'=> 'Image', 'css'=> 'wym_tools_image'), + 'InsertTable' => array('title'=> 'Table', 'css'=> 'wym_tools_table'), + 'Paste' => array('title'=> 'Paste_From_Word', 'css'=> 'wym_tools_paste'), + 'ToggleHtml' => array('title'=> 'HTML', 'css'=> 'wym_tools_html'), + 'Preview' => array('title'=> 'Preview', 'css'=> 'wym_tools_preview'), + ); +} diff --git a/editors/yui.inc b/editors/yui.inc new file mode 100644 index 00000000..7e3c697c --- /dev/null +++ b/editors/yui.inc @@ -0,0 +1,297 @@ + 'YUI editor', + 'vendor url' => 'http://developer.yahoo.com/yui/editor/', + 'download url' => 'http://developer.yahoo.com/yui/download/', + 'library path' => wysiwyg_get_path('yui') . '/build', + 'libraries' => array( + 'min' => array( + 'title' => 'Minified', + 'files' => array( + 'yahoo-dom-event/yahoo-dom-event.js', + 'animation/animation-min.js', + 'element/element-min.js', + 'container/container-min.js', + 'menu/menu-min.js', + 'button/button-min.js', + 'editor/editor-min.js', + ), + ), + 'src' => array( + 'title' => 'Source', + 'files' => array( + 'yahoo-dom-event/yahoo-dom-event.js', + 'animation/animation.js', + 'element/element.js', + 'container/container.js', + 'menu/menu.js', + 'button/button.js', + 'editor/editor.js', + ), + ), + ), + 'version callback' => 'wysiwyg_yui_version', + 'themes callback' => 'wysiwyg_yui_themes', + 'load callback' => 'wysiwyg_yui_load', + 'settings callback' => 'wysiwyg_yui_settings', + 'plugin callback' => 'wysiwyg_yui_plugins', + 'versions' => array( + '2.7.0' => array( + 'js files' => array('yui.js'), + ), + ), + ); + return $editor; +} + +/** + * Detect editor version. + * + * @param $editor + * An array containing editor properties as returned from hook_editor(). + * + * @return + * The installed editor version. + */ +function wysiwyg_yui_version($editor) { + $library = $editor['library path'] . '/editor/editor.js'; + if (!file_exists($library)) { + return; + } + $library = fopen($library, 'r'); + $max_lines = 10; + while ($max_lines && $line = fgets($library, 60)) { + if (preg_match('@version:\s([0-9\.]+)@', $line, $version)) { + fclose($library); + return $version[1]; + } + $max_lines--; + } + fclose($library); +} + +/** + * Determine available editor themes or check/reset a given one. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $profile + * A wysiwyg editor profile. + * + * @return + * An array of theme names. The first returned name should be the default + * theme name. + */ +function wysiwyg_yui_themes($editor, $profile) { + return array('sam'); +} + +/** + * Perform additional actions upon loading this editor. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $library + * The internal library name (array key) to use. + */ +function wysiwyg_yui_load($editor, $library) { + drupal_add_css($editor['library path'] . '/menu/assets/skins/sam/menu.css'); + drupal_add_css($editor['library path'] . '/button/assets/skins/sam/button.css'); + drupal_add_css($editor['library path'] . '/fonts/fonts-min.css'); + drupal_add_css($editor['library path'] . '/container/assets/skins/sam/container.css'); + drupal_add_css($editor['library path'] . '/editor/assets/skins/sam/editor.css'); +} + +/** + * Return runtime editor settings for a given wysiwyg profile. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $config + * An array containing wysiwyg editor profile settings. + * @param $theme + * The name of a theme/GUI/skin to use. + * + * @return + * A settings array to be populated in + * Drupal.settings.wysiwyg.configs.{editor} + */ +function wysiwyg_yui_settings($editor, $config, $theme) { + $settings = array( + 'theme' => $theme, + 'animate' => TRUE, + 'handleSubmit' => TRUE, + 'markup' => 'xhtml', + 'ptags' => TRUE, + ); + + if (isset($config['path_loc']) && $config['path_loc'] != 'none') { + $settings['dompath'] = $config['path_loc']; + } + // Enable auto-height feature when editor should be resizable. + if (!empty($config['resizing'])) { + $settings['autoHeight'] = TRUE; + } + + $settings += array( + 'toolbar' => array( + 'collapse' => FALSE, + 'draggable' => TRUE, + 'buttonType' => 'advanced', + 'buttons' => array(), + ), + ); + if (!empty($config['buttons'])) { + $buttons = array(); + foreach ($config['buttons'] as $plugin => $enabled_buttons) { + foreach ($enabled_buttons as $button => $enabled) { + $extra = array(); + if ($button == 'heading') { + $extra = array('menu' => array( + array('text' => 'Normal', 'value' => 'none', 'checked' => TRUE), + )); + if (!empty($config['block_formats'])) { + $headings = array( + 'p' => array('text' => 'Paragraph', 'value' => 'p'), + 'h1' => array('text' => 'Heading 1', 'value' => 'h1'), + 'h2' => array('text' => 'Heading 2', 'value' => 'h2'), + 'h3' => array('text' => 'Heading 3', 'value' => 'h3'), + 'h4' => array('text' => 'Heading 4', 'value' => 'h4'), + 'h5' => array('text' => 'Heading 5', 'value' => 'h5'), + 'h6' => array('text' => 'Heading 6', 'value' => 'h6'), + ); + foreach (explode(',', $config['block_formats']) as $tag) { + if (isset($headings[$tag])) { + $extra['menu'][] = $headings[$tag]; + } + } + } + } + else if ($button == 'fontname') { + $extra = array('menu' => array( + array('text' => 'Arial', 'checked' => TRUE), + array('text' => 'Arial Black'), + array('text' => 'Comic Sans MS'), + array('text' => 'Courier New'), + array('text' => 'Lucida Console'), + array('text' => 'Tahoma'), + array('text' => 'Times New Roman'), + array('text' => 'Trebuchet MS'), + array('text' => 'Verdana'), + )); + } + $buttons[] = wysiwyg_yui_button_setting($editor, $plugin, $button, $extra); + } + } + // Group buttons in a dummy group. + $buttons = array('group' => 'default', 'label' => '', 'buttons' => $buttons); + $settings['toolbar']['buttons'] = array($buttons); + } + + if (isset($config['css_setting'])) { + if ($config['css_setting'] == 'theme') { + $settings['extracss'] = wysiwyg_get_css(); + } + else if ($config['css_setting'] == 'self' && isset($config['css_path'])) { + $settings['extracss'] = strtr($config['css_path'], array('%b' => base_path(), '%t' => path_to_theme())); + $settings['extracss'] = explode(',', $settings['extracss']); + } + // YUI only supports inline CSS, so we need to use @import directives. + // Syntax: '@import "/base/path/to/theme/style.css"; ' + if (!empty($settings['extracss'])) { + $settings['extracss'] = '@import "' . implode('"; @import "', $settings['extracss']) . '";'; + } + } + + return $settings; +} + +/** + * Create the JavaScript structure for a YUI button. + * + * @param $editor + * A processed hook_editor() array of editor properties. + * @param $plugin + * The internal name of a plugin. + * @param $button + * The internal name of a button, defined by $plugin. + * @param $extra + * (optional) An array containing arbitrary other elements to add to the + * resulting button. + */ +function wysiwyg_yui_button_setting($editor, $plugin, $button, $extra = array()) { + static $plugins; + + if (!isset($plugins)) { + // @todo Invoke all enabled plugins, not just internals. + $plugins = wysiwyg_yui_plugins($editor); + } + + // Return a simple separator. + if ($button === 'separator') { + return array('type' => 'separator'); + } + // Setup defaults. + $type = 'push'; + $label = $plugins[$plugin]['buttons'][$button]; + + // Special handling for certain buttons. + if (in_array($button, array('heading', 'fontname'))) { + $type = 'select'; + $label = $extra['menu'][0]['text']; + } + elseif (in_array($button, array('fontsize'))) { + $type = 'spin'; + } + elseif (in_array($button, array('forecolor', 'backcolor'))) { + $type = 'color'; + } + + $button = array( + 'type' => $type, + 'label' => $label, + 'value' => $button, + ); + // Add arbitrary other elements, if defined. + if (!empty($extra)) { + $button = array_merge($button, $extra); + } + return $button; +} + +/** + * Return internal plugins for this editor; semi-implementation of hook_wysiwyg_plugin(). + */ +function wysiwyg_yui_plugins($editor) { + return array( + 'default' => array( + 'buttons' => array( + 'bold' => t('Bold'), 'italic' => t('Italic'), 'underline' => t('Underline'), + 'strikethrough' => t('Strike-through'), + 'justifyleft' => t('Align left'), 'justifycenter' => t('Align center'), 'justifyright' => t('Align right'), 'justifyfull' => t('Justify'), + 'insertunorderedlist' => t('Bullet list'), 'insertorderedlist' => t('Numbered list'), + 'outdent' => t('Outdent'), 'indent' => t('Indent'), + 'undo' => t('Undo'), 'redo' => t('Redo'), + 'createlink' => t('Link'), + 'insertimage' => t('Image'), + 'forecolor' => t('Font Color'), 'backcolor' => t('Background Color'), + 'superscript' => t('Sup'), 'subscript' => t('Sub'), + 'hiddenelements' => t('Show/hide hidden elements'), + 'removeformat' => t('Remove format'), + 'heading' => t('HTML block format'), 'fontname' => t('Font'), 'fontsize' => t('Font size'), + ), + 'internal' => TRUE, + ), + ); +} + diff --git a/plugins/break.inc b/plugins/break.inc new file mode 100644 index 00000000..887d5905 --- /dev/null +++ b/plugins/break.inc @@ -0,0 +1,21 @@ + t('Teaser break'), + 'vendor url' => 'http://drupal.org/project/wysiwyg', + 'icon file' => 'break.gif', + 'icon title' => t('Separate the teaser and body of this content'), + 'settings' => array(), + ); + return $plugins; +} + diff --git a/plugins/break/break.css b/plugins/break/break.css new file mode 100644 index 00000000..4aaab761 --- /dev/null +++ b/plugins/break/break.css @@ -0,0 +1,10 @@ + +.wysiwyg-break { + display: block; + border: 0; + border-top: 1px dotted #ccc; + margin-top: 1em; + width: 100%; + height: 12px; + background: transparent url(images/breaktext.gif) no-repeat center top; +} diff --git a/plugins/break/break.js b/plugins/break/break.js new file mode 100644 index 00000000..54aac4cd --- /dev/null +++ b/plugins/break/break.js @@ -0,0 +1,68 @@ +(function ($) { + +// @todo Array syntax required; 'break' is a predefined token in JavaScript. +Drupal.wysiwyg.plugins['break'] = { + + /** + * Return whether the passed node belongs to this plugin. + */ + isNode: function(node) { + return ($(node).is('img.wysiwyg-break')); + }, + + /** + * Execute the button. + */ + invoke: function(data, settings, instanceId) { + if (data.format == 'html') { + // Prevent duplicating a teaser break. + if ($(data.node).is('img.wysiwyg-break')) { + return; + } + var content = this._getPlaceholder(settings); + } + else { + // Prevent duplicating a teaser break. + // @todo data.content is the selection only; needs access to complete content. + if (data.content.match(//)) { + return; + } + var content = ''; + } + if (typeof content != 'undefined') { + Drupal.wysiwyg.instances[instanceId].insert(content); + } + }, + + /** + * Replace all tags with images. + */ + attach: function(content, settings, instanceId) { + content = content.replace(//g, this._getPlaceholder(settings)); + return content; + }, + + /** + * Replace images with tags in content upon detaching editor. + */ + detach: function(content, settings, instanceId) { + var $content = $('
' + content + '
'); // No .outerHTML() in jQuery :( + // #404532: document.createComment() required or IE will strip the comment. + // #474908: IE 8 breaks when using jQuery methods to replace the elements. + // @todo Add a generic implementation for all Drupal plugins for this. + $.each($('img.wysiwyg-break', $content), function (i, elem) { + elem.parentNode.insertBefore(document.createComment('break'), elem); + elem.parentNode.removeChild(elem); + }); + return $content.html(); + }, + + /** + * Helper function to return a HTML placeholder. + */ + _getPlaceholder: function (settings) { + return '<--break->'; + } +}; + +})(jQuery); diff --git a/plugins/break/images/break.gif b/plugins/break/images/break.gif new file mode 100644 index 00000000..4ff564d5 Binary files /dev/null and b/plugins/break/images/break.gif differ diff --git a/plugins/break/images/breaktext.gif b/plugins/break/images/breaktext.gif new file mode 100644 index 00000000..61978735 Binary files /dev/null and b/plugins/break/images/breaktext.gif differ diff --git a/plugins/break/images/spacer.gif b/plugins/break/images/spacer.gif new file mode 100644 index 00000000..38848651 Binary files /dev/null and b/plugins/break/images/spacer.gif differ diff --git a/plugins/break/langs/ca.js b/plugins/break/langs/ca.js new file mode 100644 index 00000000..5ead9380 --- /dev/null +++ b/plugins/break/langs/ca.js @@ -0,0 +1,6 @@ + +tinyMCE.addToLang('break', { + title: 'Inserir marcador de document retallat', + desc: 'Generar el punt de separació entre la versió retallada del document i la resta del contingut' +}); + diff --git a/plugins/break/langs/de.js b/plugins/break/langs/de.js new file mode 100644 index 00000000..d869a697 --- /dev/null +++ b/plugins/break/langs/de.js @@ -0,0 +1,6 @@ + +tinyMCE.addToLang('break', { + title: 'Anrisstext trennen', + desc: 'Separiert den Anrisstext und Textkörper des Inhalts an dieser Stelle' +}); + diff --git a/plugins/break/langs/en.js b/plugins/break/langs/en.js new file mode 100644 index 00000000..6d75ef73 --- /dev/null +++ b/plugins/break/langs/en.js @@ -0,0 +1,6 @@ + +tinyMCE.addToLang('break', { + title: 'Insert teaser break', + desc: 'Separate teaser and body of this content' +}); + diff --git a/plugins/break/langs/es.js b/plugins/break/langs/es.js new file mode 100644 index 00000000..206023da --- /dev/null +++ b/plugins/break/langs/es.js @@ -0,0 +1,6 @@ + +tinyMCE.addToLang('break', { + title: 'Insertar marcador de documento recortado', + desc: 'Generar el punto de separación entre la versión recortada del documento y el resto del contenido' +}); + diff --git a/tests/wysiwyg.test b/tests/wysiwyg.test new file mode 100644 index 00000000..263563c2 --- /dev/null +++ b/tests/wysiwyg.test @@ -0,0 +1,7 @@ +language contains its textual representation. + * $language->dir contains the language direction. It will either be 'ltr' or 'rtl'. + * - $head_title: A modified version of the page title, for use in the TITLE tag. + * - $head: Markup for the HEAD section (including meta tags, keyword tags, and + * so on). + * - $styles: Style tags necessary to import all CSS files for the page. + * - $scripts: Script tags necessary to load the JavaScript files and settings + * for the page. + * + * Site identity: + * - $site_name: The name of the site, empty when display has been disabled + * in theme settings. + * + * Page content (in order of occurrance in the default page.tpl.php): + * - $breadcrumb: The breadcrumb trail for the current page. + * - $title: The page title, for use in the actual HTML content. + * - $help: Dynamic help text, mostly for admin pages. + * - $messages: HTML for status and error messages. Should be displayed prominently. + * - $tabs: Tabs linking to any sub-pages beneath the current page (e.g., the view + * and edit tabs when displaying a node). + * + * - $content: The main content of the current Drupal page. + * + * Footer/closing data: + * - $footer : The footer region. + * - $closure: Final closing markup from any modules that have altered the page. + * This variable should always be output last, after all other dynamic content. + * + * @see template_preprocess() + * @see template_preprocess_wysiwyg_dialog_page() + */ +?> + + + + + <?php print $head_title; ?> + + + + + + +
+
+
+ + +
+

+
+ + +
+ +
+
+ +
+
+
+ + + diff --git a/wysiwyg.admin.inc b/wysiwyg.admin.inc new file mode 100644 index 00000000..510e1e31 --- /dev/null +++ b/wysiwyg.admin.inc @@ -0,0 +1,557 @@ + '', + 'editor' => '', + ); + if (empty($profile['settings'])) { + $profile['settings'] = array(); + } + $profile['settings'] += array( + 'default' => TRUE, + 'user_choose' => FALSE, + 'show_toggle' => TRUE, + 'theme' => 'advanced', + 'language' => 'en', + 'access' => 1, + 'access_pages' => "node/*\nuser/*\ncomment/*", + 'buttons' => array(), + 'toolbar_loc' => 'top', + 'toolbar_align' => 'left', + 'path_loc' => 'bottom', + 'resizing' => TRUE, + // Also available, but buggy in TinyMCE 2.x: blockquote,code,dt,dd,samp. + 'block_formats' => 'p,address,pre,h2,h3,h4,h5,h6,div', + 'verify_html' => TRUE, + 'preformatted' => FALSE, + 'convert_fonts_to_spans' => TRUE, + 'remove_linebreaks' => TRUE, + 'apply_source_formatting' => FALSE, + 'paste_auto_cleanup_on_paste' => FALSE, + 'css_setting' => 'theme', + 'css_path' => NULL, + 'css_classes' => NULL, + ); + $profile = (object) $profile; + + $formats = filter_formats(); + $editor = wysiwyg_get_editor($profile->editor); + drupal_set_title(t('%editor profile for %format', array('%editor' => $editor['title'], '%format' => $formats[$profile->format]->name)), PASS_THROUGH); + + $form['format'] = array('#type' => 'value', '#value' => $profile->format); + $form['input_format'] = array('#type' => 'value', '#value' => $formats[$profile->format]->name); + $form['editor'] = array('#type' => 'value', '#value' => $profile->editor); + + $form['basic'] = array( + '#type' => 'fieldset', + '#title' => t('Basic setup'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + + $form['basic']['default'] = array( + '#type' => 'checkbox', + '#title' => t('Enabled by default'), + '#default_value' => $profile->settings['default'], + '#return_value' => 1, + '#description' => t('The default editor state for users having access to this profile. Users are able to override this state if the next option is enabled.'), + ); + + $form['basic']['user_choose'] = array( + '#type' => 'checkbox', + '#title' => t('Allow users to choose default'), + '#default_value' => $profile->settings['user_choose'], + '#return_value' => 1, + '#description' => t('If allowed, users will be able to choose their own editor default state in their user account settings.'), + ); + + $form['basic']['show_toggle'] = array( + '#type' => 'checkbox', + '#title' => t('Show enable/disable rich text toggle link'), + '#default_value' => $profile->settings['show_toggle'], + '#return_value' => 1, + '#description' => t('Whether or not to show the enable/disable rich text toggle link below a textarea. If disabled, the user setting or global default is used (see above).'), + ); + + $form['basic']['theme'] = array( + '#type' => 'hidden', + '#value' => $profile->settings['theme'], + ); + + $form['basic']['language'] = array( + '#type' => 'select', + '#title' => t('Interface language'), + '#default_value' => $profile->settings['language'], + ); + // @see _locale_prepare_predefined_list() + require_once DRUPAL_ROOT . '/includes/iso.inc'; + $predefined = _locale_get_predefined_list(); + foreach ($predefined as $key => $value) { + // Include native name in output, if possible + if (count($value) > 1) { + $tname = t($value[0]); + $predefined[$key] = ($tname == $value[1]) ? $tname : "$tname ($value[1])"; + } + else { + $predefined[$key] = t($value[0]); + } + } + asort($predefined); + $form['basic']['language']['#options'] = $predefined; + + $form['buttons'] = array( + '#type' => 'fieldset', + '#title' => t('Buttons and plugins'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#tree' => TRUE, + '#theme' => 'wysiwyg_admin_button_table', + ); + + $plugins = wysiwyg_get_plugins($profile->editor); + // Generate the button list. + foreach ($plugins as $name => $meta) { + if (isset($meta['buttons']) && is_array($meta['buttons'])) { + foreach ($meta['buttons'] as $button => $title) { + $icon = ''; + if (!empty($meta['path'])) { + // @todo Button icon locations are different in editors, editor versions, + // and contrib/custom plugins (like Image Assist, f.e.). + $img_src = $meta['path'] . "/images/$name.gif"; + // Handle plugins that have more than one button. + if (!file_exists($img_src)) { + $img_src = $meta['path'] . "/images/$button.gif"; + } + $icon = file_exists($img_src) ? '' : ''; + } + $title = (isset($meta['url']) ? l($title, $meta['url'], array('target' => '_blank')) : $title); + $title = (!empty($icon) ? $icon . ' ' . $title : $title); + $form['buttons'][$name][$button] = array( + '#type' => 'checkbox', + '#title' => $title, + '#default_value' => !empty($profile->settings['buttons'][$name][$button]) ? $profile->settings['buttons'][$name][$button] : FALSE, + ); + } + } + else if (isset($meta['extensions']) && is_array($meta['extensions'])) { + foreach ($meta['extensions'] as $extension => $title) { + $form['buttons'][$name][$extension] = array( + '#type' => 'checkbox', + '#title' => isset($meta['url']) ? l($title, $meta['url'], array('target' => '_blank')) : $title, + '#default_value' => !empty($profile->settings['buttons'][$name][$extension]) ? $profile->settings['buttons'][$name][$extension] : FALSE, + ); + } + } + } + + $form['appearance'] = array( + '#type' => 'fieldset', + '#title' => t('Editor appearance'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + + $form['appearance']['toolbar_loc'] = array( + '#type' => 'select', + '#title' => t('Toolbar location'), + '#default_value' => $profile->settings['toolbar_loc'], + '#options' => array('bottom' => t('Bottom'), 'top' => t('Top')), + '#description' => t('This option controls whether the editor toolbar is displayed above or below the editing area.'), + ); + + $form['appearance']['toolbar_align'] = array( + '#type' => 'select', + '#title' => t('Button alignment'), + '#default_value' => $profile->settings['toolbar_align'], + '#options' => array('center' => t('Center'), 'left' => t('Left'), 'right' => t('Right')), + '#description' => t('This option controls the alignment of icons in the editor toolbar.'), + ); + + $form['appearance']['path_loc'] = array( + '#type' => 'select', + '#title' => t('Path location'), + '#default_value' => $profile->settings['path_loc'], + '#options' => array('none' => t('Hide'), 'top' => t('Top'), 'bottom' => t('Bottom')), + '#description' => t('Where to display the path to HTML elements (i.e. body > table > tr > td).'), + ); + + $form['appearance']['resizing'] = array( + '#type' => 'checkbox', + '#title' => t('Enable resizing button'), + '#default_value' => $profile->settings['resizing'], + '#return_value' => 1, + '#description' => t('This option gives you the ability to enable/disable the resizing button. If enabled, the Path location toolbar must be set to "Top" or "Bottom" in order to display the resize icon.'), + ); + + $form['output'] = array( + '#type' => 'fieldset', + '#title' => t('Cleanup and output'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + + $form['output']['verify_html'] = array( + '#type' => 'checkbox', + '#title' => t('Verify HTML'), + '#default_value' => $profile->settings['verify_html'], + '#return_value' => 1, + '#description' => t('If enabled, potentially malicious code like <HEAD> tags will be removed from HTML contents.'), + ); + + $form['output']['preformatted'] = array( + '#type' => 'checkbox', + '#title' => t('Preformatted'), + '#default_value' => $profile->settings['preformatted'], + '#return_value' => 1, + '#description' => t('If enabled, the editor will insert TAB characters on tab and preserve other whitespace characters just like a PRE element in HTML does.'), + ); + + $form['output']['convert_fonts_to_spans'] = array( + '#type' => 'checkbox', + '#title' => t('Convert <font> tags to styles'), + '#default_value' => $profile->settings['convert_fonts_to_spans'], + '#return_value' => 1, + '#description' => t('If enabled, HTML tags declaring the font size, font family, font color and font background color will be replaced by inline CSS styles.'), + ); + + $form['output']['remove_linebreaks'] = array( + '#type' => 'checkbox', + '#title' => t('Remove linebreaks'), + '#default_value' => $profile->settings['remove_linebreaks'], + '#return_value' => 1, + '#description' => t('If enabled, the editor will remove most linebreaks from contents. Disabling this option could avoid conflicts with other input filters.'), + ); + + $form['output']['apply_source_formatting'] = array( + '#type' => 'checkbox', + '#title' => t('Apply source formatting'), + '#default_value' => $profile->settings['apply_source_formatting'], + '#return_value' => 1, + '#description' => t('If enabled, the editor will re-format the HTML source code. Disabling this option could avoid conflicts with other input filters.'), + ); + + $form['output']['paste_auto_cleanup_on_paste'] = array( + '#type' => 'checkbox', + '#title' => t('Force cleanup on standard paste'), + '#default_value' => $profile->settings['paste_auto_cleanup_on_paste'], + '#return_value' => 1, + '#description' => t('If enabled, the default paste function (CTRL-V or SHIFT-INS) behaves like the "paste from word" plugin function.'), + ); + + $form['css'] = array( + '#type' => 'fieldset', + '#title' => t('CSS'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + ); + + $form['css']['block_formats'] = array( + '#type' => 'textfield', + '#title' => t('Block formats'), + '#default_value' => $profile->settings['block_formats'], + '#size' => 40, + '#maxlength' => 250, + '#description' => t('Comma separated list of HTML block formats. Possible values: @format-list.', array('@format-list' => 'p,h1,h2,h3,h4,h5,h6,div,blockquote,address,pre,code,dt,dd')), + ); + + $form['css']['css_setting'] = array( + '#type' => 'select', + '#title' => t('Editor CSS'), + '#default_value' => $profile->settings['css_setting'], + '#options' => array('theme' => t('Use theme CSS'), 'self' => t('Define CSS'), 'none' => t('Editor default CSS')), + '#description' => t('Defines the CSS to be used in the editor area.
Use theme CSS - loads stylesheets from current site theme.
Define CSS - enter path for stylesheet files below.
Editor default CSS - uses default stylesheets from editor.'), + ); + + $form['css']['css_path'] = array( + '#type' => 'textfield', + '#title' => t('CSS path'), + '#default_value' => $profile->settings['css_path'], + '#size' => 40, + '#maxlength' => 255, + '#description' => t('If "Define CSS" was selected above, enter path to a CSS file or a list of CSS files separated by a comma.') . '
' . t('Available tokens: %b (base path, eg: /), %t (path to theme, eg: themes/garland)') . '
' . t('Example:') . ' css/editor.css,/themes/garland/style.css,%b%t/style.css,http://example.com/external.css', + ); + + $form['css']['css_classes'] = array( + '#type' => 'textarea', + '#title' => t('CSS classes'), + '#default_value' => $profile->settings['css_classes'], + '#description' => t('Optionally define CSS classes for the "Font style" dropdown list.
Enter one class on each line in the format: !format. Example: !example
If left blank, CSS classes are automatically imported from all loaded stylesheet(s).', array('!format' => '[title]=[class]', '!example' => 'My heading=header1')), + ); + + $form['submit'] = array( + '#type' => 'submit', + '#value' => t('Save'), + '#weight' => 100, + ); + $form['cancel'] = array( + '#value' => l(t('Cancel'), 'admin/config/content/wysiwyg'), + '#weight' => 110, + ); + + return $form; +} + +/** + * Submit callback for Wysiwyg profile form. + * + * @see wysiwyg_profile_form() + */ +function wysiwyg_profile_form_submit($form, &$form_state) { + $values = $form_state['values']; + if (isset($values['buttons'])) { + // Store only enabled buttons for each plugin. + foreach ($values['buttons'] as $plugin => $buttons) { + $values['buttons'][$plugin] = array_filter($values['buttons'][$plugin]); + } + // Store only enabled plugins. + $values['buttons'] = array_filter($values['buttons']); + } + // Remove any white-space from 'block_formats' setting, since editor + // implementations rely on a comma-separated list to explode(). + $values['block_formats'] = preg_replace('@\s+@', '', $values['block_formats']); + + // Remove input format name. + $format = $values['format']; + $input_format = $values['input_format']; + $editor = $values['editor']; + unset($values['format'], $values['input_format'], $values['editor']); + + // Remove FAPI values. + // @see system_settings_form_submit() + unset($values['submit'], $values['form_id'], $values['op'], $values['form_token'], $values['form_build_id']); + + // Insert new profile data. + db_merge('wysiwyg') + ->key(array('format' => $format)) + ->fields(array( + 'editor' => $editor, + 'settings' => serialize($values), + )) + ->execute(); + wysiwyg_profile_cache_clear(); + + drupal_set_message(t('Wysiwyg profile for %format has been saved.', array('%format' => $input_format))); + + $form_state['redirect'] = 'admin/config/content/wysiwyg'; +} + +/** + * Layout for the buttons in the Wysiwyg Editor profile form. + */ +function theme_wysiwyg_admin_button_table($variables) { + $form = $variables['form']; + $buttons = array(); + + // Flatten forms array. + foreach (element_children($form) as $name) { + foreach (element_children($form[$name]) as $button) { + $buttons[] = drupal_render($form[$name][$button]); + } + } + + // Split checkboxes into rows with 3 columns. + $total = count($buttons); + $rows = array(); + for ($i = 0; $i < $total; $i++) { + $row = array(); + $row[] = array('data' => $buttons[$i]); + if (isset($buttons[++$i])) { + $row[] = array('data' => $buttons[$i]); + } + if (isset($buttons[++$i])) { + $row[] = array('data' => $buttons[$i]); + } + $rows[] = $row; + } + + $output = theme('table', array('rows' => $rows, 'attributes' => array('width' => '100%'))); + + return $output; +} + +/** + * Display overview of setup Wysiwyg Editor profiles; menu callback. + */ +function wysiwyg_profile_overview($form, &$form_state) { + include_once './includes/install.inc'; + + // Check which wysiwyg editors are installed. + $editors = wysiwyg_get_all_editors(); + $count = count($editors); + $status = array(); + $options = array('' => t('No editor')); + + // D7's seven theme displays links in table headers as block elements. + drupal_add_css('table.system-status-report th a {display: inline;}', 'inline'); + + foreach ($editors as $name => $editor) { + $status[$name] = array( + 'severity' => (isset($editor['error']) ? REQUIREMENT_ERROR : ($editor['installed'] ? REQUIREMENT_OK : REQUIREMENT_INFO)), + 'title' => t('@editor (Download)', array('!vendor-url' => $editor['vendor url'], '@editor' => $editor['title'], '!download-url' => $editor['download url'])), + 'value' => (isset($editor['installed version']) ? $editor['installed version'] : t('Not installed.')), + 'description' => (isset($editor['error']) ? $editor['error'] : ''), + ); + if ($editor['installed']) { + $options[$name] = $editor['title'] . (isset($editor['installed version']) ? ' ' . $editor['installed version'] : ''); + } + else { + // Build on-site installation instructions. + // @todo Setup $library in wysiwyg_load_editor() already. + $library = (isset($editor['library']) ? $editor['library'] : key($editor['libraries'])); + $targs = array( + '@editor-path' => $editor['editor path'], + '@library-filepath' => $editor['library path'] . '/' . (isset($editor['libraries'][$library]['files'][0]) ? $editor['libraries'][$library]['files'][0] : key($editor['libraries'][$library]['files'])), + ); + $instructions = '

' . t('Extract the archive and copy its contents into a new folder in the following location:
@editor-path', $targs) . '

'; + $instructions .= '

' . t('So the actual library can be found at:
@library-filepath', $targs) . '

'; + + $status[$name]['description'] .= $instructions; + $count--; + } + // In case there is an error, always show installation instructions. + if (isset($editor['error'])) { + $show_instructions = TRUE; + } + } + if (!$count) { + $show_instructions = TRUE; + } + $form['status'] = array( + '#type' => 'fieldset', + '#title' => t('Installation instructions'), + '#collapsible' => TRUE, + '#collapsed' => !isset($show_instructions), + '#description' => (!$count ? t('There are no editor libraries installed currently. The following list contains a list of currently supported editors:') : ''), + '#weight' => 10, + ); + $form['status']['report'] = array('#markup' => theme('status_report', array('requirements' => $status))); + + if (!$count) { + return $form; + } + + $formats = filter_formats(); + $profiles = wysiwyg_profile_load_all(); + $form['formats'] = array( + '#type' => 'item', + '#description' => t('To assign a different editor to a text format, click "delete" to remove the existing first.'), + '#tree' => TRUE, + ); + + $enable_save = FALSE; + foreach ($formats as $id => $format) { + $form['formats'][$id]['name'] = array( + '#markup' => check_plain($format->name), + ); + // Only display editor selection for associated input formats to avoid + // confusion about disabled selection. + if (isset($profiles[$id]) && !empty($profiles[$id]->editor)) { + $form['formats'][$id]['editor'] = array( + '#markup' => $options[$profiles[$id]->editor], + ); + } + else { + $form['formats'][$id]['editor'] = array( + '#type' => 'select', + '#default_value' => '', + '#options' => $options, + ); + $enable_save = TRUE; + } + if (isset($profiles[$id]) && !empty($profiles[$id]->editor)) { + $form['formats'][$id]['edit'] = array( + '#markup' => l(t('Edit'), "admin/config/content/wysiwyg/profile/$id/edit"), + ); + $form['formats'][$id]['delete'] = array( + '#markup' => l(t('Delete'), "admin/config/content/wysiwyg/profile/$id/delete"), + ); + } + } + + // Submitting the form when no editors can be selected causes errors. + if ($enable_save) { + $form['submit'] = array('#type' => 'submit', '#value' => t('Save')); + } + return $form; +} + +/** + * Return HTML for the Wysiwyg profile overview form. + */ +function theme_wysiwyg_profile_overview($variables) { + $form = $variables['form']; + if (!isset($form['formats'])) { + return; + } + $output = ''; + $header = array(t('Input format'), t('Editor'), array('data' => t('Operations'), 'colspan' => 2)); + $rows = array(); + foreach (element_children($form['formats']) as $item) { + $format = &$form['formats'][$item]; + $rows[] = array( + drupal_render($format['name']), + drupal_render($format['editor']), + isset($format['edit']) ? drupal_render($format['edit']) : '', + isset($format['delete']) ? drupal_render($format['delete']) : '', + ); + } + $form['formats']['table']['#markup'] = theme('table', array('header' => $header, 'rows' => $rows)); + $output .= drupal_render_children($form); + return $output; +} + +/** + * Submit callback for Wysiwyg profile overview form. + */ +function wysiwyg_profile_overview_submit($form, &$form_state) { + foreach ($form_state['values']['formats'] as $format => $values) { + db_merge('wysiwyg') + ->key(array('format' => $format)) + ->fields(array( + 'editor' => $values['editor'], + )) + ->execute(); + } + wysiwyg_profile_cache_clear(); +} + +/** + * Delete editor profile confirmation form. + */ +function wysiwyg_profile_delete_confirm($form, &$form_state, $profile) { + $formats = filter_formats(); + $format = $formats[$profile->format]; + $form['format'] = array('#type' => 'value', '#value' => $format); + return confirm_form( + $form, + t('Are you sure you want to remove the profile for %name?', array('%name' => $format->name)), + 'admin/config/content/wysiwyg', + t('This action cannot be undone.'), t('Remove'), t('Cancel') + ); +} + +/** + * Submit callback for Wysiwyg profile delete form. + * + * @see wysiwyg_profile_delete_confirm() + */ +function wysiwyg_profile_delete_confirm_submit($form, &$form_state) { + $format = $form_state['values']['format']; + wysiwyg_profile_delete($format->format); + wysiwyg_profile_cache_clear(); + + drupal_set_message(t('Wysiwyg profile for %name has been deleted.', array('%name' => $format->name))); + $form_state['redirect'] = 'admin/config/content/wysiwyg'; +} + diff --git a/wysiwyg.api.js b/wysiwyg.api.js new file mode 100644 index 00000000..0318b0b4 --- /dev/null +++ b/wysiwyg.api.js @@ -0,0 +1,97 @@ + +/** + * Wysiwyg plugin button implementation for Awesome plugin. + */ +Drupal.wysiwyg.plugins.awesome = { + /** + * Return whether the passed node belongs to this plugin. + * + * @param node + * The currently focused DOM element in the editor content. + */ + isNode: function(node) { + return ($(node).is('img.mymodule-awesome')); + }, + + /** + * Execute the button. + * + * @param data + * An object containing data about the current selection: + * - format: 'html' when the passed data is HTML content, 'text' when the + * passed data is plain-text content. + * - node: When 'format' is 'html', the focused DOM element in the editor. + * - content: The textual representation of the focused/selected editor + * content. + * @param settings + * The plugin settings, as provided in the plugin's PHP include file. + * @param instanceId + * The ID of the current editor instance. + */ + invoke: function(data, settings, instanceId) { + // Generate HTML markup. + if (data.format == 'html') { + // Prevent duplicating a teaser break. + if ($(data.node).is('img.mymodule-awesome')) { + return; + } + var content = this._getPlaceholder(settings); + } + // Generate plain text. + else { + var content = ''; + } + // Insert new content into the editor. + if (typeof content != 'undefined') { + Drupal.wysiwyg.instances[instanceId].insert(content); + } + }, + + /** + * Prepare all plain-text contents of this plugin with HTML representations. + * + * Optional; only required for "inline macro tag-processing" plugins. + * + * @param content + * The plain-text contents of a textarea. + * @param settings + * The plugin settings, as provided in the plugin's PHP include file. + * @param instanceId + * The ID of the current editor instance. + */ + attach: function(content, settings, instanceId) { + content = content.replace(//g, this._getPlaceholder(settings)); + return content; + }, + + /** + * Process all HTML placeholders of this plugin with plain-text contents. + * + * Optional; only required for "inline macro tag-processing" plugins. + * + * @param content + * The HTML content string of the editor. + * @param settings + * The plugin settings, as provided in the plugin's PHP include file. + * @param instanceId + * The ID of the current editor instance. + */ + detach: function(content, settings, instanceId) { + var $content = $('
' + content + '
'); + $.each($('img.mymodule-awesome', $content), function (i, elem) { + //... + }); + return $content.html(); + }, + + /** + * Helper function to return a HTML placeholder. + * + * The 'drupal-content' CSS class is required for HTML elements in the editor + * content that shall not trigger any editor's native buttons (such as the + * image button for this example placeholder markup). + */ + _getPlaceholder: function (settings) { + return '<--break->'; + } +}; diff --git a/wysiwyg.api.php b/wysiwyg.api.php new file mode 100644 index 00000000..5f5b8390 --- /dev/null +++ b/wysiwyg.api.php @@ -0,0 +1,206 @@ + 3) { + return array( + 'myplugin' => array( + // A URL to the plugin's homepage. + 'url' => 'http://drupal.org/project/img_assist', + // The full path to the native editor plugin, no trailing slash. + // Ignored when 'internal' is set to TRUE below. + 'path' => drupal_get_path('module', 'img_assist') . '/drupalimage', + // The name of the plugin's main JavaScript file. + // Ignored when 'internal' is set to TRUE below. + // Default value depends on which editor the plugin is for. + 'filename' => 'editor_plugin.js', + // A list of buttons provided by this native plugin. The key has to + // match the corresponding JavaScript implementation. The value is + // is displayed on the editor configuration form only. + 'buttons' => array( + 'img_assist' => t('Image Assist'), + ), + // A list of editor extensions provided by this native plugin. + // Extensions are not displayed as buttons and touch the editor's + // internals, so you should know what you are doing. + 'extensions' => array( + 'imce' => t('IMCE'), + ), + // A list of global, native editor configuration settings to + // override. To be used rarely and only when required. + 'options' => array( + 'file_browser_callback' => 'imceImageBrowser', + 'inline_styles' => TRUE, + ), + // Boolean whether the editor needs to load this plugin. When TRUE, + // the editor will automatically load the plugin based on the 'path' + // variable provided. If FALSE, the plugin either does not need to + // be loaded or is already loaded by something else on the page. + // Most plugins should define TRUE here. + 'load' => TRUE, + // Boolean whether this plugin is a native plugin, i.e. shipped with + // the editor. Definition must be ommitted for plugins provided by + // other modules. TRUE means 'path' and 'filename' above are ignored + // and the plugin is instead loaded from the editor's plugin folder. + 'internal' => TRUE, + // TinyMCE-specific: Additional HTML elements to allow in the markup. + 'extended_valid_elements' => array( + 'img[class|src|border=0|alt|title|width|height|align|name|style]', + ), + ), + ); + } + break; + } +} + +/** + * Register a directory containing Wysiwyg plugins. + * + * @param $type + * The type of objects being collected: either 'plugins' or 'editors'. + * @return + * A sub-directory of the implementing module that contains the corresponding + * plugin files. This directory must only contain integration files for + * Wysiwyg module. + */ +function hook_wysiwyg_include_directory($type) { + switch ($type) { + case 'plugins': + // You can just return $type, if you place your Wysiwyg plugins into a + // sub-directory named 'plugins'. + return $type; + } +} + +/** + * Define a Wysiwyg plugin. + * + * Supposed to be used for "Drupal plugins" (cross-editor plugins) only. + * + * @see hook_wysiwyg_plugin() + * + * Each plugin file in the specified plugin directory of a module needs to + * define meta information about the particular plugin provided. + * The plugin's hook implementation function name is built out of the following: + * - 'hook': The name of the module providing the plugin. + * - 'INCLUDE': The basename of the file containing the plugin definition. + * - 'plugin': Static. + * + * For example, if your module's name is 'mymodule' and + * mymodule_wysiwyg_include_directory() returned 'plugins' as plugin directory, + * and this directory contains an "awesome" plugin file named 'awesome.inc', i.e. + * sites/all/modules/mymodule/plugins/awesome.inc + * then the corresponding plugin hook function name is: + * mymodule_awesome_plugin() + * + * @see hook_wysiwyg_include_directory() + * + * @return + * Meta information about the buttons provided by this plugin. + */ +function hook_INCLUDE_plugin() { + $plugins['awesome'] = array( + // The plugin's title; defaulting to its internal name ('awesome'). + 'title' => t('Awesome plugin'), + // The (vendor) homepage of this plugin; defaults to ''. + 'vendor url' => 'http://drupal.org/project/wysiwyg', + // The path to the button's icon; defaults to + // '/[path-to-module]/[plugins-directory]/[plugin-name]/images'. + 'icon path' => 'path to icon', + // The button image filename; defaults to '[plugin-name].png'. + 'icon file' => 'name of the icon file with extension', + // The button title to display on hover. + 'icon title' => t('Do something'), + // An alternative path to the integration JavaScript; defaults to + // '[path-to-module]/[plugins-directory]/[plugin-name]'. + 'js path' => drupal_get_path('module', 'mymodule') . '/awesomeness', + // An alternative filename of the integration JavaScript; defaults to + // '[plugin-name].js'. + 'js file' => 'awesome.js', + // An alternative path to the integration stylesheet; defaults to + // '[path-to-module]/[plugins-directory]/[plugin-name]'. + 'css path' => drupal_get_path('module', 'mymodule') . '/awesomeness', + // An alternative filename of the integration stylesheet; defaults to + // '[plugin-name].css'. + 'css file' => 'awesome.css', + // An array of settings for this button. Required, but API is still in flux. + 'settings' => array( + ), + // TinyMCE-specific: Additional HTML elements to allow in the markup. + 'extended_valid_elements' => array( + 'tag1[attribute1|attribute2]', + 'tag2[attribute3|attribute4]', + ), + ); + return $plugins; +} + +/** + * Act on editor profile settings. + * + * This hook is invoked from wysiwyg_get_editor_config() after the JavaScript + * settings have been generated for an editor profile and before the settings + * are added to the page. The settings may be customized or enhanced; typically + * with options that cannot be controlled through Wysiwyg module's + * administrative UI currently. + * + * Modules implementing this hook to enforce settings that can also be + * controlled through the UI should also implement + * hook_form_wysiwyg_profile_form_alter() to adjust or at least indicate on the + * editor profile configuration form that certain/affected settings cannot be + * changed. + * + * @param $settings + * An associative array of JavaScript settings to pass to the editor. + * @param $context + * An associative array containing additional context information: + * - editor: The plugin definition array of the editor. + * - profile: The editor profile object, as loaded from the database. + * - theme: The name of the editor theme/skin. + */ +function hook_wysiwyg_editor_settings_alter(&$settings, $context) { + // Each editor has its own collection of native settings that may be extended + // or overridden. Please consult the respective official vendor documentation + // for details. + if ($context['profile']->editor == 'tinymce') { + // Supported values to JSON data types. + $settings['cleanup_on_startup'] = TRUE; + } +} diff --git a/wysiwyg.dialog.inc b/wysiwyg.dialog.inc new file mode 100644 index 00000000..500f78d7 --- /dev/null +++ b/wysiwyg.dialog.inc @@ -0,0 +1,63 @@ + $plugin, + 'instance' => $instance, + ); + drupal_add_js(array('wysiwyg' => $settings), 'setting'); + + echo theme('wysiwyg_dialog_page', $callback($instance)); +} + +/** + * Template preprocess function for theme_wysiwyg_dialog_page(). + * + * @see wysiwyg_dialog() + * @see wysiwyg-dialog-page.tpl.php + * @see template_preprocess() + */ +function template_preprocess_wysiwyg_dialog_page(&$variables) { + // Construct page title + $head_title = array(strip_tags(drupal_get_title()), variable_get('site_name', 'Drupal')); + + $variables['head_title'] = implode(' | ', $head_title); + $variables['base_path'] = base_path(); + $variables['front_page'] = url(); + // @todo Would a breadcrumb make sense / possible at all? + // $variables['breadcrumb'] = theme('breadcrumb', drupal_get_breadcrumb()); + $variables['head'] = drupal_get_html_head(); + $variables['help'] = theme('help'); + $variables['language'] = $GLOBALS['language']; + $variables['language']->dir = $GLOBALS['language']->direction ? 'rtl' : 'ltr'; + $variables['messages'] = $variables['show_messages'] ? theme('status_messages') : ''; + $variables['site_name'] = (theme_get_setting('toggle_name') ? variable_get('site_name', 'Drupal') : ''); + $variables['css'] = drupal_add_css(); + $variables['styles'] = drupal_get_css(); + $variables['scripts'] = drupal_get_js(); + $variables['tabs'] = theme('menu_local_tasks'); + $variables['title'] = drupal_get_title(); + // Closure should be filled last. + $variables['closure'] = theme('closure'); +} + diff --git a/wysiwyg.info b/wysiwyg.info new file mode 100644 index 00000000..840817ae --- /dev/null +++ b/wysiwyg.info @@ -0,0 +1,17 @@ +name = Wysiwyg +description = Allows to edit content with client-side editors. +package = User interface +;dependencies[] = libraries +;dependencies[] = ctools +;dependencies[] = debug +core = 7.x +configure = admin/config/content/wysiwyg +files[] = wysiwyg.module +files[] = tests/wysiwyg.test + +; Information added by drupal.org packaging script on 2011-06-19 +version = "7.x-2.1" +core = "7.x" +project = "wysiwyg" +datestamp = "1308450722" + diff --git a/wysiwyg.init.js b/wysiwyg.init.js new file mode 100644 index 00000000..6ccdb314 --- /dev/null +++ b/wysiwyg.init.js @@ -0,0 +1,19 @@ + +Drupal.wysiwyg = Drupal.wysiwyg || { 'instances': {} }; + +Drupal.wysiwyg.editor = Drupal.wysiwyg.editor || { 'init': {}, 'attach': {}, 'detach': {}, 'instance': {} }; + +Drupal.wysiwyg.plugins = Drupal.wysiwyg.plugins || {}; + +(function ($) { + // Determine support for queryCommandEnabled(). + // An exception should be thrown for non-existing commands. + // Safari and Chrome (WebKit based) return -1 instead. + try { + document.queryCommandEnabled('__wysiwygTestCommand'); + $.support.queryCommandEnabled = false; + } + catch (error) { + $.support.queryCommandEnabled = true; + } +})(jQuery); diff --git a/wysiwyg.install b/wysiwyg.install new file mode 100644 index 00000000..038ba46b --- /dev/null +++ b/wysiwyg.install @@ -0,0 +1,312 @@ + 'Stores Wysiwyg profiles.', + 'fields' => array( + 'format' => array( + 'description' => 'The {filter_format}.format of the text format.', + 'type' => 'varchar', + 'length' => 255, + // Primary keys are implicitly not null. + 'not null' => TRUE, + ), + 'editor' => array( + 'description' => 'Internal name of the editor attached to the text format.', + 'type' => 'varchar', + 'length' => 128, + 'not null' => TRUE, + 'default' => '', + ), + 'settings' => array( + 'description' => 'Configuration settings for the editor.', + 'type' => 'text', + 'size' => 'normal', + ), + ), + 'primary key' => array('format'), + 'foreign keys' => array( + 'format' => array( + 'table' => 'filter_format', + 'columns' => array('format' => 'format'), + ), + ), + ); + $schema['wysiwyg_user'] = array( + 'description' => 'Stores user preferences for wysiwyg profiles.', + 'fields' => array( + 'uid' => array( + 'description' => 'The {users}.uid of the user.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'format' => array( + 'description' => 'The {filter_format}.format of the text format.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + 'status' => array( + 'description' => 'Boolean indicating whether the format is enabled by default.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ), + ), + 'indexes' => array( + 'uid' => array('uid'), + 'format' => array('format'), + ), + 'foreign keys' => array( + 'uid' => array( + 'table' => 'users', + 'columns' => array('uid' => 'uid'), + ), + 'format' => array( + 'table' => 'filter_format', + 'columns' => array('format' => 'format'), + ), + ), + ); + return $schema; +} + +/** + * Implementation of hook_enable(). + */ +function wysiwyg_enable() { + // Disable conflicting, obsolete editor integration modules whenever this + // module is enabled. This is crude, but the only way to ensure no conflicts. + module_disable(array( + 'ckeditor', + 'editarea', + 'editonpro', + 'editor', + 'fckeditor', + 'freerte', + 'htmlarea', + 'htmlbox', + 'jwysiwyg', + 'markitup', + 'nicedit', + 'openwysiwyg', + 'pegoeditor', + 'quicktext', + 'tinymce', + 'tinymce_autoconf', + 'tinytinymce', + 'whizzywig', + 'widgeditor', + 'wymeditor', + 'xstandard', + 'yui_editor', + )); +} + +/** + * Implements hook_update_dependencies(). + */ +function wysiwyg_update_dependencies() { + // Ensure that format columns are only changed after Filter module has changed + // the primary records. + $dependencies['wysiwyg'][7000] = array( + 'filter' => 7010, + ); + + return $dependencies; +} + +/** + * Retrieve a list of input formats to associate profiles to. + */ +function _wysiwyg_install_get_formats() { + $formats = array(); + $result = db_query("SELECT format, name FROM {filter_formats}"); + while ($format = db_fetch_object($result)) { + // Build a list of all formats. + $formats[$format->format] = $format->name; + // Fetch filters. + $result2 = db_query("SELECT module, delta FROM {filters} WHERE format = %d", $format->format); + while ($filter = db_fetch_object($result2)) { + // If PHP filter is enabled, remove this format. + if ($filter->module == 'php') { + unset($formats[$format->format]); + break; + } + } + } + return $formats; +} + +/** + * Associate Wysiwyg profiles with input formats. + * + * Since there was no association yet, we can only assume that there is one + * profile only, and that profile must be duplicated and assigned to all input + * formats (except PHP code format). Also, input formats already have + * titles/names, so Wysiwyg profiles do not need an own. + * + * Because input formats are already granted to certain user roles only, we can + * remove our custom Wysiwyg profile permissions. A 1:1 relationship between + * input formats and permissions makes plugin_count obsolete, too. + * + * Since the resulting table is completely different, a new schema is installed. + */ +function wysiwyg_update_6001() { + $ret = array(); + if (db_table_exists('wysiwyg')) { + return $ret; + } + // Install new schema. + db_create_table($ret, 'wysiwyg', array( + 'fields' => array( + 'format' => array('type' => 'int', 'not null' => TRUE, 'default' => 0), + 'editor' => array('type' => 'varchar', 'length' => 128, 'not null' => TRUE, 'default' => ''), + 'settings' => array('type' => 'text', 'size' => 'normal'), + ), + 'primary key' => array('format'), + )); + + // Fetch all input formats. + $formats = _wysiwyg_install_get_formats(); + + // Fetch all profiles. + $result = db_query("SELECT name, settings FROM {wysiwyg_profile}"); + while ($profile = db_fetch_object($result)) { + $profile->settings = unserialize($profile->settings); + // Extract editor name from profile settings. + $profile->editor = $profile->settings['editor']; + // Clean-up. + unset($profile->settings['editor']); + unset($profile->settings['old_name']); + unset($profile->settings['name']); + unset($profile->settings['rids']); + // Sorry. There Can Be Only One. ;) + break; + } + + if ($profile) { + // Rebuild profiles and associate with input formats. + foreach ($formats as $format => $name) { + // Insert profiles. + // We can't use update_sql() here because of curly braces in serialized + // array. + db_query("INSERT INTO {wysiwyg} (format, editor, settings) VALUES (%d, '%s', '%s')", $format, $profile->editor, serialize($profile->settings)); + $ret[] = array( + 'success' => TRUE, + 'query' => strtr('Wysiwyg profile %profile converted and associated with input format %format.', array('%profile' => check_plain($profile->name), '%format' => check_plain($name))), + ); + } + } + + // Drop obsolete tables {wysiwyg_profile} and {wysiwyg_role}. + db_drop_table($ret, 'wysiwyg_profile'); + db_drop_table($ret, 'wysiwyg_role'); + + return $ret; +} + +/** + * Clear JS/CSS caches to ensure that clients load fresh copies. + */ +function wysiwyg_update_6200() { + $ret = array(); + // Change query-strings on css/js files to enforce reload for all users. + _drupal_flush_css_js(); + + drupal_clear_css_cache(); + drupal_clear_js_cache(); + + // Rebuild the menu to remove old admin/settings/wysiwyg/profile item. + menu_rebuild(); + + // Flush content caches. + cache_clear_all(); + + $ret[] = array( + 'success' => TRUE, + 'query' => 'Caches have been flushed.', + ); + return $ret; +} + +/** + * Change {wysiwyg}.format into a string. + */ +function wysiwyg_update_7000() { + db_drop_primary_key('wysiwyg'); + db_change_field('wysiwyg', 'format', 'format', array( + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + )); + db_add_primary_key('wysiwyg', array('format')); +} + +/** + * Create the {wysiwyg_user} table. + */ +function wysiwyg_update_7200() { + if (!db_table_exists('wysiwyg_user')) { + db_create_table('wysiwyg_user', array( + 'description' => 'Stores user preferences for wysiwyg profiles.', + 'fields' => array( + 'uid' => array( + 'description' => 'The {users}.uid of the user.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'format' => array( + 'description' => 'The {filter_format}.format of the text format.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + ), + 'status' => array( + 'description' => 'Boolean indicating whether the format is enabled by default.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + 'size' => 'tiny', + ), + ), + 'indexes' => array( + 'uid' => array('uid'), + 'format' => array('format'), + ), + 'foreign keys' => array( + 'uid' => array( + 'table' => 'users', + 'columns' => array('uid' => 'uid'), + ), + 'format' => array( + 'table' => 'filter_format', + 'columns' => array('format' => 'format'), + ), + ), + )); + } + else { + db_change_field('wysiwyg_user', 'format', 'format', array( + 'description' => 'The {filter_format}.format of the text format.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => FALSE, + )); + } +} diff --git a/wysiwyg.js b/wysiwyg.js new file mode 100644 index 00000000..72ca1569 --- /dev/null +++ b/wysiwyg.js @@ -0,0 +1,237 @@ +(function($) { + +/** + * Initialize editor libraries. + * + * Some editors need to be initialized before the DOM is fully loaded. The + * init hook gives them a chance to do so. + */ +Drupal.wysiwygInit = function() { + // This breaks in Konqueror. Prevent it from running. + if (/KDE/.test(navigator.vendor)) { + return; + } + + jQuery.each(Drupal.wysiwyg.editor.init, function(editor) { + // Clone, so original settings are not overwritten. + this(jQuery.extend(true, {}, Drupal.settings.wysiwyg.configs[editor])); + }); +}; + +/** + * Attach editors to input formats and target elements (f.e. textareas). + * + * This behavior searches for input format selectors and formatting guidelines + * that have been preprocessed by Wysiwyg API. All CSS classes of those elements + * with the prefix 'wysiwyg-' are parsed into input format parameters, defining + * the input format, configured editor, target element id, and variable other + * properties, which are passed to the attach/detach hooks of the corresponding + * editor. + * + * Furthermore, an "enable/disable rich-text" toggle link is added after the + * target element to allow users to alter its contents in plain text. + * + * This is executed once, while editor attach/detach hooks can be invoked + * multiple times. + * + * @param context + * A DOM element, supplied by Drupal.attachBehaviors(). + */ +Drupal.behaviors.attachWysiwyg = { + attach: function(context, settings) { + // This breaks in Konqueror. Prevent it from running. + if (/KDE/.test(navigator.vendor)) { + return; + } + + $('.wysiwyg', context).once('wysiwyg', function() { + if (!this.id || typeof Drupal.settings.wysiwyg.triggers[this.id] === 'undefined') { + return; + } + var $this = $(this); + var params = Drupal.settings.wysiwyg.triggers[this.id]; + for (var format in params) { + params[format].format = format; + params[format].trigger = this.id; + params[format].field = params.field; + } + var format = 'format' + this.value; + // Directly attach this editor, if the input format is enabled or there is + // only one input format at all. + if ($this.is(':input')) { + Drupal.wysiwygAttach(context, params[format]); + } + // Attach onChange handlers to input format selector elements. + if ($this.is('select')) { + $this.change(function() { + // If not disabled, detach the current and attach a new editor. + Drupal.wysiwygDetach(context, params[format]); + format = 'format' + this.value; + Drupal.wysiwygAttach(context, params[format]); + }); + } + // Detach any editor when the containing form is submitted. + $('#' + params.field).parents('form').submit(function (event) { + // Do not detach if the event was cancelled. + if (event.isDefaultPrevented()) { + return; + } + Drupal.wysiwygDetach(context, params[format]); + }); + }); + } +}; + +/** + * Attach an editor to a target element. + * + * This tests whether the passed in editor implements the attach hook and + * invokes it if available. Editor profile settings are cloned first, so they + * cannot be overridden. After attaching the editor, the toggle link is shown + * again, except in case we are attaching no editor. + * + * @param context + * A DOM element, supplied by Drupal.attachBehaviors(). + * @param params + * An object containing input format parameters. + */ +Drupal.wysiwygAttach = function(context, params) { + if (typeof Drupal.wysiwyg.editor.attach[params.editor] == 'function') { + // (Re-)initialize field instance. + Drupal.wysiwyg.instances[params.field] = {}; + // Provide all input format parameters to editor instance. + jQuery.extend(Drupal.wysiwyg.instances[params.field], params); + // Provide editor callbacks for plugins, if available. + if (typeof Drupal.wysiwyg.editor.instance[params.editor] == 'object') { + jQuery.extend(Drupal.wysiwyg.instances[params.field], Drupal.wysiwyg.editor.instance[params.editor]); + } + // Store this field id, so (external) plugins can use it. + // @todo Wrong point in time. Probably can only supported by editors which + // support an onFocus() or similar event. + Drupal.wysiwyg.activeId = params.field; + // Attach or update toggle link, if enabled. + if (params.toggle) { + Drupal.wysiwygAttachToggleLink(context, params); + } + // Otherwise, ensure that toggle link is hidden. + else { + $('#wysiwyg-toggle-' + params.field).hide(); + } + // Attach editor, if enabled by default or last state was enabled. + if (params.status) { + Drupal.wysiwyg.editor.attach[params.editor](context, params, (Drupal.settings.wysiwyg.configs[params.editor] ? jQuery.extend(true, {}, Drupal.settings.wysiwyg.configs[params.editor][params.format]) : {})); + } + // Otherwise, attach default behaviors. + else { + Drupal.wysiwyg.editor.attach.none(context, params); + Drupal.wysiwyg.instances[params.field].editor = 'none'; + } + } +}; + +/** + * Detach all editors from a target element. + * + * @param context + * A DOM element, supplied by Drupal.attachBehaviors(). + * @param params + * An object containing input format parameters. + */ +Drupal.wysiwygDetach = function(context, params) { + var editor = Drupal.wysiwyg.instances[params.field].editor; + if (jQuery.isFunction(Drupal.wysiwyg.editor.detach[editor])) { + Drupal.wysiwyg.editor.detach[editor](context, params); + } +}; + +/** + * Append or update an editor toggle link to a target element. + * + * @param context + * A DOM element, supplied by Drupal.attachBehaviors(). + * @param params + * An object containing input format parameters. + */ +Drupal.wysiwygAttachToggleLink = function(context, params) { + if (!$('#wysiwyg-toggle-' + params.field).length) { + var text = document.createTextNode(params.status ? Drupal.settings.wysiwyg.disable : Drupal.settings.wysiwyg.enable); + var a = document.createElement('a'); + $(a).attr({ id: 'wysiwyg-toggle-' + params.field, href: 'javascript:void(0);' }).append(text); + var div = document.createElement('div'); + $(div).addClass('wysiwyg-toggle-wrapper').append(a); + $('#' + params.field).after(div); + } + $('#wysiwyg-toggle-' + params.field) + .html(params.status ? Drupal.settings.wysiwyg.disable : Drupal.settings.wysiwyg.enable).show() + .unbind('click.wysiwyg', Drupal.wysiwyg.toggleWysiwyg) + .bind('click.wysiwyg', { params: params, context: context }, Drupal.wysiwyg.toggleWysiwyg); + + // Hide toggle link in case no editor is attached. + if (params.editor == 'none') { + $('#wysiwyg-toggle-' + params.field).hide(); + } +}; + +/** + * Callback for the Enable/Disable rich editor link. + */ +Drupal.wysiwyg.toggleWysiwyg = function (event) { + var context = event.data.context; + var params = event.data.params; + if (params.status) { + // Detach current editor. + params.status = false; + Drupal.wysiwygDetach(context, params); + // After disabling the editor, re-attach default behaviors. + // @todo We HAVE TO invoke Drupal.wysiwygAttach() here. + Drupal.wysiwyg.editor.attach.none(context, params); + Drupal.wysiwyg.instances[params.field] = Drupal.wysiwyg.editor.instance.none; + Drupal.wysiwyg.instances[params.field].editor = 'none'; + $(this).html(Drupal.settings.wysiwyg.enable).blur(); + } + else { + // Before enabling the editor, detach default behaviors. + Drupal.wysiwyg.editor.detach.none(context, params); + // Attach new editor using parameters of the currently selected input format. + params = Drupal.settings.wysiwyg.triggers[params.trigger]['format' + $('#' + params.trigger).val()]; + params.status = true; + Drupal.wysiwygAttach(context, params); + $(this).html(Drupal.settings.wysiwyg.disable).blur(); + } +} + +/** + * Parse the CSS classes of an input format DOM element into parameters. + * + * Syntax for CSS classes is "wysiwyg-name-value". + * + * @param element + * An input format DOM element containing CSS classes to parse. + * @param params + * (optional) An object containing input format parameters to update. + */ +Drupal.wysiwyg.getParams = function(element, params) { + var classes = element.className.split(' '); + var params = params || {}; + for (var i = 0; i < classes.length; i++) { + if (classes[i].substr(0, 8) == 'wysiwyg-') { + var parts = classes[i].split('-'); + var value = parts.slice(2).join('-'); + params[parts[1]] = value; + } + } + // Convert format id into string. + params.format = 'format' + params.format; + // Convert numeric values. + params.status = parseInt(params.status, 10); + params.toggle = parseInt(params.toggle, 10); + params.resizable = parseInt(params.resizable, 10); + return params; +}; + +/** + * Allow certain editor libraries to initialize before the DOM is loaded. + */ +Drupal.wysiwygInit(); + +})(jQuery); diff --git a/wysiwyg.module b/wysiwyg.module new file mode 100644 index 00000000..771cbd79 --- /dev/null +++ b/wysiwyg.module @@ -0,0 +1,1079 @@ + t('Wysiwyg profile'), + 'base table' => 'wysiwyg', + 'controller class' => 'WysiwygProfileController', + 'fieldable' => FALSE, + // When loading all entities, DrupalDefaultEntityController::load() ignores + // its static cache. Therefore, wysiwyg_profile_load_all() implements a + // custom static cache. + 'static cache' => FALSE, + 'entity keys' => array( + 'id' => 'format', + ), + ); + return $types; +} + +/** + * Controller class for Wysiwyg profiles. + */ +class WysiwygProfileController extends DrupalDefaultEntityController { + /** + * Overrides DrupalDefaultEntityController::attachLoad(). + */ + function attachLoad(&$queried_entities, $revision_id = FALSE) { + // Unserialize the profile settings. + foreach ($queried_entities as $key => $record) { + $queried_entities[$key]->settings = unserialize($record->settings); + } + // Call the default attachLoad() method. + parent::attachLoad($queried_entities, $revision_id); + } +} + +/** + * Implementation of hook_menu(). + */ +function wysiwyg_menu() { + $items['admin/config/content/wysiwyg'] = array( + 'title' => 'Wysiwyg profiles', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('wysiwyg_profile_overview'), + 'description' => 'Configure client-side editors.', + 'access arguments' => array('administer filters'), + 'file' => 'wysiwyg.admin.inc', + ); + $items['admin/config/content/wysiwyg/profile'] = array( + 'title' => 'List', + 'type' => MENU_DEFAULT_LOCAL_TASK, + ); + $items['admin/config/content/wysiwyg/profile/%wysiwyg_profile/edit'] = array( + 'title' => 'Edit', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('wysiwyg_profile_form', 5), + 'access arguments' => array('administer filters'), + 'file' => 'wysiwyg.admin.inc', + 'tab_root' => 'admin/config/content/wysiwyg/profile', + 'tab_parent' => 'admin/config/content/wysiwyg/profile/%wysiwyg_profile', + 'type' => MENU_LOCAL_TASK, + ); + $items['admin/config/content/wysiwyg/profile/%wysiwyg_profile/delete'] = array( + 'title' => 'Remove', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('wysiwyg_profile_delete_confirm', 5), + 'access arguments' => array('administer filters'), + 'file' => 'wysiwyg.admin.inc', + 'tab_root' => 'admin/config/content/wysiwyg/profile', + 'tab_parent' => 'admin/config/content/wysiwyg/profile/%wysiwyg_profile', + 'type' => MENU_LOCAL_TASK, + 'weight' => 10, + ); + $items['wysiwyg/%'] = array( + 'page callback' => 'wysiwyg_dialog', + 'page arguments' => array(1), + 'access arguments' => array('access content'), + 'type' => MENU_CALLBACK, + 'file' => 'wysiwyg.dialog.inc', + ); + return $items; +} + +/** + * Implementation of hook_theme(). + * + * @see drupal_common_theme(), common.inc + * @see template_preprocess_page(), theme.inc + */ +function wysiwyg_theme() { + return array( + 'wysiwyg_profile_overview' => array( + 'render element' => 'form', + ), + 'wysiwyg_admin_button_table' => array( + 'render element' => 'form', + ), + 'wysiwyg_dialog_page' => array( + 'variables' => array('content' => NULL, 'show_messages' => TRUE), + 'file' => 'wysiwyg.dialog.inc', + 'template' => 'wysiwyg-dialog-page', + ), + ); +} + +/** + * Implementation of hook_help(). + */ +function wysiwyg_help($path, $arg) { + switch ($path) { + case 'admin/config/content/wysiwyg': + $output = '

' . t('A Wysiwyg profile is associated with an input format. A Wysiwyg profile defines which client-side editor is loaded with a particular input format, what buttons or themes are enabled for the editor, how the editor is displayed, and a few other editor-specific functions.') . '

'; + return $output; + } +} + +/** + * Implementation of hook_form_alter(). + */ +function wysiwyg_form_alter(&$form, &$form_state) { + // Teaser splitter is unconditionally removed and NOT supported. + if (isset($form['body_field'])) { + unset($form['body_field']['teaser_js']); + } +} + +/** + * Implements hook_element_info_alter(). + */ +function wysiwyg_element_info_alter(&$types) { + $types['text_format']['#pre_render'][] = 'wysiwyg_pre_render_text_format'; +} + +/** + * Process a text format widget to load and attach editors. + * + * The element's #id is used as reference to attach client-side editors. + */ +function wysiwyg_pre_render_text_format($element) { + // filter_process_format() copies properties to the expanded 'value' child + // element. Skip this text format widget, if it contains no 'format' or when + // the current user does not have access to edit the value. + if (!isset($element['format']) || !empty($element['value']['#disabled'])) { + return $element; + } + // Allow modules to programmatically enforce no client-side editor by setting + // the #wysiwyg property to FALSE. + if (isset($element['#wysiwyg']) && !$element['#wysiwyg']) { + return $element; + } + + $format_field = &$element['format']; + $field = &$element['value']; + $settings = array( + 'field' => $field['#id'], + ); + + // If this textarea is #resizable and we will load at least one + // editor, then only load the behavior and let the 'none' editor + // attach/detach it to avoid hi-jacking the UI. Due to our CSS class + // parsing, we can add arbitrary parameters for each input format. + // The #resizable property will be removed below, if at least one + // profile has been loaded. + $resizable = 0; + if (!empty($field['#resizable'])) { + $resizable = 1; + drupal_add_js('misc/textarea.js'); + } + // Determine the available text formats. + foreach ($format_field['format']['#options'] as $format_id => $format_name) { + $format = 'format' . $format_id; + // Initialize default settings, defaulting to 'none' editor. + $settings[$format] = array( + 'editor' => 'none', + 'status' => 1, + 'toggle' => 1, + 'resizable' => $resizable, + ); + + // Fetch the profile associated to this text format. + $profile = wysiwyg_get_profile($format_id); + if ($profile) { + $loaded = TRUE; + $settings[$format]['editor'] = $profile->editor; + $settings[$format]['status'] = (int) wysiwyg_user_get_status($profile); + if (isset($profile->settings['show_toggle'])) { + $settings[$format]['toggle'] = (int) $profile->settings['show_toggle']; + } + // Check editor theme (and reset it if not/no longer available). + $theme = wysiwyg_get_editor_themes($profile, (isset($profile->settings['theme']) ? $profile->settings['theme'] : '')); + + // Add plugin settings (first) for this text format. + wysiwyg_add_plugin_settings($profile); + // Add profile settings for this text format. + wysiwyg_add_editor_settings($profile, $theme); + } + } + // Use a hidden element for a single text format. + if (!$format_field['format']['#access']) { + $format_field['wysiwyg'] = array( + '#type' => 'hidden', + '#name' => $format_field['format']['#name'], + '#value' => $format_id, + '#attributes' => array( + 'id' => $format_field['format']['#id'], + 'class' => array('wysiwyg'), + ), + ); + $format_field['wysiwyg']['#attached']['js'][] = array( + 'data' => array( + 'wysiwyg' => array( + 'triggers' => array( + $format_field['format']['#id'] => $settings, + ), + ), + ), + 'type' => 'setting', + ); + } + // Otherwise, attach to text format selector. + else { + $format_field['format']['#attributes']['class'][] = 'wysiwyg'; + $format_field['format']['#attached']['js'][] = array( + 'data' => array( + 'wysiwyg' => array( + 'triggers' => array( + $format_field['format']['#id'] => $settings, + ), + ), + ), + 'type' => 'setting', + ); + } + + // If we loaded at least one editor, then the 'none' editor will + // handle resizable textareas instead of core. + if (isset($loaded) && $resizable) { + $field['#resizable'] = FALSE; + } + + return $element; +} + +/** + * Determine the profile to use for a given input format id. + * + * This function also performs sanity checks for the configured editor in a + * profile to ensure that we do not load a malformed editor. + * + * @param $format + * The internal id of an input format. + * + * @return + * A wysiwyg profile. + * + * @see wysiwyg_load_editor(), wysiwyg_get_editor() + */ +function wysiwyg_get_profile($format) { + if ($profile = wysiwyg_profile_load($format)) { + if (wysiwyg_load_editor($profile)) { + return $profile; + } + } + return FALSE; +} + +/** + * Load an editor library and initialize basic Wysiwyg settings. + * + * @param $profile + * A wysiwyg editor profile. + * + * @return + * TRUE if the editor has been loaded, FALSE if not. + * + * @see wysiwyg_get_profile() + */ +function wysiwyg_load_editor($profile) { + static $settings_added; + static $loaded = array(); + + $name = $profile->editor; + // Library files must be loaded only once. + if (!isset($loaded[$name])) { + // Load editor. + $editor = wysiwyg_get_editor($name); + if ($editor) { + // Determine library files to load. + // @todo Allow to configure the library/execMode to use. + if (isset($profile->settings['library']) && isset($editor['libraries'][$profile->settings['library']])) { + $library = $profile->settings['library']; + $files = $editor['libraries'][$library]['files']; + } + else { + // Fallback to the first defined library by default (external libraries can change). + $library = key($editor['libraries']); + $files = array_shift($editor['libraries']); + $files = $files['files']; + } + foreach ($files as $file => $options) { + if (is_array($options)) { + $options += array('type' => 'file', 'scope' => 'header', 'defer' => FALSE, 'cache' => TRUE, 'preprocess' => TRUE); + drupal_add_js($editor['library path'] . '/' . $file, $options); + } + else { + drupal_add_js($editor['library path'] . '/' . $options); + } + } + // If editor defines an additional load callback, invoke it. + // @todo Isn't the settings callback sufficient? + if (isset($editor['load callback']) && function_exists($editor['load callback'])) { + $editor['load callback']($editor, $library); + } + // Load JavaScript integration files for this editor. + $files = array(); + if (isset($editor['js files'])) { + $files = $editor['js files']; + } + foreach ($files as $file) { + drupal_add_js($editor['js path'] . '/' . $file); + } + // Load CSS stylesheets for this editor. + $files = array(); + if (isset($editor['css files'])) { + $files = $editor['css files']; + } + foreach ($files as $file) { + drupal_add_css($editor['css path'] . '/' . $file); + } + + drupal_add_js(array('wysiwyg' => array( + 'configs' => array($editor['name'] => array('global' => array( + // @todo Move into (global) editor settings. + // If JS compression is enabled, at least TinyMCE is unable to determine + // its own base path and exec mode since it can't find the script name. + 'editorBasePath' => base_path() . $editor['library path'], + 'execMode' => $library, + ))), + )), 'setting'); + + $loaded[$name] = TRUE; + } + else { + $loaded[$name] = FALSE; + } + } + + // Add basic Wysiwyg settings if any editor has been added. + if (!isset($settings_added) && $loaded[$name]) { + drupal_add_js(array('wysiwyg' => array( + 'configs' => array(), + 'plugins' => array(), + 'disable' => t('Disable rich-text'), + 'enable' => t('Enable rich-text'), + )), 'setting'); + + $path = drupal_get_path('module', 'wysiwyg'); + // Initialize our namespaces in the *header* to do not force editor + // integration scripts to check and define Drupal.wysiwyg on its own. + drupal_add_js($path . '/wysiwyg.init.js', array('group' => JS_LIBRARY)); + + // The 'none' editor is a special editor implementation, allowing us to + // attach and detach regular Drupal behaviors just like any other editor. + drupal_add_js($path . '/editors/js/none.js'); + + // Add wysiwyg.js to the footer to ensure it's executed after the + // Drupal.settings array has been rendered and populated. Also, since editor + // library initialization functions must be loaded first by the browser, + // and Drupal.wysiwygInit() must be executed AFTER editors registered + // their callbacks and BEFORE Drupal.behaviors are applied, this must come + // last. + drupal_add_js($path . '/wysiwyg.js', array('scope' => 'footer')); + + $settings_added = TRUE; + } + + return $loaded[$name]; +} + +/** + * Add editor settings for a given input format. + */ +function wysiwyg_add_editor_settings($profile, $theme) { + static $formats = array(); + + if (!isset($formats[$profile->format])) { + $config = wysiwyg_get_editor_config($profile, $theme); + // drupal_to_js() does not properly convert numeric array keys, so we need + // to use a string instead of the format id. + if ($config) { + drupal_add_js(array('wysiwyg' => array('configs' => array($profile->editor => array('format' . $profile->format => $config)))), 'setting'); + } + $formats[$profile->format] = TRUE; + } +} + +/** + * Add settings for external plugins. + * + * Plugins can be used in multiple profiles, but not necessarily in all. Because + * of that, we need to process plugins for each profile, even if most of their + * settings are not stored per profile. + * + * Implementations of hook_wysiwyg_plugin() may execute different code for each + * editor. Therefore, we have to invoke those implementations for each editor, + * but process the resulting plugins separately for each profile. + * + * Drupal plugins differ to native plugins in that they have plugin-specific + * definitions and settings, which need to be processed only once. But they are + * also passed to the editor to prepare settings specific to the editor. + * Therefore, we load and process the Drupal plugins only once, and hand off the + * effective definitions for each profile to the editor. + * + * @param $profile + * A wysiwyg editor profile. + * + * @todo Rewrite wysiwyg_process_form() to build a registry of effective + * profiles in use, so we can process plugins in multiple profiles in one shot + * and simplify this entire function. + */ +function wysiwyg_add_plugin_settings($profile) { + static $plugins = array(); + static $processed_plugins = array(); + static $processed_formats = array(); + + // Each input format must only processed once. + // @todo ...as long as we do not have multiple profiles per format. + if (isset($processed_formats[$profile->format])) { + return; + } + $processed_formats[$profile->format] = TRUE; + + $editor = wysiwyg_get_editor($profile->editor); + + // Collect native plugins for this editor provided via hook_wysiwyg_plugin() + // and Drupal plugins provided via hook_wysiwyg_include_directory(). + if (!array_key_exists($editor['name'], $plugins)) { + $plugins[$editor['name']] = wysiwyg_get_plugins($editor['name']); + } + + // Nothing to do, if there are no plugins. + if (empty($plugins[$editor['name']])) { + return; + } + + // Determine name of proxy plugin for Drupal plugins. + $proxy = (isset($editor['proxy plugin']) ? key($editor['proxy plugin']) : ''); + + // Process native editor plugins. + if (isset($editor['plugin settings callback'])) { + // @todo Require PHP 5.1 in 3.x and use array_intersect_key(). + $profile_plugins_native = array(); + foreach ($plugins[$editor['name']] as $plugin => $meta) { + // Skip Drupal plugins (handled below). + if ($plugin === $proxy) { + continue; + } + // Only keep native plugins that are enabled in this profile. + if (isset($profile->settings['buttons'][$plugin])) { + $profile_plugins_native[$plugin] = $meta; + } + } + // Invoke the editor's plugin settings callback, so it can populate the + // settings for native external plugins with required values. + $settings_native = call_user_func($editor['plugin settings callback'], $editor, $profile, $profile_plugins_native); + + if ($settings_native) { + drupal_add_js(array('wysiwyg' => array('plugins' => array('format' . $profile->format => array('native' => $settings_native)))), 'setting'); + } + } + + // Process Drupal plugins. + if ($proxy && isset($editor['proxy plugin settings callback'])) { + $profile_plugins_drupal = array(); + foreach (wysiwyg_get_all_plugins() as $plugin => $meta) { + if (isset($profile->settings['buttons'][$proxy][$plugin])) { + // JavaScript and plugin-specific settings for Drupal plugins must be + // loaded and processed only once. Plugin information is cached + // statically to pass it to the editor's proxy plugin settings callback. + if (!isset($processed_plugins[$proxy][$plugin])) { + $profile_plugins_drupal[$plugin] = $processed_plugins[$proxy][$plugin] = $meta; + // Load the Drupal plugin's JavaScript. + drupal_add_js($meta['js path'] . '/' . $meta['js file']); + // Add plugin-specific settings. + if (isset($meta['settings'])) { + drupal_add_js(array('wysiwyg' => array('plugins' => array('drupal' => array($plugin => $meta['settings'])))), 'setting'); + } + } + else { + $profile_plugins_drupal[$plugin] = $processed_plugins[$proxy][$plugin]; + } + } + } + // Invoke the editor's proxy plugin settings callback, so it can populate + // the settings for Drupal plugins with custom, required values. + $settings_drupal = call_user_func($editor['proxy plugin settings callback'], $editor, $profile, $profile_plugins_drupal); + + if ($settings_drupal) { + drupal_add_js(array('wysiwyg' => array('plugins' => array('format' . $profile->format => array('drupal' => $settings_drupal)))), 'setting'); + } + } +} + +/** + * Retrieve available themes for an editor. + * + * Editor themes control the visual presentation of an editor. + * + * @param $profile + * A wysiwyg editor profile; passed/altered by reference. + * @param $selected_theme + * An optional theme name that ought to be used. + * + * @return + * An array of theme names, or a single, checked theme name if $selected_theme + * was given. + */ +function wysiwyg_get_editor_themes(&$profile, $selected_theme = NULL) { + static $themes = array(); + + if (!isset($themes[$profile->editor])) { + $editor = wysiwyg_get_editor($profile->editor); + if (isset($editor['themes callback']) && function_exists($editor['themes callback'])) { + $themes[$editor['name']] = $editor['themes callback']($editor, $profile); + } + // Fallback to 'default' otherwise. + else { + $themes[$editor['name']] = array('default'); + } + } + + // Check optional $selected_theme argument, if given. + if (isset($selected_theme)) { + // If the passed theme name does not exist, use the first available. + if (!in_array($selected_theme, $themes[$profile->editor])) { + $selected_theme = $profile->settings['theme'] = $themes[$profile->editor][0]; + } + } + + return isset($selected_theme) ? $selected_theme : $themes[$profile->editor]; +} + +/** + * Return plugin metadata from the plugin registry. + * + * @param $editor_name + * The internal name of an editor to return plugins for. + * + * @return + * An array for each plugin. + */ +function wysiwyg_get_plugins($editor_name) { + $plugins = array(); + if (!empty($editor_name)) { + $editor = wysiwyg_get_editor($editor_name); + // Add internal editor plugins. + if (isset($editor['plugin callback']) && function_exists($editor['plugin callback'])) { + $plugins = $editor['plugin callback']($editor); + } + // Add editor plugins provided via hook_wysiwyg_plugin(). + $plugins = array_merge($plugins, module_invoke_all('wysiwyg_plugin', $editor['name'], $editor['installed version'])); + // Add API plugins provided by Drupal modules. + // @todo We need to pass the filepath to the plugin icon for Drupal plugins. + if (isset($editor['proxy plugin'])) { + $plugins += $editor['proxy plugin']; + $proxy = key($editor['proxy plugin']); + foreach (wysiwyg_get_all_plugins() as $plugin_name => $info) { + $plugins[$proxy]['buttons'][$plugin_name] = $info['title']; + } + } + } + return $plugins; +} + +/** + * Return an array of initial editor settings for a Wysiwyg profile. + */ +function wysiwyg_get_editor_config($profile, $theme) { + $editor = wysiwyg_get_editor($profile->editor); + $settings = array(); + if (!empty($editor['settings callback']) && function_exists($editor['settings callback'])) { + $settings = $editor['settings callback']($editor, $profile->settings, $theme); + + // Allow other modules to alter the editor settings for this format. + $context = array('editor' => $editor, 'profile' => $profile, 'theme' => $theme); + drupal_alter('wysiwyg_editor_settings', $settings, $context); + } + return $settings; +} + +/** + * Retrieve stylesheets for HTML/IFRAME-based editors. + * + * This assumes that the content editing area only needs stylesheets defined + * for the scope 'theme'. + * + * @return + * An array containing CSS files, including proper base path. + */ +function wysiwyg_get_css() { + static $files; + + if (isset($files)) { + return $files; + } + // In node form previews, the theme has not been initialized yet. + if (!empty($_POST)) { + drupal_theme_initialize(); + } + + $files = array(); + foreach (drupal_add_css() as $filepath => $info) { + if ($info['group'] >= CSS_THEME && $info['media'] != 'print') { + if (file_exists($filepath)) { + $files[] = base_path() . $filepath; + } + } + } + return $files; +} + +/** + * Loads a profile for a given text format. + * + * Since there are commonly not many text formats, and each text format-enabled + * form element will possibly have to load every single profile, all existing + * profiles are loaded and cached once to reduce the amount of database queries. + */ +function wysiwyg_profile_load($format) { + $profiles = wysiwyg_profile_load_all(); + return (isset($profiles[$format]) ? $profiles[$format] : FALSE); +} + +/** + * Loads all profiles. + */ +function wysiwyg_profile_load_all() { + // entity_load(..., FALSE) does not re-use its own static cache upon + // repetitive calls, so a custom static cache is required. + // @see wysiwyg_entity_info() + $profiles = &drupal_static(__FUNCTION__); + + if (!isset($profiles)) { + // Additional database cache to support alternative caches like memcache. + if ($cached = cache_get('wysiwyg_profiles')) { + $profiles = $cached->data; + } + else { + $profiles = entity_load('wysiwyg_profile', FALSE); + cache_set('wysiwyg_profiles', $profiles); + } + } + + return $profiles; +} + +/** + * Deletes a profile from the database. + */ +function wysiwyg_profile_delete($format) { + db_delete('wysiwyg') + ->condition('format', $format) + ->execute(); +} + +/** + * Clear all Wysiwyg profile caches. + */ +function wysiwyg_profile_cache_clear() { + entity_get_controller('wysiwyg_profile')->resetCache(); + drupal_static_reset('wysiwyg_profile_load_all'); + cache_clear_all('wysiwyg_profiles', 'cache'); +} + +/** + * Implements hook_form_FORM_ID_alter(). + */ +function wysiwyg_form_user_profile_form_alter(&$form, &$form_state, $form_id) { + $account = $form['#user']; + $user_formats = filter_formats($account); + $options = array(); + $options_default = array(); + foreach (wysiwyg_profile_load_all() as $format => $profile) { + // Only show profiles that have user_choose enabled. + if (!empty($profile->settings['user_choose']) && isset($user_formats[$format])) { + $options[$format] = check_plain($user_formats[$format]->name); + if (wysiwyg_user_get_status($profile, $account)) { + $options_default[] = $format; + } + } + } + if (!empty($options)) { + $form['wysiwyg']['wysiwyg_status'] = array( + '#type' => 'checkboxes', + '#title' => t('Text formats enabled for rich-text editing'), + '#options' => $options, + '#default_value' => $options_default, + ); + } +} + +/** + * Implements hook_user_insert(). + * + * Wysiwyg's user preferences are normally not exposed on the user registration + * form, but in case they are manually altered in, we invoke + * wysiwyg_user_update() accordingly. + */ +function wysiwyg_user_insert(&$edit, $account, $category) { + wysiwyg_user_update($edit, $account, $category); +} + +/** + * Implements hook_user_update(). + */ +function wysiwyg_user_update(&$edit, $account, $category) { + if (isset($edit['wysiwyg_status'])) { + db_delete('wysiwyg_user') + ->condition('uid', $account->uid) + ->execute(); + $query = db_insert('wysiwyg_user') + ->fields(array('uid', 'format', 'status')); + foreach ($edit['wysiwyg_status'] as $format => $status) { + $query->values(array( + 'uid' => $account->uid, + 'format' => $format, + 'status' => (int) (bool) $status, + )); + } + $query->execute(); + } +} + +function wysiwyg_user_get_status($profile, $account = NULL) { + global $user; + + if (!isset($account)) { + $account = $user; + } + + // Default wysiwyg editor status information is only required on forms, so we + // do not pre-emptively load and attach this information on every user_load(). + if (!isset($account->wysiwyg_status)) { + $account->wysiwyg_status = db_query("SELECT format, status FROM {wysiwyg_user} WHERE uid = :uid", array( + ':uid' => $account->uid, + ))->fetchAllKeyed(); + } + + if (!empty($profile->settings['user_choose']) && isset($account->wysiwyg_status[$profile->format])) { + $status = $account->wysiwyg_status[$profile->format]; + } + else { + $status = isset($profile->settings['default']) ? $profile->settings['default'] : TRUE; + } + + return (bool) $status; +} + +/** + * @defgroup wysiwyg_api Wysiwyg API + * @{ + * + * @todo Forked from Panels; abstract into a separate API module that allows + * contrib modules to define supported include/plugin types. + */ + +/** + * Return library information for a given editor. + * + * @param $name + * The internal name of an editor. + * + * @return + * The library information for the editor, or FALSE if $name is unknown or not + * installed properly. + */ +function wysiwyg_get_editor($name) { + $editors = wysiwyg_get_all_editors(); + return isset($editors[$name]) && $editors[$name]['installed'] ? $editors[$name] : FALSE; +} + +/** + * Compile a list holding all supported editors including installed editor version information. + */ +function wysiwyg_get_all_editors() { + static $editors; + + if (isset($editors)) { + return $editors; + } + + $editors = wysiwyg_load_includes('editors', 'editor'); + foreach ($editors as $editor => $properties) { + // Fill in required properties. + $editors[$editor] += array( + 'title' => '', + 'vendor url' => '', + 'download url' => '', + 'editor path' => wysiwyg_get_path($editors[$editor]['name']), + 'library path' => wysiwyg_get_path($editors[$editor]['name']), + 'libraries' => array(), + 'version callback' => NULL, + 'themes callback' => NULL, + 'settings callback' => NULL, + 'plugin callback' => NULL, + 'plugin settings callback' => NULL, + 'versions' => array(), + 'js path' => $editors[$editor]['path'] . '/js', + 'css path' => $editors[$editor]['path'] . '/css', + ); + // Check whether library is present. + if (!($editors[$editor]['installed'] = file_exists($editors[$editor]['library path']))) { + continue; + } + // Detect library version. + if (function_exists($editors[$editor]['version callback'])) { + $editors[$editor]['installed version'] = $editors[$editor]['version callback']($editors[$editor]); + } + if (empty($editors[$editor]['installed version'])) { + $editors[$editor]['error'] = t('The version of %editor could not be detected.', array('%editor' => $properties['title'])); + $editors[$editor]['installed'] = FALSE; + continue; + } + // Determine to which supported version the installed version maps. + ksort($editors[$editor]['versions']); + $version = 0; + foreach ($editors[$editor]['versions'] as $supported_version => $version_properties) { + if (version_compare($editors[$editor]['installed version'], $supported_version, '>=')) { + $version = $supported_version; + } + } + if (!$version) { + $editors[$editor]['error'] = t('The installed version %version of %editor is not supported.', array('%version' => $editors[$editor]['installed version'], '%editor' => $editors[$editor]['title'])); + $editors[$editor]['installed'] = FALSE; + continue; + } + // Apply library version specific definitions and overrides. + $editors[$editor] = array_merge($editors[$editor], $editors[$editor]['versions'][$version]); + unset($editors[$editor]['versions']); + } + return $editors; +} + +/** + * Invoke hook_wysiwyg_plugin() in all modules. + */ +function wysiwyg_get_all_plugins() { + static $plugins; + + if (isset($plugins)) { + return $plugins; + } + + $plugins = wysiwyg_load_includes('plugins', 'plugin'); + foreach ($plugins as $name => $properties) { + $plugin = &$plugins[$name]; + // Fill in required/default properties. + $plugin += array( + 'title' => $plugin['name'], + 'vendor url' => '', + 'js path' => $plugin['path'] . '/' . $plugin['name'], + 'js file' => $plugin['name'] . '.js', + 'css path' => $plugin['path'] . '/' . $plugin['name'], + 'css file' => $plugin['name'] . '.css', + 'icon path' => $plugin['path'] . '/' . $plugin['name'] . '/images', + 'icon file' => $plugin['name'] . '.png', + 'dialog path' => $plugin['name'], + 'dialog settings' => array(), + 'settings callback' => NULL, + 'settings form callback' => NULL, + ); + // Fill in default settings. + $plugin['settings'] += array( + 'path' => base_path() . $plugin['path'] . '/' . $plugin['name'], + ); + // Check whether library is present. + if (!($plugin['installed'] = file_exists($plugin['js path'] . '/' . $plugin['js file']))) { + continue; + } + } + return $plugins; +} + +/** + * Load include files for wysiwyg implemented by all modules. + * + * @param $type + * The type of includes to search for, can be 'editors'. + * @param $hook + * The hook name to invoke. + * @param $file + * An optional include file name without .inc extension to limit the search to. + * + * @see wysiwyg_get_directories(), _wysiwyg_process_include() + */ +function wysiwyg_load_includes($type = 'editors', $hook = 'editor', $file = NULL) { + // Determine implementations. + $directories = wysiwyg_get_directories($type); + $directories['wysiwyg'] = drupal_get_path('module', 'wysiwyg') . '/' . $type; + $file_list = array(); + foreach ($directories as $module => $path) { + $file_list[$module] = drupal_system_listing("/{$file}.inc\$/", $path, 'name', 0); + } + + // Load implementations. + $info = array(); + foreach (array_filter($file_list) as $module => $files) { + foreach ($files as $file) { + include_once './' . $file->uri; + $result = _wysiwyg_process_include($module, $module . '_' . $file->name, dirname($file->uri), $hook); + if (is_array($result)) { + $info = array_merge($info, $result); + } + } + } + return $info; +} + +/** + * Helper function to build paths to libraries. + * + * @param $library + * The external library name to return the path for. + * @param $base_path + * Whether to prefix the resulting path with base_path(). + * + * @return + * The path to the specified library. + * + * @ingroup libraries + */ +function wysiwyg_get_path($library, $base_path = FALSE) { + static $libraries; + + if (!isset($libraries)) { + $libraries = wysiwyg_get_libraries(); + } + if (!isset($libraries[$library])) { + // Most often, external libraries can be shared across multiple sites. + return 'sites/all/libraries/' . $library; + } + + $path = ($base_path ? base_path() : ''); + $path .= $libraries[$library]; + + return $path; +} + +/** + * Return an array of library directories. + * + * Returns an array of library directories from the all-sites directory + * (i.e. sites/all/libraries/), the profiles directory, and site-specific + * directory (i.e. sites/somesite/libraries/). The returned array will be keyed + * by the library name. Site-specific libraries are prioritized over libraries + * in the default directories. That is, if a library with the same name appears + * in both the site-wide directory and site-specific directory, only the + * site-specific version will be listed. + * + * @return + * A list of library directories. + * + * @ingroup libraries + */ +function wysiwyg_get_libraries() { + global $profile; + + // When this function is called during Drupal's initial installation process, + // the name of the profile that is about to be installed is stored in the + // global $profile variable. At all other times, the regular system variable + // contains the name of the current profile, and we can call variable_get() + // to determine the profile. + if (!isset($profile)) { + $profile = variable_get('install_profile', 'default'); + } + + $directory = 'libraries'; + $searchdir = array(); + $config = conf_path(); + + // The 'profiles' directory contains pristine collections of modules and + // themes as organized by a distribution. It is pristine in the same way + // that /modules is pristine for core; users should avoid changing anything + // there in favor of sites/all or sites/ directories. + if (file_exists("profiles/$profile/$directory")) { + $searchdir[] = "profiles/$profile/$directory"; + } + + // Always search sites/all/*. + $searchdir[] = 'sites/all/' . $directory; + + // Also search sites//*. + if (file_exists("$config/$directory")) { + $searchdir[] = "$config/$directory"; + } + + // Retrieve list of directories. + // @todo Core: Allow to scan for directories. + $directories = array(); + $nomask = array('CVS'); + foreach ($searchdir as $dir) { + if (is_dir($dir) && $handle = opendir($dir)) { + while (FALSE !== ($file = readdir($handle))) { + if (!in_array($file, $nomask) && $file[0] != '.') { + if (is_dir("$dir/$file")) { + $directories[$file] = "$dir/$file"; + } + } + } + closedir($handle); + } + } + + return $directories; +} + +/** + * Return a list of directories by modules implementing wysiwyg_include_directory(). + * + * @param $plugintype + * The type of a plugin; can be 'editors'. + * + * @return + * An array containing module names suffixed with '_' and their defined + * directory. + * + * @see wysiwyg_load_includes(), _wysiwyg_process_include() + */ +function wysiwyg_get_directories($plugintype) { + $directories = array(); + foreach (module_implements('wysiwyg_include_directory') as $module) { + $result = module_invoke($module, 'wysiwyg_include_directory', $plugintype); + if (isset($result) && is_string($result)) { + $directories[$module] = drupal_get_path('module', $module) . '/' . $result; + } + } + return $directories; +} + +/** + * Process a single hook implementation of a wysiwyg editor. + * + * @param $module + * The module that owns the hook. + * @param $identifier + * Either the module or 'wysiwyg_' . $file->name + * @param $hook + * The name of the hook being invoked. + */ +function _wysiwyg_process_include($module, $identifier, $path, $hook) { + $function = $identifier . '_' . $hook; + if (!function_exists($function)) { + return NULL; + } + $result = $function(); + if (!isset($result) || !is_array($result)) { + return NULL; + } + + // Fill in defaults. + foreach ($result as $editor => $properties) { + $result[$editor]['module'] = $module; + $result[$editor]['name'] = $editor; + $result[$editor]['path'] = $path; + } + return $result; +} + +/** + * @} End of "defgroup wysiwyg_api". + */