diff --git a/living_word/living_word.field.inc b/living_word/living_word.field.inc index e4ed12c..2ab6a22 100644 --- a/living_word/living_word.field.inc +++ b/living_word/living_word.field.inc @@ -116,6 +116,16 @@ function living_word_field_config() { )); } + + $fieldname = "{$prefix}_subverse"; + if (!in_array($fieldname, $fieldnames)) { + field_create_field(array( + 'field_name' => $fieldname, + 'type' => 'living_word_quote', + 'cardinality' => 1, + )); + } + } @@ -145,7 +155,7 @@ function living_word_field_instance_config() { 'label' => 'hidden', ), ), - 'widget' => array('weight' => 0), + 'widget' => array('weight' => 10), )); $instances[] = $fieldname; } @@ -165,7 +175,7 @@ function living_word_field_instance_config() { 'label' => 'hidden', ), ), - 'widget' => array('weight' => 1), + 'widget' => array('weight' => 20), )); $instances[] = $fieldname; } @@ -184,7 +194,7 @@ function living_word_field_instance_config() { 'type' => 'hidden', ), ), - 'widget' => array('weight' => 2), + 'widget' => array('weight' => 30), 'default_value' => array(array('value' => 0)), )); $instances[] = $fieldname; @@ -200,10 +210,7 @@ function living_word_field_instance_config() { 'description' => $t('Reference one or more verses from the Bible'), 'required' => true, 'widget' => array( - 'weight' => 3, -// 'type' => 'scripture_picker', -// 'settings' => array( -// ), + 'weight' => 40, ), 'display' => array( 'default' => array( @@ -221,6 +228,27 @@ function living_word_field_instance_config() { $instances[] = $fieldname; } + $fieldname = "{$prefix}_subverse"; + if (!in_array($fieldname, $instances) && in_array($fieldname, $fields)) { + field_create_instance(array( + 'field_name' => $fieldname, + 'label' => $t('Subverse'), + 'entity_type' => 'node', + 'bundle' => $bundle, + 'description' => $t('Quote a selection of the scripture verse'), + 'required' => true, + 'widget' => array( + 'weight' => 50, + ), + 'display' => array( + 'default' => array( + 'type' => 'hidden', + ), + ), + )); + $instances[] = $fieldname; + } + $settings = variable_get("living_word_vocabs",NULL); // load vocabulary for position @@ -247,7 +275,7 @@ function living_word_field_instance_config() { ), 'widget' => array( 'type' => 'options_select', - 'weight' => 4, + 'weight' => 60, ), )); $instances[] = $fieldname; @@ -283,7 +311,7 @@ function living_word_field_instance_config() { ), 'widget' => array( 'type' => 'options_select', - 'weight' => 5, + 'weight' => 70, ), )); $instances[] = $fieldname; @@ -310,7 +338,7 @@ function living_word_field_instance_config() { ), ), 'widget' => array( - 'weight' => 6, + 'weight' => 80, 'type' => 'options_buttons', ), )); @@ -332,7 +360,7 @@ function living_word_field_instance_config() { ), ), 'widget' => array( - 'weight' => 6, + 'weight' => 90, 'type' => 'options_buttons', ), )); diff --git a/living_word/living_word.info b/living_word/living_word.info index 7c47994..06c7bde 100644 --- a/living_word/living_word.info +++ b/living_word/living_word.info @@ -9,4 +9,5 @@ dependencies[] = taxonomy dependencies[] = scripture dependencies[] = options dependencies[] = list -dependencies[] = number \ No newline at end of file +dependencies[] = number +dependencies[] = living_word_quote \ No newline at end of file diff --git a/living_word/living_word.module b/living_word/living_word.module index 8573336..799c578 100644 --- a/living_word/living_word.module +++ b/living_word/living_word.module @@ -17,4 +17,17 @@ define('LIVING_WORD_LISTING_ORDER_APPLICATION_TAGS', 0x0003); */ function living_word_get_comments($picker_values = array(), $detail_values = array(), $position_values = array()) { -} \ No newline at end of file +} + + +/** + * Implements hook_field_widget_WIDGET_TYPE_form_alter(&$element, &$form_state, $context). + * https://api.drupal.org/api/drupal/modules!field!field.api.php/function/hook_field_widget_WIDGET_TYPE_form_alter/7 + * Specify the source text for the JS field widget, from which the quoted text + * must be selected. + */ +function living_word_field_widget_living_word_quote_widget_js_form_alter(&$element, &$form_state, $context) { + $element['#source_text'] = t("The Word is alive"); + // TODO: get the verse range associated with the node, etc. +} + diff --git a/living_word_quote/jquery.selection.js b/living_word_quote/jquery.selection.js new file mode 100644 index 0000000..5886074 --- /dev/null +++ b/living_word_quote/jquery.selection.js @@ -0,0 +1,354 @@ +/*! + * jQuery.selection - jQuery Plugin + * + * Copyright (c) 2010-2014 IWASAKI Koji (@madapaja). + * http://blog.madapaja.net/ + * Under The MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE + * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION + * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +(function($, win, doc) { + /** + * get caret status of the selection of the element + * + * @param {Element} element target DOM element + * @return {Object} return + * @return {String} return.text selected text + * @return {Number} return.start start position of the selection + * @return {Number} return.end end position of the selection + */ + var _getCaretInfo = function(element){ + var res = { + text: '', + start: 0, + end: 0 + }; + + if (!element.value) { + /* no value or empty string */ + return res; + } + + try { + if (win.getSelection) { + /* except IE */ + res.start = element.selectionStart; + res.end = element.selectionEnd; + res.text = element.value.slice(res.start, res.end); + } else if (doc.selection) { + /* for IE */ + element.focus(); + + var range = doc.selection.createRange(), + range2 = doc.body.createTextRange(); + + res.text = range.text; + + try { + range2.moveToElementText(element); + range2.setEndPoint('StartToStart', range); + } catch (e) { + range2 = element.createTextRange(); + range2.setEndPoint('StartToStart', range); + } + + res.start = element.value.length - range2.text.length; + res.end = res.start + range.text.length; + } + } catch (e) { + /* give up */ + } + + return res; + }; + + /** + * caret operation for the element + * @type {Object} + */ + var _CaretOperation = { + /** + * get caret position + * + * @param {Element} element target element + * @return {Object} return + * @return {Number} return.start start position for the selection + * @return {Number} return.end end position for the selection + */ + getPos: function(element) { + var tmp = _getCaretInfo(element); + return {start: tmp.start, end: tmp.end}; + }, + + /** + * set caret position + * + * @param {Element} element target element + * @param {Object} toRange caret position + * @param {Number} toRange.start start position for the selection + * @param {Number} toRange.end end position for the selection + * @param {String} caret caret mode: any of the following: "keep" | "start" | "end" + */ + setPos: function(element, toRange, caret) { + caret = this._caretMode(caret); + + if (caret === 'start') { + toRange.end = toRange.start; + } else if (caret === 'end') { + toRange.start = toRange.end; + } + + element.focus(); + try { + if (element.createTextRange) { + var range = element.createTextRange(); + + if (win.navigator.userAgent.toLowerCase().indexOf("msie") >= 0) { + toRange.start = element.value.substr(0, toRange.start).replace(/\r/g, '').length; + toRange.end = element.value.substr(0, toRange.end).replace(/\r/g, '').length; + } + + range.collapse(true); + range.moveStart('character', toRange.start); + range.moveEnd('character', toRange.end - toRange.start); + + range.select(); + } else if (element.setSelectionRange) { + element.setSelectionRange(toRange.start, toRange.end); + } + } catch (e) { + /* give up */ + } + }, + + /** + * get selected text + * + * @param {Element} element target element + * @return {String} return selected text + */ + getText: function(element) { + return _getCaretInfo(element).text; + }, + + /** + * get caret mode + * + * @param {String} caret caret mode + * @return {String} return any of the following: "keep" | "start" | "end" + */ + _caretMode: function(caret) { + caret = caret || "keep"; + if (caret === false) { + caret = 'end'; + } + + switch (caret) { + case 'keep': + case 'start': + case 'end': + break; + + default: + caret = 'keep'; + } + + return caret; + }, + + /** + * replace selected text + * + * @param {Element} element target element + * @param {String} text replacement text + * @param {String} caret caret mode: any of the following: "keep" | "start" | "end" + */ + replace: function(element, text, caret) { + var tmp = _getCaretInfo(element), + orig = element.value, + pos = $(element).scrollTop(), + range = {start: tmp.start, end: tmp.start + text.length}; + + element.value = orig.substr(0, tmp.start) + text + orig.substr(tmp.end); + + $(element).scrollTop(pos); + this.setPos(element, range, caret); + }, + + /** + * insert before the selected text + * + * @param {Element} element target element + * @param {String} text insertion text + * @param {String} caret caret mode: any of the following: "keep" | "start" | "end" + */ + insertBefore: function(element, text, caret) { + var tmp = _getCaretInfo(element), + orig = element.value, + pos = $(element).scrollTop(), + range = {start: tmp.start + text.length, end: tmp.end + text.length}; + + element.value = orig.substr(0, tmp.start) + text + orig.substr(tmp.start); + + $(element).scrollTop(pos); + this.setPos(element, range, caret); + }, + + /** + * insert after the selected text + * + * @param {Element} element target element + * @param {String} text insertion text + * @param {String} caret caret mode: any of the following: "keep" | "start" | "end" + */ + insertAfter: function(element, text, caret) { + var tmp = _getCaretInfo(element), + orig = element.value, + pos = $(element).scrollTop(), + range = {start: tmp.start, end: tmp.end}; + + element.value = orig.substr(0, tmp.end) + text + orig.substr(tmp.end); + + $(element).scrollTop(pos); + this.setPos(element, range, caret); + } + }; + + /* add jQuery.selection */ + $.extend({ + /** + * get selected text on the window + * + * @param {String} mode selection mode: any of the following: "text" | "html" + * @return {String} return + */ + selection: function(mode) { + var getText = ((mode || 'text').toLowerCase() === 'text'); + + try { + if (win.getSelection) { + if (getText) { + // get text + return win.getSelection().toString(); + } else { + // get html + var sel = win.getSelection(), range; + + if (sel.getRangeAt) { + range = sel.getRangeAt(0); + } else { + range = doc.createRange(); + range.setStart(sel.anchorNode, sel.anchorOffset); + range.setEnd(sel.focusNode, sel.focusOffset); + } + + return $('
').append(range.cloneContents()).html(); + } + } else if (doc.selection) { + if (getText) { + // get text + return doc.selection.createRange().text; + } else { + // get html + return doc.selection.createRange().htmlText; + } + } + } catch (e) { + /* give up */ + } + + return ''; + } + }); + + /* add selection */ + $.fn.extend({ + selection: function(mode, opts) { + opts = opts || {}; + + switch (mode) { + /** + * selection('getPos') + * get caret position + * + * @return {Object} return + * @return {Number} return.start start position for the selection + * @return {Number} return.end end position for the selection + */ + case 'getPos': + return _CaretOperation.getPos(this[0]); + + /** + * selection('setPos', opts) + * set caret position + * + * @param {Number} opts.start start position for the selection + * @param {Number} opts.end end position for the selection + */ + case 'setPos': + return this.each(function() { + _CaretOperation.setPos(this, opts); + }); + + /** + * selection('replace', opts) + * replace the selected text + * + * @param {String} opts.text replacement text + * @param {String} opts.caret caret mode: any of the following: "keep" | "start" | "end" + */ + case 'replace': + return this.each(function() { + _CaretOperation.replace(this, opts.text, opts.caret); + }); + + /** + * selection('insert', opts) + * insert before/after the selected text + * + * @param {String} opts.text insertion text + * @param {String} opts.caret caret mode: any of the following: "keep" | "start" | "end" + * @param {String} opts.mode insertion mode: any of the following: "before" | "after" + */ + case 'insert': + return this.each(function() { + if (opts.mode === 'before') { + _CaretOperation.insertBefore(this, opts.text, opts.caret); + } else { + _CaretOperation.insertAfter(this, opts.text, opts.caret); + } + }); + + /** + * selection('get') + * get selected text + * + * @return {String} return + */ + case 'get': + /* falls through */ + default: + return _CaretOperation.getText(this[0]); + } + + return this; + } + }); +})(jQuery, window, window.document); diff --git a/living_word_quote/living_word_quote.info b/living_word_quote/living_word_quote.info new file mode 100644 index 0000000..f5a2e50 --- /dev/null +++ b/living_word_quote/living_word_quote.info @@ -0,0 +1,6 @@ +name = Living Word Quote +description = Defines field types for quoting part of a text and caching that it in a field value. +core = 7.x +package = "RSC" + +dependencies[] = field \ No newline at end of file diff --git a/living_word_quote/living_word_quote.install b/living_word_quote/living_word_quote.install new file mode 100644 index 0000000..297dda2 --- /dev/null +++ b/living_word_quote/living_word_quote.install @@ -0,0 +1,32 @@ + array( + 'start' => array( + 'description' => 'Starting character of the quote', + 'type' => 'int', + 'not-null' => true, + 'unsigned' => true, + ), + 'text' => array( + 'description' => 'The cached quote', + 'type' => 'varchar', + 'length' => 100, + 'not-null' => true, + ), + ), + ); +} + diff --git a/living_word_quote/living_word_quote.js b/living_word_quote/living_word_quote.js new file mode 100644 index 0000000..ad09c88 --- /dev/null +++ b/living_word_quote/living_word_quote.js @@ -0,0 +1,39 @@ +(function ($) { + Drupal.behaviors.livingWordQuoteBehavior = { + attach: function (context, settings) { + + // find the form elements + var form_id = Drupal.settings.living_word_quote.form_id; + var form = $('#'+form_id+" div.fieldset-wrapper", context); + var form_start = form.find("input.living-word-quote-start"); + var form_text = form.find("input.living-word-quote-text"); + + // get the source text + var source_text = Drupal.settings.living_word_quote.source_text; + + // hide the text fields + // TODO: uncomment the following line once the eye candy is done + //form.find('div.form-item').hide(); + + // build the user interface + var textarea = $(""); + form.append(textarea); + + // act on text being selected + textarea.select(function (select_event) { + + // inspect the selection + var position = textarea.selection('getPos'); + var text = textarea.selection(); + + // fill in the form + form_start.val(position.start); + form_text.val(text); + + // TODO: eye candy selection thingy (like on android phones) + + }) + + } + }; +})(jQuery); \ No newline at end of file diff --git a/living_word_quote/living_word_quote.module b/living_word_quote/living_word_quote.module new file mode 100644 index 0000000..b7d8c88 --- /dev/null +++ b/living_word_quote/living_word_quote.module @@ -0,0 +1,246 @@ + t('LW quote'), + 'description' => t('Quote a part of a given text and cache that quote in the field.'), + 'default_widget' => 'living_word_quote_widget_js', + 'default_formatter' => 'text_plain' + ); + + return $fields; +} + + +/** + * Implements hook_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors). + * https://api.drupal.org/api/drupal/modules!field!field.api.php/function/hook_field_validate/7 + * Validate this module's field data. + * If there are validation problems, add to the $errors array (passed by reference). There is no return value. + */ +function living_word_quote_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) { + + // FAPI already checks that required fields are filled in + +} + + +/** + * Implements hook_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items). + * https://api.drupal.org/api/drupal/modules!field!field.api.php/function/hook_field_presave/7 + * Define custom presave behavior for this module's field types. + * Make changes or additions to field values by altering the $items parameter by reference. There is no return value. + */ +function living_word_quote_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) { + + foreach($items as &$item) { + if (empty($item['start'])) { + $item['start'] = 0; + } + } + +} + + +/* + * FIELD WIDGETS + * https://api.drupal.org/api/drupal/modules%21field%21field.api.php/group/field_widget/7 + */ + + +/** + * Implements hook_field_widget_info(). + * https://api.drupal.org/api/drupal/modules%21field%21field.api.php/function/hook_field_widget_info/7 + */ +function living_word_quote_field_widget_info() { + $widgets = array(); + + $widgets['living_word_quote_widget_simple'] = array( + 'label' => t('LW quote form'), + 'description' => t('A compound form element using two plain text fields.'), + 'field types' => array( + 'living_word_quote' + ), + 'behaviours' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, // the entire widget accepts one entry + 'default_value' => FIELD_BEHAVIOR_NONE // the widget can have no default value + ), + 'weight' => 10, + ); + + $widgets['living_word_quote_widget_js'] = array( + 'label' => t('LW quote selector'), + 'description' => t('A JS widget that allows the user to select a portion of the given text.'), + 'field types' => array( + 'living_word_quote' + ), + 'behaviours' => array( + 'multiple values' => FIELD_BEHAVIOR_DEFAULT, // the entire widget accepts one entry + 'default_value' => FIELD_BEHAVIOR_NONE // the widget can have no default value + ), + 'weight' => 20, + ); + + return $widgets; +} + + +/** + * Implements hook_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element). + * https://api.drupal.org/api/drupal/modules%21field%21field.api.php/function/hook_field_widget_form/7 + * Create the complex form element that constitutes the widget, using basic form elements. + */ +function living_word_quote_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) { + +// dpm($form, "form"); +// dpm($form_state, "form_state"); +// dpm($field, "field"); +// dpm($instance, "instance"); +// dpm($langcode, "langcode"); +// dpm($items, "items"); +// dpm($delta, "delta"); +// dpm($element, "element"); + + switch($instance['widget']['type']) { + case 'living_word_quote_widget_simple': + living_word_quote_field_widget_form_simple($form, $form_state, $field, $instance, $langcode, $items, $delta, $element); + break; + case 'living_word_quote_widget_js': + living_word_quote_field_widget_form_js($form, $form_state, $field, $instance, $langcode, $items, $delta, $element); + break; + } + + return $element; + +} + + +/** + * Build the simple widget form. Called from living_word_quote_field_widget_form(). + */ +function living_word_quote_field_widget_form_simple(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, &$element) { + + $element['#type'] = 'fieldset'; // put a nice border with title around the elements belonging to this field + + $element['#id'] = drupal_html_id("living-word-quote-form"); + + $element['start'] = array( + '#type' => 'textfield', + '#title' => t('Starting character position'), + '#size' => 3, + '#description' => t('This would be zero if the quote starts on the first character of the source text.'), + '#default_value' => empty($items[$delta]['start']) ? 0 : $items[$delta]['start'], + '#element_validate' => array('living_word_quote_element_validate_integer_positive_or_zero'), + '#attributes' => array( + 'class' => array('living-word-quote-start'), + ), + ); + + $element['text'] = array( + '#type' => 'textfield', + '#title' => t('Quoted text'), + '#default_value' => empty($items[$delta]['text']) ? '' : $items[$delta]['text'], + '#attributes' => array( + 'class' => array('living-word-quote-text'), + ), + ); + +} + + +/** + * Build the fancy JS widget form. Called from living_word_quote_field_widget_form(). + */ +function living_word_quote_field_widget_form_js(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, &$element) { + + // Base this form on the simple form + living_word_quote_field_widget_form_simple($form, $form_state, $field, $instance, $langcode, $items, $delta, $element); + + /* + * Add default source text. This should be changed by implementing + * hook_field_widget_living_word_quote_widget_js_form_alter + */ + $element['#source_text'] = t("The quick brown fox jumps over the lazy dog."); + + // Attach the javascript magic + $element['#attached']['js'] = array( + drupal_get_path('module', 'living_word_quote') . '/living_word_quote.js', + drupal_get_path('module', 'living_word_quote') . '/jquery.selection.js', + array( + 'data' => array( + 'living_word_quote' => array( + 'form_id' => &$element['#id'], + 'source_text' => &$element['#source_text'] + ), + ), + 'type' => 'setting', + ), + ); + +} + + +/** + * Implements hook_field_is_empty($item, $field). + * https://api.drupal.org/api/drupal/modules!field!field.api.php/function/hook_field_is_empty/7 + * Define what constitutes an empty item for a field type by returning TRUE or FALSE + */ +function living_word_quote_field_is_empty($item, $field) { + + return empty($item['text']); + +} + + +/** + * Just like the built in element_validate_integer_positive, but it allows zero + * https://api.drupal.org/api/drupal/includes!form.inc/function/element_validate_integer_positive/7 + */ +function living_word_quote_element_validate_integer_positive_or_zero($element, &$form_state) { + $value = $element['#value']; + if ($value !== '' && (!is_numeric($value) || intval($value) != $value || $value < 0)) { + form_error($element, t('%name must be a positive integer or zero.', array('%name' => $element['#title']))); + } +} + + +/** + * Implements hook_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items). + * https://api.drupal.org/api/drupal/modules!field!field.api.php/function/hook_field_prepare_view/7 + */ +function living_word_quote_field_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items) { + foreach (array_keys($entities) as $eid) { + foreach ($items[$eid] as &$item) { + + // we use text_plain as the field formatter, which expects the text at the 'value' key + $item['value'] = &$item['text']; + + } + } +} + + +/** + * Implements hook_field_widget_WIDGET_TYPE_form_alter(&$element, &$form_state, $context). + * https://api.drupal.org/api/drupal/modules!field!field.api.php/function/hook_field_widget_WIDGET_TYPE_form_alter/7 + * This is an example of how other modules, such as living_word_admin, can + * influence the JS field widget to specify the source text from which the + * quoted text must be selected. + */ +function living_word_quote_field_widget_living_word_quote_widget_js_form_alter(&$element, &$form_state, $context) { +// $element['#source_text'] = t("Why is the brown fox quick, but the dog lazy?"); +} +