diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 60476e4db2..a35bf0f3cc 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -14,6 +14,7 @@ //= require angular //= require angular-resource //= require angular-animate +//= require angular-sanitize //= require angularjs-file-upload //= require ../shared/ng-infinite-scroll.min.js //= require ../shared/ng-tags-input.min.js @@ -60,11 +61,6 @@ //= require ./variant_overrides/variant_overrides // text, dates and translations -//= require textAngular-rangy.min.js -// This replaces angular-sanitize. We should include only one. -// https://github.com/textAngular/textAngular#where-to-get-it -//= require textAngular-sanitize.min.js -//= require textAngular.min.js //= require i18n/translations //= require darkswarm/i18n.translate.js diff --git a/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee b/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee index 0ff8e4f515..81dbb30200 100644 --- a/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee +++ b/app/assets/javascripts/admin/enterprise_groups/enterprise_groups.js.coffee @@ -1 +1 @@ -angular.module("admin.enterprise_groups", ["admin.side_menu", "admin.users", "textAngular"]) +angular.module("admin.enterprise_groups", ["admin.side_menu", "admin.users", "ngSanitize"]) diff --git a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee index bd721c14e4..36c43fb440 100644 --- a/app/assets/javascripts/admin/enterprises/enterprises.js.coffee +++ b/app/assets/javascripts/admin/enterprises/enterprises.js.coffee @@ -3,7 +3,6 @@ angular.module("admin.enterprises", [ "admin.utils", "admin.shippingMethods", "admin.users", - "textAngular", "admin.side_menu", "admin.taxons", 'admin.indexUtils', @@ -11,16 +10,3 @@ angular.module("admin.enterprises", [ 'admin.dropdown', 'ngSanitize'] ) -# For more options: https://github.com/textAngular/textAngular/blob/master/src/textAngularSetup.js -.config [ - '$provide', ($provide) -> - $provide.decorator 'taTranslations', [ - '$delegate' - (taTranslations) -> - taTranslations.insertLink = { - tooltip: t('admin.enterprises.form.shop_preferences.shopfront_message_link_tooltip'), - dialogPrompt: t('admin.enterprises.form.shop_preferences.shopfront_message_link_prompt') - } - taTranslations - ] -] diff --git a/app/assets/javascripts/admin/products/products.js.coffee b/app/assets/javascripts/admin/products/products.js.coffee index e8c02e9763..f3f5213e72 100644 --- a/app/assets/javascripts/admin/products/products.js.coffee +++ b/app/assets/javascripts/admin/products/products.js.coffee @@ -1 +1 @@ -angular.module("admin.products", ["textAngular", "admin.utils", "OFNShared"]) +angular.module("admin.products", ["ngSanitize", "admin.utils", "OFNShared"]) diff --git a/app/assets/javascripts/admin/utils/directives/textangular_links_target_blank.js.coffee b/app/assets/javascripts/admin/utils/directives/textangular_links_target_blank.js.coffee deleted file mode 100644 index 0c8237ef5c..0000000000 --- a/app/assets/javascripts/admin/utils/directives/textangular_links_target_blank.js.coffee +++ /dev/null @@ -1,6 +0,0 @@ -angular.module("admin.utils").directive "textangularLinksTargetBlank", () -> - restrict: 'CA' - link: (scope, element, attrs) -> - setTimeout -> - element.find(".ta-editor").scope().defaultTagAttributes.a.target = '_blank' - , 500 diff --git a/app/assets/javascripts/admin/utils/directives/textangular_strip.js.coffee b/app/assets/javascripts/admin/utils/directives/textangular_strip.js.coffee deleted file mode 100644 index 5c43ca9167..0000000000 --- a/app/assets/javascripts/admin/utils/directives/textangular_strip.js.coffee +++ /dev/null @@ -1,11 +0,0 @@ -angular.module("admin.utils").directive "textangularStrip", () -> - restrict: 'CA' - link: (scope, element, attrs) -> - scope.stripFormatting = ($html) -> - element = document.createElement("div") - element.innerHTML = String($html) - allTags = element.getElementsByTagName("*") - for child in allTags - child.removeAttribute("style") - child.removeAttribute("class") - return element.innerHTML diff --git a/app/models/enterprise.rb b/app/models/enterprise.rb index 5a0c84d7ef..dfee85fef6 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -247,9 +247,17 @@ class Enterprise < ApplicationRecord count(distinct: true) end - # Remove any unsupported HTML. def long_description=(html) - super(HtmlSanitizer.sanitize(html)) + super(HtmlSanitizer.sanitize_and_enforce_link_target_blank(html)) + end + + def preferred_shopfront_message=(html) + self.prefers_shopfront_message = HtmlSanitizer.sanitize_and_enforce_link_target_blank(html) + end + + def preferred_shopfront_closed_message=(html) + self.prefers_shopfront_closed_message = + HtmlSanitizer.sanitize_and_enforce_link_target_blank(html) end def contact diff --git a/app/models/enterprise_group.rb b/app/models/enterprise_group.rb index 47a0ef4303..608cf8c879 100644 --- a/app/models/enterprise_group.rb +++ b/app/models/enterprise_group.rb @@ -76,12 +76,12 @@ class EnterpriseGroup < ApplicationRecord # Remove any unsupported HTML. def long_description - HtmlSanitizer.sanitize(super) + HtmlSanitizer.sanitize_and_enforce_link_target_blank(super) end # Remove any unsupported HTML. def long_description=(html) - super(HtmlSanitizer.sanitize(html)) + super(HtmlSanitizer.sanitize_and_enforce_link_target_blank(html)) end private diff --git a/app/services/html_sanitizer.rb b/app/services/html_sanitizer.rb index 84d78f6f5c..f5ef6aba8f 100644 --- a/app/services/html_sanitizer.rb +++ b/app/services/html_sanitizer.rb @@ -18,4 +18,16 @@ class HtmlSanitizer html, tags: ALLOWED_TAGS, attributes: (ALLOWED_ATTRIBUTES + ALLOWED_TRIX_DATA_ATTRIBUTES) ) end + + def self.sanitize_and_enforce_link_target_blank(html) + sanitize(enforce_link_target_blank(html)) + end + + def self.enforce_link_target_blank(html) + return if html.nil? + + Nokogiri::HTML::DocumentFragment.parse(html).tap do |document| + document.css("a").each { |link| link["target"] = "_blank" } + end.to_s + end end diff --git a/app/views/admin/enterprise_groups/_form_about.html.haml b/app/views/admin/enterprise_groups/_form_about.html.haml index ce8d03fc37..8eac772392 100644 --- a/app/views/admin/enterprise_groups/_form_about.html.haml +++ b/app/views/admin/enterprise_groups/_form_about.html.haml @@ -1,6 +1,5 @@ %fieldset.alpha.no-border-bottom#about_panel{ data: { "tabs-and-panels-target": "panel" } } %legend= t('.about') = f.field_container :long_description do - %text-angular{'id' => 'enterprise_group_long_description', 'name' => 'enterprise_group[long_description]', 'class' => 'text-angular', "textangular-links-target-blank" => true, - 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]"} - != @enterprise_group[:long_description] + = f.hidden_field :long_description, id: "enterprise_group_long_description" + %trix-editor{ input: "enterprise_group_long_description", "data-controller": "trixeditor" } diff --git a/app/views/admin/enterprises/form/_about_us.html.haml b/app/views/admin/enterprises/form/_about_us.html.haml index 77f2822511..2f4bed6069 100644 --- a/app/views/admin/enterprises/form/_about_us.html.haml +++ b/app/views/admin/enterprises/form/_about_us.html.haml @@ -4,14 +4,7 @@ .omega.eight.columns = f.text_field :description, maxlength: 255, placeholder: t('.desc_short_placeholder') .row - .alpha.three.columns + .alpha.eleven.columns = f.label :long_description, t('.desc_long') - .omega.eight.columns - -# textAngular toolbar options, add to the ta-toolbar array below and separate into groups with extra ],[ if needed: - -# ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'pre', 'quote'], - -# ['bold', 'italics', 'underline', 'strikeThrough', 'ul', 'ol', 'redo', 'undo', 'clear'], - -# ['justifyLeft','justifyCenter','justifyRight','indent','outdent'], - -# ['html', 'insertImage', 'insertLink', 'insertVideo'] - %text-angular{'ng-model' => 'Enterprise.long_description', 'id' => 'enterprise_long_description', 'name' => 'enterprise[long_description]', 'class' => 'text-angular', "textangular-links-target-blank" => true, - 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", - 'placeholder' => t('.desc_long_placeholder')} + = f.hidden_field :long_description, id: "enterprise_long_description" + %trix-editor{ input: "enterprise_long_description", "data-controller": "trixeditor" } diff --git a/app/views/admin/enterprises/form/_shop_preferences.html.haml b/app/views/admin/enterprises/form/_shop_preferences.html.haml index 616d631509..5b75001514 100644 --- a/app/views/admin/enterprises/form/_shop_preferences.html.haml +++ b/app/views/admin/enterprises/form/_shop_preferences.html.haml @@ -1,17 +1,13 @@ .row - .three.columns.alpha + .eleven.columns.alpha = f.label "enterprise_preferred_shopfront_message", t('.shopfront_message') - .eight.columns.omega - %text-angular{'ng-model' => 'Enterprise.preferred_shopfront_message', 'id' => 'enterprise_preferred_shopfront_message', 'name' => 'enterprise[preferred_shopfront_message]', 'class' => 'text-angular textangular-strip', 'ta-paste' => "stripFormatting($html)", "textangular-links-target-blank" => true, - 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", - 'placeholder' => t('.shopfront_message_placeholder')} + = f.hidden_field :preferred_shopfront_message, id: "enterprise_preferred_shopfront_message" + %trix-editor{ input: "enterprise_preferred_shopfront_message", "data-controller": "trixeditor", placeholder: t('.shopfront_message_placeholder') } .row - .three.columns.alpha + .eleven.columns.alpha = f.label "enterprise_preferred_shopfront_closed_message", t('.shopfront_closed_message') - .eight.columns.omega - %text-angular{'ng-model' => 'Enterprise.preferred_shopfront_closed_message', 'id' => 'enterprise_preferred_shopfront_closed_message', 'name' => 'enterprise[preferred_shopfront_closed_message]', 'class' => 'text-angular textangular-strip', 'ta-paste' => "stripFormatting($html)", "textangular-links-target-blank" => true, - 'ta-toolbar' => "[['h1','h2','h3','h4','p'],['bold','italics','underline','clear'],['insertLink']]", - 'placeholder' => t('.shopfront_closed_message_placeholder')} + = f.hidden_field :preferred_shopfront_closed_message, id: "enterprise_preferred_shopfront_closed_message" + %trix-editor{ input: "enterprise_preferred_shopfront_closed_message", "data-controller": "trixeditor", placeholder: t('.shopfront_closed_message_placeholder') } .row .text-normal @@ -77,7 +73,7 @@ = f.radio_button :preferred_product_low_stock_display, true, 'ng-model' => 'Enterprise.preferred_product_low_stock_display', 'ng-value' => 'true' = f.label :preferred_product_low_stock_display, t('.enabled'), value: :true .five.columns.omega - = f.radio_button :preferred_product_low_stock_display, false, 'ng-model' => 'Enterprise.preferred_product_low_stock_display', 'ng-value' => 'false' + = f.radio_button :preferred_product_low_stock_display, false, 'ng-model' => 'Enterprise.preferred_product_low_stock_display', 'ng-value' => 'false' = f.label :preferred_product_low_stock_display, t('.disabled'), value: :false .row diff --git a/app/webpacker/css/admin/all.scss b/app/webpacker/css/admin/all.scss index 0534149116..a63e543f90 100644 --- a/app/webpacker/css/admin/all.scss +++ b/app/webpacker/css/admin/all.scss @@ -6,7 +6,6 @@ @import "~jquery-ui/themes/base/resizable"; @import "vendor/assets/stylesheets/jquery-ui-theme"; @import "~jquery-ui/themes/base/dialog"; -@import "../shared/textAngular"; @import "../shared/ng-tags-input.min"; @import "vendor/assets/stylesheets/select2.css.scss"; @import "~flatpickr/dist/flatpickr"; @@ -71,7 +70,6 @@ @import "components/todo"; @import "components/tooltip"; @import "components/wizard_progress"; -@import "components/text-angular"; @import "pages/enterprise_form"; @import "pages/subscription_form"; diff --git a/app/webpacker/css/admin/components/text-angular.scss b/app/webpacker/css/admin/components/text-angular.scss deleted file mode 100644 index d6eab98151..0000000000 --- a/app/webpacker/css/admin/components/text-angular.scss +++ /dev/null @@ -1,43 +0,0 @@ -// textAngular wysiwyg -text-angular { - .ta-editor { - border: 1px solid $pale-blue; - border-radius: 3px; - } - - .ta-toolbar { - border: 1px solid #cdd9e4; - padding: 0.4em; - margin-bottom: -1px; - background-color: #f1f1f1; - border-radius: 0.25em 0.25em 0 0; - } - .ta-scroll-window > .ta-bind { - max-height: 400px; - min-height: 100px; - outline: none; - p { - margin-bottom: 1.5rem; - } - } - .ta-scroll-window.form-control { - min-height: 100px; - box-shadow: none !important; - } - .btn-group { - display: inline; - margin-right: 8px; - button { - padding: 5px 10px !important; // Add `!important` to be more specific than the default button styles (app/webpacker/css/admin/components/buttons.scss) - // Hope this (text-angular) will be removed soon in order to use trix editor - margin-right: 0.25em; - } - button.active:not(:hover) { - box-shadow: 0 0 0.7em rgba(0, 0, 0, 0.3) inset; - background-color: #4583bf; - } - } - a { - color: $spree-green; - } -} diff --git a/app/webpacker/css/admin/trix.scss b/app/webpacker/css/admin/trix.scss index 78c149dc32..036e31f235 100644 --- a/app/webpacker/css/admin/trix.scss +++ b/app/webpacker/css/admin/trix.scss @@ -1,3 +1,7 @@ +trix-toolbar .trix-button-row { + flex-wrap: wrap; +} + trix-toolbar [data-trix-button-group="file-tools"] { display: none; } @@ -11,7 +15,7 @@ trix-editor { color: #222; a { - color: #f27052; // Equivalent to text-angular a + color: #f27052; } @include trix-styles; diff --git a/app/webpacker/css/admin_v3/all.scss b/app/webpacker/css/admin_v3/all.scss index 808a426caa..a5c673b909 100644 --- a/app/webpacker/css/admin_v3/all.scss +++ b/app/webpacker/css/admin_v3/all.scss @@ -10,7 +10,6 @@ @import "~jquery-ui/themes/base/resizable"; @import "vendor/assets/stylesheets/jquery-ui-theme"; @import "~jquery-ui/themes/base/dialog"; -@import "../shared/textAngular"; @import "../shared/ng-tags-input.min"; @import "vendor/assets/stylesheets/select2.css.scss"; @import "~flatpickr/dist/flatpickr"; @@ -75,7 +74,6 @@ @import "../admin/components/todo"; @import "../admin/components/tooltip"; @import "../admin/components/wizard_progress"; -@import "components/text-angular"; // admin_v3 @import "../admin/pages/enterprise_form"; @import "../admin/pages/subscription_form"; diff --git a/app/webpacker/css/admin_v3/components/text-angular.scss b/app/webpacker/css/admin_v3/components/text-angular.scss deleted file mode 100644 index ca4f162d32..0000000000 --- a/app/webpacker/css/admin_v3/components/text-angular.scss +++ /dev/null @@ -1,43 +0,0 @@ -// textAngular wysiwyg -text-angular { - .ta-editor { - border: 1px solid $pale-blue; - border-radius: 3px; - } - - .ta-toolbar { - border: 1px solid #cdd9e4; - padding: 0.4em; - margin-bottom: -1px; - background-color: #f1f1f1; - border-radius: 0.25em 0.25em 0 0; - } - .ta-scroll-window > .ta-bind { - max-height: 400px; - min-height: 100px; - outline: none; - p { - margin-bottom: 1.5rem; - } - } - .ta-scroll-window.form-control { - min-height: 100px; - box-shadow: none !important; - } - .btn-group { - display: inline; - margin-right: 8px; - button { - padding: 0 8px !important; // Add `!important` to be more specific than the default button styles (app/webpacker/css/admin/components/buttons.scss) - // Hope this (text-angular) will be removed soon in order to use trix editor - margin-right: 0.25em; - } - button.active:not(:hover) { - box-shadow: 0 0 0.7em rgba(0, 0, 0, 0.3) inset; - background-color: #4583bf; - } - } - a { - color: $spree-green; - } -} diff --git a/app/webpacker/css/shared/textAngular.css b/app/webpacker/css/shared/textAngular.css deleted file mode 100644 index a2f76234dc..0000000000 --- a/app/webpacker/css/shared/textAngular.css +++ /dev/null @@ -1,193 +0,0 @@ -.ta-hidden-input { - width: 1px; - height: 1px; - border: none; - margin: 0; - padding: 0; - position: absolute; - top: -10000px; - left: -10000px; - opacity: 0; - overflow: hidden; -} - -/* add generic styling for the editor */ -.ta-root.focussed > .ta-scroll-window.form-control { - border-color: #66afe9; - outline: 0; - -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); - -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); - box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); -} - -.ta-editor.ta-html, .ta-scroll-window.form-control { - min-height: 300px; - height: auto; - overflow: auto; - font-family: inherit; - font-size: 100%; -} - -.ta-scroll-window.form-control { - position: relative; - padding: 0; -} - -.ta-scroll-window > .ta-bind { - height: auto; - min-height: 300px; - padding: 6px 12px; -} - -.ta-editor:focus { - user-select: text; -} - -/* add the styling for the awesomness of the resizer */ -.ta-resizer-handle-overlay { - z-index: 100; - position: absolute; - display: none; -} - -.ta-resizer-handle-overlay > .ta-resizer-handle-info { - position: absolute; - bottom: 16px; - right: 16px; - border: 1px solid black; - background-color: #FFF; - padding: 0 4px; - opacity: 0.7; -} - -.ta-resizer-handle-overlay > .ta-resizer-handle-background { - position: absolute; - bottom: 5px; - right: 5px; - left: 5px; - top: 5px; - border: 1px solid black; - background-color: rgba(0, 0, 0, 0.2); -} - -.ta-resizer-handle-overlay > .ta-resizer-handle-corner { - width: 10px; - height: 10px; - position: absolute; -} - -.ta-resizer-handle-overlay > .ta-resizer-handle-corner-tl{ - top: 0; - left: 0; - border-left: 1px solid black; - border-top: 1px solid black; -} - -.ta-resizer-handle-overlay > .ta-resizer-handle-corner-tr{ - top: 0; - right: 0; - border-right: 1px solid black; - border-top: 1px solid black; -} - -.ta-resizer-handle-overlay > .ta-resizer-handle-corner-bl{ - bottom: 0; - left: 0; - border-left: 1px solid black; - border-bottom: 1px solid black; -} - -.ta-resizer-handle-overlay > .ta-resizer-handle-corner-br{ - bottom: 0; - right: 0; - border: 1px solid black; - cursor: se-resize; - background-color: white; -} - -/* copy the popover code from bootstrap so this will work even without it */ -.popover { - position: absolute; - top: 0; - left: 0; - z-index: 1060; - display: none; - max-width: 276px; - padding: 1px; - font-size: 14px; - font-weight: normal; - line-height: 1.42857143; - text-align: left; - white-space: normal; - background-color: #fff; - -webkit-background-clip: padding-box; - background-clip: padding-box; - border: 1px solid #ccc; - border: 1px solid rgba(0, 0, 0, .2); - border-radius: 6px; - -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); - box-shadow: 0 5px 10px rgba(0, 0, 0, .2); -} -.popover.top { - margin-top: -10px; -} -.popover.bottom { - margin-top: 10px; -} -.popover-title { - padding: 8px 14px; - margin: 0; - font-size: 14px; - background-color: #f7f7f7; - border-bottom: 1px solid #ebebeb; - border-radius: 5px 5px 0 0; -} -.popover-content { - padding: 9px 14px; -} -.popover > .arrow, -.popover > .arrow:after { - position: absolute; - display: block; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; -} -.popover > .arrow { - border-width: 11px; -} -.popover > .arrow:after { - content: ""; - border-width: 10px; -} -.popover.top > .arrow { - bottom: -11px; - left: 50%; - margin-left: -11px; - border-top-color: #999; - border-top-color: rgba(0, 0, 0, .25); - border-bottom-width: 0; -} -.popover.top > .arrow:after { - bottom: 1px; - margin-left: -10px; - content: " "; - border-top-color: #fff; - border-bottom-width: 0; -} -.popover.bottom > .arrow { - top: -11px; - left: 50%; - margin-left: -11px; - border-top-width: 0; - border-bottom-color: #999; - border-bottom-color: rgba(0, 0, 0, .25); -} -.popover.bottom > .arrow:after { - top: 1px; - margin-left: -10px; - content: " "; - border-top-width: 0; - border-bottom-color: #fff; -} diff --git a/app/webpacker/css/shared/trix.scss b/app/webpacker/css/shared/trix.scss index 7953954df7..10cc0c805a 100644 --- a/app/webpacker/css/shared/trix.scss +++ b/app/webpacker/css/shared/trix.scss @@ -16,7 +16,7 @@ div, pre, h1 { - margin-bottom: 1.5rem; // Equivalent to text-angular p (trix doesn't use p as default one, since we could not include figures inside p) + margin-bottom: 1.5rem; // // Equivalent to p (trix doesn't use p as separator by default, so emulate div as p to be backward compatible) } h1 { diff --git a/app/webpacker/packs/admin.js b/app/webpacker/packs/admin.js index a2a61ce7a8..1625ce11bb 100644 --- a/app/webpacker/packs/admin.js +++ b/app/webpacker/packs/admin.js @@ -11,6 +11,11 @@ window.bigDecimal = bigDecimal; import Trix from "trix"; +document.addEventListener("trix-before-initialize", (event) => { + // Set Trix translations + Object.assign(Trix.config.lang, I18n.t("js.trix")); +}); + document.addEventListener("trix-file-accept", (event) => { event.preventDefault(); }); diff --git a/config/initializers/haml.rb b/config/initializers/haml.rb index ec536959f1..eff82bc417 100644 --- a/config/initializers/haml.rb +++ b/config/initializers/haml.rb @@ -21,6 +21,5 @@ Haml::BOOLEAN_ATTRIBUTES.push( ofn-disable-enter question-mark-with-tooltip-animation scroll-after-load - textangular-links-target-blank ] ) diff --git a/config/locales/en.yml b/config/locales/en.yml index 44ce0f6c86..f801cec245 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1283,8 +1283,6 @@ en: shopfront_message: "Shopfront Message" shopfront_message_placeholder: > An optional message to welcome customers and explain how to shop with you. If text is entered here it will be displayed in a home tab when customers first arrive at your shopfront. - shopfront_message_link_tooltip: "Insert / edit link" - shopfront_message_link_prompt: "Please enter a URL to insert" shopfront_closed_message: "Shopfront Closed Message" shopfront_closed_message_placeholder: > A message which provides a more detailed explanation about why your shop is @@ -3735,6 +3733,24 @@ See the %{link} to find out more about %{sitename}'s features and to start using There was a problem setting up your card in our payments gateway. Please refresh the page and try again, if it fails a second time, please contact us for support. + trix: + bold: "Bold" + bullets: "Bullets" + code: "Code" + heading1: "Heading" + hr: "Horizontal rule" + indent: "Increase Level" + italic: "Italic" + link: "Link" + numbers: "Numbers" + outdent: "Decrease Level" + quote: "Quote" + redo: "Redo" + strike: "Strikethrough" + undo: "Undo" + unlink: "Unlink" + url: "URL" + urlPlaceholder: "Please enter a URL to insert" # Singular and plural forms of commonly used words. # We use these entries to pluralize unit names in every language. diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js index 33a5b2bdda..76e961e315 100644 --- a/spec/javascripts/application_spec.js +++ b/spec/javascripts/application_spec.js @@ -8,9 +8,7 @@ //= require angular-flash.min.js //= require shared/ng-tags-input.min.js //= require shared/mm-foundation-tpls-0.9.0-20180826174721.min.js -//= require textAngular-rangy.min.js -//= require textAngular-sanitize.min.js -//= require textAngular.min.js +//= require angular-sanitize //= require moment/min/moment.min.js //= require i18n //= require handlebars diff --git a/spec/models/enterprise_spec.rb b/spec/models/enterprise_spec.rb index 62cf6ced6e..de519e52e8 100644 --- a/spec/models/enterprise_spec.rb +++ b/spec/models/enterprise_spec.rb @@ -298,6 +298,24 @@ RSpec.describe Enterprise do end end + describe "preferred_shopfront_message" do + it "sanitises HTML" do + enterprise = build(:enterprise, preferred_shopfront_message: + 'Hello dearest monster.') + expect(enterprise.preferred_shopfront_message) + .to eq "Hello alert dearest monster." + end + end + + describe "preferred_shopfront_closed_message" do + it "sanitises HTML" do + enterprise = build(:enterprise, preferred_shopfront_closed_message: + 'Hello dearest monster.') + expect(enterprise.preferred_shopfront_closed_message) + .to eq "Hello alert dearest monster." + end + end + describe "preferred_shopfront_taxon_order" do it "empty strings are valid" do enterprise = build(:enterprise, preferred_shopfront_taxon_order: "") diff --git a/spec/services/html_sanitizer_spec.rb b/spec/services/html_sanitizer_spec.rb index bda9eb2188..60a247a63f 100644 --- a/spec/services/html_sanitizer_spec.rb +++ b/spec/services/html_sanitizer_spec.rb @@ -98,4 +98,21 @@ RSpec.describe HtmlSanitizer do expect(subject.sanitize(html)).to eq('
...
') end end + + context "when HTML has links" do + describe "#sanitize" do + it "doesn't add target blank to links" do + html = 'Link' + expect(subject.sanitize(html)).to eq('Link') + end + end + + describe "#sanitize_and_enforece_link_target_blank" do + it "adds target blank to links so they open in new windows" do + html = 'Link' + expect(subject.sanitize_and_enforce_link_target_blank(html)) + .to eq('Link') + end + end + end end diff --git a/spec/system/admin/enterprises_spec.rb b/spec/system/admin/enterprises_spec.rb index effdc83c2b..86d007a46b 100644 --- a/spec/system/admin/enterprises_spec.rb +++ b/spec/system/admin/enterprises_spec.rb @@ -112,10 +112,8 @@ RSpec.describe ' click_link "About" end fill_in 'enterprise_description', with: 'Connecting farmers and eaters' - - description_input = - page.find("text-angular#enterprise_long_description div[id^='taTextElement']") - description_input.native.send_keys('This is an interesting long description') + fill_in_trix_editor 'enterprise_long_description', + with: 'This is an interesting long description' # Check StimulusJs switching of sidebar elements accept_alert do @@ -202,9 +200,8 @@ RSpec.describe ' accept_alert do within(".side_menu") { find(:link, "Shop Preferences").trigger("click") } end - shop_message_input = - page.find("text-angular#enterprise_preferred_shopfront_message div[id^='taTextElement']") - shop_message_input.native.send_keys('This is my shopfront message.') + fill_in_trix_editor 'enterprise_preferred_shopfront_message', + with: 'This is my shopfront message.' expect(page) .to have_checked_field "enterprise_preferred_shopfront_order_cycle_order_orders_close_at" # using "find" as fields outside of the screen and are not visible @@ -258,11 +255,11 @@ RSpec.describe ' page.execute_script('window.history.forward()') expect(page).to have_content 'This is my shopfront message.' - # Test that the right input alert text is displayed - accept_alert('Please enter a URL to insert') do - first('.ta-text').click - first('button[name="insertLink"]').click - end + # Test Trix editor translations are loaded + find(".trix-button--icon-link").click + expect(page).to have_selector( + "input[aria-label=URL][placeholder='Please enter a URL to insert']" + ) end describe "producer properties" do diff --git a/vendor/assets/javascripts/textAngular-rangy.min.js b/vendor/assets/javascripts/textAngular-rangy.min.js deleted file mode 100644 index 1c7e71a2fa..0000000000 --- a/vendor/assets/javascripts/textAngular-rangy.min.js +++ /dev/null @@ -1,478 +0,0 @@ -/** - * Rangy, a cross-browser JavaScript range and selection library - * https://github.com/timdown/rangy - * - * Copyright 2015, Tim Down - * Licensed under the MIT license. - * Version: 1.3.0 - * Build date: 10 May 2015 - */ -!function(a,b){"function"==typeof define&&define.amd? -// AMD. Register as an anonymous module. -define(a):"undefined"!=typeof module&&"object"==typeof exports? -// Node/CommonJS style -module.exports=a(): -// No AMD or CommonJS support so we place Rangy in (probably) the global variable -b.rangy=a()}(function(){/*----------------------------------------------------------------------------------------------------------------*/ -// Trio of functions taken from Peter Michaux's article: -// http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting -function a(a,b){var c=typeof a[b];return c==u||!(c!=t||!a[b])||"unknown"==c}function b(a,b){return!(typeof a[b]!=t||!a[b])}function c(a,b){return typeof a[b]!=v} -// Creates a convenience function to save verbose repeated calls to tests functions -function d(a){return function(b,c){for(var d=c.length;d--;)if(!a(b,c[d]))return!1;return!0}}function e(a){return a&&A(a,z)&&C(a,y)}function f(a){return b(a,"body")?a.body:a.getElementsByTagName("body")[0]}function g(b){typeof console!=v&&a(console,"log")&&console.log(b)}function h(a,b){F&&b?alert(a):g(a)}function i(a){H.initialized=!0,H.supported=!1,h("Rangy is not supported in this environment. Reason: "+a,H.config.alertOnFail)}function j(a){h("Rangy warning: "+a,H.config.alertOnWarn)}function k(a){return a.message||a.description||String(a)} -// Initialization -function l(){if(F&&!H.initialized){var b,c=!1,d=!1; -// First, perform basic feature tests -a(document,"createRange")&&(b=document.createRange(),A(b,x)&&C(b,w)&&(c=!0));var h=f(document);if(!h||"body"!=h.nodeName.toLowerCase())return void i("No body element found");if(h&&a(h,"createTextRange")&&(b=h.createTextRange(),e(b)&&(d=!0)),!c&&!d)return void i("Neither Range nor TextRange are available");H.initialized=!0,H.features={implementsDomRange:c,implementsTextRange:d}; -// Initialize modules -var j,l;for(var m in E)(j=E[m])instanceof p&&j.init(j,H); -// Call init listeners -for(var n=0,o=K.length;nb?(f.node=d,f.offset-=b):f.node==a.parentNode&&f.offset>e(a)&&++f.offset;return d}function o(a){if(9==a.nodeType)return a;if(typeof a.ownerDocument!=F)return a.ownerDocument;if(typeof a.document!=F)return a.document;if(a.parentNode)return o(a.parentNode);throw b.createError("getDocument: no document found for node")}function p(a){var c=o(a);if(typeof c.defaultView!=F)return c.defaultView;if(typeof c.parentWindow!=F)return c.parentWindow;throw b.createError("Cannot get a window object for node")}function q(a){if(typeof a.contentDocument!=F)return a.contentDocument;if(typeof a.contentWindow!=F)return a.contentWindow.document;throw b.createError("getIframeDocument: No Document object found for iframe element")}function r(a){if(typeof a.contentWindow!=F)return a.contentWindow;if(typeof a.contentDocument!=F)return a.contentDocument.defaultView;throw b.createError("getIframeWindow: No Window object found for iframe element")} -// This looks bad. Is it worth it? -function s(a){return a&&G.isHostMethod(a,"setTimeout")&&G.isHostObject(a,"document")}function t(a,b,c){var d;if(a?G.isHostProperty(a,"nodeType")?d=1==a.nodeType&&"iframe"==a.tagName.toLowerCase()?q(a):o(a):s(a)&&(d=a.document):d=document,!d)throw b.createError(c+"(): Parameter must be a Window object or DOM node");return d}function u(a){for(var b;b=a.parentNode;)a=b;return a}function v(a,c,d,f){ -// See http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Comparing -var h,i,k,l,m;if(a==d) -// Case 1: nodes are the same -return c===f?0:c[index:"+e(a)+",length:"+a.childNodes.length+"]["+(a.innerHTML||"[innerHTML not supported]").slice(0,25)+"]"}return a.nodeName}function y(a){for(var b,c=o(a).createDocumentFragment();b=a.firstChild;)c.appendChild(b);return c}function z(a,b,c){var d=H(a),e=a.createElement("div");e.contentEditable=""+!!c,b&&(e.innerHTML=b); -// Insert the test element at the start of the body to prevent scrolling to the bottom in iOS (issue #292) -var f=d.firstChild;return f?d.insertBefore(e,f):d.appendChild(e),e}function A(a){return a.parentNode.removeChild(a)}function B(a){this.root=a,this._next=a}function C(a){return new B(a)}function D(a,b){this.node=a,this.offset=b}function E(a){this.code=this[a],this.codeName=a,this.message="DOMException: "+this.codeName}var F="undefined",G=a.util,H=G.getBody; -// Perform feature tests -G.areHostMethods(document,["createDocumentFragment","createElement","createTextNode"])||b.fail("document missing a Node creation method"),G.isHostMethod(document,"getElementsByTagName")||b.fail("document missing getElementsByTagName method");var I=document.createElement("div");G.areHostMethods(I,["insertBefore","appendChild","cloneNode"]||!G.areHostObjects(I,["previousSibling","nextSibling","childNodes","parentNode"]))||b.fail("Incomplete Element implementation"), -// innerHTML is required for Range's createContextualFragment method -G.isHostProperty(I,"innerHTML")||b.fail("Element is missing innerHTML property");var J=document.createTextNode("test");G.areHostMethods(J,["splitText","deleteData","insertData","appendData","cloneNode"]||!G.areHostObjects(I,["previousSibling","nextSibling","childNodes","parentNode"])||!G.areHostProperties(J,["data"]))||b.fail("Incomplete Text Node implementation");/*----------------------------------------------------------------------------------------------------------------*/ -// Removed use of indexOf because of a bizarre bug in Opera that is thrown in one of the Acid3 tests. I haven't been -// able to replicate it outside of the test. The bug is that indexOf returns -1 when called on an Array that -// contains just the document as a single element and the value searched for is the document. -var K=/*Array.prototype.indexOf ? - function(arr, val) { - return arr.indexOf(val) > -1; - }:*/ -function(a,b){for(var c=a.length;c--;)if(a[c]===b)return!0;return!1},L=!1;!function(){var b=document.createElement("b");b.innerHTML="1";var c=b.firstChild;b.innerHTML="
",L=w(c),a.features.crashyTextNodes=L}();var M;typeof window.getComputedStyle!=F?M=function(a,b){return p(a).getComputedStyle(a,null)[b]}:typeof document.documentElement.currentStyle!=F?M=function(a,b){return a.currentStyle?a.currentStyle[b]:""}:b.fail("No means of obtaining computed style properties found"),B.prototype={_current:null,hasNext:function(){return!!this._next},next:function(){var a,b,c=this._current=this._next;if(this._current)if(a=c.firstChild)this._next=a;else{for(b=null;c!==this.root&&!(b=c.nextSibling);)c=c.parentNode;this._next=b}return this._current},detach:function(){this._current=this._next=this.root=null}},D.prototype={equals:function(a){return!!a&&this.node===a.node&&this.offset==a.offset},inspect:function(){return"[DomPosition("+x(this.node)+":"+this.offset+")]"},toString:function(){return this.inspect()}},E.prototype={INDEX_SIZE_ERR:1,HIERARCHY_REQUEST_ERR:3,WRONG_DOCUMENT_ERR:4,NO_MODIFICATION_ALLOWED_ERR:7,NOT_FOUND_ERR:8,NOT_SUPPORTED_ERR:9,INVALID_STATE_ERR:11,INVALID_NODE_TYPE_ERR:24},E.prototype.toString=function(){return this.message},a.dom={arrayContains:K,isHtmlNamespace:c,parentElement:d,getNodeIndex:e,getNodeLength:f,getCommonAncestor:g,isAncestorOf:h,isOrIsAncestorOf:i,getClosestAncestorIn:j,isCharacterDataNode:k,isTextOrCommentNode:l,insertAfter:m,splitDataNode:n,getDocument:o,getWindow:p,getIframeWindow:r,getIframeDocument:q,getBody:H,isWindow:s,getContentDocument:t,getRootContainer:u,comparePoints:v,isBrokenNode:w,inspectNode:x,getComputedStyleProperty:M,createTestElement:z,removeNode:A,fragmentFromNodeChildren:y,createIterator:C,DomPosition:D},a.DOMException=E}),/*----------------------------------------------------------------------------------------------------------------*/ -// Pure JavaScript implementation of DOM Range -H.createCoreModule("DomRange",["DomUtil"],function(a,b){/*----------------------------------------------------------------------------------------------------------------*/ -// Utility functions -function c(a,b){return 3!=a.nodeType&&(P(a,b.startContainer)||P(a,b.endContainer))}function d(a){return a.document||Q(a.startContainer)}function e(a){return W(a.startContainer)}function f(a){return new L(a.parentNode,O(a))}function g(a){return new L(a.parentNode,O(a)+1)}function h(a,b,c){var d=11==a.nodeType?a.firstChild:a;return N(b)?c==b.length?J.insertAfter(a,b):b.parentNode.insertBefore(a,0==c?b:S(b,c)):c>=b.childNodes.length?b.appendChild(a):b.insertBefore(a,b.childNodes[c]),d}function i(a,b,c){if(z(a),z(b),d(b)!=d(a))throw new M("WRONG_DOCUMENT_ERR");var e=R(a.startContainer,a.startOffset,b.endContainer,b.endOffset),f=R(a.endContainer,a.endOffset,b.startContainer,b.startOffset);return c?e<=0&&f>=0:e<0&&f>0}function j(a){for(var b,c,e,f=d(a.range).createDocumentFragment();c=a.next();){if(b=a.isPartiallySelectedSubtree(),c=c.cloneNode(!b),b&&(e=a.getSubtreeIterator(),c.appendChild(j(e)),e.detach()),10==c.nodeType)// DocumentType -throw new M("HIERARCHY_REQUEST_ERR");f.appendChild(c)}return f}function k(a,b,c){var d,e;c=c||{stop:!1};for(var f,g;f=a.next();)if(a.isPartiallySelectedSubtree()){if(b(f)===!1)return void(c.stop=!0);if( -// The node is partially selected by the Range, so we can use a new RangeIterator on the portion of -// the node selected by the Range. -g=a.getSubtreeIterator(),k(g,b,c),g.detach(),c.stop)return}else for( -// The whole node is selected, so we can use efficient DOM iteration to iterate over the node and its -// descendants -d=J.createIterator(f);e=d.next();)if(b(e)===!1)return void(c.stop=!0)}function l(a){for(var b;a.next();)a.isPartiallySelectedSubtree()?(b=a.getSubtreeIterator(),l(b),b.detach()):a.remove()}function m(a){for(var b,c,e=d(a.range).createDocumentFragment();b=a.next();){if(a.isPartiallySelectedSubtree()?(b=b.cloneNode(!1),c=a.getSubtreeIterator(),b.appendChild(m(c)),c.detach()):a.remove(),10==b.nodeType)// DocumentType -throw new M("HIERARCHY_REQUEST_ERR");e.appendChild(b)}return e}function n(a,b,c){var d,e=!(!b||!b.length),f=!!c;e&&(d=new RegExp("^("+b.join("|")+")$"));var g=[];return k(new p(a,!1),function(b){if((!e||d.test(b.nodeType))&&(!f||c(b))){ -// Don't include a boundary container if it is a character data node and the range does not contain any -// of its character data. See issue 190. -var h=a.startContainer;if(b!=h||!N(h)||a.startOffset!=h.length){var i=a.endContainer;b==i&&N(i)&&0==a.endOffset||g.push(b)}}}),g}function o(a){var b="undefined"==typeof a.getName?"Range":a.getName();return"["+b+"("+J.inspectNode(a.startContainer)+":"+a.startOffset+", "+J.inspectNode(a.endContainer)+":"+a.endOffset+")]"}/*----------------------------------------------------------------------------------------------------------------*/ -// RangeIterator code partially borrows from IERange by Tim Ryan (http://github.com/timcameronryan/IERange) -function p(a,b){if(this.range=a,this.clonePartiallySelectedTextNodes=b,!a.collapsed){this.sc=a.startContainer,this.so=a.startOffset,this.ec=a.endContainer,this.eo=a.endOffset;var c=a.commonAncestorContainer;this.sc===this.ec&&N(this.sc)?(this.isSingleCharacterDataNode=!0,this._first=this._last=this._next=this.sc):(this._first=this._next=this.sc!==c||N(this.sc)?T(this.sc,c,!0):this.sc.childNodes[this.so],this._last=this.ec!==c||N(this.ec)?T(this.ec,c,!0):this.ec.childNodes[this.eo-1])}}function q(a){return function(b,c){for(var d,e=c?b:b.parentNode;e;){if(d=e.nodeType,V(a,d))return e;e=e.parentNode}return null}}function r(a,b){if(ea(a,b))throw new M("INVALID_NODE_TYPE_ERR")}function s(a,b){if(!V(b,a.nodeType))throw new M("INVALID_NODE_TYPE_ERR")}function t(a,b){if(b<0||b>(N(a)?a.length:a.childNodes.length))throw new M("INDEX_SIZE_ERR")}function u(a,b){if(ca(a,!0)!==ca(b,!0))throw new M("WRONG_DOCUMENT_ERR")}function v(a){if(da(a,!0))throw new M("NO_MODIFICATION_ALLOWED_ERR")}function w(a,b){if(!a)throw new M(b)}function x(a,b){return b<=(N(a)?a.length:a.childNodes.length)}function y(a){return!!a.startContainer&&!!a.endContainer&&!(X&&(J.isBrokenNode(a.startContainer)||J.isBrokenNode(a.endContainer)))&&W(a.startContainer)==W(a.endContainer)&&x(a.startContainer,a.startOffset)&&x(a.endContainer,a.endOffset)}function z(a){if(!y(a))throw new Error("Range error: Range is not valid. This usually happens after DOM mutation. Range: ("+a.inspect()+")")}function A(a,b){z(a);var c=a.startContainer,d=a.startOffset,e=a.endContainer,f=a.endOffset,g=c===e;N(e)&&f>0&&f0&&d=O(c)&&f++,d=0),a.setStartAndEnd(c,d,e,f)}function B(a){z(a);var b=a.commonAncestorContainer.parentNode.cloneNode(!1);return b.appendChild(a.cloneContents()),b.innerHTML}function C(a){a.START_TO_START=ja,a.START_TO_END=ka,a.END_TO_END=la,a.END_TO_START=ma,a.NODE_BEFORE=na,a.NODE_AFTER=oa,a.NODE_BEFORE_AND_AFTER=pa,a.NODE_INSIDE=qa}function D(a){C(a),C(a.prototype)}function E(a,b){return function(){z(this);var c,d,e=this.startContainer,f=this.startOffset,h=this.commonAncestorContainer,i=new p(this,!0);e!==h&&(c=T(e,h,!0),d=g(c),e=d.node,f=d.offset), -// Check none of the range is read-only -k(i,v),i.reset(); -// Remove the content -var j=a(i); -// Move to the new position -return i.detach(),b(this,e,f,e,f),j}}function F(b,d){function e(a,b){return function(c){s(c,Z),s(W(c),$);var d=(a?f:g)(c);(b?h:i)(this,d.node,d.offset)}}function h(a,b,c){var e=a.endContainer,f=a.endOffset;b===a.startContainer&&c===a.startOffset||( -// Check the root containers of the range and the new boundary, and also check whether the new boundary -// is after the current end. In either case, collapse the range to the new position -W(b)==W(e)&&1!=R(b,c,e,f)||(e=b,f=c),d(a,b,c,e,f))}function i(a,b,c){var e=a.startContainer,f=a.startOffset;b===a.endContainer&&c===a.endOffset||( -// Check the root containers of the range and the new boundary, and also check whether the new boundary -// is after the current end. In either case, collapse the range to the new position -W(b)==W(e)&&R(b,c,e,f)!=-1||(e=b,f=c),d(a,e,f,b,c))} -// Set up inheritance -var j=function(){};j.prototype=a.rangePrototype,b.prototype=new j,K.extend(b.prototype,{setStart:function(a,b){r(a,!0),t(a,b),h(this,a,b)},setEnd:function(a,b){r(a,!0),t(a,b),i(this,a,b)},/** - * Convenience method to set a range's start and end boundaries. Overloaded as follows: - * - Two parameters (node, offset) creates a collapsed range at that position - * - Three parameters (node, startOffset, endOffset) creates a range contained with node starting at - * startOffset and ending at endOffset - * - Four parameters (startNode, startOffset, endNode, endOffset) creates a range starting at startOffset in - * startNode and ending at endOffset in endNode - */ -setStartAndEnd:function(){var a=arguments,b=a[0],c=a[1],e=b,f=c;switch(a.length){case 3:f=a[2];break;case 4:e=a[2],f=a[3]}d(this,b,c,e,f)},setBoundary:function(a,b,c){this["set"+(c?"Start":"End")](a,b)},setStartBefore:e(!0,!0),setStartAfter:e(!1,!0),setEndBefore:e(!0,!1),setEndAfter:e(!1,!1),collapse:function(a){z(this),a?d(this,this.startContainer,this.startOffset,this.startContainer,this.startOffset):d(this,this.endContainer,this.endOffset,this.endContainer,this.endOffset)},selectNodeContents:function(a){r(a,!0),d(this,a,0,a,U(a))},selectNode:function(a){r(a,!1),s(a,Z);var b=f(a),c=g(a);d(this,b.node,b.offset,c.node,c.offset)},extractContents:E(m,d),deleteContents:E(l,d),canSurroundContents:function(){z(this),v(this.startContainer),v(this.endContainer); -// Check if the contents can be surrounded. Specifically, this means whether the range partially selects -// no non-text nodes. -var a=new p(this,!0),b=a._first&&c(a._first,this)||a._last&&c(a._last,this);return a.detach(),!b},splitBoundaries:function(){A(this)},splitBoundariesPreservingPositions:function(a){A(this,a)},normalizeBoundaries:function(){z(this);var a,b=this.startContainer,c=this.startOffset,e=this.endContainer,f=this.endOffset,g=function(a){var b=a.nextSibling;b&&b.nodeType==a.nodeType&&(e=a,f=a.length,a.appendData(b.data),Y(b))},h=function(a){var d=a.previousSibling;if(d&&d.nodeType==a.nodeType){b=a;var g=a.length;if(c=d.length,a.insertData(0,d.data),Y(d),b==e)f+=c,e=b;else if(e==a.parentNode){var h=O(a);f==h?(e=a,f=g):f>h&&f--}}},i=!0;if(N(e))f==e.length?g(e):0==f&&(a=e.previousSibling,a&&a.nodeType==e.nodeType&&(f=a.length,b==e&&(i=!1),a.appendData(e.data),Y(e),e=a));else{if(f>0){var j=e.childNodes[f-1];j&&N(j)&&g(j)}i=!this.collapsed}if(i){if(N(b))0==c?h(b):c==b.length&&(a=b.nextSibling,a&&a.nodeType==b.nodeType&&(e==a&&(e=b,f+=b.length),b.appendData(a.data),Y(a)));else if(cx",ga=3==fa.firstChild.nodeType}catch(a){}a.features.htmlParsingConforms=ga;var ha=ga? -// Implementation as per HTML parsing spec, trusting in the browser's implementation of innerHTML. See -// discussion and base code for this implementation at issue 67. -// Spec: http://html5.org/specs/dom-parsing.html#extensions-to-the-range-interface -// Thanks to Aleks Williams. -function(a){ -// "Let node the context object's start's node." -var b=this.startContainer,c=Q(b); -// "If the context object's start's node is null, raise an INVALID_STATE_ERR -// exception and abort these steps." -if(!b)throw new M("INVALID_STATE_ERR"); -// "Let element be as follows, depending on node's interface:" -// Document, Document Fragment: null -var d=null; -// "If this raises an exception, then abort these steps. Otherwise, let new -// children be the nodes returned." -// "Let fragment be a new DocumentFragment." -// "Append all new children to fragment." -// "Return fragment." -// "Element: node" -// "If either element is null or element's ownerDocument is an HTML document -// and element's local name is "html" and element's namespace is the HTML -// namespace" -// "let element be a new Element with "body" as its local name and the HTML -// namespace as its namespace."" -// "If the node's document is an HTML document: Invoke the HTML fragment parsing algorithm." -// "If the node's document is an XML document: Invoke the XML fragment parsing algorithm." -// "In either case, the algorithm must be invoked with fragment as the input -// and element as the context element." -return 1==b.nodeType?d=b:N(b)&&(d=J.parentElement(b)),d=null===d||"HTML"==d.nodeName&&J.isHtmlNamespace(Q(d).documentElement)&&J.isHtmlNamespace(d)?c.createElement("body"):d.cloneNode(!1),d.innerHTML=a,J.fragmentFromNodeChildren(d)}: -// In this case, innerHTML cannot be trusted, so fall back to a simpler, non-conformant implementation that -// previous versions of Rangy used (with the exception of using a body element rather than a div) -function(a){var b=d(this),c=b.createElement("body");return c.innerHTML=a,J.fragmentFromNodeChildren(c)},ia=["startContainer","startOffset","endContainer","endOffset","collapsed","commonAncestorContainer"],ja=0,ka=1,la=2,ma=3,na=0,oa=1,pa=2,qa=3;K.extend(a.rangePrototype,{compareBoundaryPoints:function(a,b){z(this),u(this.startContainer,b.startContainer);var c,d,e,f,g=a==ma||a==ja?"start":"end",h=a==ka||a==ja?"start":"end";return c=this[g+"Container"],d=this[g+"Offset"],e=b[h+"Container"],f=b[h+"Offset"],R(c,d,e,f)},insertNode:function(a){if(z(this),s(a,aa),v(this.startContainer),P(a,this.startContainer))throw new M("HIERARCHY_REQUEST_ERR"); -// No check for whether the container of the start of the Range is of a type that does not allow -// children of the type of node: the browser's DOM implementation should do this for us when we attempt -// to add the node -var b=h(a,this.startContainer,this.startOffset);this.setStartBefore(b)},cloneContents:function(){z(this);var a,b;if(this.collapsed)return d(this).createDocumentFragment();if(this.startContainer===this.endContainer&&N(this.startContainer))return a=this.startContainer.cloneNode(!0),a.data=a.data.slice(this.startOffset,this.endOffset),b=d(this).createDocumentFragment(),b.appendChild(a),b;var c=new p(this,!0);return a=j(c),c.detach(),a},canSurroundContents:function(){z(this),v(this.startContainer),v(this.endContainer); -// Check if the contents can be surrounded. Specifically, this means whether the range partially selects -// no non-text nodes. -var a=new p(this,!0),b=a._first&&c(a._first,this)||a._last&&c(a._last,this);return a.detach(),!b},surroundContents:function(a){if(s(a,ba),!this.canSurroundContents())throw new M("INVALID_STATE_ERR"); -// Extract the contents -var b=this.extractContents(); -// Clear the children of the node -if(a.hasChildNodes())for(;a.lastChild;)a.removeChild(a.lastChild); -// Insert the new node and add the extracted contents -h(a,this.startContainer,this.startOffset),a.appendChild(b),this.selectNode(a)},cloneRange:function(){z(this);for(var a,b=new I(d(this)),c=ia.length;c--;)a=ia[c],b[a]=this[a];return b},toString:function(){z(this);var a=this.startContainer;if(a===this.endContainer&&N(a))return 3==a.nodeType||4==a.nodeType?a.data.slice(this.startOffset,this.endOffset):"";var b=[],c=new p(this,!0);return k(c,function(a){ -// Accept only text or CDATA nodes, not comments -3!=a.nodeType&&4!=a.nodeType||b.push(a.data)}),c.detach(),b.join("")}, -// The methods below are all non-standard. The following batch were introduced by Mozilla but have since -// been removed from Mozilla. -compareNode:function(a){z(this);var b=a.parentNode,c=O(a);if(!b)throw new M("NOT_FOUND_ERR");var d=this.comparePoint(b,c),e=this.comparePoint(b,c+1);return d<0?e>0?pa:na:e>0?oa:qa},comparePoint:function(a,b){return z(this),w(a,"HIERARCHY_REQUEST_ERR"),u(a,this.startContainer),R(a,b,this.startContainer,this.startOffset)<0?-1:R(a,b,this.endContainer,this.endOffset)>0?1:0},createContextualFragment:ha,toHtml:function(){return B(this)}, -// touchingIsIntersecting determines whether this method considers a node that borders a range intersects -// with it (as in WebKit) or not (as in Gecko pre-1.9, and the default) -intersectsNode:function(a,b){if(z(this),W(a)!=e(this))return!1;var c=a.parentNode,d=O(a);if(!c)return!0;var f=R(c,d,this.endContainer,this.endOffset),g=R(c,d+1,this.startContainer,this.startOffset);return b?f<=0&&g>=0:f<0&&g>0},isPointInRange:function(a,b){return z(this),w(a,"HIERARCHY_REQUEST_ERR"),u(a,this.startContainer),R(a,b,this.startContainer,this.startOffset)>=0&&R(a,b,this.endContainer,this.endOffset)<=0}, -// The methods below are non-standard and invented by me. -// Sharing a boundary start-to-end or end-to-start does not count as intersection. -intersectsRange:function(a){return i(this,a,!1)}, -// Sharing a boundary start-to-end or end-to-start does count as intersection. -intersectsOrTouchesRange:function(a){return i(this,a,!0)},intersection:function(a){if(this.intersectsRange(a)){var b=R(this.startContainer,this.startOffset,a.startContainer,a.startOffset),c=R(this.endContainer,this.endOffset,a.endContainer,a.endOffset),d=this.cloneRange();return b==-1&&d.setStart(a.startContainer,a.startOffset),1==c&&d.setEnd(a.endContainer,a.endOffset),d}return null},union:function(a){if(this.intersectsOrTouchesRange(a)){var b=this.cloneRange();return R(a.startContainer,a.startOffset,this.startContainer,this.startOffset)==-1&&b.setStart(a.startContainer,a.startOffset),1==R(a.endContainer,a.endOffset,this.endContainer,this.endOffset)&&b.setEnd(a.endContainer,a.endOffset),b}throw new M("Ranges do not intersect")},containsNode:function(a,b){return b?this.intersectsNode(a,!1):this.compareNode(a)==qa},containsNodeContents:function(a){return this.comparePoint(a,0)>=0&&this.comparePoint(a,U(a))<=0},containsRange:function(a){var b=this.intersection(a);return null!==b&&a.equals(b)},containsNodeText:function(a){var b=this.cloneRange();b.selectNode(a);var c=b.getNodes([3]);if(c.length>0){b.setStart(c[0],0);var d=c.pop();return b.setEnd(d,d.length),this.containsRange(b)}return this.containsNodeContents(a)},getNodes:function(a,b){return z(this),n(this,a,b)},getDocument:function(){return d(this)},collapseBefore:function(a){this.setEndBefore(a),this.collapse(!1)},collapseAfter:function(a){this.setStartAfter(a),this.collapse(!0)},getBookmark:function(b){var c=d(this),e=a.createRange(c);b=b||J.getBody(c),e.selectNodeContents(b);var f=this.intersection(e),g=0,h=0;return f&&(e.setEnd(f.startContainer,f.startOffset),g=e.toString().length,h=g+f.toString().length),{start:g,end:h,containerNode:b}},moveToBookmark:function(a){var b=a.containerNode,c=0;this.setStart(b,0),this.collapse(!0);for(var d,e,f,g,h=[b],i=!1,j=!1;!j&&(d=h.pop());)if(3==d.nodeType)e=c+d.length,!i&&a.start>=c&&a.start<=e&&(this.setStart(d,a.start-c),i=!0),i&&a.end>=c&&a.end<=e&&(this.setEnd(d,a.end-c),j=!0),c=e;else for(g=d.childNodes,f=g.length;f--;)h.push(g[f])},getName:function(){return"DomRange"},equals:function(a){return I.rangesEqual(this,a)},isValid:function(){return y(this)},inspect:function(){return o(this)},detach:function(){}}),F(I,H),K.extend(I,{rangeProperties:ia,RangeIterator:p,copyComparisonConstants:D,createPrototypeRange:F,inspect:o,toHtml:B,getRangeDocument:d,rangesEqual:function(a,b){return a.startContainer===b.startContainer&&a.startOffset===b.startOffset&&a.endContainer===b.endContainer&&a.endOffset===b.endOffset}}),a.DomRange=I}),/*----------------------------------------------------------------------------------------------------------------*/ -// Wrappers for the browser's native DOM Range and/or TextRange implementation -H.createCoreModule("WrappedRange",["DomRange"],function(a,b){var c,d,e=a.dom,f=a.util,g=e.DomPosition,h=a.DomRange,i=e.getBody,j=e.getContentDocument,k=e.isCharacterDataNode;if(/*----------------------------------------------------------------------------------------------------------------*/ -a.features.implementsDomRange&& -// This is a wrapper around the browser's native DOM Range. It has two aims: -// - Provide workarounds for specific browser bugs -// - provide convenient extensions, which are inherited from Rangy's DomRange -!function(){function d(a){for(var b,c=m.length;c--;)b=m[c],a[b]=a.nativeRange[b]; -// Fix for broken collapsed property in IE 9. -a.collapsed=a.startContainer===a.endContainer&&a.startOffset===a.endOffset}function g(a,b,c,d,e){var f=a.startContainer!==b||a.startOffset!=c,g=a.endContainer!==d||a.endOffset!=e,h=!a.equals(a.nativeRange); -// Always set both boundaries for the benefit of IE9 (see issue 35) -(f||g||h)&&(a.setEnd(d,e),a.setStart(b,c))}var k,l,m=h.rangeProperties;c=function(a){if(!a)throw b.createError("WrappedRange: Range must be specified");this.nativeRange=a,d(this)},h.createPrototypeRange(c,g),k=c.prototype,k.selectNode=function(a){this.nativeRange.selectNode(a),d(this)},k.cloneContents=function(){return this.nativeRange.cloneContents()}, -// Due to a long-standing Firefox bug that I have not been able to find a reliable way to detect, -// insertNode() is never delegated to the native range. -k.surroundContents=function(a){this.nativeRange.surroundContents(a),d(this)},k.collapse=function(a){this.nativeRange.collapse(a),d(this)},k.cloneRange=function(){return new c(this.nativeRange.cloneRange())},k.refresh=function(){d(this)},k.toString=function(){return this.nativeRange.toString()}; -// Create test range and node for feature detection -var n=document.createTextNode("test");i(document).appendChild(n);var o=document.createRange();/*--------------------------------------------------------------------------------------------------------*/ -// Test for Firefox 2 bug that prevents moving the start of a Range to a point after its current end and -// correct for it -o.setStart(n,0),o.setEnd(n,0);try{o.setStart(n,1),k.setStart=function(a,b){this.nativeRange.setStart(a,b),d(this)},k.setEnd=function(a,b){this.nativeRange.setEnd(a,b),d(this)},l=function(a){return function(b){this.nativeRange[a](b),d(this)}}}catch(a){k.setStart=function(a,b){try{this.nativeRange.setStart(a,b)}catch(c){this.nativeRange.setEnd(a,b),this.nativeRange.setStart(a,b)}d(this)},k.setEnd=function(a,b){try{this.nativeRange.setEnd(a,b)}catch(c){this.nativeRange.setStart(a,b),this.nativeRange.setEnd(a,b)}d(this)},l=function(a,b){return function(c){try{this.nativeRange[a](c)}catch(d){this.nativeRange[b](c),this.nativeRange[a](c)}d(this)}}}k.setStartBefore=l("setStartBefore","setEndBefore"),k.setStartAfter=l("setStartAfter","setEndAfter"),k.setEndBefore=l("setEndBefore","setStartBefore"),k.setEndAfter=l("setEndAfter","setStartAfter"),/*--------------------------------------------------------------------------------------------------------*/ -// Always use DOM4-compliant selectNodeContents implementation: it's simpler and less code than testing -// whether the native implementation can be trusted -k.selectNodeContents=function(a){this.setStartAndEnd(a,0,e.getNodeLength(a))},/*--------------------------------------------------------------------------------------------------------*/ -// Test for and correct WebKit bug that has the behaviour of compareBoundaryPoints round the wrong way for -// constants START_TO_END and END_TO_START: https://bugs.webkit.org/show_bug.cgi?id=20738 -o.selectNodeContents(n),o.setEnd(n,3);var p=document.createRange();p.selectNodeContents(n),p.setEnd(n,4),p.setStart(n,2),o.compareBoundaryPoints(o.START_TO_END,p)==-1&&1==o.compareBoundaryPoints(o.END_TO_START,p)? -// This is the wrong way round, so correct for it -k.compareBoundaryPoints=function(a,b){return b=b.nativeRange||b,a==b.START_TO_END?a=b.END_TO_START:a==b.END_TO_START&&(a=b.START_TO_END),this.nativeRange.compareBoundaryPoints(a,b)}:k.compareBoundaryPoints=function(a,b){return this.nativeRange.compareBoundaryPoints(a,b.nativeRange||b)};/*--------------------------------------------------------------------------------------------------------*/ -// Test for IE deleteContents() and extractContents() bug and correct it. See issue 107. -var q=document.createElement("div");q.innerHTML="123";var r=q.firstChild,s=i(document);s.appendChild(q),o.setStart(r,1),o.setEnd(r,2),o.deleteContents(),"13"==r.data&&( -// Behaviour is correct per DOM4 Range so wrap the browser's implementation of deleteContents() and -// extractContents() -k.deleteContents=function(){this.nativeRange.deleteContents(),d(this)},k.extractContents=function(){var a=this.nativeRange.extractContents();return d(this),a}),s.removeChild(q),s=null,/*--------------------------------------------------------------------------------------------------------*/ -// Test for existence of createContextualFragment and delegate to it if it exists -f.isHostMethod(o,"createContextualFragment")&&(k.createContextualFragment=function(a){return this.nativeRange.createContextualFragment(a)}),/*--------------------------------------------------------------------------------------------------------*/ -// Clean up -i(document).removeChild(n),k.getName=function(){return"WrappedRange"},a.WrappedRange=c,a.createNativeRange=function(a){return a=j(a,b,"createNativeRange"),a.createRange()}}(),a.features.implementsTextRange){/* - This is a workaround for a bug where IE returns the wrong container element from the TextRange's parentElement() - method. For example, in the following (where pipes denote the selection boundaries): - -
  • | a
  • b |
- - var range = document.selection.createRange(); - alert(range.parentElement().id); // Should alert "ul" but alerts "b" - - This method returns the common ancestor node of the following: - - the parentElement() of the textRange - - the parentElement() of the textRange after calling collapse(true) - - the parentElement() of the textRange after calling collapse(false) - */ -var l=function(a){var b=a.parentElement(),c=a.duplicate();c.collapse(!0);var d=c.parentElement();c=a.duplicate(),c.collapse(!1);var f=c.parentElement(),g=d==f?d:e.getCommonAncestor(d,f);return g==b?g:e.getCommonAncestor(b,g)},m=function(a){return 0==a.compareEndPoints("StartToEnd",a)},n=function(a,b,c,d,f){var h=a.duplicate();h.collapse(c);var i=h.parentElement(); -// Deal with nodes that cannot "contain rich HTML markup". In practice, this means form inputs, images and -// similar. See http://msdn.microsoft.com/en-us/library/aa703950%28VS.85%29.aspx -if( -// Sometimes collapsing a TextRange that's at the start of a text node can move it into the previous node, so -// check for that -e.isOrIsAncestorOf(b,i)||(i=b),!i.canHaveHTML){var j=new g(i.parentNode,e.getNodeIndex(i));return{boundaryPosition:j,nodeInfo:{nodeIndex:j.offset,containerElement:j.node}}}var l=e.getDocument(i).createElement("span"); -// Workaround for HTML5 Shiv's insane violation of document.createElement(). See Rangy issue 104 and HTML5 -// Shiv issue 64: https://github.com/aFarkas/html5shiv/issues/64 -l.parentNode&&e.removeNode(l);for(var m,n,o,p,q,r=c?"StartToStart":"StartToEnd",s=f&&f.containerElement==i?f.nodeIndex:0,t=i.childNodes.length,u=t,v=u;;){if(v==t?i.appendChild(l):i.insertBefore(l,i.childNodes[v]),h.moveToElementText(l),m=h.compareEndPoints(r,a),0==m||s==u)break;if(m==-1){if(u==s+1) -// We know the endth child node is after the range boundary, so we must be done. -break;s=v}else u=u==s+1?s:v;v=Math.floor((s+u)/2),i.removeChild(l)}if( -// We've now reached or gone past the boundary of the text range we're interested in -// so have identified the node we want -q=l.nextSibling,m==-1&&q&&k(q)){ -// This is a character data node (text, comment, cdata). The working range is collapsed at the start of -// the node containing the text range's boundary, so we move the end of the working range to the -// boundary point and measure the length of its text to get the boundary's offset within the node. -h.setEndPoint(c?"EndToStart":"EndToEnd",a);var w;if(/[\r\n]/.test(q.data)){/* - For the particular case of a boundary within a text node containing rendered line breaks (within a -
 element, for example), we need a slightly complicated approach to get the boundary's offset in
-                        IE. The facts:
-
-                        - Each line break is represented as \r in the text node's data/nodeValue properties
-                        - Each line break is represented as \r\n in the TextRange's 'text' property
-                        - The 'text' property of the TextRange does not contain trailing line breaks
-
-                        To get round the problem presented by the final fact above, we can use the fact that TextRange's
-                        moveStart() and moveEnd() methods return the actual number of characters moved, which is not
-                        necessarily the same as the number of characters it was instructed to move. The simplest approach is
-                        to use this to store the characters moved when moving both the start and end of the range to the
-                        start of the document body and subtracting the start offset from the end offset (the
-                        "move-negative-gazillion" method). However, this is extremely slow when the document is large and
-                        the range is near the end of it. Clearly doing the mirror image (i.e. moving the range boundaries to
-                        the end of the document) has the same problem.
-
-                        Another approach that works is to use moveStart() to move the start boundary of the range up to the
-                        end boundary one character at a time and incrementing a counter with the value returned by the
-                        moveStart() call. However, the check for whether the start boundary has reached the end boundary is
-                        expensive, so this method is slow (although unlike "move-negative-gazillion" is largely unaffected
-                        by the location of the range within the document).
-
-                        The approach used below is a hybrid of the two methods above. It uses the fact that a string
-                        containing the TextRange's 'text' property with each \r\n converted to a single \r character cannot
-                        be longer than the text of the TextRange, so the start of the range is moved that length initially
-                        and then a character at a time to make up for any trailing line breaks not contained in the 'text'
-                        property. This has good performance in most situations compared to the previous two methods.
-                        */
-var x=h.duplicate(),y=x.text.replace(/\r\n/g,"\r").length;for(w=x.moveStart("character",y);(m=x.compareEndPoints("StartToEnd",x))==-1;)w++,x.moveStart("character",1)}else w=h.text.length;p=new g(q,w)}else
-// If the boundary immediately follows a character data node and this is the end boundary, we should favour
-// a position within that, and likewise for a start boundary preceding a character data node
-n=(d||!c)&&l.previousSibling,o=(d||c)&&l.nextSibling,p=o&&k(o)?new g(o,0):n&&k(n)?new g(n,n.data.length):new g(i,e.getNodeIndex(l));
-// Clean up
-return e.removeNode(l),{boundaryPosition:p,nodeInfo:{nodeIndex:v,containerElement:i}}},o=function(a,b){var c,d,f,g,h=a.offset,j=e.getDocument(a.node),l=i(j).createTextRange(),m=k(a.node);
-// Position the range immediately before the node containing the boundary
-// Making the working element non-empty element persuades IE to consider the TextRange boundary to be within
-// the element rather than immediately before or after it
-// insertBefore is supposed to work like appendChild if the second parameter is null. However, a bug report
-// for IERange suggests that it can crash the browser: http://code.google.com/p/ierange/issues/detail?id=12
-// Clean up
-// Move the working range to the text offset, if required
-return m?(c=a.node,d=c.parentNode):(g=a.node.childNodes,c=h1,e=[],f=g(b),h=0;h=36)X=!1;else{var m=k.cloneRange();k.setStart(j,0),m.setEnd(j,3),m.setStart(j,2),b.addRange(k),b.addRange(m),X=2==b.rangeCount}}for(
-// Clean up
-C.removeNode(i),b.removeAllRanges(),h=0;h1)u(this,a);else{this.removeAllRanges();for(var b=0,c=a.length;b1?u(this,a):b&&this.addRange(a[0])}}da.getRangeAt=function(a){if(a<0||a>=this.rangeCount)throw new H("INDEX_SIZE_ERR");
-// Clone the range to preserve selection-range independence. See issue 80.
-return this._ranges[a].cloneRange()};var fa;if(Q)fa=function(b){var c;a.isSelectionValid(b.win)?c=b.docSelection.createRange():(c=M(b.win.document).createTextRange(),c.collapse(!0)),b.docSelection.type==K?p(b):n(c)?o(b,c):j(b)};else if(E(R,"getRangeAt")&&typeof R.rangeCount==B)fa=function(b){if(_&&P&&b.docSelection.type==K)p(b);else if(b._ranges.length=b.rangeCount=b.nativeSelection.rangeCount,b.rangeCount){for(var c=0,d=b.rangeCount;c0)return a.WrappedTextRange.rangeToTextRange(this.getRangeAt(0));throw b.createError("getNativeTextRange: selection contains no range")}),da.getName=function(){return"WrappedSelection"},da.inspect=function(){return x(this)},da.detach=function(){t(this.win,"delete"),s(this)},r.detachAll=function(){t(null,"deleteAll")},r.inspect=x,r.isDirectionBackward=c,a.Selection=r,a.selectionPrototype=da,a.addShimListener(function(a){"undefined"==typeof a.getSelection&&(a.getSelection=function(){return ca(a)}),a=null})});/*----------------------------------------------------------------------------------------------------------------*/
-// Wait for document to load before initializing
-var M=!1,N=function(a){M||(M=!0,!H.initialized&&H.config.autoInitialize&&l())};
-// Test whether the document has already been loaded and initialize immediately if so
-// Add a fallback in case the DOMContentLoaded event isn't supported
-return F&&("complete"==document.readyState?N():(a(document,"addEventListener")&&document.addEventListener("DOMContentLoaded",N,!1),J(window,"load",N))),H},this),/**
- * Selection save and restore module for Rangy.
- * Saves and restores user selections using marker invisible elements in the DOM.
- *
- * Part of Rangy, a cross-browser JavaScript range and selection library
- * https://github.com/timdown/rangy
- *
- * Depends on Rangy core.
- *
- * Copyright 2015, Tim Down
- * Licensed under the MIT license.
- * Version: 1.3.0
- * Build date: 10 May 2015
- */
-function(a,b){"function"==typeof define&&define.amd?
-// AMD. Register as an anonymous module with a dependency on Rangy.
-define(["./rangy-core"],a):"undefined"!=typeof module&&"object"==typeof exports?
-// Node/CommonJS style
-module.exports=a(require("rangy")):
-// No AMD or CommonJS support so we use the rangy property of root (probably the global variable)
-a(b.rangy)}(function(a){return a.createModule("SaveRestore",["WrappedRange"],function(a,b){function c(a,b){return(b||document).getElementById(a)}function d(a,b){var c,d="selectionBoundary_"+ +new Date+"_"+(""+Math.random()).slice(2),e=o.getDocument(a.startContainer),f=a.cloneRange();
-// Create the marker element containing a single invisible character using DOM methods and insert it
-return f.collapse(b),c=e.createElement("span"),c.id=d,c.style.lineHeight="0",c.style.display="none",c.className="rangySelectionBoundary",c.appendChild(e.createTextNode(r)),f.insertNode(c),c}function e(a,d,e,f){var g=c(e,a);g?(d[f?"setStartBefore":"setEndBefore"](g),p(g)):b.warn("Marker element has been removed. Cannot restore selection.")}function f(a,b){return b.compareBoundaryPoints(a.START_TO_START,a)}function g(b,c){var e,f,g=a.DomRange.getRangeDocument(b),h=b.toString(),i=q(c);return b.collapsed?(f=d(b,!1),{document:g,markerId:f.id,collapsed:!0}):(f=d(b,!1),e=d(b,!0),{document:g,startMarkerId:e.id,endMarkerId:f.id,collapsed:!1,backward:i,toString:function(){return"original text: '"+h+"', new text: '"+b.toString()+"'"}})}function h(d,f){var g=d.document;"undefined"==typeof f&&(f=!0);var h=a.createRange(g);if(d.collapsed){var i=c(d.markerId,g);if(i){i.style.display="inline";var j=i.previousSibling;
-// Workaround for issue 17
-j&&3==j.nodeType?(p(i),h.collapseToPoint(j,j.length)):(h.collapseBefore(i),p(i))}else b.warn("Marker element has been removed. Cannot restore selection.")}else e(g,h,d.startMarkerId,!0),e(g,h,d.endMarkerId,!1);return f&&h.normalizeBoundaries(),h}function i(b,d){var e,h,i=[],j=q(d);
-// Order the ranges by position within the DOM, latest first, cloning the array to leave the original untouched
-b=b.slice(0),b.sort(f);for(var k=0,l=b.length;k=0;--k)e=b[k],h=a.DomRange.getRangeDocument(e),e.collapsed?e.collapseAfter(c(i[k].markerId,h)):(e.setEndBefore(c(i[k].endMarkerId,h)),e.setStartAfter(c(i[k].startMarkerId,h)));return i}function j(c){if(!a.isSelectionValid(c))return b.warn("Cannot save selection. This usually happens when the selection is collapsed and the selection document has lost focus."),null;var d=a.getSelection(c),e=d.getAllRanges(),f=1==e.length&&d.isBackward(),g=i(e,f);
-// Ensure current selection is unaffected
-return f?d.setSingleRange(e[0],f):d.setRanges(e),{win:c,rangeInfos:g,restored:!1}}function k(a){for(var b=[],c=a.length,d=c-1;d>=0;d--)b[d]=h(a[d],!0);return b}function l(b,c){if(!b.restored){var d=b.rangeInfos,e=a.getSelection(b.win),f=k(d),g=d.length;1==g&&c&&a.features.selectionHasExtend&&d[0].backward?(e.removeAllRanges(),e.addRange(f[0],!0)):e.setRanges(f),b.restored=!0}}function m(a,b){var d=c(b,a);d&&p(d)}function n(a){for(var b,c=a.rangeInfos,d=0,e=c.length;d angular.isString(str) ? str.toLowerCase() : str;
-
-!function(a,b,c){"use strict";/**
- * @ngdoc module
- * @name ngSanitize
- * @description
- *
- * # ngSanitize
- *
- * The `ngSanitize` module provides functionality to sanitize HTML.
- *
- *
- * 
- * - * See {@link ngSanitize.$sanitize `$sanitize`} for usage. - */ -/* - * HTML Parser By Misko Hevery (misko@hevery.com) - * based on: HTML Parser By John Resig (ejohn.org) - * Original code by Erik Arvidsson, Mozilla Public License - * http://erik.eae.net/simplehtmlparser/simplehtmlparser.js - * - * // Use like so: - * htmlParser(htmlString, { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * }); - * - */ -/** - * @ngdoc service - * @name $sanitize - * @kind function - * - * @description - * The input is sanitized by parsing the HTML into tokens. All safe tokens (from a whitelist) are - * then serialized back to properly escaped html string. This means that no unsafe input can make - * it into the returned string, however, since our parser is more strict than a typical browser - * parser, it's possible that some obscure input, which would be recognized as valid HTML by a - * browser, won't make it through the sanitizer. The input may also contain SVG markup. - * The whitelist is configured using the functions `aHrefSanitizationWhitelist` and - * `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}. - * - * @param {string} html HTML input. - * @returns {string} Sanitized HTML. - * - * @example - - - -
- Snippet: - - - - - - - - - - - - - - - - - - - - - - - - - -
DirectiveHowSourceRendered
ng-bind-htmlAutomatically uses $sanitize
<div ng-bind-html="snippet">
</div>
ng-bind-htmlBypass $sanitize by explicitly trusting the dangerous value -
<div ng-bind-html="deliberatelyTrustDangerousSnippet()">
-</div>
-
ng-bindAutomatically escapes
<div ng-bind="snippet">
</div>
-
-
- - it('should sanitize the html snippet by default', function() { - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). - toBe('

an html\nclick here\nsnippet

'); - }); - - it('should inline raw snippet if bound to a trusted value', function() { - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()). - toBe("

an html\n" + - "click here\n" + - "snippet

"); - }); - - it('should escape snippet without any filter', function() { - expect(element(by.css('#bind-default div')).getInnerHtml()). - toBe("<p style=\"color:blue\">an html\n" + - "<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" + - "snippet</p>"); - }); - - it('should update', function() { - element(by.model('snippet')).clear(); - element(by.model('snippet')).sendKeys('new text'); - expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()). - toBe('new text'); - expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe( - 'new text'); - expect(element(by.css('#bind-default div')).getInnerHtml()).toBe( - "new <b onclick=\"alert(1)\">text</b>"); - }); -
-
- */ -function d(){this.$get=["$$sanitizeUri",function(a){return function(b){"undefined"!=typeof arguments[1]&&(arguments[1].version="taSanitize");var c=[];return g(b,l(c,function(b,c){return!/^unsafe/.test(a(b,c))})),c.join("")}}]}function e(a){var c=[],d=l(c,b.noop);return d.chars(a),c.join("")}function f(a){var b,c={},d=a.split(",");for(b=0;b=0&&k[f]!=d;f--);if(f>=0){ -// Close all the open elements, up the stack -for(e=k.length-1;e>=f;e--)c.end&&c.end(k[e]); -// Remove the open elements from the stack -k.length=f}}"string"!=typeof a&&(a=null===a||"undefined"==typeof a?"":""+a);var f,g,i,j,k=[],l=a;for(k.last=function(){return k[k.length-1]};a;){ -// Make sure we're not in a script or style element -if(j="",g=!0,k.last()&&G[k.last()])a=a.replace(new RegExp("([^]*)<\\s*\\/\\s*"+k.last()+"[^>]*>","i"),function(a,b){return b=b.replace(s,"$1").replace(v,"$1"),c.chars&&c.chars(h(b)),""}),e("",k.last());else{ -// White space -if(y.test(a)){if(i=a.match(y)){i[0];c.whitespace&&c.whitespace(i[0]),a=a.replace(i[0],""),g=!1}}else t.test(a)?(i=a.match(t),i&&(c.comment&&c.comment(i[1]),a=a.replace(i[0],""),g=!1)):u.test(a)?(i=a.match(u),i&&(a=a.replace(i[0],""),g=!1)):r.test(a)?(i=a.match(o),i&&(a=a.substring(i[0].length),i[0].replace(o,e),g=!1)):q.test(a)&&(i=a.match(n),i?( -// We only have a valid start-tag if there is a '>'. -i[4]&&(a=a.substring(i[0].length),i[0].replace(n,d)),g=!1):( -// no ending tag found --- this piece should be encoded as an entity. -j+="<",a=a.substring(1)));g&&(f=a.indexOf("<"),j+=f<0?a:a.substring(0,f),a=f<0?"":a.substring(f),c.chars&&c.chars(h(j)))}if(a==l)throw m("badparse","The sanitizer was unable to parse the following block of html: {0}",a);l=a} -// Clean up any remaining tags -e()}/** - * decodes all entities into regular string - * @param value - * @returns {string} A string with decoded entities. - */ -function h(a){if(!a)return""; -// Note: IE8 does not preserve spaces at the start/end of innerHTML -// so we must capture them and reattach them afterward -var b=N.exec(a),c=b[1],d=b[3],e=b[2]; -// innerText depends on styling as it doesn't display hidden elements. -// Therefore, it's better to use textContent not to cause unnecessary -// reflows. However, IE<9 don't support textContent so the innerText -// fallback is necessary. -return e&&(M.innerHTML=e.replace(/=1536&&b<=1540||1807==b||6068==b||6069==b||b>=8204&&b<=8207||b>=8232&&b<=8239||b>=8288&&b<=8303||65279==b||b>=65520&&b<=65535?"&#"+b+";":a}).replace(//g,">")} -// Custom logic for accepting certain style options only - textAngular -// Currently allows only the color, background-color, text-align, float, width and height attributes -// all other attributes should be easily done through classes. -function j(a){var c="",d=a.split(";");return b.forEach(d,function(a){var d=a.split(":");if(2==d.length){var e=O(b.lowercase(d[0])),a=O(b.lowercase(d[1]));(("color"===e||"background-color"===e)&&(a.match(/^rgb\([0-9%,\. ]*\)$/i)||a.match(/^rgba\([0-9%,\. ]*\)$/i)||a.match(/^hsl\([0-9%,\. ]*\)$/i)||a.match(/^hsla\([0-9%,\. ]*\)$/i)||a.match(/^#[0-9a-f]{3,6}$/i)||a.match(/^[a-z]*$/i))||"text-align"===e&&("left"===a||"right"===a||"center"===a||"justify"===a)||"text-decoration"===e&&("underline"===a||"line-through"===a)||"font-weight"===e&&"bold"===a||"font-style"===e&&"italic"===a||"float"===e&&("left"===a||"right"===a||"none"===a)||"vertical-align"===e&&("baseline"===a||"sub"===a||"super"===a||"test-top"===a||"text-bottom"===a||"middle"===a||"top"===a||"bottom"===a||a.match(/[0-9]*(px|em)/)||a.match(/[0-9]+?%/))||"font-size"===e&&("xx-small"===a||"x-small"===a||"small"===a||"medium"===a||"large"===a||"x-large"===a||"xx-large"===a||"larger"===a||"smaller"===a||a.match(/[0-9]*\.?[0-9]*(px|em|rem|mm|q|cm|in|pt|pc|%)/))||("width"===e||"height"===e)&&a.match(/[0-9\.]*(px|em|rem|%)/)||// Reference #520 -"direction"===e&&a.match(/^ltr|rtl|initial|inherit$/))&&(c+=e+": "+a+";")}}),c} -// this function is used to manually allow specific attributes on specific tags with certain prerequisites -function k(a,b,c,d){ -// catch the div placeholder for the iframe replacement -return!("img"!==a||!b["ta-insert-video"]||"ta-insert-video"!==c&&"allowfullscreen"!==c&&"frameborder"!==c&&("contenteditable"!==c||"false"!==d))}/** - * create an HTML/XML writer which writes to buffer - * @param {Array} buf use buf.jain('') to get out sanitized html string - * @returns {object} in the form of { - * start: function(tag, attrs, unary) {}, - * end: function(tag) {}, - * chars: function(text) {}, - * comment: function(text) {} - * } - */ -function l(a,c){var d=!1,e=b.bind(a,a.push);return{start:function(a,f,g){a=b.lowercase(a),!d&&G[a]&&(d=a),d||H[a]!==!0||(e("<"),e(a),b.forEach(f,function(d,g){var h=b.lowercase(g),l="img"===a&&"src"===h||"background"===h;("style"===h&&""!==(d=j(d))||k(a,f,h,d)||L[h]===!0&&(I[h]!==!0||c(d,l)))&&(e(" "),e(g),e('="'),e(i(d)),e('"'))}),e(g?"/>":">"))},comment:function(a){e(a)},whitespace:function(a){e(i(a))},end:function(a){a=b.lowercase(a),d||H[a]!==!0||(e("")),a==d&&(d=!1)},chars:function(a){d||e(i(a))}}}var m=b.$$minErr("$sanitize"),n=/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,o=/^<\/\s*([\w:-]+)[^>]*>/,p=/([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,q=/^/g,t=/(^)/,u=/]*?)>/i,v=//g,w=/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, -// Match everything outside of normal chars and " (quote character) -x=/([^\#-~| |!])/g,y=/^(\s+)/,z=f("area,br,col,hr,img,wbr,input"),A=f("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),B=f("rp,rt"),C=b.extend({},B,A),D=b.extend({},A,f("address,article,aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul")),E=b.extend({},B,f("a,abbr,acronym,b,bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,samp,small,span,strike,strong,sub,sup,time,tt,u,var")),F=f("animate,animateColor,animateMotion,animateTransform,circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,radialGradient,rect,set,stop,svg,switch,text,title,tspan,use"),G=f("script,style"),H=b.extend({},z,D,E,C,F),I=f("background,cite,href,longdesc,src,usemap,xlink:href"),J=f("abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,id,ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,scope,scrolling,shape,size,span,start,summary,target,title,type,valign,value,vspace,width"),K=f("accent-height,accumulate,additive,alphabetic,arabic-form,ascent,attributeName,attributeType,baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,height,horiz-adv-x,horiz-origin-x,ideographic,k,keyPoints,keySplines,keyTimes,lang,marker-end,marker-mid,marker-start,markerHeight,markerUnits,markerWidth,mathematical,max,min,offset,opacity,orient,origin,overline-position,overline-thickness,panose-1,path,pathLength,points,preserveAspectRatio,r,refX,refY,repeatCount,repeatDur,requiredExtensions,requiredFeatures,restart,rotate,rx,ry,slope,stemh,stemv,stop-color,stop-opacity,strikethrough-position,strikethrough-thickness,stroke,stroke-dasharray,stroke-dashoffset,stroke-linecap,stroke-linejoin,stroke-miterlimit,stroke-opacity,stroke-width,systemLanguage,target,text-anchor,to,transform,type,u1,u2,underline-position,underline-thickness,unicode,unicode-range,units-per-em,values,version,viewBox,visibility,width,widths,x,x-height,x1,x2,xlink:actuate,xlink:arcrole,xlink:role,xlink:show,xlink:title,xlink:type,xml:base,xml:lang,xml:space,xmlns,xmlns:xlink,y,y1,y2,zoomAndPan"),L=b.extend({},I,K,J),M=document.createElement("pre"),N=/^(\s*)([\s\S]*?)(\s*)$/,O=function(){ -// native trim is way faster: http://jsperf.com/angular-trim-test -// but IE doesn't have it... :-( -// TODO: we should move this into IE/ES5 polyfill -// native trim is way faster: http://jsperf.com/angular-trim-test -// but IE doesn't have it... :-( -// TODO: we should move this into IE/ES5 polyfill -return String.prototype.trim?function(a){return b.isString(a)?a.trim():a}:function(a){return b.isString(a)?a.replace(/^\s\s*/,"").replace(/\s\s*$/,""):a}}(); -// define ngSanitize module and register $sanitize service -b.module("ngSanitize",[]).provider("$sanitize",d),/* global sanitizeText: false */ -/** - * @ngdoc filter - * @name linky - * @kind function - * - * @description - * Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and - * plain email address links. - * - * Requires the {@link ngSanitize `ngSanitize`} module to be installed. - * - * @param {string} text Input text. - * @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in. - * @returns {string} Html-linkified text. - * - * @usage - - * - * @example - - - -
- Snippet: - - - - - - - - - - - - - - - - - - - - - -
FilterSourceRendered
linky filter -
<div ng-bind-html="snippet | linky">
</div>
-
-
-
linky target -
<div ng-bind-html="snippetWithTarget | linky:'_blank'">
</div>
-
-
-
no filter
<div ng-bind="snippet">
</div>
- - - it('should linkify the snippet with urls', function() { - expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). - toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' + - 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); - expect(element.all(by.css('#linky-filter a')).count()).toEqual(4); - }); - - it('should not linkify snippet without the linky filter', function() { - expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()). - toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' + - 'another@somewhere.org, and one more: ftp://127.0.0.1/.'); - expect(element.all(by.css('#escaped-html a')).count()).toEqual(0); - }); - - it('should update', function() { - element(by.model('snippet')).clear(); - element(by.model('snippet')).sendKeys('new http://link.'); - expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()). - toBe('new http://link.'); - expect(element.all(by.css('#linky-filter a')).count()).toEqual(1); - expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()) - .toBe('new http://link.'); - }); - - it('should work with the target property', function() { - expect(element(by.id('linky-target')). - element(by.binding("snippetWithTarget | linky:'_blank'")).getText()). - toBe('http://angularjs.org/'); - expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank'); - }); - - - */ -b.module("ngSanitize").filter("linky",["$sanitize",function(a){var c=/((ftp|https?):\/\/|(www\.)|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"”’]/,d=/^mailto:/;return function(f,g){function h(a){a&&n.push(e(a))}function i(a,c){n.push("'),h(c),n.push("")}if(!f)return f;for(var j,k,l,m=f,n=[];j=m.match(c);) -// We can not end in these as they are sometimes found at the end of the sentence -k=j[0], -// if we did not match ftp/http/www/mailto then assume mailto -j[2]||j[4]||(k=(j[3]?"http://":"mailto:")+k),l=j.index,h(m.substr(0,l)),i(k,j[0].replace(d,"")),m=m.substring(l+j[0].length);return h(m),a(n.join(""))}}])}(window,window.angular); \ No newline at end of file diff --git a/vendor/assets/javascripts/textAngular.min.js b/vendor/assets/javascripts/textAngular.min.js deleted file mode 100644 index cc3a4a97a6..0000000000 --- a/vendor/assets/javascripts/textAngular.min.js +++ /dev/null @@ -1,1481 +0,0 @@ -!function(a,b){"function"==typeof define&&define.amd? -// AMD. Register as an anonymous module unless amdModuleId is set -define("textAngular",["rangy","rangy/lib/rangy-selectionsaverestore"],function(c,d){return a["textAngular.name"]=b(c,d)}):"object"==typeof exports? -// Node. Does not work with strict CommonJS, but -// only CommonJS-like environments that support module.exports, -// like Node. -module.exports=b(require("rangy"),require("rangy/lib/rangy-selectionsaverestore")):a.textAngular=b(rangy)}(this,function(a){ -// tests against the current jqLite/jquery implementation if this can be an element -function b(a){try{return 0!==angular.element(a).length}catch(a){return!1}}/* - A tool definition is an object with the following key/value parameters: - action: [function(deferred, restoreSelection)] - a function that is executed on clicking on the button - this will allways be executed using ng-click and will - overwrite any ng-click value in the display attribute. - The function is passed a deferred object ($q.defer()), if this is wanted to be used `return false;` from the action and - manually call `deferred.resolve();` elsewhere to notify the editor that the action has finished. - restoreSelection is only defined if the rangy library is included and it can be called as `restoreSelection()` to restore the users - selection in the WYSIWYG editor. - display: [string]? - Optional, an HTML element to be displayed as the button. The `scope` of the button is the tool definition object with some additional functions - If set this will cause buttontext and iconclass to be ignored - class: [string]? - Optional, if set will override the taOptions.classes.toolbarButton class. - buttontext: [string]? - if this is defined it will replace the contents of the element contained in the `display` element - iconclass: [string]? - if this is defined an icon () will be appended to the `display` element with this string as it's class - tooltiptext: [string]? - Optional, a plain text description of the action, used for the title attribute of the action button in the toolbar by default. - activestate: [function(commonElement)]? - this function is called on every caret movement, if it returns true then the class taOptions.classes.toolbarButtonActive - will be applied to the `display` element, else the class will be removed - disabled: [function()]? - if this function returns true then the tool will have the class taOptions.classes.disabled applied to it, else it will be removed - Other functions available on the scope are: - name: [string] - the name of the tool, this is the first parameter passed into taRegisterTool - isDisabled: [function()] - returns true if the tool is disabled, false if it isn't - displayActiveToolClass: [function(boolean)] - returns true if the tool is 'active' in the currently focussed toolbar - onElementSelect: [Object] - This object contains the following key/value pairs and is used to trigger the ta-element-select event - element: [String] - an element name, will only trigger the onElementSelect action if the tagName of the element matches this string - filter: [function(element)]? - an optional filter that returns a boolean, if true it will trigger the onElementSelect. - action: [function(event, element, editorScope)] - the action that should be executed if the onElementSelect function runs -*/ -// name and toolDefinition to add into the tools available to be added on the toolbar -function c(a,c){if(!a||""===a||e.hasOwnProperty(a))throw"textAngular Error: A unique name is required for a Tool Definition";if(c.display&&(""===c.display||!b(c.display))||!c.display&&!c.buttontext&&!c.iconclass)throw'textAngular Error: Tool Definition for "'+a+'" does not have a valid display/iconclass/buttontext value';e[a]=c} -// usage is: -// var t0 = performance.now(); -// doSomething(); -// var t1 = performance.now(); -// console.log('Took', (t1 - t0).toFixed(4), 'milliseconds to do something!'); -// -// turn html into pure text that shows visiblity -function d(a){var b=document.createElement("DIV");b.innerHTML=a;var c=b.textContent||b.innerText||"";// zero width space -return c.replace("​",""),c=c.trim()} -// setup the global contstant functions for setting up the toolbar -// all tool definitions -var e={};angular.module("textAngularSetup",[]).constant("taRegisterTool",c).value("taTools",e).value("taOptions",{ -////////////////////////////////////////////////////////////////////////////////////// -// forceTextAngularSanitize -// set false to allow the textAngular-sanitize provider to be replaced -// with angular-sanitize or a custom provider. -forceTextAngularSanitize:!0, -/////////////////////////////////////////////////////////////////////////////////////// -// keyMappings -// allow customizable keyMappings for specialized key boards or languages -// -// keyMappings provides key mappings that are attached to a given commandKeyCode. -// To modify a specific keyboard binding, simply provide function which returns true -// for the event you wish to map to. -// Or to disable a specific keyboard binding, provide a function which returns false. -// Note: 'RedoKey' and 'UndoKey' are internally bound to the redo and undo functionality. -// At present, the following commandKeyCodes are in use: -// 98, 'TabKey', 'ShiftTabKey', 105, 117, 'UndoKey', 'RedoKey' -// -// To map to an new commandKeyCode, add a new key mapping such as: -// {commandKeyCode: 'CustomKey', testForKey: function (event) { -// if (event.keyCode=57 && event.ctrlKey && !event.shiftKey && !event.altKey) return true; -// } } -// to the keyMappings. This example maps ctrl+9 to 'CustomKey' -// Then where taRegisterTool(...) is called, add a commandKeyCode: 'CustomKey' and your -// tool will be bound to ctrl+9. -// -// To disble one of the already bound commandKeyCodes such as 'RedoKey' or 'UndoKey' add: -// {commandKeyCode: 'RedoKey', testForKey: function (event) { return false; } }, -// {commandKeyCode: 'UndoKey', testForKey: function (event) { return false; } }, -// to disable them. -// -keyMappings:[],toolbar:[["h1","h2","h3","h4","h5","h6","p","pre","quote"],["bold","italics","underline","strikeThrough","ul","ol","redo","undo","clear"],["justifyLeft","justifyCenter","justifyRight","justifyFull","indent","outdent"],["html","insertImage","insertLink","insertVideo","wordcount","charcount"]],classes:{focussed:"focussed",toolbar:"btn-toolbar",toolbarGroup:"btn-group",toolbarButton:"btn btn-default",toolbarButtonActive:"active",disabled:"disabled",textEditor:"form-control",htmlEditor:"form-control"},defaultTagAttributes:{a:{target:""}},setup:{ -// wysiwyg mode -textEditorSetup:function(a){}, -// raw html -htmlEditorSetup:function(a){}},defaultFileDropHandler:/* istanbul ignore next: untestable image processing */ -function(a,b){var c=new FileReader;return"image"===a.type.substring(0,5)&&(c.onload=function(){""!==c.result&&b("insertImage",c.result,!0)},c.readAsDataURL(a),!0)}}).value("taSelectableElements",["a","img"]).value("taCustomRenderers",[{ -// Parse back out: '
' -// To correct video element. For now only support youtube -selector:"img",customAttribute:"ta-insert-video",renderLogic:function(a){var b=angular.element(""),c=a.prop("attributes"); -// loop through element attributes and apply them on iframe -angular.forEach(c,function(a){b.attr(a.name,a.value)}),b.attr("src",b.attr("ta-insert-video")),a.replaceWith(b)}}]).value("taTranslations",{ -// moved to sub-elements -//toggleHTML: "Toggle HTML", -//insertImage: "Please enter a image URL to insert", -//insertLink: "Please enter a URL to insert", -//insertVideo: "Please enter a youtube URL to embed", -html:{tooltip:"Toggle html / Rich Text"}, -// tooltip for heading - might be worth splitting -heading:{tooltip:"Heading "},p:{tooltip:"Paragraph"},pre:{tooltip:"Preformatted text"},ul:{tooltip:"Unordered List"},ol:{tooltip:"Ordered List"},quote:{tooltip:"Quote/unquote selection or paragraph"},undo:{tooltip:"Undo"},redo:{tooltip:"Redo"},bold:{tooltip:"Bold"},italic:{tooltip:"Italic"},underline:{tooltip:"Underline"},strikeThrough:{tooltip:"Strikethrough"},justifyLeft:{tooltip:"Align text left"},justifyRight:{tooltip:"Align text right"},justifyFull:{tooltip:"Justify text"},justifyCenter:{tooltip:"Center"},indent:{tooltip:"Increase indent"},outdent:{tooltip:"Decrease indent"},clear:{tooltip:"Clear formatting"},insertImage:{dialogPrompt:"Please enter an image URL to insert",tooltip:"Insert image",hotkey:"the - possibly language dependent hotkey ... for some future implementation"},insertVideo:{tooltip:"Insert video",dialogPrompt:"Please enter a youtube URL to embed"},insertLink:{tooltip:"Insert / edit link",dialogPrompt:"Please enter a URL to insert"},editLink:{reLinkButton:{tooltip:"Relink"},unLinkButton:{tooltip:"Unlink"},targetToggle:{buttontext:"Open in New Window"}},wordcount:{tooltip:"Display words Count"},charcount:{tooltip:"Display characters Count"}}).factory("taToolFunctions",["$window","taTranslations",function(a,b){return{imgOnSelectAction:function(a,b,c){ -// setup the editor toolbar -// Credit to the work at http://hackerwins.github.io/summernote/ for this editbar logic/display -var d=function(){c.updateTaBindtaTextElement(),c.hidePopover()};a.preventDefault(),c.displayElements.popover.css("width","375px");var e=c.displayElements.popoverContainer;e.empty();var f=angular.element('
'),g=angular.element('');g.on("click",function(a){a.preventDefault(),b.css({width:"100%",height:""}),d()});var h=angular.element('');h.on("click",function(a){a.preventDefault(),b.css({width:"50%",height:""}),d()});var i=angular.element('');i.on("click",function(a){a.preventDefault(),b.css({width:"25%",height:""}),d()});var j=angular.element('');j.on("click",function(a){a.preventDefault(),b.css({width:"",height:""}),d()}),f.append(g),f.append(h),f.append(i),f.append(j),e.append(f),f=angular.element('
');var k=angular.element('');k.on("click",function(a){a.preventDefault(), -// webkit -b.css("float","left"), -// firefox -b.css("cssFloat","left"), -// IE < 8 -b.css("styleFloat","left"),d()});var l=angular.element('');l.on("click",function(a){a.preventDefault(), -// webkit -b.css("float","right"), -// firefox -b.css("cssFloat","right"), -// IE < 8 -b.css("styleFloat","right"),d()});var m=angular.element('');m.on("click",function(a){a.preventDefault(), -// webkit -b.css("float",""), -// firefox -b.css("cssFloat",""), -// IE < 8 -b.css("styleFloat",""),d()}),f.append(k),f.append(m),f.append(l),e.append(f),f=angular.element('
');var n=angular.element('');n.on("click",function(a){a.preventDefault(),b.remove(),d()}),f.append(n),e.append(f),c.showPopover(b),c.showResizeOverlay(b)},aOnSelectAction:function(c,d,e){ -// setup the editor toolbar -// Credit to the work at http://hackerwins.github.io/summernote/ for this editbar logic -c.preventDefault(),e.displayElements.popover.css("width","436px");var f=e.displayElements.popoverContainer;f.empty(),f.css("line-height","28px");var g=angular.element(''+d.attr("href")+"");g.css({display:"inline-block","max-width":"200px",overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap","vertical-align":"middle"}),f.append(g);var h=angular.element('
'),i=angular.element('');i.on("click",function(c){c.preventDefault();var f=a.prompt(b.insertLink.dialogPrompt,d.attr("href"));f&&""!==f&&"http://"!==f&&(d.attr("href",f),e.updateTaBindtaTextElement()),e.hidePopover()}),h.append(i);var j=angular.element(''); -// directly before this click event is fired a digest is fired off whereby the reference to $element is orphaned off -j.on("click",function(a){a.preventDefault(),d.replaceWith(d.contents()),e.updateTaBindtaTextElement(),e.hidePopover()}),h.append(j);var k=angular.element('");"_blank"===d.attr("target")&&k.addClass("active"),k.on("click",function(a){a.preventDefault(),d.attr("target","_blank"===d.attr("target")?"":"_blank"),k.toggleClass("active"),e.updateTaBindtaTextElement()}),h.append(k),f.append(h),e.showPopover(d)},extractYoutubeVideoId:function(a){var b=/(?:youtube(?:-nocookie)?\.com\/(?:[^\/\n\s]+\/\S+\/|(?:v|e(?:mbed)?)\/|\S*?[?&]v=)|youtu\.be\/)([a-zA-Z0-9_-]{11})/i,c=a.match(b);return c&&c[1]||null}}}]).run(["taRegisterTool","$window","taTranslations","taSelection","taToolFunctions","$sanitize","taOptions","$log",function(a,b,c,d,e,f,g,h){ -// test for the version of $sanitize that is in use -// You can disable this check by setting taOptions.textAngularSanitize == false -var i={};/* istanbul ignore next, throws error */ -if(f("",i),g.forceTextAngularSanitize===!0&&"taSanitize"!==i.version)throw angular.$$minErr("textAngular")("textAngularSetup","The textAngular-sanitize provider has been replaced by another -- have you included angular-sanitize by mistake?");a("html",{iconclass:"fa fa-code",tooltiptext:c.html.tooltip,action:function(){this.$editor().switchView()},activeState:function(){return this.$editor().showHtml}}); -// add the Header tools -// convenience functions so that the loop works correctly -var j=function(a){return function(){return this.$editor().queryFormatBlockState(a)}},k=function(){return this.$editor().wrapSelection("formatBlock","<"+this.name.toUpperCase()+">")};angular.forEach(["h1","h2","h3","h4","h5","h6"],function(b){a(b.toLowerCase(),{buttontext:b.toUpperCase(),tooltiptext:c.heading.tooltip+b.charAt(1),action:k,activeState:j(b.toLowerCase())})}),a("p",{buttontext:"P",tooltiptext:c.p.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","

")},activeState:function(){return this.$editor().queryFormatBlockState("p")}}), -// key: pre -> taTranslations[key].tooltip, taTranslations[key].buttontext -a("pre",{buttontext:"pre",tooltiptext:c.pre.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","

")},activeState:function(){return this.$editor().queryFormatBlockState("pre")}}),a("ul",{iconclass:"fa fa-list-ul",tooltiptext:c.ul.tooltip,action:function(){return this.$editor().wrapSelection("insertUnorderedList",null)},activeState:function(){return this.$editor().queryCommandState("insertUnorderedList")}}),a("ol",{iconclass:"fa fa-list-ol",tooltiptext:c.ol.tooltip,action:function(){return this.$editor().wrapSelection("insertOrderedList",null)},activeState:function(){return this.$editor().queryCommandState("insertOrderedList")}}),a("quote",{iconclass:"fa fa-quote-right",tooltiptext:c.quote.tooltip,action:function(){return this.$editor().wrapSelection("formatBlock","
")},activeState:function(){return this.$editor().queryFormatBlockState("blockquote")}}),a("undo",{iconclass:"fa fa-undo",tooltiptext:c.undo.tooltip,action:function(){return this.$editor().wrapSelection("undo",null)}}),a("redo",{iconclass:"fa fa-repeat",tooltiptext:c.redo.tooltip,action:function(){return this.$editor().wrapSelection("redo",null)}}),a("bold",{iconclass:"fa fa-bold",tooltiptext:c.bold.tooltip,action:function(){return this.$editor().wrapSelection("bold",null)},activeState:function(){return this.$editor().queryCommandState("bold")},commandKeyCode:98}),a("justifyLeft",{iconclass:"fa fa-align-left",tooltiptext:c.justifyLeft.tooltip,action:function(){return this.$editor().wrapSelection("justifyLeft",null)},activeState:function(a){/* istanbul ignore next: */ -if(a&&"#document"===a.nodeName)return!1;var b=!1;if(a) -// commonELement.css('text-align') can throw an error 'Cannot read property 'defaultView' of null' in rare conditions -// so we do try catch here... -try{b="left"===a.css("text-align")||"left"===a.attr("align")||"right"!==a.css("text-align")&&"center"!==a.css("text-align")&&"justify"!==a.css("text-align")&&!this.$editor().queryCommandState("justifyRight")&&!this.$editor().queryCommandState("justifyCenter")&&!this.$editor().queryCommandState("justifyFull")}catch(a){/* istanbul ignore next: error handler */ -//console.log(e); -b=!1}return b=b||this.$editor().queryCommandState("justifyLeft")}}),a("justifyRight",{iconclass:"fa fa-align-right",tooltiptext:c.justifyRight.tooltip,action:function(){return this.$editor().wrapSelection("justifyRight",null)},activeState:function(a){/* istanbul ignore next: */ -if(a&&"#document"===a.nodeName)return!1;var b=!1;if(a) -// commonELement.css('text-align') can throw an error 'Cannot read property 'defaultView' of null' in rare conditions -// so we do try catch here... -try{b="right"===a.css("text-align")}catch(a){/* istanbul ignore next: error handler */ -//console.log(e); -b=!1}return b=b||this.$editor().queryCommandState("justifyRight")}}),a("justifyFull",{iconclass:"fa fa-align-justify",tooltiptext:c.justifyFull.tooltip,action:function(){return this.$editor().wrapSelection("justifyFull",null)},activeState:function(a){var b=!1;if(a) -// commonELement.css('text-align') can throw an error 'Cannot read property 'defaultView' of null' in rare conditions -// so we do try catch here... -try{b="justify"===a.css("text-align")}catch(a){/* istanbul ignore next: error handler */ -//console.log(e); -b=!1}return b=b||this.$editor().queryCommandState("justifyFull")}}),a("justifyCenter",{iconclass:"fa fa-align-center",tooltiptext:c.justifyCenter.tooltip,action:function(){return this.$editor().wrapSelection("justifyCenter",null)},activeState:function(a){/* istanbul ignore next: */ -if(a&&"#document"===a.nodeName)return!1;var b=!1;if(a) -// commonELement.css('text-align') can throw an error 'Cannot read property 'defaultView' of null' in rare conditions -// so we do try catch here... -try{b="center"===a.css("text-align")}catch(a){/* istanbul ignore next: error handler */ -//console.log(e); -b=!1}return b=b||this.$editor().queryCommandState("justifyCenter")}}),a("indent",{iconclass:"fa fa-indent",tooltiptext:c.indent.tooltip,action:function(){return this.$editor().wrapSelection("indent",null)},activeState:function(){return this.$editor().queryFormatBlockState("blockquote")},commandKeyCode:"TabKey"}),a("outdent",{iconclass:"fa fa-outdent",tooltiptext:c.outdent.tooltip,action:function(){return this.$editor().wrapSelection("outdent",null)},activeState:function(){return!1},commandKeyCode:"ShiftTabKey"}),a("italics",{iconclass:"fa fa-italic",tooltiptext:c.italic.tooltip,action:function(){return this.$editor().wrapSelection("italic",null)},activeState:function(){return this.$editor().queryCommandState("italic")},commandKeyCode:105}),a("underline",{iconclass:"fa fa-underline",tooltiptext:c.underline.tooltip,action:function(){return this.$editor().wrapSelection("underline",null)},activeState:function(){return this.$editor().queryCommandState("underline")},commandKeyCode:117}),a("strikeThrough",{iconclass:"fa fa-strikethrough",tooltiptext:c.strikeThrough.tooltip,action:function(){return this.$editor().wrapSelection("strikeThrough",null)},activeState:function(){return document.queryCommandState("strikeThrough")}}),a("clear",{iconclass:"fa fa-ban",tooltiptext:c.clear.tooltip,action:function(a,b){var c;this.$editor().wrapSelection("removeFormat",null);var e=angular.element(d.getSelectionElement());c=d.getAllSelectedElements(); -//$log.log('selectedElements:', selectedElements); -// remove lists -var f=function(a,b){a=angular.element(a);var c=b;return b||(c=a),angular.forEach(a.children(),function(a){if("ul"===a.tagName.toLowerCase()||"ol"===a.tagName.toLowerCase())c=f(a,c);else{var b=angular.element("

");b.html(angular.element(a).html()),c.after(b),c=b}}),a.remove(),c};angular.forEach(c,function(a){"ul"!==a.nodeName.toLowerCase()&&"ol"!==a.nodeName.toLowerCase()|| -//console.log('removeListElements', element); -f(a)}),angular.forEach(e.find("ul"),f),angular.forEach(e.find("ol"),f); -// clear out all class attributes. These do not seem to be cleared via removeFormat -var g=this.$editor(),h=function(a){a=angular.element(a),/* istanbul ignore next: this is not triggered in tests any longer since we now never select the whole displayELement */ -a[0]!==g.displayElements.text[0]&&a.removeAttr("class"),angular.forEach(a.children(),h)};angular.forEach(e,h), -// check if in list. If not in list then use formatBlock option -e[0]&&"li"!==e[0].tagName.toLowerCase()&&"ol"!==e[0].tagName.toLowerCase()&&"ul"!==e[0].tagName.toLowerCase()&&"true"!==e[0].getAttribute("contenteditable")&&this.$editor().wrapSelection("formatBlock","default"),b()}});/* jshint -W099 */ -/**************************** - // we don't use this code - since the previous way CLEAR is expected to work does not clear partially selected
  • - - var removeListElement = function(listE){ - console.log(listE); - var _list = listE.parentNode.childNodes; - console.log('_list', _list); - var _preLis = [], _postLis = [], _found = false; - for (i = 0; i < _list.length; i++) { - if (_list[i] === listE) { - _found = true; - } else if (!_found) _preLis.push(_list[i]); - else _postLis.push(_list[i]); - } - var _parent = angular.element(listE.parentNode); - var newElem = angular.element('

    '); - newElem.html(angular.element(listE).html()); - if (_preLis.length === 0 || _postLis.length === 0) { - if (_postLis.length === 0) _parent.after(newElem); - else _parent[0].parentNode.insertBefore(newElem[0], _parent[0]); - - if (_preLis.length === 0 && _postLis.length === 0) _parent.remove(); - else angular.element(listE).remove(); - } else { - var _firstList = angular.element('<' + _parent[0].tagName + '>'); - var _secondList = angular.element('<' + _parent[0].tagName + '>'); - for (i = 0; i < _preLis.length; i++) _firstList.append(angular.element(_preLis[i])); - for (i = 0; i < _postLis.length; i++) _secondList.append(angular.element(_postLis[i])); - _parent.after(_secondList); - _parent.after(newElem); - _parent.after(_firstList); - _parent.remove(); - } - taSelection.setSelectionToElementEnd(newElem[0]); - }; - - elementsSeen = []; - if (selectedElements.length !==0) console.log(selectedElements); - angular.forEach(selectedElements, function (element) { - if (elementsSeen.indexOf(element) !== -1 || elementsSeen.indexOf(element.parentElement) !== -1) { - return; - } - elementsSeen.push(element); - if (element.nodeName.toLowerCase() === 'li') { - console.log('removeListElement', element); - removeListElement(element); - } - else if (element.parentElement && element.parentElement.nodeName.toLowerCase() === 'li') { - console.log('removeListElement', element.parentElement); - elementsSeen.push(element.parentElement); - removeListElement(element.parentElement); - } - }); - **********************/ -/********************** - if(possibleNodes[0].tagName.toLowerCase() === 'li'){ - var _list = possibleNodes[0].parentNode.childNodes; - var _preLis = [], _postLis = [], _found = false; - for(i = 0; i < _list.length; i++){ - if(_list[i] === possibleNodes[0]){ - _found = true; - }else if(!_found) _preLis.push(_list[i]); - else _postLis.push(_list[i]); - } - var _parent = angular.element(possibleNodes[0].parentNode); - var newElem = angular.element('

    '); - newElem.html(angular.element(possibleNodes[0]).html()); - if(_preLis.length === 0 || _postLis.length === 0){ - if(_postLis.length === 0) _parent.after(newElem); - else _parent[0].parentNode.insertBefore(newElem[0], _parent[0]); - - if(_preLis.length === 0 && _postLis.length === 0) _parent.remove(); - else angular.element(possibleNodes[0]).remove(); - }else{ - var _firstList = angular.element('<'+_parent[0].tagName+'>'); - var _secondList = angular.element('<'+_parent[0].tagName+'>'); - for(i = 0; i < _preLis.length; i++) _firstList.append(angular.element(_preLis[i])); - for(i = 0; i < _postLis.length; i++) _secondList.append(angular.element(_postLis[i])); - _parent.after(_secondList); - _parent.after(newElem); - _parent.after(_firstList); - _parent.remove(); - } - taSelection.setSelectionToElementEnd(newElem[0]); - } - *******************/ -/* istanbul ignore next: if it's javascript don't worry - though probably should show some kind of error message */ -var l=function(a){return a.toLowerCase().indexOf("javascript")!==-1};a("insertImage",{iconclass:"fa fa-picture-o",tooltiptext:c.insertImage.tooltip,action:function(){var a;if(a=b.prompt(c.insertImage.dialogPrompt,"http://"),a&&""!==a&&"http://"!==a&&!l(a)){d.getSelectionElement().tagName&&"a"===d.getSelectionElement().tagName.toLowerCase()&& -// due to differences in implementation between FireFox and Chrome, we must move the -// insertion point past the element, otherwise FireFox inserts inside the -// With this change, both FireFox and Chrome behave the same way! -d.setSelectionAfterElement(d.getSelectionElement()); -// In the past we used the simple statement: -//return this.$editor().wrapSelection('insertImage', imageLink, true); -// -// However on Firefox only, when the content is empty this is a problem -// See Issue #1201 -// Investigation reveals that Firefox only inserts a

    only!!!! -// So now we use insertHTML here and all is fine. -// NOTE: this is what 'insertImage' is supposed to do anyway! -var e='';return this.$editor().wrapSelection("insertHTML",e,!0)}},onElementSelect:{element:"img",action:e.imgOnSelectAction}}),a("insertVideo",{iconclass:"fa fa-youtube-play",tooltiptext:c.insertVideo.tooltip,action:function(){var a; -// block javascript here -/* istanbul ignore else: if it's javascript don't worry - though probably should show some kind of error message */ -if(a=b.prompt(c.insertVideo.dialogPrompt,"https://"),!l(a)&&a&&""!==a&&"https://"!==a&&(videoId=e.extractYoutubeVideoId(a),videoId)){ -// create the embed link -var f="https://www.youtube.com/embed/"+videoId,g=''; -// insert -/* istanbul ignore next: don't know how to test this... since it needs a dialogPrompt */ -// due to differences in implementation between FireFox and Chrome, we must move the -// insertion point past the element, otherwise FireFox inserts inside the -// With this change, both FireFox and Chrome behave the same way! -return d.getSelectionElement().tagName&&"a"===d.getSelectionElement().tagName.toLowerCase()&&d.setSelectionAfterElement(d.getSelectionElement()),this.$editor().wrapSelection("insertHTML",g,!0)}},onElementSelect:{element:"img",onlyWithAttrs:["ta-insert-video"],action:e.imgOnSelectAction}}),a("insertLink",{tooltiptext:c.insertLink.tooltip,iconclass:"fa fa-link",action:function(){var a;if( -// if this link has already been set, we need to just edit the existing link -/* istanbul ignore if: we do not test this */ -a=d.getSelectionElement().tagName&&"a"===d.getSelectionElement().tagName.toLowerCase()?b.prompt(c.insertLink.dialogPrompt,d.getSelectionElement().href):b.prompt(c.insertLink.dialogPrompt,"http://"),a&&""!==a&&"http://"!==a&&!l(a))return this.$editor().wrapSelection("createLink",a,!0)},activeState:function(a){return!!a&&"A"===a[0].tagName},onElementSelect:{element:"a",action:e.aOnSelectAction}}),a("wordcount",{display:'

    ',disabled:!0,wordcount:0,activeState:function(){// this fires on keyup -var a=this.$editor().displayElements.text,b=a[0].innerHTML||"",c=0;/* istanbul ignore if: will default to '' when undefined */ -//Set current scope -//Set editor scope -return""!==b.replace(/\s*<[^>]*?>\s*/g,"")&&""!==b.trim()&&(c=b.replace(/<\/?(b|i|em|strong|span|u|strikethrough|a|img|small|sub|sup|label)( [^>*?])?>/gi,"").replace(/(<[^>]*?>\s*<[^>]*?>)/gi," ").replace(/(<[^>]*?>)/gi,"").replace(/\s+/gi," ").match(/\S+/g).length),this.wordcount=c,this.$editor().wordcount=c,!1}}),a("charcount",{display:'
    Characters:
    ',disabled:!0,charcount:0,activeState:function(){// this fires on keyup -var a=this.$editor().displayElements.text,b=a[0].innerText||a[0].textContent,c=b.replace(/(\r\n|\n|\r)/gm,"").replace(/^\s+/g," ").replace(/\s+$/g," ").length; -//Set current scope -//Set editor scope -return this.charcount=c,this.$editor().charcount=c,!1}})}]);// NOTE: textAngularVersion must match the Gruntfile.js 'setVersion' task.... and have format v/d+./d+./d+ -var f="v1.5.16",g={ie:function(){for(var a,b=3,c=document.createElement("div"),d=c.getElementsByTagName("i");c.innerHTML="",d[0];);return b>4?b:a}(),webkit:/AppleWebKit\/([\d.]+)/i.test(navigator.userAgent),isFirefox:navigator.userAgent.toLowerCase().indexOf("firefox")>-1},h=h||{};/* istanbul ignore next: untestable browser check */ -h.now=function(){return h.now||h.mozNow||h.msNow||h.oNow||h.webkitNow||function(){return(new Date).getTime()}}(); -// Global to textAngular REGEXP vars for block and list elements. -var i=/^(address|article|aside|audio|blockquote|canvas|center|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video)$/i,j=/^(ul|li|ol)$/i,k=/^(#text|span|address|article|aside|audio|blockquote|canvas|center|dd|div|dl|fieldset|figcaption|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|noscript|ol|output|p|pre|section|table|tfoot|ul|video|li)$/i; -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Compatibility -/* istanbul ignore next: trim shim for older browsers */ -String.prototype.trim||(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")});/* - Custom stylesheet for the placeholders rules. - Credit to: http://davidwalsh.name/add-rules-stylesheets -*/ -var l,m,n,o,p,q;/* istanbul ignore else: IE <8 test*/ -if(g.ie>8||void 0===g.ie){/* istanbul ignore next: preference for stylesheet loaded externally */ -for(var r=document.styleSheets,s=0;s tag -var a=document.createElement("style");/* istanbul ignore else : WebKit hack :( */ -// Add the