diff --git a/picture.admin.inc b/picture.admin.inc
index d9a1d74..930605e 100644
--- a/picture.admin.inc
+++ b/picture.admin.inc
@@ -216,3 +216,118 @@ function picture_admin_settings($form, &$form_state) {
return system_settings_form($form);
}
+
+/**
+ * Chooses which picture groups are available in the CKEditor image dialog.
+ */
+function picture_ckeditor_settings() {
+ $form = array();
+ $picture_groups = picture_get_mapping_options();
+ $ckeditor_groups = array();
+
+ // Check if picture group mappings have been configured before proceeding.
+ if ($picture_groups) {
+ // Create a settings form.
+ $form['description'] = array(
+ '#type' => 'item',
+ '#title' => t('Choose which picture groups will be available in the CKEditor image dialog.'),
+ '#description' => 'See picture_wysiwyg.css for an example of how to style these images in your theme using the selectors suggested below.',
+ );
+
+ // Retrieve pre-existing settings.
+ $ckeditor_groups = variable_get('picture_ckeditor_groups', array());
+
+ // Loop through each picture group and place a checkbox and weight.
+ foreach ($picture_groups as $machine_name => $display_name) {
+ $form[$machine_name] = array(
+ '#type' => 'fieldset',
+ '#title' => t('@name picture group', array('@name' => $display_name)),
+ );
+ $form[$machine_name]['enabled'] = array(
+ '#type' => 'checkbox',
+ '#default_value' => isset($ckeditor_groups[$machine_name]) ? $ckeditor_groups[$machine_name]['enabled'] : 0,
+ '#title' => t('Include @name picture group in the CKEditor image dialog', array('@name' => $display_name)),
+ );
+ $form[$machine_name]['css'] = array(
+ '#type' => 'item',
+ '#markup' => 'Consider using the selector span[data-picture-group="' . $machine_name . '"]
in your theme CSS.',
+ );
+ $form[$machine_name]['weight'] = array(
+ '#type' => 'select',
+ '#title' => t('Weight'),
+ '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)),
+ '#default_value' => isset($ckeditor_groups[$machine_name]) ? $ckeditor_groups[$machine_name]['weight'] : 1,
+ '#description' => t('Control the sort order of picture groups in the CKEditor "size" drop-down. Higher weights sink to the bottom of the list.'),
+ );
+ $form[$machine_name]['fallback'] = array(
+ '#type' => 'select',
+ '#title' => t('Fallback image style'),
+ '#options' => drupal_map_assoc(array_keys(image_styles())),
+ '#default_value' => isset($ckeditor_groups[$machine_name]) ? $ckeditor_groups[$machine_name]['fallback'] : NULL,
+ );
+ }
+ $form['#tree'] = TRUE;
+ $form['ckeditor_label'] = array(
+ '#type' => 'textfield',
+ '#title' => t('Label in the CKEditor image dialog'),
+ '#description' => t('This sets the label for the drop-down select box containing these picture groups in the CKEditor image dialog'),
+ '#default_value' => variable_get('picture_ckeditor_label', 'Image size (required)'),
+ );
+
+ $form['submit'] = array(
+ '#type' => 'submit',
+ '#value' => 'Save',
+ );
+ }
+ return $form;
+}
+
+/**
+ * Validate handler for the picture_ckeditor_settings form.
+ * It checks that a fallback image style is selected for every
+ * picture group that has been enabled for the CKEditor image dialog.
+ */
+function picture_ckeditor_settings_validate($form, &$form_state) {
+ $picture_groups = picture_mapping_load();
+ $ckeditor_groups = array();
+ foreach ($picture_groups as $picture_group) {
+ $machine_name = $picture_group->machine_name;
+ if ($form_state['values'][$machine_name]['enabled'] == 1) {
+ if (empty($form_state['values'][$machine_name]['fallback'])) {
+ form_set_error($machine_name . '][fallback', t('Please choose a fallback image style for this picture group'));
+ }
+ }
+ }
+}
+
+/**
+ * Submit handler for the picture_ckeditor_settings form. Places chosen picture
+ * groups into the variables table.
+ */
+function picture_ckeditor_settings_submit($form, &$form_state) {
+ $picture_groups = picture_mapping_load();
+ $ckeditor_groups = array();
+
+ // Loop each picture group and record the settings.
+ foreach ($picture_groups as $picture_group) {
+ $machine_name = $picture_group->machine_name;
+ $ckeditor_groups[$machine_name]['enabled'] = $form_state['values'][$machine_name]['enabled'];
+ $ckeditor_groups[$machine_name]['weight'] = $form_state['values'][$machine_name]['weight'];
+ $ckeditor_groups[$machine_name]['fallback'] = $form_state['values'][$machine_name]['fallback'];
+ }
+
+ uasort($ckeditor_groups, 'picture_compare_weights');
+ variable_set('picture_ckeditor_groups', $ckeditor_groups);
+ variable_set('picture_ckeditor_label', $form_state['values']['ckeditor_label']);
+ drupal_set_message(t('Your settings have been saved'));
+}
+
+/**
+ * Helper function to sort picture groups for the CKEditor image dialog
+ */
+function picture_compare_weights($a, $b) {
+ if ($a['weight'] == $b['weight']) {
+ return 0;
+ }
+ return ($a['weight'] < $b['weight']) ? -1 : 1;
+}
diff --git a/picture.info b/picture.info
index 59cc9da..75d4bbe 100644
--- a/picture.info
+++ b/picture.info
@@ -4,3 +4,4 @@ core = 7.x
dependencies[] = breakpoints
configure = admin/config/media/picture
package = Picture
+stylesheets[all][] = picture_wysiwyg.css
diff --git a/picture.module b/picture.module
index 523b24c..510ff09 100644
--- a/picture.module
+++ b/picture.module
@@ -90,6 +90,17 @@ function picture_menu() {
}
}
+ $items['admin/config/media/picture/ckeditor'] = array(
+ 'title' => 'CKEditor',
+ 'type' => MENU_LOCAL_TASK,
+ 'description' => 'Choose picture groups to present in the CKEditor image dialog',
+ 'page callback' => 'drupal_get_form',
+ 'page arguments' => array('picture_ckeditor_settings'),
+ 'access arguments' => array('administer pictures'),
+ 'file' => 'picture.admin.inc',
+ 'weight' => 0,
+ );
+
return $items;
}
@@ -687,6 +698,12 @@ function theme_picture($variables) {
$attributes['data-' . $key] = $variables[$key];
}
}
+ // Add attributes that are already prefixed by 'data-'
+ foreach (array('data-picture-group', 'data-picture-align') as $key) {
+ if (isset($variables[$key])) {
+ $attributes[$key] = $variables[$key];
+ }
+ }
$output[] = '';
// Add source tags to the output.
@@ -935,3 +952,281 @@ function picture_file_formatter_picture_settings($form, &$form_state, $settings)
return $element;
}
+
+/**
+ * Implements hook_filter_info().
+ */
+function picture_filter_info() {
+ $filters = array();
+ $filters['picture'] = array(
+ 'title' => t('Make images responsive with the picture module'),
+ 'description' => t('Replace img tags with markup that contains media width breakpoints. The appropriate image file size will be chosen.'),
+ 'process callback' => '_picture_filter_process',
+ 'tips callback' => '_picture_filter_tips',
+ );
+
+ return $filters;
+}
+
+/**
+ * Process callback for inline image filter.
+ */
+function _picture_filter_process($text, $filter) {
+
+ // Find all img tags with a data-picture-group attribute.
+ preg_match_all('//i', $text, $images);
+
+ if (!empty($images[0])) {
+ foreach ($images[0] as $image) {
+ // Create the render array expected by theme_picture_formatter.
+ $image_render_array = _picture_filter_prepare_image($image);
+ if (!$image_render_array) {
+ return $text;
+ }
+
+ // Get the responsive markup for this image.
+ $new_markup = theme('picture_formatter', $image_render_array);
+
+ // Replace the original img tag with the responsive markup.
+ $text = str_replace($image, $new_markup, $text);
+ }
+ }
+ return $text;
+}
+
+/**
+ * Prepares a Render Array for theme_picture_formatter().
+ * It is similar to picture_field_formatter_view()
+ * with modifications for inline images.
+ *
+ * @param $image
+ * An img tag
+ * @see picture_field_formatter_view()
+ */
+function _picture_filter_prepare_image($image) {
+ // Parse the tag as xml.
+ $xml = simplexml_load_string('' . html_entity_decode($image, ENT_QUOTES, "utf-8") . '');
+ if (isset($xml->img[0]) && is_object($xml->img[0])) {
+ $attributes = array();
+ foreach ($xml->img[0]->attributes() as $a => $b) {
+ $attributes[$a] = (string) $b;
+ }
+ }
+
+ $image_render_array = array();
+ $breakpoint_styles = array();
+ $fallback_image_style = '';
+ $group_name = $attributes['data-picture-group'];
+ $mappings = picture_mapping_load($group_name);
+
+ if ($mappings) {
+ foreach ($mappings->mapping as $breakpoint_name => $multipliers) {
+ if (!empty($multipliers)) {
+ foreach ($multipliers as $multiplier => $image_style) {
+ if (!empty($image_style)) {
+ if (empty($fallback_image_style)) {
+ $fallback_image_style = $image_style;
+ }
+ if (!isset($breakpoint_styles[$breakpoint_name])) {
+ $breakpoint_styles[$breakpoint_name] = array();
+ }
+ $breakpoint_styles[$breakpoint_name][$multiplier] = $image_style;
+ }
+ }
+ }
+ }
+ }
+
+ if(!isset($attributes['src'])) {
+ return FALSE;
+ }
+
+ $src = $attributes['src'];
+
+ $uri = picture_image_uri($src);
+ if (!$uri) {
+ return FALSE;
+ }
+
+ $image_info = image_get_info($uri);
+ if (!$image_info) {
+ // It's not an image.
+ return FALSE;
+ }
+ $picture_groups = variable_get('picture_ckeditor_groups', array());
+ $image_render_array = array(
+ '#theme' => 'picture_formatter',
+ '#item' => array(
+ 'style_name' => $picture_groups[$attributes['data-picture-group']]['fallback'],
+ 'uri' => $uri,
+ 'width' => $image_info['width'],
+ 'height' => $image_info['height'],
+ 'data-picture-group' => $attributes['data-picture-group'],
+ 'data-picture-align' => isset($attributes['data-picture-align']) ? $attributes['data-picture-align'] : '',
+ 'alt' => isset($attributes['alt']) ? $attributes['alt'] : '',
+ 'title' => isset($attributes['title']) ? $attributes['title'] : '',
+ 'filemime' => $image_info['mime_type'],
+ ),
+ '#image_style' => $picture_groups[$attributes['data-picture-group']]['fallback'],
+ '#breakpoints' => $breakpoint_styles,
+ '#path' => '',
+ );
+
+ return $image_render_array;
+
+}
+/**
+ * Implements picture filter tips callback.
+ */
+function _picture_filter_tips($filter, $format, $long = FALSE) {
+ $tips = 'Images with a data-picture-group attribute will be responsive, with a file size appropriate for the browser width.';
+
+ return $tips;
+}
+
+/**
+ * Implements hook_page_build().
+ *
+ * Add the image processing javascript to every page. This allows these scripts
+ * to get included in aggregation, which is probably good since there will be
+ * pictures needing this javascript on most pages. The library does not get
+ * added twice, even if it's attached to multiple fields that are also being
+ * displayed with responsive images. Maybe this should check that the
+ * page is not an admin theme page?
+ */
+function picture_page_build(&$page) {
+ drupal_add_library('picture', 'matchmedia', TRUE);
+ drupal_add_library('picture', 'picturefill', TRUE);
+ drupal_add_library('picture', 'picture.ajax', TRUE);
+}
+
+/**
+ * Helper function to figure out the uri of an image given the
+ * image src.
+ *
+ * @param
+ * Image src starting with http://, https://, or root relative /.
+ * It must point to a local file.
+ */
+function picture_image_uri($src) {
+ $uri = '';
+ // Prepare the src by removing http:// or https://.
+ $src = preg_replace('/https?:\/\//', '', $src);
+ // Remove leading or trailing slashes.
+ $src = trim($src, '/');
+
+ // List all visible stream wrappers. Make sure they're also local since the
+ // getDirectoryPath method exists only for classes that extend
+ // DrupalLocalStreamWrapper.
+ $local_visible_stream_wrappers = array_intersect_key(file_get_stream_wrappers(STREAM_WRAPPERS_LOCAL), file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE));
+ $needles = array();
+ $matches = array();
+ foreach ($local_visible_stream_wrappers as $scheme => $data) {
+ $class = file_stream_wrapper_get_class($scheme);
+ $stream_wrapper = new $class();
+ // Trim leading or trailing slashes since the Directory could be root
+ // relative.
+ $needles[$scheme] = trim($stream_wrapper->getDirectoryPath(), '/');
+
+ // Check whether the file stream directory is at the beginning of
+ // the image src. Use === since strpos could return false.
+ if (strpos($src, $needles[$scheme]) === 0) {
+ $matches[$scheme] = $needles[$scheme];
+ }
+ }
+
+ // If one file scheme directory is a subdirectory of another file
+ // scheme directory, choose the longer one. This issue is possible with
+ // the following scenario:
+ // public file dir: /sites/default/files/
+ // private file dir: /sites/default/files/private/
+ // image src: /sites/default/files/private/the-image.jpg
+ // In this example, the intended scheme would be 'private'.
+
+ if (empty($matches)) {
+ // Can't figure out the Drupal uri.
+ return FALSE;
+ }
+ // Find the length of each matching directory path.
+ $lengths = array_map('strlen', $matches);
+
+ // Determine the key of the longest one.
+ $the_scheme = array_search(max($lengths), $lengths);
+
+ // Construct the Drupal uri.
+ $uri = $the_scheme . '://' . str_replace($matches[$the_scheme], '', $src);
+ $uri = file_stream_wrapper_uri_normalize($uri);
+
+ return $uri;
+}
+
+/**
+ * Implements hook_wysiwyg_plugin to modify the CKEditor image dialog for use
+ * with the picture module. The integration technique here is borrowed from
+ * imce_wysiwyg.
+ *
+ * Normally an array of plugin settings would be returned, but since it's
+ * problematic to have the WYSIWYG module load a plugin which doesn't have a
+ * button (an 'extension', in other words), this module takes care of
+ * loading the plugin itself. As a consequence, this plugin does not appear
+ * on the WYSIWYG button/plugin configuration page, and is always enabled
+ * for all WYSIWYG CKEditor profiles when picture groups have been enabled from
+ * /admin/config/media/picture/ckeditor
+ * @TODO: low priority - make a checkbox control loading of this extension. The
+ * imce_wysiwyg module does this by passing a CKEditor options array in the
+ * extension definition which is loaded only when the extension is enabled on
+ * the WYSIWYG config page.
+ *
+ * example code to get started with a checkbox would be:
+ *
+ * return array(
+ * 'picture_ckeditor' => array(
+ * 'extensions' => array('picture_ckeditor' => t('Responsive images
+ * with the Picture module')),
+ * 'url' => 'http://drupal.org/projects/picture',
+ * 'load' => FALSE,
+ * ),
+ * );
+ */
+function picture_wysiwyg_plugin($editor, $version) {
+
+ // Keep track of whether the JS has already been added since this hook is
+ // called multiple times on a page load.
+ static $picture_ckeditor_integrated = FALSE;
+
+ if ($editor == 'ckeditor' ) {
+ // Load our invocation scripts.
+ if (!$picture_ckeditor_integrated) {
+ // Create the Javascript array of picture group options.
+ $picture_groups = picture_get_mapping_options();
+ $ckeditor_groups = variable_get('picture_ckeditor_groups', array());
+ $groups = array();
+ // CKEditor expects an array of options formatted as
+ // ['Display name', 'machine_name'].
+ foreach ($picture_groups as $key => $value) {
+ if ($ckeditor_groups[$key]['enabled'] == 1) {
+ $groups[] = array($value, $key);
+ }
+ }
+ if (!empty($groups)) {
+ $groups[] = array('Not Set', 'not_set');
+ drupal_add_js(array(
+ 'picture' => array(
+ 'groups' => $groups,
+ 'label' => variable_get('picture_ckeditor_label', 'Image size (required)'),
+ ),
+ ), 'setting');
+ drupal_add_js(drupal_get_path('module', 'picture') . '/picture_ckeditor.js');
+ $picture_ckeditor_integrated = TRUE;
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function picture_uninstall() {
+ variable_del('picture_ckeditor_groups');
+ variable_del('picture_ckeditor_label');
+}
diff --git a/picture_ckeditor.js b/picture_ckeditor.js
new file mode 100644
index 0000000..69f5922
--- /dev/null
+++ b/picture_ckeditor.js
@@ -0,0 +1,181 @@
+/**
+ * @file picture_ckeditor.js
+ * Modify the CKEditor image dialog to insert responsive images.
+ *
+ * Supports inline responsive images with the Picture module.
+ * It also simplifies for the CKEditor image dialog.
+ */
+
+CKEDITOR.editorConfig = function(config)
+{
+ // This disables the browser resize handles since the width will now be set
+ // in the image dialog. This also prevents CKEditor from automatically adding
+ // pesky inline width and height styles, although these inline styles are
+ // still added when an image is dragged and dropped.
+ // Resize handles are also removed from tables as an unintended consequence.
+ config.disableObjectResizing = true;
+
+};
+
+// When opening a dialog, a 'definition' is created for it. For
+// each editor instance the 'dialogDefinition' event is then
+// fired. We can use this event to make customizations to the
+// definition of existing dialogs.
+CKEDITOR.on('dialogDefinition', function(event) {
+ // Take the dialog name.
+ var dialogName = event.data.name;
+ // The definition holds the structured data that is used to eventually
+ // build the dialog and we can use it to customize just about anything.
+ // In Drupal terms, it's sort of like CKEditor's version of a Forms API and
+ // what we're doing here is a bit like a hook_form_alter.
+ var dialogDefinition = event.data.definition;
+
+
+ // Resources for the following:
+ // Download: https://github.com/ckeditor/ckeditor-dev
+ // See /plugins/image/dialogs/image.js
+ // and refer to http://docs.ckeditor.com/#!/api/CKEDITOR.dialog.definition
+ // Visit: file:///[path_to_ckeditor-dev]/plugins/devtools/samples/devtools.html
+ // for an excellent way to find machine names for dialog elements.
+ if (dialogName == 'image') {
+ dialogDefinition.removeContents('advanced');
+ dialogDefinition.removeContents('Link');
+ var infoTab = dialogDefinition.getContents('info');
+ var altText = infoTab.get('txtAlt');
+ var IMAGE = 1,
+ LINK = 2,
+ PREVIEW = 4,
+ CLEANUP = 8;
+ // UpdatePreview is copied from ckeditor image plugin.
+ var updatePreview = function(dialog) {
+ // Don't load before onShow.
+ if (!dialog.originalElement || !dialog.preview) {
+ return 1;
+ }
+
+ // Read attributes and update imagePreview.
+ dialog.commitContent(PREVIEW, dialog.preview);
+ return 0;
+ };
+ // Add the select list for choosing the image width.
+ infoTab.add({
+ type: 'select',
+ id: 'imageSize',
+ label: Drupal.settings.picture.label,
+ items: Drupal.settings.picture.groups,
+ 'default': 'not_set',
+ onChange: function() {
+ var dialog = this.getDialog();
+ var element = dialog.originalElement;
+ element.setAttribute('data-picture-group', this.getValue());
+ updatePreview(this.getDialog());
+ },
+ setup: function(type, element) {
+ if (type == IMAGE) {
+ var value = element.getAttribute('data-picture-group');
+ this.setValue(value);
+ }
+ },
+ // Create a custom data-picture-group attribute.
+ commit: function(type, element) {
+ if (type == IMAGE) {
+ if (this.getValue() || this.isChanged()) {
+ element.setAttribute('data-picture-group', this.getValue());
+ }
+ } else if (type == PREVIEW) {
+ element.setAttribute('data-picture-group', this.getValue());
+ } else if (type == CLEANUP) {
+ element.setAttribute('data-picture-group', '');
+ }
+ },
+ validate: function() {
+ if (this.getValue() == 'not_set') {
+ var message = 'Please make a selection from ' + Drupal.settings.picture.label;
+ alert(message);
+ return false;
+ } else {
+ return true;
+ }
+ }
+ },
+ // Position before preview.
+ 'htmlPreview'
+ );
+
+ // Put a title attribute field on the main 'info' tab.
+ infoTab.add( {
+ type: 'text',
+ id: 'txtTitle',
+ label: 'The title attribute is used as a tooltip when the mouse hovers over the image.',
+ onChange: function() {
+ updatePreview(this.getDialog());
+ },
+ setup: function(type, element) {
+ if (type == IMAGE)
+ this.setValue(element.getAttribute('title'));
+ },
+ commit: function(type, element) {
+ if (type == IMAGE) {
+ if (this.getValue() || this.isChanged())
+ element.setAttribute('title', this.getValue());
+ } else if (type == PREVIEW) {
+ element.setAttribute('title', this.getValue());
+ } else if (type == CLEANUP) {
+ element.removeAttribute('title');
+ }
+ }
+ },
+ // Position before the imageSize select box.
+ 'htmlPreview'
+ );
+
+ // Add a select widget to choose image alignment.
+ infoTab.add({
+ type: 'select',
+ id: 'imageAlign',
+ label: 'Image Alignment',
+ items: [ [ 'Not Set', '' ], [ 'Left', 'left'],
+ [ 'Right', 'right' ], [ 'Center', 'center'] ],
+ 'default': '',
+ onChange: function() {
+ updatePreview(this.getDialog());
+ },
+ setup: function(type, element) {
+ if (type == IMAGE) {
+ var value = element.getAttribute('data-picture-align');
+ this.setValue(value);
+ }
+ },
+ // Creates a custom data-picture-align attribute since working with classes
+ // is more difficult. If we used classes, then we'd have to search for
+ // exisiting alignment classes and remove them before adding a new one.
+ // With the custom attribute we can always just overwrite it's value.
+ commit: function(type, element) {
+ if (type == IMAGE) {
+ if (this.getValue() || this.isChanged()) {
+ element.setAttribute('data-picture-align', this.getValue());
+ }
+ } else if (type == PREVIEW) {
+ element.setAttribute('data-picture-align', this.getValue());
+ } else if (type == CLEANUP) {
+ element.setAttribute('data-picture-align', '');
+ }
+ }
+
+ },
+ // Position before imageSize.
+ 'imageSize'
+ );
+
+ // Improve the alt field label. Copied from Drupal's image field.
+ altText.label = 'The alt attribute may be used by search engines, and screen readers.';
+
+ // Remove a bunch of extraneous fields. These properties will be set in
+ // the theme or module CSS.
+ infoFieldsRemove = [ 'cmbAlign', 'txtWidth', 'txtHeight', 'ratioLock', 'txtBorder',
+ 'txtHSpace', 'txtVSpace' ];
+ for (i = 0; i < infoFieldsRemove.length; i++) {
+ infoTab.remove(infoFieldsRemove[i]);
+ }
+ }
+});
diff --git a/picture_wysiwyg.css b/picture_wysiwyg.css
new file mode 100644
index 0000000..5b2086b
--- /dev/null
+++ b/picture_wysiwyg.css
@@ -0,0 +1,36 @@
+/* This CSS file needs to be included either in the theme used for
+* editing content in order to be included in the WYSIWYG edit iframe,
+* or specifically included in the WYSIWYG config page's
+* "Define CSS" textfield.
+*/
+span[data-picture-align="left"], img[data-picture-align="left"] {
+ float: left;
+}
+span[data-picture-align="right"], img[data-picture-align="right"] {
+ float: right;
+}
+span[data-picture-align="center"], img[data-picture-align="center"] {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+}
+/* Remove ugly boarders that bunch up in the image dialog table. */
+.cke_dialog_body tr td:last-child {
+ border-right: 0px;
+}
+/* The following is an example of what you could put in your theme
+ * to control the size of images. It is formatted as
+ * span[data-picture-group="[The machine name of your picture group]"
+span[data-picture-group="wide"] {
+ width: 100%;
+}
+span[data-picture-group="normal"] {
+ width: 50%;
+}
+span[data-picture-group="narrow"] {
+ width: 33%;
+}
+span[data-picture-group] img {
+ width: 100%;
+ height: auto;
+}*/