diff --git a/app/assets/javascripts/admin/all.js b/app/assets/javascripts/admin/all.js index 7c89801bcd..6135d8cb5e 100644 --- a/app/assets/javascripts/admin/all.js +++ b/app/assets/javascripts/admin/all.js @@ -15,6 +15,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 @@ -61,11 +62,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 9af23e4068..16ecf14af8 100644 --- a/app/models/enterprise.rb +++ b/app/models/enterprise.rb @@ -251,12 +251,21 @@ class Enterprise < 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 + + 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..4c92c577ca 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", value: @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..3b7954f77c 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", value: @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..1761e47544 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", value: @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", value: @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 diff --git a/app/webpacker/controllers/trixeditor_controller.js b/app/webpacker/controllers/trixeditor_controller.js index 506086fb2b..205befbc69 100644 --- a/app/webpacker/controllers/trixeditor_controller.js +++ b/app/webpacker/controllers/trixeditor_controller.js @@ -21,6 +21,15 @@ export default class extends Controller { }; #trixInitialize = () => { + // Set I18n translations on Trix. + if(I18n.t("js.trix")) { + // Calling 'Trix.config.lang = I18n.t("js.trix")' doesn't work due to read-only error, so set + // translations one at a time. + for (const [key, translation] of Object.entries(I18n.t("js.trix"))) { + Trix.config.lang[key] = translation; + } + } + // Add HR button to the Trix toolbar if it's not already there and the editor is present if ( this.element.editor && diff --git a/app/webpacker/css/admin/all.scss b/app/webpacker/css/admin/all.scss index d9ad7c0008..254b188a78 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"; @@ -72,7 +71,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 7ee27d28ac..843204ec1b 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"; @@ -76,7 +75,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/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/ar.yml b/config/locales/ar.yml index c762b981bb..3e557148a7 100644 --- a/config/locales/ar.yml +++ b/config/locales/ar.yml @@ -1051,8 +1051,6 @@ ar: رسالة اختيارية للترحيب بالعملاء وشرح كيفية التسوق معك. إذا تم إدخال النص هنا ، فسيتم عرضه في علامة تبويب الصفحة الرئيسية عندما يصل العملاء إلى واجهة المتجر الخاصة بك. - shopfront_message_link_tooltip: "إدراج / تحرير الرابط" - shopfront_message_link_prompt: "الرجاء إدخال عنوان URL لإدراجه" shopfront_closed_message: "واجهة المتجر رسالة مغلقة" shopfront_closed_message_placeholder: > الرسالة تقدم شرحًا أكثر تفصيلًا حول سبب إغلاق متجرك و / أو متى يمكن diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 6e1732d504..5ba93b6878 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -1116,8 +1116,6 @@ ca: com comprar amb vosaltres. Si s'introdueix text aquí, es mostrarà a la pestanya d'inici quan els clients arribin per primera vegada a la vostra botiga. - shopfront_message_link_tooltip: "Inserir / editar enllaç" - shopfront_message_link_prompt: "Introduïu un URL per inserir" shopfront_closed_message: "Missatge de tancament de la botiga" shopfront_closed_message_placeholder: > Un missatge que proporciona una explicació més detallada sobre per què diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 9b111b2005..52625b65c6 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -1158,8 +1158,6 @@ cy: Neges ddewisol i groesawu cwsmeriaid ac eluro sut i siopa gyda chi. Os nodir testun fan hyn, caiff ei arddangos mewn tab hafan pan fydd cwsmeriaid yn cyrraedd ffrynt eich siop - shopfront_message_link_tooltip: "Mewnosod / golygu dolen" - shopfront_message_link_prompt: "Noder URL i'w fewnosod" shopfront_closed_message: "Neges - Ffrynt Siop Ar Gau " shopfront_closed_message_placeholder: > Neges sy'n rhoi eglurhad mwy manwl ynghylch pam mae'ch siop ar gau a/neu diff --git a/config/locales/de_CH.yml b/config/locales/de_CH.yml index 3606cc8eee..0a80f4ba29 100644 --- a/config/locales/de_CH.yml +++ b/config/locales/de_CH.yml @@ -1033,8 +1033,6 @@ de_CH: wie sie bei Ihnen einkaufen können. Wenn hier Text eingegeben wird, wird dieser auf einer Startseite in Ihrem Laden angezeigt, wenn Kunden ihn zum ersten Mal besuchen. - shopfront_message_link_tooltip: "Link einfügen/bearbeiten" - shopfront_message_link_prompt: "Bitte geben Sie die einzufügende URL ein:" shopfront_closed_message: "'Laden geschlossen'-Nachricht im Laden" shopfront_closed_message_placeholder: > Eine Nachricht, die eine detailliertere Erklärung liefert, warum Ihr diff --git a/config/locales/de_DE.yml b/config/locales/de_DE.yml index dee8187e4c..d60b11d25b 100644 --- a/config/locales/de_DE.yml +++ b/config/locales/de_DE.yml @@ -1143,8 +1143,6 @@ de_DE: wie sie bei Ihnen einkaufen können. Wenn hier Text eingegeben wird, wird dieser auf einer Startseite in Ihrem Laden angezeigt, wenn Kunden ihn zum ersten Mal besuchen. - shopfront_message_link_tooltip: "Link einfügen/bearbeiten" - shopfront_message_link_prompt: "Bitte geben Sie die einzufügende URL ein:" shopfront_closed_message: "'Laden geschlossen'-Nachricht im Laden" shopfront_closed_message_placeholder: > Eine Nachricht, die eine detailliertere Erklärung liefert, warum Ihr diff --git a/config/locales/el.yml b/config/locales/el.yml index e8a12eee70..a95f376fcf 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -1193,8 +1193,6 @@ el: πώς να ψωνίζουν από εσάς. Εάν εισαχθεί κείμενο εδώ, θα εμφανιστεί σε μια καρτέλα αρχικής σελίδας όταν οι πελάτες φτάσουν για πρώτη φορά στην πρόσοψη του καταστήματός σας. - shopfront_message_link_tooltip: "Εισαγωγή / επεξεργασία συνδέσμου" - shopfront_message_link_prompt: "Εισαγάγετε μια διεύθυνση URL για εισαγωγή" shopfront_closed_message: "Κλειστό μήνυμα βιτρίνας" shopfront_closed_message_placeholder: > Ένα μήνυμα που παρέχει μια πιο λεπτομερή εξήγηση για το λόγο που το diff --git a/config/locales/en.yml b/config/locales/en.yml index 74e5b24b68..6ee22739ab 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1279,8 +1279,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 @@ -3725,6 +3723,33 @@ 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: + attachFiles: "Attach Files" + bold: "Bold" + bullets: "Bullets" + byte: "Byte" + bytes: "Bytes" + captionPlaceholder: "Add a caption…" + code: "Code" + heading1: "Heading" + indent: "Increase Level" + italic: "Italic" + link: "Link" + numbers: "Numbers" + outdent: "Decrease Level" + quote: "Quote" + redo: "Redo" + remove: "Remove" + strike: "Strikethrough" + undo: "Undo" + unlink: "Unlink" + url: "URL" + urlPlaceholder: "Enter a URL…" + GB: "GB" + KB: "KB" + MB: "MB" + PB: "PB" + TB: "TB" # Singular and plural forms of commonly used words. # We use these entries to pluralize unit names in every language. diff --git a/config/locales/en_AU.yml b/config/locales/en_AU.yml index a8e1c84b4c..a8799e1263 100644 --- a/config/locales/en_AU.yml +++ b/config/locales/en_AU.yml @@ -874,8 +874,6 @@ en_AU: will need to understand the process of buying from you. You can also include links to your newsletter sign up, so that people can connect with you to hear when your next order cycle opens. - 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 diff --git a/config/locales/en_BE.yml b/config/locales/en_BE.yml index f2ac862297..44f917d648 100644 --- a/config/locales/en_BE.yml +++ b/config/locales/en_BE.yml @@ -815,8 +815,6 @@ en_BE: customer_names_false: "Disabled" customer_names_true: "Enabled" shopfront_message: "Shopfront Message" - 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 diff --git a/config/locales/en_CA.yml b/config/locales/en_CA.yml index f97338b584..3facf59021 100644 --- a/config/locales/en_CA.yml +++ b/config/locales/en_CA.yml @@ -1195,8 +1195,6 @@ en_CA: 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 diff --git a/config/locales/en_DE.yml b/config/locales/en_DE.yml index 643eb4af53..0baba83fda 100644 --- a/config/locales/en_DE.yml +++ b/config/locales/en_DE.yml @@ -823,8 +823,6 @@ en_DE: customer_names_false: "Disabled" customer_names_true: "Enabled" shopfront_message: "Shopfront Message" - 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 diff --git a/config/locales/en_FR.yml b/config/locales/en_FR.yml index 07e8a98094..e5672e0e6b 100644 --- a/config/locales/en_FR.yml +++ b/config/locales/en_FR.yml @@ -1197,8 +1197,6 @@ en_FR: 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 diff --git a/config/locales/en_GB.yml b/config/locales/en_GB.yml index db025ca486..8b1144c203 100644 --- a/config/locales/en_GB.yml +++ b/config/locales/en_GB.yml @@ -1170,8 +1170,6 @@ en_GB: 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 diff --git a/config/locales/en_IE.yml b/config/locales/en_IE.yml index 8c8b70123b..aa9e5c32af 100644 --- a/config/locales/en_IE.yml +++ b/config/locales/en_IE.yml @@ -1167,8 +1167,6 @@ en_IE: 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 diff --git a/config/locales/en_IN.yml b/config/locales/en_IN.yml index f6f4f26cf3..b7d7c6b359 100644 --- a/config/locales/en_IN.yml +++ b/config/locales/en_IN.yml @@ -847,8 +847,6 @@ en_IN: 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 diff --git a/config/locales/en_NZ.yml b/config/locales/en_NZ.yml index a1add26c14..e6682fb675 100644 --- a/config/locales/en_NZ.yml +++ b/config/locales/en_NZ.yml @@ -1034,8 +1034,6 @@ en_NZ: 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 diff --git a/config/locales/en_PH.yml b/config/locales/en_PH.yml index f6dc004c68..f908e4846f 100644 --- a/config/locales/en_PH.yml +++ b/config/locales/en_PH.yml @@ -838,8 +838,6 @@ en_PH: 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 diff --git a/config/locales/en_US.yml b/config/locales/en_US.yml index 183564ef11..c90e480b26 100644 --- a/config/locales/en_US.yml +++ b/config/locales/en_US.yml @@ -997,8 +997,6 @@ en_US: 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 diff --git a/config/locales/en_ZA.yml b/config/locales/en_ZA.yml index 0bdc4080a1..4478c417f4 100644 --- a/config/locales/en_ZA.yml +++ b/config/locales/en_ZA.yml @@ -839,8 +839,6 @@ en_ZA: An optional message to welcome customers and explain how to shop with you. Any text entered here will be displayed 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 diff --git a/config/locales/es.yml b/config/locales/es.yml index 908ef3ad36..48e38d37c8 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1055,8 +1055,6 @@ es: en el sitio. si el texto se agrega en este campo, va a ser mostrado en la pestaña de inicio cuando los clientes ingresen por primera vez a la tienda. - shopfront_message_link_tooltip: "Insertar / editar enlace" - shopfront_message_link_prompt: "Por favor introduzca una URL para insertar" shopfront_closed_message: "Mensaje de tienda cerrada" shopfront_closed_message_placeholder: > Un mensaje que proporciona una explicación más detallada sobre por qué diff --git a/config/locales/es_CO.yml b/config/locales/es_CO.yml index c6e9fb8c23..966de352be 100644 --- a/config/locales/es_CO.yml +++ b/config/locales/es_CO.yml @@ -871,8 +871,6 @@ es_CO: en el sitio. Si el texto se agrega en este campo, va a ser mostrado en la pestaña de inicio cuando los clientes ingresen por primera vez a la tienda. - shopfront_message_link_tooltip: "Insertar / editar enlace" - shopfront_message_link_prompt: "Por favor introduzca una URL para insertar" shopfront_closed_message: "Mensaje de tienda cerrada" shopfront_closed_message_placeholder: > Un mensaje que proporciona una explicación más detallada sobre por qué diff --git a/config/locales/es_CR.yml b/config/locales/es_CR.yml index c281024b48..567efca082 100644 --- a/config/locales/es_CR.yml +++ b/config/locales/es_CR.yml @@ -1041,8 +1041,6 @@ es_CR: en el sitio. Si el texto se agrega en este campo, va a ser mostrado en la pestaña de inicio cuando los clientes ingresen por primera vez a la tienda. - shopfront_message_link_tooltip: "Insertar / editar enlace" - shopfront_message_link_prompt: "Por favor introduzca una URL para insertar" shopfront_closed_message: "Mensaje de tienda cerrada" shopfront_closed_message_placeholder: > Un mensaje que proporciona una explicación más detallada sobre por qué diff --git a/config/locales/es_US.yml b/config/locales/es_US.yml index 21f59cf2fc..d52ee79b42 100644 --- a/config/locales/es_US.yml +++ b/config/locales/es_US.yml @@ -996,8 +996,6 @@ es_US: en el sitio. si el texto se agrega en este campo, va a ser mostrado en la pestaña de inicio cuando los clientes ingresen por primera vez a la tienda. - shopfront_message_link_tooltip: "Insertar / editar enlace" - shopfront_message_link_prompt: "Por favor introduzca una URL para insertar" shopfront_closed_message: "Mensaje de tienda cerrada" shopfront_closed_message_placeholder: > Un mensaje que proporciona una explicación más detallada sobre por qué diff --git a/config/locales/fil_PH.yml b/config/locales/fil_PH.yml index f9a4b73116..da00b2ddaa 100644 --- a/config/locales/fil_PH.yml +++ b/config/locales/fil_PH.yml @@ -839,8 +839,6 @@ fil_PH: kung paano mamili sa inyong shop. kung ang teksto ay ipinasok dito, ito ay makikita sa home tab kapag ang customer ay nasa loob na ng inyong shopfront. - shopfront_message_link_tooltip: "ilagay/ i-edit ang link" - shopfront_message_link_prompt: "ipasok ang URL na ilalagay" shopfront_closed_message: "pagsasarang mensahe ng Shopfront" shopfront_closed_message_placeholder: > isang mensahe na nagbibigay ng mas detalyadong paliwanag kung bakit diff --git a/config/locales/fr.yml b/config/locales/fr.yml index ca96bfcd67..f5353195e2 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1199,8 +1199,6 @@ fr: les particularités de votre boutique. Ce message s'affiche dans l'onglet Accueil de votre boutique. Si aucun message n'est affiché, l'onglet Accueil n’apparaîtra pas. - shopfront_message_link_tooltip: "Insérer / modifier un lien" - shopfront_message_link_prompt: "Veuillez entrer l'URL à insérer" shopfront_closed_message: "Message d'accueil ventes terminées" shopfront_closed_message_placeholder: > Vous pouvez expliquer ici à vos acheteurs pourquoi vos ventes sont fermées diff --git a/config/locales/fr_BE.yml b/config/locales/fr_BE.yml index a05d383b5b..7867da62cb 100644 --- a/config/locales/fr_BE.yml +++ b/config/locales/fr_BE.yml @@ -1085,8 +1085,6 @@ fr_BE: expliquer comment faire leurs achats avec vous. Si le texte est saisi ici, il sera affiché dans un onglet d'accueil lorsque les clients arriveront pour la première fois à votre vitrine. - shopfront_message_link_tooltip: "Insérer / éditer le lien" - shopfront_message_link_prompt: "Entrer l'URL pour insérer" shopfront_closed_message: "Message d'accueil comptoir fermé" shopfront_closed_message_placeholder: > Vous pouvez ici expliquer à vos acheteurs pourquoi votre comptoir est diff --git a/config/locales/fr_CA.yml b/config/locales/fr_CA.yml index cdae1f1664..f2eaf62c84 100644 --- a/config/locales/fr_CA.yml +++ b/config/locales/fr_CA.yml @@ -1196,8 +1196,6 @@ fr_CA: les particularités de votre boutique. Ce message s'affiche dans l'onglet Accueil de votre boutique. Si aucun message n'est affiché, l'onglet Accueil n'apparaitra pas. - shopfront_message_link_tooltip: "Insérer / modifier un lien" - shopfront_message_link_prompt: "Veuillez entrer l'URL à insérer" shopfront_closed_message: "Message d'accueil de la boutique fermée" shopfront_closed_message_placeholder: > Vous pouvez ici expliquer à vos acheteurs pourquoi votre boutique est diff --git a/config/locales/fr_CH.yml b/config/locales/fr_CH.yml index 3526797864..d28cff9bcb 100644 --- a/config/locales/fr_CH.yml +++ b/config/locales/fr_CH.yml @@ -1027,8 +1027,6 @@ fr_CH: les particularités de votre boutique. Ce message s'affiche dans l'onglet Accueil de votre boutique. Si aucun message n'est affiché, l'onglet Accueil n'apparaitra pas. - shopfront_message_link_tooltip: "Insérer / modifier un lien" - shopfront_message_link_prompt: "Veuillez entrer l'URL à insérer" shopfront_closed_message: "Message d'accueil boutique fermée" shopfront_closed_message_placeholder: > Vous pouvez ici expliquer à vos acheteurs pourquoi votre boutique est diff --git a/config/locales/fr_CM.yml b/config/locales/fr_CM.yml index 0a0ff82302..e16ed79962 100644 --- a/config/locales/fr_CM.yml +++ b/config/locales/fr_CM.yml @@ -953,8 +953,6 @@ fr_CM: les particularités de votre boutique. Ce message s'affiche dans l'onglet Accueil de votre boutique. Si aucun message n'est affiché, l'onglet Accueil n'apparaitra pas. - shopfront_message_link_tooltip: "Insérer / modifier un lien" - shopfront_message_link_prompt: "Veuillez entrer l'URL à insérer" shopfront_closed_message: "Message d'accueil boutique fermée" shopfront_closed_message_placeholder: > Vous pouvez ici expliquer à vos acheteurs pourquoi votre boutique est diff --git a/config/locales/hi.yml b/config/locales/hi.yml index cf469138a5..66a282cdd6 100644 --- a/config/locales/hi.yml +++ b/config/locales/hi.yml @@ -1137,8 +1137,6 @@ hi: ग्राहकों का स्वागत करने और कैसे आपके साथ खरीदारी करें यह समझाने लिए एक वैकल्पिक संदेश। यदि टेक्स्ट यहां एंटर किया गया है, तो जब ग्राहक पहली बार आपके शॉपफ्रन्ट पर पहुंचेंगे यह होम टैब में तब प्रदर्शित होगा - shopfront_message_link_tooltip: "लिंक डालें/एडिट करें" - shopfront_message_link_prompt: "कृपया डालने के लिए URL डालें" shopfront_closed_message: "शॉपफ्रन्ट बंद किए गए संदेश" shopfront_closed_message_placeholder: > एक संदेश जो इस बारे में अधिक विस्तृत विवरण प्रदान करता है कि आपकी दुकान diff --git a/config/locales/hu.yml b/config/locales/hu.yml index eb9338a8be..48dfcca39e 100644 --- a/config/locales/hu.yml +++ b/config/locales/hu.yml @@ -1197,8 +1197,6 @@ hu: Opcionális üzenet, amely üdvözli az ügyfeleket, és elmagyarázza, hogyan vásárolhatnak Nálad. Ha itt szöveget írsz be, az megjelenik a kezdőlapon, amikor az ügyfelek először érkeznek az oldaladra. - shopfront_message_link_tooltip: "Hivatkozás beszúrása / szerkesztése" - shopfront_message_link_prompt: "Kérünk, adj meg egy URL-t" shopfront_closed_message: "Kirakat zárt üzenet" shopfront_closed_message_placeholder: > Üzenet a fogyasztóknak az oldaladon, ha épp nincs aktív rendelési ciklus diff --git a/config/locales/it.yml b/config/locales/it.yml index 33c73cdd42..0fdc031245 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1072,8 +1072,6 @@ it: Un messaggio opzionale per dare il benvenuto ai clienti e spiegare come acquistare. Il testo inserito qui verrà mostrato nel tab principale della vetrina. - shopfront_message_link_tooltip: "Inserisci / modifica link" - shopfront_message_link_prompt: "Per favore inserisci un URL" shopfront_closed_message: "Messaggio Chiusura Vetrina" shopfront_closed_message_placeholder: > Un messaggio che fornisce una spiegazione più dettagliata sul perché diff --git a/config/locales/it_CH.yml b/config/locales/it_CH.yml index b3fb3fac14..013ae49caa 100644 --- a/config/locales/it_CH.yml +++ b/config/locales/it_CH.yml @@ -1001,8 +1001,6 @@ it_CH: Un messaggio opzionale per dare il benvenuto ai clienti e spiegare come acquistare. Il testo inserito qui verrà mostrato nel tab principale della vetrina. - shopfront_message_link_tooltip: "Inserisci / modifica link" - shopfront_message_link_prompt: "Per favore inserisci un URL" shopfront_closed_message: "Messaggio Chiusura Vetrina" shopfront_closed_message_placeholder: > Un messaggio che fornisce una spiegazione più dettagliata sul perché diff --git a/config/locales/ko.yml b/config/locales/ko.yml index d0078b9015..60b23beb0a 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1023,8 +1023,6 @@ ko: shopfront_message_placeholder: > 고객을 환영하고 함께 쇼핑하는 방법을 설명하는 선택적 메시지입니다. 여기에 텍스트를 입력하면 고객이 처음 상점에 도착했을 때 홈 탭에 표시됩니다. - shopfront_message_link_tooltip: "링크 삽입 / 수정" - shopfront_message_link_prompt: "삽입할 URL을 입력하십시오." shopfront_closed_message: "매장 폐쇄 메시지" shopfront_closed_message_placeholder: > 가게가 문을 닫은 이유 및/또는 고객이 가게가 언제 다시 문을 열 것으로 예상할 수 있는지에 대하여 보다 자세한 설명을 제공하는 diff --git a/config/locales/ml.yml b/config/locales/ml.yml index 2ab418835a..749d751558 100644 --- a/config/locales/ml.yml +++ b/config/locales/ml.yml @@ -1143,8 +1143,6 @@ ml: വിശദീകരിക്കുന്നതിനുമുള്ള ഒരു ഓപ്‌ഷണൽ സന്ദേശം. ഇവിടെ മൂലവാക്യം നൽകിയാൽ, ഉപഭോക്താക്കൾ ആദ്യം നിങ്ങളുടെ ഷോപ്പ് ഫ്രണ്ടിൽ എത്തുമ്പോൾ അത് ഹോം ടാബിൽ പ്രദർശിപ്പിക്കും. - shopfront_message_link_tooltip: "ലിങ്ക് ചേർക്കുക / എഡിറ്റ് ചെയ്യുക" - shopfront_message_link_prompt: "ചേർക്കുന്നതിന് ദയവായി ഒരു യുആർഎൽ നൽകുക" shopfront_closed_message: "ഷോപ്പ്ഫ്രണ്ട് അടച്ച സന്ദേശം" shopfront_closed_message_placeholder: > നിങ്ങളുടെ ഷോപ്പ് എന്തിനാണ് അടച്ചിരിക്കുന്നത് എന്നതിനെക്കുറിച്ചും/അല്ലെങ്കിൽ diff --git a/config/locales/mr.yml b/config/locales/mr.yml index fa0807fbf8..a4b9e79960 100644 --- a/config/locales/mr.yml +++ b/config/locales/mr.yml @@ -1131,8 +1131,6 @@ mr: ग्राहकांसाठी स्वागतपर तसेच तुमच्याकडे खरेदी कशी करावी हे स्पष्ट करणारा पर्यायी संदेश. येथे मजकूर लिहिल्यास, जेव्हा ग्राहक प्रथम तुमच्या शॉपफ्रंटवर येतील तेव्हा तो होम टॅबमध्ये दर्शवला जाईल. - shopfront_message_link_tooltip: "लिंक जोडा / संपादित करा" - shopfront_message_link_prompt: "कृपया URL प्रविष्ट करा" shopfront_closed_message: "शॉपफ्रंट बंद संदेश" shopfront_closed_message_placeholder: > तुमचे शॉप का बंद आहे आणि/किंवा ते पुन्हा कधी उघडेल याबद्दल अधिक तपशीलवार diff --git a/config/locales/nb.yml b/config/locales/nb.yml index 79d6eea4fd..a7e1430a90 100644 --- a/config/locales/nb.yml +++ b/config/locales/nb.yml @@ -1197,8 +1197,6 @@ nb: En valgfri melding for å ønske kunder velkommen og forklare hvordan de handler hos deg. Hvis tekst legges inn her, vil den vises i en hjemmefane når kundene først ankommer butikken din. - shopfront_message_link_tooltip: "Sett inn / rediger lenke" - shopfront_message_link_prompt: "Vennligst skriv inn en URL for å sette inn" shopfront_closed_message: "Melding Butikk Stengt" shopfront_closed_message_placeholder: > En melding som gir en mer detaljert forklaring om hvorfor din butikk diff --git a/config/locales/nl_BE.yml b/config/locales/nl_BE.yml index 02bce0f0f6..1d0cfe2e34 100644 --- a/config/locales/nl_BE.yml +++ b/config/locales/nl_BE.yml @@ -861,8 +861,6 @@ nl_BE: Een optionele tekst om klanten te verwelkomen en om uittelegen hoe met jouw te shoppen. Als tekst hier is ingevuld gaat het zichtbaar zijn in de home tab waneer klanten eerst aankomen in je online vitrine. - shopfront_message_link_tooltip: "Link invoegen / bewerken" - shopfront_message_link_prompt: "Voer een URL in om in te voegen" shopfront_closed_message: "Bericht Gesloten Etalage" shopfront_closed_message_placeholder: > Een boodschap die een gedetailleerdere verklaring voorziet over waarom diff --git a/config/locales/pa.yml b/config/locales/pa.yml index a0da7d78e1..7c98110442 100644 --- a/config/locales/pa.yml +++ b/config/locales/pa.yml @@ -1121,8 +1121,6 @@ pa: ਗਾਹਕਾਂ ਦਾ ਸੁਆਗਤ ਕਰਨ ਅਤੇ ਤੁਹਾਡੇ ਨਾਲ ਖਰੀਦਦਾਰੀ ਕਰਨ ਦਾ ਢੰਗ ਦੱਸਣ ਲਈ ਇੱਕ ਵਿਕਲਪਿਕ ਸੰਦੇਸ਼। ਜੇਕਰ ਟੈਕਸਟ ਇੱਥੇ ਦਰਜ ਕੀਤਾ ਗਿਆ ਹੈ ਤਾਂ ਉਹ ਹੋਮ ਟੈਬ ਵਿੱਚ ਪ੍ਰਦਰਸ਼ਿਤ ਹੋਵੇਗਾ ਜਦੋਂ ਗਾਹਕ ਪਹਿਲੀ ਵਾਰ ਤੁਹਾਡੇ ਸ਼ਾਪ ਦੇ ਸਾਹਮਣੇ ਆਉਂਦੇ ਹਨ। - shopfront_message_link_tooltip: "ਲਿੰਕ ਪਾਓ / ਸੰਪਾਦਿਤ ਕਰੋ" - shopfront_message_link_prompt: "ਸਮਿਲਿਤ ਕਰਨ ਲਈ ਕਿਰਪਾ ਕਰਕੇ ਇੱਕ URL ਦਾਖਲ ਕਰੋ" shopfront_closed_message: "ਸ਼ਾਪਫ੍ਰੰਟ ਦੇ ਬੰਦ ਹੋਣ ਦਾ ਸੰਦੇਸ਼" shopfront_closed_message_placeholder: > ਇੱਕ ਸੰਦੇਸ਼ ਜੋ ਇਸ ਬਾਰੇ ਵਧੇਰੇ ਵਿਸਤ੍ਰਿਤ ਵਿਆਖਿਆ ਪ੍ਰਦਾਨ ਕਰਦਾ ਹੈ ਕਿ ਤੁਹਾਡੀ diff --git a/config/locales/pl.yml b/config/locales/pl.yml index c88ef74532..445fcc9ccf 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -820,8 +820,6 @@ pl: Opcjonalna wiadomość witająca klientów i wyjaśniająca, jak robić u Ciebie zakupy. Jeśli tu wprowadzisz tekst, zostanie on wyświetlony na karcie głównej, gdy klienci po raz pierwszy pojawią się w Twoim sklepie. - shopfront_message_link_tooltip: "Wstaw / edytuj link" - shopfront_message_link_prompt: "Wprowadź adres URL do wstawienia" shopfront_closed_message: "Komunikat o zamknięciu witryny sklepowej" shopfront_closed_message_placeholder: > Wiadomość, która zawiera bardziej szczegółowe wyjaśnienie, dlaczego diff --git a/config/locales/pt.yml b/config/locales/pt.yml index bc8ab99d9d..1ac7799adb 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -901,8 +901,6 @@ pt: customer_names_false: "Desactivado" customer_names_true: "Activo" shopfront_message: "Mensagem da Loja" - shopfront_message_link_tooltip: "Inserir / editar link" - shopfront_message_link_prompt: "Por favor insira um URL" shopfront_closed_message: "Mensagem de Loja Fechada" shopfront_closed_message_placeholder: > Uma mensagem que forneça uma explicação mais detalhada sobre o motivo diff --git a/config/locales/pt_BR.yml b/config/locales/pt_BR.yml index 26b7bbfa2d..87de80d6b3 100644 --- a/config/locales/pt_BR.yml +++ b/config/locales/pt_BR.yml @@ -941,8 +941,6 @@ pt_BR: Uma mensagem opcional para dar as boas-vindas aos clientes e explicar como comprar com você. Se o texto for inserido aqui, ele será exibido em uma guia inicial quando os clientes chegarem à sua loja virtual. - shopfront_message_link_tooltip: "Inserir / editar link" - shopfront_message_link_prompt: "Digite um URL para inserir" shopfront_closed_message: "Mensagem de loja virtual fechada" shopfront_closed_message_placeholder: > Uma mensagem que forneça uma explicação detalhada sobre o porque de diff --git a/config/locales/ru.yml b/config/locales/ru.yml index a442599358..72be136db3 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1178,8 +1178,6 @@ ru: Дополнительное сообщение, чтобы приветствовать клиентов и объяснить, как у Вас делать покупки. Если текст введен здесь, он будет отображаться на главной вкладке, когда клиенты впервые придут в Ваш магазин. - shopfront_message_link_tooltip: "Вставить / изменить ссылку" - shopfront_message_link_prompt: "Пожалуйста, введите URL для вставки" shopfront_closed_message: "Сообщение при Закрытии Витрины" shopfront_closed_message_placeholder: > Сообщение, которое содержит более подробное объяснение того, почему diff --git a/config/locales/tr.yml b/config/locales/tr.yml index e9525c3929..9eccf14b26 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -925,8 +925,6 @@ tr: Müşterilerinize merhaba diyebilir, dükkanınız ve alışveriş şartlarınız ile ilgili bilgi verebilirsiniz. Yazdıklarınız, müşteriler mağazanızı ziyaret ettiğinde görünür olacak. - shopfront_message_link_tooltip: "Bağlantı ekle/düzenle" - shopfront_message_link_prompt: "Lütfen eklemek için bir URL girin" shopfront_closed_message: "KAPALI DÜKKAN MESAJI" shopfront_closed_message_placeholder: > Dükkanınızın neden satışa kapalı olduğunu ve ne zaman açılacağını müşterilerinize diff --git a/config/locales/uk.yml b/config/locales/uk.yml index a60b0cef26..e807686500 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -1038,8 +1038,6 @@ uk: Додаткове повідомлення для привітання клієнтів і пояснення, як робити покупки у вас. Якщо тут ввести текст, він відображатиметься на вкладці «Домашня сторінка», коли клієнти вперше прийдуть у ваш магазин. - shopfront_message_link_tooltip: "Вставити/редагувати посилання" - shopfront_message_link_prompt: "Введіть URL-адресу для вставки" shopfront_closed_message: " Повідомлення закритої вітрини магазину" shopfront_closed_message_placeholder: > Повідомлення, яке містить більш детальне пояснення про те, чому ваш 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 6e26d0415f..d3661313e4 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..c155015a8c 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,9 @@ 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='Enter a URL…']") 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