From b526f0442c5eb3674e5a8bea72b83869b378e305 Mon Sep 17 00:00:00 2001 From: Micael Nussbaumer Date: Tue, 15 Aug 2017 17:35:07 +0100 Subject: [PATCH 1/4] =?UTF-8?q?Added=20.scss=20and=20dist/.css=20changes?= =?UTF-8?q?=20for=20beagle=20theme=20to=20accomodate=20=C2=B4medium-editor?= =?UTF-8?q?-insert-plugins=C2=B4=20buttons=20and=20toolbar=20-=20On=20the?= =?UTF-8?q?=20original=20one=20it=20would=20change=20to=20a=20square=20but?= =?UTF-8?q?ton=20on=20hover,=20and=20the=20last=20button=20wouldn't=20show?= =?UTF-8?q?=20correctly=20either?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dist/css/themes/beagle.css | 4 ++++ src/sass/themes/beagle.scss | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/dist/css/themes/beagle.css b/dist/css/themes/beagle.css index 7068d24f2..533b71bb3 100644 --- a/dist/css/themes/beagle.css +++ b/dist/css/themes/beagle.css @@ -76,3 +76,7 @@ .medium-editor-anchor-preview a { color: #ccc; text-decoration: none; } + +.medium-editor-toolbar-actions li, +.medium-editor-toolbar-actions button { + border-radius: 50px; } diff --git a/src/sass/themes/beagle.scss b/src/sass/themes/beagle.scss index 040a926c5..9b0e62e90 100644 --- a/src/sass/themes/beagle.scss +++ b/src/sass/themes/beagle.scss @@ -112,3 +112,9 @@ $medium-editor-placeholder-color: #f8f5f3; text-decoration: none; } } + +.medium-editor-toolbar-actions { + li, button { + border-radius: 50px; + } +} From 50d917637af8443673ccc1c5f428ca1e21d842b0 Mon Sep 17 00:00:00 2001 From: Micael Nussbaumer Date: Tue, 15 Aug 2017 18:03:44 +0100 Subject: [PATCH 2/4] removed dist/ directory --- .gitignore | 1 + dist/css/medium-editor.css | 230 - dist/css/medium-editor.min.css | 1 - dist/css/themes/beagle.css | 82 - dist/css/themes/beagle.min.css | 1 - dist/css/themes/bootstrap.css | 67 - dist/css/themes/bootstrap.min.css | 1 - dist/css/themes/default.css | 63 - dist/css/themes/default.min.css | 1 - dist/css/themes/flat.css | 58 - dist/css/themes/flat.min.css | 1 - dist/css/themes/mani.css | 57 - dist/css/themes/mani.min.css | 1 - dist/css/themes/roman.css | 58 - dist/css/themes/roman.min.css | 1 - dist/css/themes/tim.css | 67 - dist/css/themes/tim.min.css | 1 - dist/js/medium-editor.js | 7885 ----------------------------- dist/js/medium-editor.min.js | 4 - 19 files changed, 1 insertion(+), 8579 deletions(-) delete mode 100644 dist/css/medium-editor.css delete mode 100644 dist/css/medium-editor.min.css delete mode 100644 dist/css/themes/beagle.css delete mode 100644 dist/css/themes/beagle.min.css delete mode 100644 dist/css/themes/bootstrap.css delete mode 100644 dist/css/themes/bootstrap.min.css delete mode 100644 dist/css/themes/default.css delete mode 100644 dist/css/themes/default.min.css delete mode 100644 dist/css/themes/flat.css delete mode 100644 dist/css/themes/flat.min.css delete mode 100644 dist/css/themes/mani.css delete mode 100644 dist/css/themes/mani.min.css delete mode 100644 dist/css/themes/roman.css delete mode 100644 dist/css/themes/roman.min.css delete mode 100644 dist/css/themes/tim.css delete mode 100644 dist/css/themes/tim.min.css delete mode 100644 dist/js/medium-editor.js delete mode 100644 dist/js/medium-editor.min.js diff --git a/.gitignore b/.gitignore index f215ea27a..631c90d55 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ npm-debug.log _SpecRunner.html reports/ coverage/ +dist/ ._* # IDE diff --git a/dist/css/medium-editor.css b/dist/css/medium-editor.css deleted file mode 100644 index de9813251..000000000 --- a/dist/css/medium-editor.css +++ /dev/null @@ -1,230 +0,0 @@ -@-webkit-keyframes medium-editor-image-loading { - 0% { - -webkit-transform: scale(0); - transform: scale(0); } - 100% { - -webkit-transform: scale(1); - transform: scale(1); } } - -@keyframes medium-editor-image-loading { - 0% { - -webkit-transform: scale(0); - transform: scale(0); } - 100% { - -webkit-transform: scale(1); - transform: scale(1); } } - -@-webkit-keyframes medium-editor-pop-upwards { - 0% { - opacity: 0; - -webkit-transform: matrix(0.97, 0, 0, 1, 0, 12); - transform: matrix(0.97, 0, 0, 1, 0, 12); } - 20% { - opacity: .7; - -webkit-transform: matrix(0.99, 0, 0, 1, 0, 2); - transform: matrix(0.99, 0, 0, 1, 0, 2); } - 40% { - opacity: 1; - -webkit-transform: matrix(1, 0, 0, 1, 0, -1); - transform: matrix(1, 0, 0, 1, 0, -1); } - 100% { - -webkit-transform: matrix(1, 0, 0, 1, 0, 0); - transform: matrix(1, 0, 0, 1, 0, 0); } } - -@keyframes medium-editor-pop-upwards { - 0% { - opacity: 0; - -webkit-transform: matrix(0.97, 0, 0, 1, 0, 12); - transform: matrix(0.97, 0, 0, 1, 0, 12); } - 20% { - opacity: .7; - -webkit-transform: matrix(0.99, 0, 0, 1, 0, 2); - transform: matrix(0.99, 0, 0, 1, 0, 2); } - 40% { - opacity: 1; - -webkit-transform: matrix(1, 0, 0, 1, 0, -1); - transform: matrix(1, 0, 0, 1, 0, -1); } - 100% { - -webkit-transform: matrix(1, 0, 0, 1, 0, 0); - transform: matrix(1, 0, 0, 1, 0, 0); } } - -.medium-editor-anchor-preview { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 16px; - left: 0; - line-height: 1.4; - max-width: 280px; - position: absolute; - text-align: center; - top: 0; - word-break: break-all; - word-wrap: break-word; - visibility: hidden; - z-index: 2000; } - .medium-editor-anchor-preview a { - color: #fff; - display: inline-block; - margin: 5px 5px 10px; } - -.medium-editor-anchor-preview-active { - visibility: visible; } - -.medium-editor-dragover { - background: #ddd; } - -.medium-editor-image-loading { - -webkit-animation: medium-editor-image-loading 1s infinite ease-in-out; - animation: medium-editor-image-loading 1s infinite ease-in-out; - background-color: #333; - border-radius: 100%; - display: inline-block; - height: 40px; - width: 40px; } - -.medium-editor-placeholder { - position: relative; } - .medium-editor-placeholder:after { - content: attr(data-placeholder) !important; - font-style: italic; - position: absolute; - left: 0; - top: 0; - white-space: pre; - padding: inherit; - margin: inherit; } - -.medium-editor-placeholder-relative { - position: relative; } - .medium-editor-placeholder-relative:after { - content: attr(data-placeholder) !important; - font-style: italic; - position: relative; - white-space: pre; - padding: inherit; - margin: inherit; } - -.medium-toolbar-arrow-under:after, .medium-toolbar-arrow-over:before { - border-style: solid; - content: ''; - display: block; - height: 0; - left: 50%; - margin-left: -8px; - position: absolute; - width: 0; } - -.medium-toolbar-arrow-under:after { - border-width: 8px 8px 0 8px; } - -.medium-toolbar-arrow-over:before { - border-width: 0 8px 8px 8px; - top: -8px; } - -.medium-editor-toolbar { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 16px; - left: 0; - position: absolute; - top: 0; - visibility: hidden; - z-index: 2000; } - .medium-editor-toolbar ul { - margin: 0; - padding: 0; } - .medium-editor-toolbar li { - float: left; - list-style: none; - margin: 0; - padding: 0; } - .medium-editor-toolbar li button { - box-sizing: border-box; - cursor: pointer; - display: block; - font-size: 14px; - line-height: 1.33; - margin: 0; - padding: 15px; - text-decoration: none; } - .medium-editor-toolbar li button:focus { - outline: none; } - .medium-editor-toolbar li .medium-editor-action-underline { - text-decoration: underline; } - .medium-editor-toolbar li .medium-editor-action-pre { - font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; - font-size: 12px; - font-weight: 100; - padding: 15px 0; } - -.medium-editor-toolbar-active { - visibility: visible; } - -.medium-editor-sticky-toolbar { - position: fixed; - top: 1px; } - -.medium-editor-relative-toolbar { - position: relative; } - -.medium-editor-toolbar-active.medium-editor-stalker-toolbar { - -webkit-animation: medium-editor-pop-upwards 160ms forwards linear; - animation: medium-editor-pop-upwards 160ms forwards linear; } - -.medium-editor-action-bold { - font-weight: bolder; } - -.medium-editor-action-italic { - font-style: italic; } - -.medium-editor-toolbar-form { - display: none; } - .medium-editor-toolbar-form input, - .medium-editor-toolbar-form a { - font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } - .medium-editor-toolbar-form .medium-editor-toolbar-form-row { - line-height: 14px; - margin-left: 5px; - padding-bottom: 5px; } - .medium-editor-toolbar-form .medium-editor-toolbar-input, - .medium-editor-toolbar-form label { - border: none; - box-sizing: border-box; - font-size: 14px; - margin: 0; - padding: 6px; - width: 316px; - display: inline-block; } - .medium-editor-toolbar-form .medium-editor-toolbar-input:focus, - .medium-editor-toolbar-form label:focus { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - border: none; - box-shadow: none; - outline: 0; } - .medium-editor-toolbar-form a { - display: inline-block; - font-size: 24px; - font-weight: bolder; - margin: 0 10px; - text-decoration: none; } - -.medium-editor-toolbar-form-active { - display: block; } - -.medium-editor-toolbar-actions:after { - clear: both; - content: ""; - display: table; } - -.medium-editor-element { - word-wrap: break-word; - min-height: 30px; } - .medium-editor-element img { - max-width: 100%; } - .medium-editor-element sub { - vertical-align: sub; } - .medium-editor-element sup { - vertical-align: super; } - -.medium-editor-hidden { - display: none; } diff --git a/dist/css/medium-editor.min.css b/dist/css/medium-editor.min.css deleted file mode 100644 index e46f81c02..000000000 --- a/dist/css/medium-editor.min.css +++ /dev/null @@ -1 +0,0 @@ -.medium-editor-anchor-preview,.medium-editor-toolbar{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:16px;z-index:2000}@-webkit-keyframes medium-editor-image-loading{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes medium-editor-image-loading{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes medium-editor-pop-upwards{0%{opacity:0;-webkit-transform:matrix(.97,0,0,1,0,12);transform:matrix(.97,0,0,1,0,12)}20%{opacity:.7;-webkit-transform:matrix(.99,0,0,1,0,2);transform:matrix(.99,0,0,1,0,2)}40%{opacity:1;-webkit-transform:matrix(1,0,0,1,0,-1);transform:matrix(1,0,0,1,0,-1)}100%{-webkit-transform:matrix(1,0,0,1,0,0);transform:matrix(1,0,0,1,0,0)}}@keyframes medium-editor-pop-upwards{0%{opacity:0;-webkit-transform:matrix(.97,0,0,1,0,12);transform:matrix(.97,0,0,1,0,12)}20%{opacity:.7;-webkit-transform:matrix(.99,0,0,1,0,2);transform:matrix(.99,0,0,1,0,2)}40%{opacity:1;-webkit-transform:matrix(1,0,0,1,0,-1);transform:matrix(1,0,0,1,0,-1)}100%{-webkit-transform:matrix(1,0,0,1,0,0);transform:matrix(1,0,0,1,0,0)}}.medium-editor-anchor-preview{left:0;line-height:1.4;max-width:280px;position:absolute;text-align:center;top:0;word-break:break-all;word-wrap:break-word;visibility:hidden}.medium-editor-anchor-preview a{color:#fff;display:inline-block;margin:5px 5px 10px}.medium-editor-placeholder-relative:after,.medium-editor-placeholder:after{content:attr(data-placeholder)!important;white-space:pre;padding:inherit;margin:inherit;font-style:italic}.medium-editor-anchor-preview-active{visibility:visible}.medium-editor-dragover{background:#ddd}.medium-editor-image-loading{-webkit-animation:medium-editor-image-loading 1s infinite ease-in-out;animation:medium-editor-image-loading 1s infinite ease-in-out;background-color:#333;border-radius:100%;display:inline-block;height:40px;width:40px}.medium-editor-placeholder{position:relative}.medium-editor-placeholder:after{position:absolute;left:0;top:0}.medium-editor-placeholder-relative,.medium-editor-placeholder-relative:after{position:relative}.medium-toolbar-arrow-over:before,.medium-toolbar-arrow-under:after{border-style:solid;content:'';display:block;height:0;left:50%;margin-left:-8px;position:absolute;width:0}.medium-toolbar-arrow-under:after{border-width:8px 8px 0}.medium-toolbar-arrow-over:before{border-width:0 8px 8px;top:-8px}.medium-editor-toolbar{left:0;position:absolute;top:0;visibility:hidden}.medium-editor-toolbar ul{margin:0;padding:0}.medium-editor-toolbar li{float:left;list-style:none;margin:0;padding:0}.medium-editor-toolbar li button{box-sizing:border-box;cursor:pointer;display:block;font-size:14px;line-height:1.33;margin:0;padding:15px;text-decoration:none}.medium-editor-toolbar li button:focus{outline:0}.medium-editor-toolbar li .medium-editor-action-underline{text-decoration:underline}.medium-editor-toolbar li .medium-editor-action-pre{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:12px;font-weight:100;padding:15px 0}.medium-editor-toolbar-active{visibility:visible}.medium-editor-sticky-toolbar{position:fixed;top:1px}.medium-editor-relative-toolbar{position:relative}.medium-editor-toolbar-active.medium-editor-stalker-toolbar{-webkit-animation:medium-editor-pop-upwards 160ms forwards linear;animation:medium-editor-pop-upwards 160ms forwards linear}.medium-editor-action-bold{font-weight:bolder}.medium-editor-action-italic{font-style:italic}.medium-editor-toolbar-form{display:none}.medium-editor-toolbar-form a,.medium-editor-toolbar-form input{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.medium-editor-toolbar-form .medium-editor-toolbar-form-row{line-height:14px;margin-left:5px;padding-bottom:5px}.medium-editor-toolbar-form .medium-editor-toolbar-input,.medium-editor-toolbar-form label{border:none;box-sizing:border-box;font-size:14px;margin:0;padding:6px;width:316px;display:inline-block}.medium-editor-toolbar-form .medium-editor-toolbar-input:focus,.medium-editor-toolbar-form label:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;box-shadow:none;outline:0}.medium-editor-toolbar-form a{display:inline-block;font-size:24px;font-weight:bolder;margin:0 10px;text-decoration:none}.medium-editor-toolbar-form-active{display:block}.medium-editor-toolbar-actions:after{clear:both;content:"";display:table}.medium-editor-element{word-wrap:break-word;min-height:30px}.medium-editor-element img{max-width:100%}.medium-editor-element sub{vertical-align:sub}.medium-editor-element sup{vertical-align:super}.medium-editor-hidden{display:none} \ No newline at end of file diff --git a/dist/css/themes/beagle.css b/dist/css/themes/beagle.css deleted file mode 100644 index 533b71bb3..000000000 --- a/dist/css/themes/beagle.css +++ /dev/null @@ -1,82 +0,0 @@ -.medium-toolbar-arrow-under:after { - border-color: #000 transparent transparent transparent; - top: 40px; } - -.medium-toolbar-arrow-over:before { - border-color: transparent transparent #000 transparent; } - -.medium-editor-toolbar { - background-color: #000; - border: none; - border-radius: 50px; } - .medium-editor-toolbar li button { - background-color: transparent; - border: none; - box-sizing: border-box; - color: #ccc; - height: 40px; - min-width: 40px; - padding: 5px 12px; - -webkit-transition: background-color .2s ease-in, color .2s ease-in; - transition: background-color .2s ease-in, color .2s ease-in; } - .medium-editor-toolbar li button:hover { - background-color: #000; - color: #a2d7c7; } - .medium-editor-toolbar li .medium-editor-button-first { - border-bottom-left-radius: 50px; - border-top-left-radius: 50px; - padding-left: 24px; } - .medium-editor-toolbar li .medium-editor-button-last { - border-bottom-right-radius: 50px; - border-right: none; - border-top-right-radius: 50px; - padding-right: 24px; } - .medium-editor-toolbar li .medium-editor-button-active { - background-color: #000; - color: #a2d7c7; } - -.medium-editor-toolbar-form { - background: #000; - border-radius: 50px; - color: #ccc; - overflow: hidden; } - .medium-editor-toolbar-form .medium-editor-toolbar-input { - background: #000; - box-sizing: border-box; - color: #ccc; - height: 40px; - padding-left: 16px; - width: 220px; } - .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder { - color: #f8f5f3; - color: rgba(248, 245, 243, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder { - /* Firefox 18- */ - color: #f8f5f3; - color: rgba(248, 245, 243, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder { - /* Firefox 19+ */ - color: #f8f5f3; - color: rgba(248, 245, 243, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder { - color: #f8f5f3; - color: rgba(248, 245, 243, 0.8); } - .medium-editor-toolbar-form a { - color: #ccc; - -webkit-transform: translateY(2px); - transform: translateY(2px); } - .medium-editor-toolbar-form .medium-editor-toolbar-close { - margin-right: 16px; } - -.medium-editor-toolbar-anchor-preview { - background: #000; - border-radius: 50px; - padding: 5px 12px; } - -.medium-editor-anchor-preview a { - color: #ccc; - text-decoration: none; } - -.medium-editor-toolbar-actions li, -.medium-editor-toolbar-actions button { - border-radius: 50px; } diff --git a/dist/css/themes/beagle.min.css b/dist/css/themes/beagle.min.css deleted file mode 100644 index 966f8c931..000000000 --- a/dist/css/themes/beagle.min.css +++ /dev/null @@ -1 +0,0 @@ -.medium-toolbar-arrow-under:after{border-color:#000 transparent transparent;top:40px}.medium-toolbar-arrow-over:before{border-color:transparent transparent #000}.medium-editor-toolbar{background-color:#000;border:none;border-radius:50px}.medium-editor-toolbar li button{background-color:transparent;border:none;box-sizing:border-box;color:#ccc;height:40px;min-width:40px;padding:5px 12px;-webkit-transition:background-color .2s ease-in,color .2s ease-in;transition:background-color .2s ease-in,color .2s ease-in}.medium-editor-toolbar li .medium-editor-button-active,.medium-editor-toolbar li button:hover{background-color:#000;color:#a2d7c7}.medium-editor-toolbar li .medium-editor-button-first{border-bottom-left-radius:50px;border-top-left-radius:50px;padding-left:24px}.medium-editor-toolbar li .medium-editor-button-last{border-bottom-right-radius:50px;border-right:none;border-top-right-radius:50px;padding-right:24px}.medium-editor-toolbar-form{background:#000;border-radius:50px;color:#ccc;overflow:hidden}.medium-editor-toolbar-form .medium-editor-toolbar-input{background:#000;box-sizing:border-box;color:#ccc;height:40px;padding-left:16px;width:220px}.medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder{color:#f8f5f3;color:rgba(248,245,243,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder{color:#f8f5f3;color:rgba(248,245,243,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder{color:#f8f5f3;color:rgba(248,245,243,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder{color:#f8f5f3;color:rgba(248,245,243,.8)}.medium-editor-toolbar-form a{color:#ccc;-webkit-transform:translateY(2px);transform:translateY(2px)}.medium-editor-toolbar-form .medium-editor-toolbar-close{margin-right:16px}.medium-editor-toolbar-anchor-preview{background:#000;border-radius:50px;padding:5px 12px}.medium-editor-anchor-preview a{color:#ccc;text-decoration:none} \ No newline at end of file diff --git a/dist/css/themes/bootstrap.css b/dist/css/themes/bootstrap.css deleted file mode 100644 index a1f4ef28a..000000000 --- a/dist/css/themes/bootstrap.css +++ /dev/null @@ -1,67 +0,0 @@ -.medium-toolbar-arrow-under:after { - border-color: #428bca transparent transparent transparent; - top: 60px; } - -.medium-toolbar-arrow-over:before { - border-color: transparent transparent #428bca transparent; } - -.medium-editor-toolbar { - background-color: #428bca; - border: 1px solid #357ebd; - border-radius: 4px; } - .medium-editor-toolbar li button { - background-color: transparent; - border: none; - border-right: 1px solid #357ebd; - box-sizing: border-box; - color: #fff; - height: 60px; - min-width: 60px; - -webkit-transition: background-color .2s ease-in, color .2s ease-in; - transition: background-color .2s ease-in, color .2s ease-in; } - .medium-editor-toolbar li button:hover { - background-color: #3276b1; - color: #fff; } - .medium-editor-toolbar li .medium-editor-button-first { - border-bottom-left-radius: 4px; - border-top-left-radius: 4px; } - .medium-editor-toolbar li .medium-editor-button-last { - border-bottom-right-radius: 4px; - border-right: none; - border-top-right-radius: 4px; } - .medium-editor-toolbar li .medium-editor-button-active { - background-color: #3276b1; - color: #fff; } - -.medium-editor-toolbar-form { - background: #428bca; - border-radius: 4px; - color: #fff; } - .medium-editor-toolbar-form .medium-editor-toolbar-input { - background: #428bca; - color: #fff; - height: 60px; } - .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder { - color: #fff; - color: rgba(255, 255, 255, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder { - /* Firefox 18- */ - color: #fff; - color: rgba(255, 255, 255, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder { - /* Firefox 19+ */ - color: #fff; - color: rgba(255, 255, 255, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder { - color: #fff; - color: rgba(255, 255, 255, 0.8); } - .medium-editor-toolbar-form a { - color: #fff; } - -.medium-editor-toolbar-anchor-preview { - background: #428bca; - border-radius: 4px; - color: #fff; } - -.medium-editor-placeholder:after { - color: #357ebd; } diff --git a/dist/css/themes/bootstrap.min.css b/dist/css/themes/bootstrap.min.css deleted file mode 100644 index 4063c7505..000000000 --- a/dist/css/themes/bootstrap.min.css +++ /dev/null @@ -1 +0,0 @@ -.medium-toolbar-arrow-under:after{border-color:#428bca transparent transparent;top:60px}.medium-toolbar-arrow-over:before{border-color:transparent transparent #428bca}.medium-editor-toolbar{background-color:#428bca;border:1px solid #357ebd;border-radius:4px}.medium-editor-toolbar li button{background-color:transparent;border:none;border-right:1px solid #357ebd;box-sizing:border-box;color:#fff;height:60px;min-width:60px;-webkit-transition:background-color .2s ease-in,color .2s ease-in;transition:background-color .2s ease-in,color .2s ease-in}.medium-editor-toolbar li .medium-editor-button-active,.medium-editor-toolbar li button:hover{background-color:#3276b1;color:#fff}.medium-editor-toolbar li .medium-editor-button-first{border-bottom-left-radius:4px;border-top-left-radius:4px}.medium-editor-toolbar li .medium-editor-button-last{border-bottom-right-radius:4px;border-right:none;border-top-right-radius:4px}.medium-editor-toolbar-form{background:#428bca;border-radius:4px;color:#fff}.medium-editor-toolbar-form .medium-editor-toolbar-input{background:#428bca;color:#fff;height:60px}.medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form a{color:#fff}.medium-editor-toolbar-anchor-preview{background:#428bca;border-radius:4px;color:#fff}.medium-editor-placeholder:after{color:#357ebd} \ No newline at end of file diff --git a/dist/css/themes/default.css b/dist/css/themes/default.css deleted file mode 100644 index 02668c6b9..000000000 --- a/dist/css/themes/default.css +++ /dev/null @@ -1,63 +0,0 @@ -.medium-toolbar-arrow-under:after { - border-color: #242424 transparent transparent transparent; - top: 50px; } - -.medium-toolbar-arrow-over:before { - border-color: transparent transparent #242424 transparent; - top: -8px; } - -.medium-editor-toolbar { - background-color: #242424; - background: -webkit-linear-gradient(top, #242424, rgba(36, 36, 36, 0.75)); - background: linear-gradient(to bottom, #242424, rgba(36, 36, 36, 0.75)); - border: 1px solid #000; - border-radius: 5px; - box-shadow: 0 0 3px #000; } - .medium-editor-toolbar li button { - background-color: #242424; - background: -webkit-linear-gradient(top, #242424, rgba(36, 36, 36, 0.89)); - background: linear-gradient(to bottom, #242424, rgba(36, 36, 36, 0.89)); - border: 0; - border-right: 1px solid #000; - border-left: 1px solid #333; - border-left: 1px solid rgba(255, 255, 255, 0.1); - box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3); - color: #fff; - height: 50px; - min-width: 50px; - -webkit-transition: background-color .2s ease-in; - transition: background-color .2s ease-in; } - .medium-editor-toolbar li button:hover { - background-color: #000; - color: yellow; } - .medium-editor-toolbar li .medium-editor-button-first { - border-bottom-left-radius: 5px; - border-top-left-radius: 5px; } - .medium-editor-toolbar li .medium-editor-button-last { - border-bottom-right-radius: 5px; - border-top-right-radius: 5px; } - .medium-editor-toolbar li .medium-editor-button-active { - background-color: #000; - background: -webkit-linear-gradient(top, #242424, rgba(0, 0, 0, 0.89)); - background: linear-gradient(to bottom, #242424, rgba(0, 0, 0, 0.89)); - color: #fff; } - -.medium-editor-toolbar-form { - background: #242424; - border-radius: 5px; - color: #999; } - .medium-editor-toolbar-form .medium-editor-toolbar-input { - background: #242424; - box-sizing: border-box; - color: #ccc; - height: 50px; } - .medium-editor-toolbar-form a { - color: #fff; } - -.medium-editor-toolbar-anchor-preview { - background: #242424; - border-radius: 5px; - color: #fff; } - -.medium-editor-placeholder:after { - color: #b3b3b1; } diff --git a/dist/css/themes/default.min.css b/dist/css/themes/default.min.css deleted file mode 100644 index ac5847843..000000000 --- a/dist/css/themes/default.min.css +++ /dev/null @@ -1 +0,0 @@ -.medium-toolbar-arrow-under:after{border-color:#242424 transparent transparent;top:50px}.medium-toolbar-arrow-over:before{border-color:transparent transparent #242424;top:-8px}.medium-editor-toolbar{background-color:#242424;background:-webkit-linear-gradient(top,#242424,rgba(36,36,36,.75));background:linear-gradient(to bottom,#242424,rgba(36,36,36,.75));border:1px solid #000;border-radius:5px;box-shadow:0 0 3px #000}.medium-editor-toolbar li button{background-color:#242424;background:-webkit-linear-gradient(top,#242424,rgba(36,36,36,.89));background:linear-gradient(to bottom,#242424,rgba(36,36,36,.89));border:0;border-right:1px solid #000;border-left:1px solid #333;border-left:1px solid rgba(255,255,255,.1);box-shadow:0 2px 2px rgba(0,0,0,.3);color:#fff;height:50px;min-width:50px;-webkit-transition:background-color .2s ease-in;transition:background-color .2s ease-in}.medium-editor-toolbar li button:hover{background-color:#000;color:#ff0}.medium-editor-toolbar li .medium-editor-button-first{border-bottom-left-radius:5px;border-top-left-radius:5px}.medium-editor-toolbar li .medium-editor-button-last{border-bottom-right-radius:5px;border-top-right-radius:5px}.medium-editor-toolbar li .medium-editor-button-active{background-color:#000;background:-webkit-linear-gradient(top,#242424,rgba(0,0,0,.89));background:linear-gradient(to bottom,#242424,rgba(0,0,0,.89));color:#fff}.medium-editor-toolbar-form{background:#242424;border-radius:5px;color:#999}.medium-editor-toolbar-form .medium-editor-toolbar-input{background:#242424;box-sizing:border-box;color:#ccc;height:50px}.medium-editor-toolbar-form a{color:#fff}.medium-editor-toolbar-anchor-preview{background:#242424;border-radius:5px;color:#fff}.medium-editor-placeholder:after{color:#b3b3b1} \ No newline at end of file diff --git a/dist/css/themes/flat.css b/dist/css/themes/flat.css deleted file mode 100644 index ae809d489..000000000 --- a/dist/css/themes/flat.css +++ /dev/null @@ -1,58 +0,0 @@ -.medium-toolbar-arrow-under:after { - top: 60px; - border-color: #57ad68 transparent transparent transparent; } - -.medium-toolbar-arrow-over:before { - top: -8px; - border-color: transparent transparent #57ad68 transparent; } - -.medium-editor-toolbar { - background-color: #57ad68; } - .medium-editor-toolbar li { - padding: 0; } - .medium-editor-toolbar li button { - min-width: 60px; - height: 60px; - border: none; - border-right: 1px solid #9ccea6; - background-color: transparent; - color: #fff; - -webkit-transition: background-color .2s ease-in, color .2s ease-in; - transition: background-color .2s ease-in, color .2s ease-in; } - .medium-editor-toolbar li button:hover { - background-color: #346a3f; - color: #fff; } - .medium-editor-toolbar li .medium-editor-button-active { - background-color: #23482a; - color: #fff; } - .medium-editor-toolbar li .medium-editor-button-last { - border-right: none; } - -.medium-editor-toolbar-form .medium-editor-toolbar-input { - height: 60px; - background: #57ad68; - color: #fff; } - .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder { - color: #fff; - color: rgba(255, 255, 255, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder { - /* Firefox 18- */ - color: #fff; - color: rgba(255, 255, 255, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder { - /* Firefox 19+ */ - color: #fff; - color: rgba(255, 255, 255, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder { - color: #fff; - color: rgba(255, 255, 255, 0.8); } - -.medium-editor-toolbar-form a { - color: #fff; } - -.medium-editor-toolbar-anchor-preview { - background: #57ad68; - color: #fff; } - -.medium-editor-placeholder:after { - color: #9ccea6; } diff --git a/dist/css/themes/flat.min.css b/dist/css/themes/flat.min.css deleted file mode 100644 index b97ec7d97..000000000 --- a/dist/css/themes/flat.min.css +++ /dev/null @@ -1 +0,0 @@ -.medium-toolbar-arrow-under:after{top:60px;border-color:#57ad68 transparent transparent}.medium-toolbar-arrow-over:before{top:-8px;border-color:transparent transparent #57ad68}.medium-editor-toolbar{background-color:#57ad68}.medium-editor-toolbar li{padding:0}.medium-editor-toolbar li button{min-width:60px;height:60px;border:none;border-right:1px solid #9ccea6;background-color:transparent;color:#fff;-webkit-transition:background-color .2s ease-in,color .2s ease-in;transition:background-color .2s ease-in,color .2s ease-in}.medium-editor-toolbar li button:hover{background-color:#346a3f;color:#fff}.medium-editor-toolbar li .medium-editor-button-active{background-color:#23482a;color:#fff}.medium-editor-toolbar li .medium-editor-button-last{border-right:none}.medium-editor-toolbar-form .medium-editor-toolbar-input{height:60px;background:#57ad68;color:#fff}.medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form a{color:#fff}.medium-editor-toolbar-anchor-preview{background:#57ad68;color:#fff}.medium-editor-placeholder:after{color:#9ccea6} \ No newline at end of file diff --git a/dist/css/themes/mani.css b/dist/css/themes/mani.css deleted file mode 100644 index 1b1567de8..000000000 --- a/dist/css/themes/mani.css +++ /dev/null @@ -1,57 +0,0 @@ -.medium-toolbar-arrow-under:after, -.medium-toolbar-arrow-over:before { - display: none; } - -.medium-editor-toolbar { - border: 1px solid #cdd6e0; - background-color: #dee7f0; - background-color: rgba(222, 231, 240, 0.95); - background: -webkit-linear-gradient(bottom, #dee7f0, white); - background: linear-gradient(to top, #dee7f0, white); - border-radius: 2px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45); } - .medium-editor-toolbar li button { - min-width: 50px; - height: 50px; - border: none; - border-right: 1px solid #cdd6e0; - background-color: transparent; - color: #40648a; - -webkit-transition: background-color .2s ease-in, color .2s ease-in; - transition: background-color .2s ease-in, color .2s ease-in; } - .medium-editor-toolbar li button:hover { - background-color: #5c90c7; - background-color: rgba(92, 144, 199, 0.45); - color: #fff; } - .medium-editor-toolbar li .medium-editor-button-first { - border-top-left-radius: 2px; - border-bottom-left-radius: 2px; } - .medium-editor-toolbar li .medium-editor-button-last { - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; } - .medium-editor-toolbar li .medium-editor-button-active { - background-color: #5c90c7; - background-color: rgba(92, 144, 199, 0.45); - color: #000; - background: -webkit-linear-gradient(top, #dee7f0, rgba(0, 0, 0, 0.1)); - background: linear-gradient(to bottom, #dee7f0, rgba(0, 0, 0, 0.1)); } - -.medium-editor-toolbar-form { - background: #dee7f0; - color: #999; - border-radius: 2px; } - .medium-editor-toolbar-form .medium-editor-toolbar-input { - height: 50px; - background: #dee7f0; - color: #40648a; - box-sizing: border-box; } - .medium-editor-toolbar-form a { - color: #40648a; } - -.medium-editor-toolbar-anchor-preview { - background: #dee7f0; - color: #40648a; - border-radius: 2px; } - -.medium-editor-placeholder:after { - color: #cdd6e0; } diff --git a/dist/css/themes/mani.min.css b/dist/css/themes/mani.min.css deleted file mode 100644 index 7a10b7a5d..000000000 --- a/dist/css/themes/mani.min.css +++ /dev/null @@ -1 +0,0 @@ -.medium-toolbar-arrow-over:before,.medium-toolbar-arrow-under:after{display:none}.medium-editor-toolbar{border:1px solid #cdd6e0;background-color:#dee7f0;background-color:rgba(222,231,240,.95);background:-webkit-linear-gradient(bottom,#dee7f0,#fff);background:linear-gradient(to top,#dee7f0,#fff);border-radius:2px;box-shadow:0 2px 6px rgba(0,0,0,.45)}.medium-editor-toolbar li button{min-width:50px;height:50px;border:none;border-right:1px solid #cdd6e0;background-color:transparent;color:#40648a;-webkit-transition:background-color .2s ease-in,color .2s ease-in;transition:background-color .2s ease-in,color .2s ease-in}.medium-editor-toolbar li button:hover{background-color:#5c90c7;background-color:rgba(92,144,199,.45);color:#fff}.medium-editor-toolbar li .medium-editor-button-first{border-top-left-radius:2px;border-bottom-left-radius:2px}.medium-editor-toolbar li .medium-editor-button-last{border-top-right-radius:2px;border-bottom-right-radius:2px}.medium-editor-toolbar li .medium-editor-button-active{background-color:#5c90c7;background-color:rgba(92,144,199,.45);color:#000;background:-webkit-linear-gradient(top,#dee7f0,rgba(0,0,0,.1));background:linear-gradient(to bottom,#dee7f0,rgba(0,0,0,.1))}.medium-editor-toolbar-form{background:#dee7f0;color:#999;border-radius:2px}.medium-editor-toolbar-form .medium-editor-toolbar-input{height:50px;background:#dee7f0;color:#40648a;box-sizing:border-box}.medium-editor-toolbar-form a{color:#40648a}.medium-editor-toolbar-anchor-preview{background:#dee7f0;color:#40648a;border-radius:2px}.medium-editor-placeholder:after{color:#cdd6e0} \ No newline at end of file diff --git a/dist/css/themes/roman.css b/dist/css/themes/roman.css deleted file mode 100644 index 1929119fb..000000000 --- a/dist/css/themes/roman.css +++ /dev/null @@ -1,58 +0,0 @@ -.medium-toolbar-arrow-under:after, -.medium-toolbar-arrow-over:before { - display: none; } - -.medium-editor-toolbar { - background-color: #fff; - background-color: rgba(255, 255, 255, 0.95); - border-radius: 5px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45); } - .medium-editor-toolbar li button { - min-width: 50px; - height: 50px; - border: none; - border-right: 1px solid #a8a8a8; - background-color: transparent; - color: #889aac; - box-shadow: inset 0 0 3px #f8f8e6; - background: -webkit-linear-gradient(top, #fff, rgba(0, 0, 0, 0.2)); - background: linear-gradient(to bottom, #fff, rgba(0, 0, 0, 0.2)); - text-shadow: 1px 4px 6px #def, 0 0 0 #000, 1px 4px 6px #def; - -webkit-transition: background-color .2s ease-in; - transition: background-color .2s ease-in; } - .medium-editor-toolbar li button:hover { - background-color: #fff; - color: #fff; - color: rgba(0, 0, 0, 0.8); } - .medium-editor-toolbar li .medium-editor-button-first { - border-top-left-radius: 5px; - border-bottom-left-radius: 5px; } - .medium-editor-toolbar li .medium-editor-button-last { - border-top-right-radius: 5px; - border-bottom-right-radius: 5px; } - .medium-editor-toolbar li .medium-editor-button-active { - background-color: #ccc; - color: #000; - color: rgba(0, 0, 0, 0.8); - background: -webkit-linear-gradient(bottom, #fff, rgba(0, 0, 0, 0.1)); - background: linear-gradient(to top, #fff, rgba(0, 0, 0, 0.1)); } - -.medium-editor-toolbar-form { - background: #fff; - color: #999; - border-radius: 5px; } - .medium-editor-toolbar-form .medium-editor-toolbar-input { - margin: 0; - height: 50px; - background: #fff; - color: #a8a8a8; } - .medium-editor-toolbar-form a { - color: #889aac; } - -.medium-editor-toolbar-anchor-preview { - background: #fff; - color: #889aac; - border-radius: 5px; } - -.medium-editor-placeholder:after { - color: #a8a8a8; } diff --git a/dist/css/themes/roman.min.css b/dist/css/themes/roman.min.css deleted file mode 100644 index 1568f684c..000000000 --- a/dist/css/themes/roman.min.css +++ /dev/null @@ -1 +0,0 @@ -.medium-toolbar-arrow-over:before,.medium-toolbar-arrow-under:after{display:none}.medium-editor-toolbar{background-color:#fff;background-color:rgba(255,255,255,.95);border-radius:5px;box-shadow:0 2px 6px rgba(0,0,0,.45)}.medium-editor-toolbar li button{min-width:50px;height:50px;border:none;border-right:1px solid #a8a8a8;background-color:transparent;color:#889aac;box-shadow:inset 0 0 3px #f8f8e6;background:-webkit-linear-gradient(top,#fff,rgba(0,0,0,.2));background:linear-gradient(to bottom,#fff,rgba(0,0,0,.2));text-shadow:1px 4px 6px #def,0 0 0 #000,1px 4px 6px #def;-webkit-transition:background-color .2s ease-in;transition:background-color .2s ease-in}.medium-editor-toolbar li button:hover{background-color:#fff;color:#fff;color:rgba(0,0,0,.8)}.medium-editor-toolbar li .medium-editor-button-first{border-top-left-radius:5px;border-bottom-left-radius:5px}.medium-editor-toolbar li .medium-editor-button-last{border-top-right-radius:5px;border-bottom-right-radius:5px}.medium-editor-toolbar li .medium-editor-button-active{background-color:#ccc;color:#000;color:rgba(0,0,0,.8);background:-webkit-linear-gradient(bottom,#fff,rgba(0,0,0,.1));background:linear-gradient(to top,#fff,rgba(0,0,0,.1))}.medium-editor-toolbar-form{background:#fff;color:#999;border-radius:5px}.medium-editor-toolbar-form .medium-editor-toolbar-input{margin:0;height:50px;background:#fff;color:#a8a8a8}.medium-editor-toolbar-form a{color:#889aac}.medium-editor-toolbar-anchor-preview{background:#fff;color:#889aac;border-radius:5px}.medium-editor-placeholder:after{color:#a8a8a8} \ No newline at end of file diff --git a/dist/css/themes/tim.css b/dist/css/themes/tim.css deleted file mode 100644 index a3576bd0f..000000000 --- a/dist/css/themes/tim.css +++ /dev/null @@ -1,67 +0,0 @@ -.medium-toolbar-arrow-under:after { - border-color: #2f1e07 transparent transparent transparent; - top: 60px; } - -.medium-toolbar-arrow-over:before { - border-color: transparent transparent #2f1e07 transparent; } - -.medium-editor-toolbar { - background-color: #2f1e07; - border: 1px solid #5b3a0e; - border-radius: 6px; } - .medium-editor-toolbar li button { - background-color: transparent; - border: none; - border-right: 1px solid #5b3a0e; - box-sizing: border-box; - color: #ffedd5; - height: 60px; - min-width: 60px; - -webkit-transition: background-color .2s ease-in, color .2s ease-in; - transition: background-color .2s ease-in, color .2s ease-in; } - .medium-editor-toolbar li button:hover { - background-color: #030200; - color: #ffedd5; } - .medium-editor-toolbar li .medium-editor-button-first { - border-bottom-left-radius: 6px; - border-top-left-radius: 6px; } - .medium-editor-toolbar li .medium-editor-button-last { - border-bottom-right-radius: 6px; - border-right: none; - border-top-right-radius: 6px; } - .medium-editor-toolbar li .medium-editor-button-active { - background-color: #030200; - color: #ffedd5; } - -.medium-editor-toolbar-form { - background: #2f1e07; - border-radius: 6px; - color: #ffedd5; } - .medium-editor-toolbar-form .medium-editor-toolbar-input { - background: #2f1e07; - color: #ffedd5; - height: 60px; } - .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder { - color: #ffedd5; - color: rgba(255, 237, 213, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder { - /* Firefox 18- */ - color: #ffedd5; - color: rgba(255, 237, 213, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder { - /* Firefox 19+ */ - color: #ffedd5; - color: rgba(255, 237, 213, 0.8); } - .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder { - color: #ffedd5; - color: rgba(255, 237, 213, 0.8); } - .medium-editor-toolbar-form a { - color: #ffedd5; } - -.medium-editor-toolbar-anchor-preview { - background: #2f1e07; - border-radius: 6px; - color: #ffedd5; } - -.medium-editor-placeholder:after { - color: #5b3a0e; } diff --git a/dist/css/themes/tim.min.css b/dist/css/themes/tim.min.css deleted file mode 100644 index 882dfe3f6..000000000 --- a/dist/css/themes/tim.min.css +++ /dev/null @@ -1 +0,0 @@ -.medium-toolbar-arrow-under:after{border-color:#2f1e07 transparent transparent;top:60px}.medium-toolbar-arrow-over:before{border-color:transparent transparent #2f1e07}.medium-editor-toolbar{background-color:#2f1e07;border:1px solid #5b3a0e;border-radius:6px}.medium-editor-toolbar li button{background-color:transparent;border:none;border-right:1px solid #5b3a0e;box-sizing:border-box;color:#ffedd5;height:60px;min-width:60px;-webkit-transition:background-color .2s ease-in,color .2s ease-in;transition:background-color .2s ease-in,color .2s ease-in}.medium-editor-toolbar li .medium-editor-button-active,.medium-editor-toolbar li button:hover{background-color:#030200;color:#ffedd5}.medium-editor-toolbar li .medium-editor-button-first{border-bottom-left-radius:6px;border-top-left-radius:6px}.medium-editor-toolbar li .medium-editor-button-last{border-bottom-right-radius:6px;border-right:none;border-top-right-radius:6px}.medium-editor-toolbar-form{background:#2f1e07;border-radius:6px;color:#ffedd5}.medium-editor-toolbar-form .medium-editor-toolbar-input{background:#2f1e07;color:#ffedd5;height:60px}.medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder{color:#ffedd5;color:rgba(255,237,213,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder{color:#ffedd5;color:rgba(255,237,213,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder{color:#ffedd5;color:rgba(255,237,213,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder{color:#ffedd5;color:rgba(255,237,213,.8)}.medium-editor-toolbar-form a{color:#ffedd5}.medium-editor-toolbar-anchor-preview{background:#2f1e07;border-radius:6px;color:#ffedd5}.medium-editor-placeholder:after{color:#5b3a0e} \ No newline at end of file diff --git a/dist/js/medium-editor.js b/dist/js/medium-editor.js deleted file mode 100644 index d329b8434..000000000 --- a/dist/js/medium-editor.js +++ /dev/null @@ -1,7885 +0,0 @@ -/*global self, document, DOMException */ - -/*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */ - -// Full polyfill for browsers with no classList support -if (!("classList" in document.createElement("_"))) { - (function (view) { - - "use strict"; - - if (!('Element' in view)) return; - - var - classListProp = "classList" - , protoProp = "prototype" - , elemCtrProto = view.Element[protoProp] - , objCtr = Object - , strTrim = String[protoProp].trim || function () { - return this.replace(/^\s+|\s+$/g, ""); - } - , arrIndexOf = Array[protoProp].indexOf || function (item) { - var - i = 0 - , len = this.length - ; - for (; i < len; i++) { - if (i in this && this[i] === item) { - return i; - } - } - return -1; - } - // Vendors: please allow content code to instantiate DOMExceptions - , DOMEx = function (type, message) { - this.name = type; - this.code = DOMException[type]; - this.message = message; - } - , checkTokenAndGetIndex = function (classList, token) { - if (token === "") { - throw new DOMEx( - "SYNTAX_ERR" - , "An invalid or illegal string was specified" - ); - } - if (/\s/.test(token)) { - throw new DOMEx( - "INVALID_CHARACTER_ERR" - , "String contains an invalid character" - ); - } - return arrIndexOf.call(classList, token); - } - , ClassList = function (elem) { - var - trimmedClasses = strTrim.call(elem.getAttribute("class") || "") - , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [] - , i = 0 - , len = classes.length - ; - for (; i < len; i++) { - this.push(classes[i]); - } - this._updateClassName = function () { - elem.setAttribute("class", this.toString()); - }; - } - , classListProto = ClassList[protoProp] = [] - , classListGetter = function () { - return new ClassList(this); - } - ; - // Most DOMException implementations don't allow calling DOMException's toString() - // on non-DOMExceptions. Error's toString() is sufficient here. - DOMEx[protoProp] = Error[protoProp]; - classListProto.item = function (i) { - return this[i] || null; - }; - classListProto.contains = function (token) { - token += ""; - return checkTokenAndGetIndex(this, token) !== -1; - }; - classListProto.add = function () { - var - tokens = arguments - , i = 0 - , l = tokens.length - , token - , updated = false - ; - do { - token = tokens[i] + ""; - if (checkTokenAndGetIndex(this, token) === -1) { - this.push(token); - updated = true; - } - } - while (++i < l); - - if (updated) { - this._updateClassName(); - } - }; - classListProto.remove = function () { - var - tokens = arguments - , i = 0 - , l = tokens.length - , token - , updated = false - , index - ; - do { - token = tokens[i] + ""; - index = checkTokenAndGetIndex(this, token); - while (index !== -1) { - this.splice(index, 1); - updated = true; - index = checkTokenAndGetIndex(this, token); - } - } - while (++i < l); - - if (updated) { - this._updateClassName(); - } - }; - classListProto.toggle = function (token, force) { - token += ""; - - var - result = this.contains(token) - , method = result ? - force !== true && "remove" - : - force !== false && "add" - ; - - if (method) { - this[method](token); - } - - if (force === true || force === false) { - return force; - } else { - return !result; - } - }; - classListProto.toString = function () { - return this.join(" "); - }; - - if (objCtr.defineProperty) { - var classListPropDesc = { - get: classListGetter - , enumerable: true - , configurable: true - }; - try { - objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); - } catch (ex) { // IE 8 doesn't support enumerable:true - if (ex.number === -0x7FF5EC54) { - classListPropDesc.enumerable = false; - objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); - } - } - } else if (objCtr[protoProp].__defineGetter__) { - elemCtrProto.__defineGetter__(classListProp, classListGetter); - } - - }(self)); -} - -/* Blob.js - * A Blob implementation. - * 2014-07-24 - * - * By Eli Grey, http://eligrey.com - * By Devin Samarin, https://github.com/dsamarin - * License: X11/MIT - * See https://github.com/eligrey/Blob.js/blob/master/LICENSE.md - */ - -/*global self, unescape */ -/*jslint bitwise: true, regexp: true, confusion: true, es5: true, vars: true, white: true, - plusplus: true */ - -/*! @source http://purl.eligrey.com/github/Blob.js/blob/master/Blob.js */ - -(function (view) { - "use strict"; - - view.URL = view.URL || view.webkitURL; - - if (view.Blob && view.URL) { - try { - new Blob; - return; - } catch (e) {} - } - - // Internally we use a BlobBuilder implementation to base Blob off of - // in order to support older browsers that only have BlobBuilder - var BlobBuilder = view.BlobBuilder || view.WebKitBlobBuilder || view.MozBlobBuilder || (function(view) { - var - get_class = function(object) { - return Object.prototype.toString.call(object).match(/^\[object\s(.*)\]$/)[1]; - } - , FakeBlobBuilder = function BlobBuilder() { - this.data = []; - } - , FakeBlob = function Blob(data, type, encoding) { - this.data = data; - this.size = data.length; - this.type = type; - this.encoding = encoding; - } - , FBB_proto = FakeBlobBuilder.prototype - , FB_proto = FakeBlob.prototype - , FileReaderSync = view.FileReaderSync - , FileException = function(type) { - this.code = this[this.name = type]; - } - , file_ex_codes = ( - "NOT_FOUND_ERR SECURITY_ERR ABORT_ERR NOT_READABLE_ERR ENCODING_ERR " - + "NO_MODIFICATION_ALLOWED_ERR INVALID_STATE_ERR SYNTAX_ERR" - ).split(" ") - , file_ex_code = file_ex_codes.length - , real_URL = view.URL || view.webkitURL || view - , real_create_object_URL = real_URL.createObjectURL - , real_revoke_object_URL = real_URL.revokeObjectURL - , URL = real_URL - , btoa = view.btoa - , atob = view.atob - - , ArrayBuffer = view.ArrayBuffer - , Uint8Array = view.Uint8Array - - , origin = /^[\w-]+:\/*\[?[\w\.:-]+\]?(?::[0-9]+)?/ - ; - FakeBlob.fake = FB_proto.fake = true; - while (file_ex_code--) { - FileException.prototype[file_ex_codes[file_ex_code]] = file_ex_code + 1; - } - // Polyfill URL - if (!real_URL.createObjectURL) { - URL = view.URL = function(uri) { - var - uri_info = document.createElementNS("http://www.w3.org/1999/xhtml", "a") - , uri_origin - ; - uri_info.href = uri; - if (!("origin" in uri_info)) { - if (uri_info.protocol.toLowerCase() === "data:") { - uri_info.origin = null; - } else { - uri_origin = uri.match(origin); - uri_info.origin = uri_origin && uri_origin[1]; - } - } - return uri_info; - }; - } - URL.createObjectURL = function(blob) { - var - type = blob.type - , data_URI_header - ; - if (type === null) { - type = "application/octet-stream"; - } - if (blob instanceof FakeBlob) { - data_URI_header = "data:" + type; - if (blob.encoding === "base64") { - return data_URI_header + ";base64," + blob.data; - } else if (blob.encoding === "URI") { - return data_URI_header + "," + decodeURIComponent(blob.data); - } if (btoa) { - return data_URI_header + ";base64," + btoa(blob.data); - } else { - return data_URI_header + "," + encodeURIComponent(blob.data); - } - } else if (real_create_object_URL) { - return real_create_object_URL.call(real_URL, blob); - } - }; - URL.revokeObjectURL = function(object_URL) { - if (object_URL.substring(0, 5) !== "data:" && real_revoke_object_URL) { - real_revoke_object_URL.call(real_URL, object_URL); - } - }; - FBB_proto.append = function(data/*, endings*/) { - var bb = this.data; - // decode data to a binary string - if (Uint8Array && (data instanceof ArrayBuffer || data instanceof Uint8Array)) { - var - str = "" - , buf = new Uint8Array(data) - , i = 0 - , buf_len = buf.length - ; - for (; i < buf_len; i++) { - str += String.fromCharCode(buf[i]); - } - bb.push(str); - } else if (get_class(data) === "Blob" || get_class(data) === "File") { - if (FileReaderSync) { - var fr = new FileReaderSync; - bb.push(fr.readAsBinaryString(data)); - } else { - // async FileReader won't work as BlobBuilder is sync - throw new FileException("NOT_READABLE_ERR"); - } - } else if (data instanceof FakeBlob) { - if (data.encoding === "base64" && atob) { - bb.push(atob(data.data)); - } else if (data.encoding === "URI") { - bb.push(decodeURIComponent(data.data)); - } else if (data.encoding === "raw") { - bb.push(data.data); - } - } else { - if (typeof data !== "string") { - data += ""; // convert unsupported types to strings - } - // decode UTF-16 to binary string - bb.push(unescape(encodeURIComponent(data))); - } - }; - FBB_proto.getBlob = function(type) { - if (!arguments.length) { - type = null; - } - return new FakeBlob(this.data.join(""), type, "raw"); - }; - FBB_proto.toString = function() { - return "[object BlobBuilder]"; - }; - FB_proto.slice = function(start, end, type) { - var args = arguments.length; - if (args < 3) { - type = null; - } - return new FakeBlob( - this.data.slice(start, args > 1 ? end : this.data.length) - , type - , this.encoding - ); - }; - FB_proto.toString = function() { - return "[object Blob]"; - }; - FB_proto.close = function() { - this.size = 0; - delete this.data; - }; - return FakeBlobBuilder; - }(view)); - - view.Blob = function(blobParts, options) { - var type = options ? (options.type || "") : ""; - var builder = new BlobBuilder(); - if (blobParts) { - for (var i = 0, len = blobParts.length; i < len; i++) { - if (Uint8Array && blobParts[i] instanceof Uint8Array) { - builder.append(blobParts[i].buffer); - } - else { - builder.append(blobParts[i]); - } - } - } - var blob = builder.getBlob(type); - if (!blob.slice && blob.webkitSlice) { - blob.slice = blob.webkitSlice; - } - return blob; - }; - - var getPrototypeOf = Object.getPrototypeOf || function(object) { - return object.__proto__; - }; - view.Blob.prototype = getPrototypeOf(new view.Blob()); -}(typeof self !== "undefined" && self || typeof window !== "undefined" && window || this.content || this)); - -(function (root, factory) { - 'use strict'; - var isElectron = typeof module === 'object' && typeof process !== 'undefined' && process && process.versions && process.versions.electron; - if (!isElectron && typeof module === 'object') { - module.exports = factory; - } else if (typeof define === 'function' && define.amd) { - define(function () { - return factory; - }); - } else { - root.MediumEditor = factory; - } -}(this, function () { - - 'use strict'; - -function MediumEditor(elements, options) { - 'use strict'; - return this.init(elements, options); -} - -MediumEditor.extensions = {}; -/*jshint unused: true */ -(function (window) { - 'use strict'; - - function copyInto(overwrite, dest) { - var prop, - sources = Array.prototype.slice.call(arguments, 2); - dest = dest || {}; - for (var i = 0; i < sources.length; i++) { - var source = sources[i]; - if (source) { - for (prop in source) { - if (source.hasOwnProperty(prop) && - typeof source[prop] !== 'undefined' && - (overwrite || dest.hasOwnProperty(prop) === false)) { - dest[prop] = source[prop]; - } - } - } - } - return dest; - } - - // https://developer.mozilla.org/en-US/docs/Web/API/Node/contains - // Some browsers (including phantom) don't return true for Node.contains(child) - // if child is a text node. Detect these cases here and use a fallback - // for calls to Util.isDescendant() - var nodeContainsWorksWithTextNodes = false; - try { - var testParent = document.createElement('div'), - testText = document.createTextNode(' '); - testParent.appendChild(testText); - nodeContainsWorksWithTextNodes = testParent.contains(testText); - } catch (exc) {} - - var Util = { - - // http://stackoverflow.com/questions/17907445/how-to-detect-ie11#comment30165888_17907562 - // by rg89 - isIE: ((navigator.appName === 'Microsoft Internet Explorer') || ((navigator.appName === 'Netscape') && (new RegExp('Trident/.*rv:([0-9]{1,}[.0-9]{0,})').exec(navigator.userAgent) !== null))), - - isEdge: (/Edge\/\d+/).exec(navigator.userAgent) !== null, - - // if firefox - isFF: (navigator.userAgent.toLowerCase().indexOf('firefox') > -1), - - // http://stackoverflow.com/a/11752084/569101 - isMac: (window.navigator.platform.toUpperCase().indexOf('MAC') >= 0), - - // https://github.com/jashkenas/underscore - // Lonely letter MUST USE the uppercase code - keyCode: { - BACKSPACE: 8, - TAB: 9, - ENTER: 13, - ESCAPE: 27, - SPACE: 32, - DELETE: 46, - K: 75, // K keycode, and not k - M: 77, - V: 86 - }, - - /** - * Returns true if it's metaKey on Mac, or ctrlKey on non-Mac. - * See #591 - */ - isMetaCtrlKey: function (event) { - if ((Util.isMac && event.metaKey) || (!Util.isMac && event.ctrlKey)) { - return true; - } - - return false; - }, - - /** - * Returns true if the key associated to the event is inside keys array - * - * @see : https://github.com/jquery/jquery/blob/0705be475092aede1eddae01319ec931fb9c65fc/src/event.js#L473-L484 - * @see : http://stackoverflow.com/q/4471582/569101 - */ - isKey: function (event, keys) { - var keyCode = Util.getKeyCode(event); - - // it's not an array let's just compare strings! - if (false === Array.isArray(keys)) { - return keyCode === keys; - } - - if (-1 === keys.indexOf(keyCode)) { - return false; - } - - return true; - }, - - getKeyCode: function (event) { - var keyCode = event.which; - - // getting the key code from event - if (null === keyCode) { - keyCode = event.charCode !== null ? event.charCode : event.keyCode; - } - - return keyCode; - }, - - blockContainerElementNames: [ - // elements our editor generates - 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'pre', 'ul', 'li', 'ol', - // all other known block elements - 'address', 'article', 'aside', 'audio', 'canvas', 'dd', 'dl', 'dt', 'fieldset', - 'figcaption', 'figure', 'footer', 'form', 'header', 'hgroup', 'main', 'nav', - 'noscript', 'output', 'section', 'video', - 'table', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td' - ], - - emptyElementNames: ['br', 'col', 'colgroup', 'hr', 'img', 'input', 'source', 'wbr'], - - extend: function extend(/* dest, source1, source2, ...*/) { - var args = [true].concat(Array.prototype.slice.call(arguments)); - return copyInto.apply(this, args); - }, - - defaults: function defaults(/*dest, source1, source2, ...*/) { - var args = [false].concat(Array.prototype.slice.call(arguments)); - return copyInto.apply(this, args); - }, - - /* - * Create a link around the provided text nodes which must be adjacent to each other and all be - * descendants of the same closest block container. If the preconditions are not met, unexpected - * behavior will result. - */ - createLink: function (document, textNodes, href, target) { - var anchor = document.createElement('a'); - Util.moveTextRangeIntoElement(textNodes[0], textNodes[textNodes.length - 1], anchor); - anchor.setAttribute('href', href); - if (target) { - if (target === '_blank') { - anchor.setAttribute('rel', 'noopener noreferrer'); - } - anchor.setAttribute('target', target); - } - return anchor; - }, - - /* - * Given the provided match in the format {start: 1, end: 2} where start and end are indices into the - * textContent of the provided element argument, modify the DOM inside element to ensure that the text - * identified by the provided match can be returned as text nodes that contain exactly that text, without - * any additional text at the beginning or end of the returned array of adjacent text nodes. - * - * The only DOM manipulation performed by this function is splitting the text nodes, non-text nodes are - * not affected in any way. - */ - findOrCreateMatchingTextNodes: function (document, element, match) { - var treeWalker = document.createTreeWalker(element, NodeFilter.SHOW_ALL, null, false), - matchedNodes = [], - currentTextIndex = 0, - startReached = false, - currentNode = null, - newNode = null; - - while ((currentNode = treeWalker.nextNode()) !== null) { - if (currentNode.nodeType > 3) { - continue; - } else if (currentNode.nodeType === 3) { - if (!startReached && match.start < (currentTextIndex + currentNode.nodeValue.length)) { - startReached = true; - newNode = Util.splitStartNodeIfNeeded(currentNode, match.start, currentTextIndex); - } - if (startReached) { - Util.splitEndNodeIfNeeded(currentNode, newNode, match.end, currentTextIndex); - } - if (startReached && currentTextIndex === match.end) { - break; // Found the node(s) corresponding to the link. Break out and move on to the next. - } else if (startReached && currentTextIndex > (match.end + 1)) { - throw new Error('PerformLinking overshot the target!'); // should never happen... - } - - if (startReached) { - matchedNodes.push(newNode || currentNode); - } - - currentTextIndex += currentNode.nodeValue.length; - if (newNode !== null) { - currentTextIndex += newNode.nodeValue.length; - // Skip the newNode as we'll already have pushed it to the matches - treeWalker.nextNode(); - } - newNode = null; - } else if (currentNode.tagName.toLowerCase() === 'img') { - if (!startReached && (match.start <= currentTextIndex)) { - startReached = true; - } - if (startReached) { - matchedNodes.push(currentNode); - } - } - } - return matchedNodes; - }, - - /* - * Given the provided text node and text coordinates, split the text node if needed to make it align - * precisely with the coordinates. - * - * This function is intended to be called from Util.findOrCreateMatchingTextNodes. - */ - splitStartNodeIfNeeded: function (currentNode, matchStartIndex, currentTextIndex) { - if (matchStartIndex !== currentTextIndex) { - return currentNode.splitText(matchStartIndex - currentTextIndex); - } - return null; - }, - - /* - * Given the provided text node and text coordinates, split the text node if needed to make it align - * precisely with the coordinates. The newNode argument should from the result of Util.splitStartNodeIfNeeded, - * if that function has been called on the same currentNode. - * - * This function is intended to be called from Util.findOrCreateMatchingTextNodes. - */ - splitEndNodeIfNeeded: function (currentNode, newNode, matchEndIndex, currentTextIndex) { - var textIndexOfEndOfFarthestNode, - endSplitPoint; - textIndexOfEndOfFarthestNode = currentTextIndex + currentNode.nodeValue.length + - (newNode ? newNode.nodeValue.length : 0) - 1; - endSplitPoint = matchEndIndex - currentTextIndex - - (newNode ? currentNode.nodeValue.length : 0); - if (textIndexOfEndOfFarthestNode >= matchEndIndex && - currentTextIndex !== textIndexOfEndOfFarthestNode && - endSplitPoint !== 0) { - (newNode || currentNode).splitText(endSplitPoint); - } - }, - - /* - * Take an element, and break up all of its text content into unique pieces such that: - * 1) All text content of the elements are in separate blocks. No piece of text content should span - * across multiple blocks. This means no element return by this function should have - * any blocks as children. - * 2) The union of the textcontent of all of the elements returned here covers all - * of the text within the element. - * - * - * EXAMPLE: - * In the event that we have something like: - * - *
- *

Some Text

- *
    - *
  1. List Item 1
  2. - *
  3. List Item 2
  4. - *
- *
- * - * This function would return these elements as an array: - * [

Some Text

,
  • List Item 1
  • ,
  • List Item 2
  • ] - * - * Since the
    and
      elements contain blocks within them they are not returned. - * Since the

      and

    1. 's don't contain block elements and cover all the text content of the - *
      container, they are the elements returned. - */ - splitByBlockElements: function (element) { - if (element.nodeType !== 3 && element.nodeType !== 1) { - return []; - } - - var toRet = [], - blockElementQuery = MediumEditor.util.blockContainerElementNames.join(','); - - if (element.nodeType === 3 || element.querySelectorAll(blockElementQuery).length === 0) { - return [element]; - } - - for (var i = 0; i < element.childNodes.length; i++) { - var child = element.childNodes[i]; - if (child.nodeType === 3) { - toRet.push(child); - } else if (child.nodeType === 1) { - var blockElements = child.querySelectorAll(blockElementQuery); - if (blockElements.length === 0) { - toRet.push(child); - } else { - toRet = toRet.concat(MediumEditor.util.splitByBlockElements(child)); - } - } - } - - return toRet; - }, - - // Find the next node in the DOM tree that represents any text that is being - // displayed directly next to the targetNode (passed as an argument) - // Text that appears directly next to the current node can be: - // - A sibling text node - // - A descendant of a sibling element - // - A sibling text node of an ancestor - // - A descendant of a sibling element of an ancestor - findAdjacentTextNodeWithContent: function findAdjacentTextNodeWithContent(rootNode, targetNode, ownerDocument) { - var pastTarget = false, - nextNode, - nodeIterator = ownerDocument.createNodeIterator(rootNode, NodeFilter.SHOW_TEXT, null, false); - - // Use a native NodeIterator to iterate over all the text nodes that are descendants - // of the rootNode. Once past the targetNode, choose the first non-empty text node - nextNode = nodeIterator.nextNode(); - while (nextNode) { - if (nextNode === targetNode) { - pastTarget = true; - } else if (pastTarget) { - if (nextNode.nodeType === 3 && nextNode.nodeValue && nextNode.nodeValue.trim().length > 0) { - break; - } - } - nextNode = nodeIterator.nextNode(); - } - - return nextNode; - }, - - // Find an element's previous sibling within a medium-editor element - // If one doesn't exist, find the closest ancestor's previous sibling - findPreviousSibling: function (node) { - if (!node || Util.isMediumEditorElement(node)) { - return false; - } - - var previousSibling = node.previousSibling; - while (!previousSibling && !Util.isMediumEditorElement(node.parentNode)) { - node = node.parentNode; - previousSibling = node.previousSibling; - } - - return previousSibling; - }, - - isDescendant: function isDescendant(parent, child, checkEquality) { - if (!parent || !child) { - return false; - } - if (parent === child) { - return !!checkEquality; - } - // If parent is not an element, it can't have any descendants - if (parent.nodeType !== 1) { - return false; - } - if (nodeContainsWorksWithTextNodes || child.nodeType !== 3) { - return parent.contains(child); - } - var node = child.parentNode; - while (node !== null) { - if (node === parent) { - return true; - } - node = node.parentNode; - } - return false; - }, - - // https://github.com/jashkenas/underscore - isElement: function isElement(obj) { - return !!(obj && obj.nodeType === 1); - }, - - // https://github.com/jashkenas/underscore - throttle: function (func, wait) { - var THROTTLE_INTERVAL = 50, - context, - args, - result, - timeout = null, - previous = 0, - later = function () { - previous = Date.now(); - timeout = null; - result = func.apply(context, args); - if (!timeout) { - context = args = null; - } - }; - - if (!wait && wait !== 0) { - wait = THROTTLE_INTERVAL; - } - - return function () { - var now = Date.now(), - remaining = wait - (now - previous); - - context = this; - args = arguments; - if (remaining <= 0 || remaining > wait) { - if (timeout) { - clearTimeout(timeout); - timeout = null; - } - previous = now; - result = func.apply(context, args); - if (!timeout) { - context = args = null; - } - } else if (!timeout) { - timeout = setTimeout(later, remaining); - } - return result; - }; - }, - - traverseUp: function (current, testElementFunction) { - if (!current) { - return false; - } - - do { - if (current.nodeType === 1) { - if (testElementFunction(current)) { - return current; - } - // do not traverse upwards past the nearest containing editor - if (Util.isMediumEditorElement(current)) { - return false; - } - } - - current = current.parentNode; - } while (current); - - return false; - }, - - htmlEntities: function (str) { - // converts special characters (like <) into their escaped/encoded values (like <). - // This allows you to show to display the string without the browser reading it as HTML. - return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - }, - - // http://stackoverflow.com/questions/6690752/insert-html-at-caret-in-a-contenteditable-div - insertHTMLCommand: function (doc, html) { - var selection, range, el, fragment, node, lastNode, toReplace, - res = false, - ecArgs = ['insertHTML', false, html]; - - /* Edge's implementation of insertHTML is just buggy right now: - * - Doesn't allow leading white space at the beginning of an element - * - Found a case when a tag was inserted when calling alignCenter inside a blockquote - * - * There are likely other bugs, these are just the ones we found so far. - * For now, let's just use the same fallback we did for IE - */ - if (!MediumEditor.util.isEdge && doc.queryCommandSupported('insertHTML')) { - try { - return doc.execCommand.apply(doc, ecArgs); - } catch (ignore) {} - } - - selection = doc.getSelection(); - if (selection.rangeCount) { - range = selection.getRangeAt(0); - toReplace = range.commonAncestorContainer; - - // https://github.com/yabwe/medium-editor/issues/748 - // If the selection is an empty editor element, create a temporary text node inside of the editor - // and select it so that we don't delete the editor element - if (Util.isMediumEditorElement(toReplace) && !toReplace.firstChild) { - range.selectNode(toReplace.appendChild(doc.createTextNode(''))); - } else if ((toReplace.nodeType === 3 && range.startOffset === 0 && range.endOffset === toReplace.nodeValue.length) || - (toReplace.nodeType !== 3 && toReplace.innerHTML === range.toString())) { - // Ensure range covers maximum amount of nodes as possible - // By moving up the DOM and selecting ancestors whose only child is the range - while (!Util.isMediumEditorElement(toReplace) && - toReplace.parentNode && - toReplace.parentNode.childNodes.length === 1 && - !Util.isMediumEditorElement(toReplace.parentNode)) { - toReplace = toReplace.parentNode; - } - range.selectNode(toReplace); - } - range.deleteContents(); - - el = doc.createElement('div'); - el.innerHTML = html; - fragment = doc.createDocumentFragment(); - while (el.firstChild) { - node = el.firstChild; - lastNode = fragment.appendChild(node); - } - range.insertNode(fragment); - - // Preserve the selection: - if (lastNode) { - range = range.cloneRange(); - range.setStartAfter(lastNode); - range.collapse(true); - MediumEditor.selection.selectRange(doc, range); - } - res = true; - } - - // https://github.com/yabwe/medium-editor/issues/992 - // If we're monitoring calls to execCommand, notify listeners as if a real call had happened - if (doc.execCommand.callListeners) { - doc.execCommand.callListeners(ecArgs, res); - } - return res; - }, - - execFormatBlock: function (doc, tagName) { - // Get the top level block element that contains the selection - var blockContainer = Util.getTopBlockContainer(MediumEditor.selection.getSelectionStart(doc)), - childNodes; - - // Special handling for blockquote - if (tagName === 'blockquote') { - if (blockContainer) { - childNodes = Array.prototype.slice.call(blockContainer.childNodes); - // Check if the blockquote has a block element as a child (nested blocks) - if (childNodes.some(function (childNode) { - return Util.isBlockContainer(childNode); - })) { - // FF handles blockquote differently on formatBlock - // allowing nesting, we need to use outdent - // https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla - return doc.execCommand('outdent', false, null); - } - } - - // When IE blockquote needs to be called as indent - // http://stackoverflow.com/questions/1816223/rich-text-editor-with-blockquote-function/1821777#1821777 - if (Util.isIE) { - return doc.execCommand('indent', false, tagName); - } - } - - // If the blockContainer is already the element type being passed in - // treat it as 'undo' formatting and just convert it to a

      - if (blockContainer && tagName === blockContainer.nodeName.toLowerCase()) { - tagName = 'p'; - } - - // When IE we need to add <> to heading elements - // http://stackoverflow.com/questions/10741831/execcommand-formatblock-headings-in-ie - if (Util.isIE) { - tagName = '<' + tagName + '>'; - } - - // When FF, IE and Edge, we have to handle blockquote node seperately as 'formatblock' does not work. - // https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#Commands - if (blockContainer && blockContainer.nodeName.toLowerCase() === 'blockquote') { - // For IE, just use outdent - if (Util.isIE && tagName === '

      ') { - return doc.execCommand('outdent', false, tagName); - } - - // For Firefox and Edge, make sure there's a nested block element before calling outdent - if ((Util.isFF || Util.isEdge) && tagName === 'p') { - childNodes = Array.prototype.slice.call(blockContainer.childNodes); - // If there are some non-block elements we need to wrap everything in a

      before we outdent - if (childNodes.some(function (childNode) { - return !Util.isBlockContainer(childNode); - })) { - doc.execCommand('formatBlock', false, tagName); - } - return doc.execCommand('outdent', false, tagName); - } - } - - return doc.execCommand('formatBlock', false, tagName); - }, - - /** - * Set target to blank on the given el element - * - * TODO: not sure if this should be here - * - * When creating a link (using core -> createLink) the selection returned by Firefox will be the parent of the created link - * instead of the created link itself (as it is for Chrome for example), so we retrieve all "a" children to grab the good one by - * using `anchorUrl` to ensure that we are adding target="_blank" on the good one. - * This isn't a bulletproof solution anyway .. - */ - setTargetBlank: function (el, anchorUrl) { - var i, url = anchorUrl || false; - if (el.nodeName.toLowerCase() === 'a') { - el.target = '_blank'; - el.rel = 'noopener noreferrer'; - } else { - el = el.getElementsByTagName('a'); - - for (i = 0; i < el.length; i += 1) { - if (false === url || url === el[i].attributes.href.value) { - el[i].target = '_blank'; - el[i].rel = 'noopener noreferrer'; - } - } - } - }, - - /* - * this function is called to explicitly remove the target='_blank' as FF holds on to _blank value even - * after unchecking the checkbox on anchor form - */ - removeTargetBlank: function (el, anchorUrl) { - var i; - if (el.nodeName.toLowerCase() === 'a') { - el.removeAttribute('target'); - el.removeAttribute('rel'); - } else { - el = el.getElementsByTagName('a'); - - for (i = 0; i < el.length; i += 1) { - if (anchorUrl === el[i].attributes.href.value) { - el[i].removeAttribute('target'); - el[i].removeAttribute('rel'); - } - } - } - }, - - /* - * this function adds one or several classes on an a element. - * if el parameter is not an a, it will look for a children of el. - * if no a children are found, it will look for the a parent. - */ - addClassToAnchors: function (el, buttonClass) { - var classes = buttonClass.split(' '), - i, - j; - if (el.nodeName.toLowerCase() === 'a') { - for (j = 0; j < classes.length; j += 1) { - el.classList.add(classes[j]); - } - } else { - var aChildren = el.getElementsByTagName('a'); - if (aChildren.length === 0) { - var parentAnchor = Util.getClosestTag(el, 'a'); - el = parentAnchor ? [parentAnchor] : []; - } else { - el = aChildren; - } - for (i = 0; i < el.length; i += 1) { - for (j = 0; j < classes.length; j += 1) { - el[i].classList.add(classes[j]); - } - } - } - }, - - isListItem: function (node) { - if (!node) { - return false; - } - if (node.nodeName.toLowerCase() === 'li') { - return true; - } - - var parentNode = node.parentNode, - tagName = parentNode.nodeName.toLowerCase(); - while (tagName === 'li' || (!Util.isBlockContainer(parentNode) && tagName !== 'div')) { - if (tagName === 'li') { - return true; - } - parentNode = parentNode.parentNode; - if (parentNode) { - tagName = parentNode.nodeName.toLowerCase(); - } else { - return false; - } - } - return false; - }, - - cleanListDOM: function (ownerDocument, element) { - if (element.nodeName.toLowerCase() !== 'li') { - return; - } - - var list = element.parentElement; - - if (list.parentElement.nodeName.toLowerCase() === 'p') { // yes we need to clean up - Util.unwrap(list.parentElement, ownerDocument); - - // move cursor at the end of the text inside the list - // for some unknown reason, the cursor is moved to end of the "visual" line - MediumEditor.selection.moveCursor(ownerDocument, element.firstChild, element.firstChild.textContent.length); - } - }, - - /* splitDOMTree - * - * Given a root element some descendant element, split the root element - * into its own element containing the descendant element and all elements - * on the left or right side of the descendant ('right' is default) - * - * example: - * - *

      - * / | \ - * - * / \ / \ / \ - * 1 2 3 4 5 6 - * - * If I wanted to split this tree given the
      as the root and "4" as the leaf - * the result would be (the prime ' marks indicates nodes that are created as clones): - * - * SPLITTING OFF 'RIGHT' TREE SPLITTING OFF 'LEFT' TREE - * - *
      '
      '
      - * / \ / \ / \ | - * ' - * / \ | | / \ /\ /\ /\ - * 1 2 3 4 5 6 1 2 3 4 5 6 - * - * The above example represents splitting off the 'right' or 'left' part of a tree, where - * the
      ' would be returned as an element not appended to the DOM, and the
      - * would remain in place where it was - * - */ - splitOffDOMTree: function (rootNode, leafNode, splitLeft) { - var splitOnNode = leafNode, - createdNode = null, - splitRight = !splitLeft; - - // loop until we hit the root - while (splitOnNode !== rootNode) { - var currParent = splitOnNode.parentNode, - newParent = currParent.cloneNode(false), - targetNode = (splitRight ? splitOnNode : currParent.firstChild), - appendLast; - - // Create a new parent element which is a clone of the current parent - if (createdNode) { - if (splitRight) { - // If we're splitting right, add previous created element before siblings - newParent.appendChild(createdNode); - } else { - // If we're splitting left, add previous created element last - appendLast = createdNode; - } - } - createdNode = newParent; - - while (targetNode) { - var sibling = targetNode.nextSibling; - // Special handling for the 'splitNode' - if (targetNode === splitOnNode) { - if (!targetNode.hasChildNodes()) { - targetNode.parentNode.removeChild(targetNode); - } else { - // For the node we're splitting on, if it has children, we need to clone it - // and not just move it - targetNode = targetNode.cloneNode(false); - } - // If the resulting split node has content, add it - if (targetNode.textContent) { - createdNode.appendChild(targetNode); - } - - targetNode = (splitRight ? sibling : null); - } else { - // For general case, just remove the element and only - // add it to the split tree if it contains something - targetNode.parentNode.removeChild(targetNode); - if (targetNode.hasChildNodes() || targetNode.textContent) { - createdNode.appendChild(targetNode); - } - - targetNode = sibling; - } - } - - // If we had an element we wanted to append at the end, do that now - if (appendLast) { - createdNode.appendChild(appendLast); - } - - splitOnNode = currParent; - } - - return createdNode; - }, - - moveTextRangeIntoElement: function (startNode, endNode, newElement) { - if (!startNode || !endNode) { - return false; - } - - var rootNode = Util.findCommonRoot(startNode, endNode); - if (!rootNode) { - return false; - } - - if (endNode === startNode) { - var temp = startNode.parentNode, - sibling = startNode.nextSibling; - temp.removeChild(startNode); - newElement.appendChild(startNode); - if (sibling) { - temp.insertBefore(newElement, sibling); - } else { - temp.appendChild(newElement); - } - return newElement.hasChildNodes(); - } - - // create rootChildren array which includes all the children - // we care about - var rootChildren = [], - firstChild, - lastChild, - nextNode; - for (var i = 0; i < rootNode.childNodes.length; i++) { - nextNode = rootNode.childNodes[i]; - if (!firstChild) { - if (Util.isDescendant(nextNode, startNode, true)) { - firstChild = nextNode; - } - } else { - if (Util.isDescendant(nextNode, endNode, true)) { - lastChild = nextNode; - break; - } else { - rootChildren.push(nextNode); - } - } - } - - var afterLast = lastChild.nextSibling, - fragment = rootNode.ownerDocument.createDocumentFragment(); - - // build up fragment on startNode side of tree - if (firstChild === startNode) { - firstChild.parentNode.removeChild(firstChild); - fragment.appendChild(firstChild); - } else { - fragment.appendChild(Util.splitOffDOMTree(firstChild, startNode)); - } - - // add any elements between firstChild & lastChild - rootChildren.forEach(function (element) { - element.parentNode.removeChild(element); - fragment.appendChild(element); - }); - - // build up fragment on endNode side of the tree - if (lastChild === endNode) { - lastChild.parentNode.removeChild(lastChild); - fragment.appendChild(lastChild); - } else { - fragment.appendChild(Util.splitOffDOMTree(lastChild, endNode, true)); - } - - // Add fragment into passed in element - newElement.appendChild(fragment); - - if (lastChild.parentNode === rootNode) { - // If last child is in the root, insert newElement in front of it - rootNode.insertBefore(newElement, lastChild); - } else if (afterLast) { - // If last child was removed, but it had a sibling, insert in front of it - rootNode.insertBefore(newElement, afterLast); - } else { - // lastChild was removed and was the last actual element just append - rootNode.appendChild(newElement); - } - - return newElement.hasChildNodes(); - }, - - /* based on http://stackoverflow.com/a/6183069 */ - depthOfNode: function (inNode) { - var theDepth = 0, - node = inNode; - while (node.parentNode !== null) { - node = node.parentNode; - theDepth++; - } - return theDepth; - }, - - findCommonRoot: function (inNode1, inNode2) { - var depth1 = Util.depthOfNode(inNode1), - depth2 = Util.depthOfNode(inNode2), - node1 = inNode1, - node2 = inNode2; - - while (depth1 !== depth2) { - if (depth1 > depth2) { - node1 = node1.parentNode; - depth1 -= 1; - } else { - node2 = node2.parentNode; - depth2 -= 1; - } - } - - while (node1 !== node2) { - node1 = node1.parentNode; - node2 = node2.parentNode; - } - - return node1; - }, - /* END - based on http://stackoverflow.com/a/6183069 */ - - isElementAtBeginningOfBlock: function (node) { - var textVal, - sibling; - while (!Util.isBlockContainer(node) && !Util.isMediumEditorElement(node)) { - sibling = node; - while (sibling = sibling.previousSibling) { - textVal = sibling.nodeType === 3 ? sibling.nodeValue : sibling.textContent; - if (textVal.length > 0) { - return false; - } - } - node = node.parentNode; - } - return true; - }, - - isMediumEditorElement: function (element) { - return element && element.getAttribute && !!element.getAttribute('data-medium-editor-element'); - }, - - getContainerEditorElement: function (element) { - return Util.traverseUp(element, function (node) { - return Util.isMediumEditorElement(node); - }); - }, - - isBlockContainer: function (element) { - return element && element.nodeType !== 3 && Util.blockContainerElementNames.indexOf(element.nodeName.toLowerCase()) !== -1; - }, - - /* Finds the closest ancestor which is a block container element - * If element is within editor element but not within any other block element, - * the editor element is returned - */ - getClosestBlockContainer: function (node) { - return Util.traverseUp(node, function (node) { - return Util.isBlockContainer(node) || Util.isMediumEditorElement(node); - }); - }, - - /* Finds highest level ancestor element which is a block container element - * If element is within editor element but not within any other block element, - * the editor element is returned - */ - getTopBlockContainer: function (element) { - var topBlock = Util.isBlockContainer(element) ? element : false; - Util.traverseUp(element, function (el) { - if (Util.isBlockContainer(el)) { - topBlock = el; - } - if (!topBlock && Util.isMediumEditorElement(el)) { - topBlock = el; - return true; - } - return false; - }); - return topBlock; - }, - - getFirstSelectableLeafNode: function (element) { - while (element && element.firstChild) { - element = element.firstChild; - } - - // We don't want to set the selection to an element that can't have children, this messes up Gecko. - element = Util.traverseUp(element, function (el) { - return Util.emptyElementNames.indexOf(el.nodeName.toLowerCase()) === -1; - }); - // Selecting at the beginning of a table doesn't work in PhantomJS. - if (element.nodeName.toLowerCase() === 'table') { - var firstCell = element.querySelector('th, td'); - if (firstCell) { - element = firstCell; - } - } - return element; - }, - - // TODO: remove getFirstTextNode AND _getFirstTextNode when jumping in 6.0.0 (no code references) - getFirstTextNode: function (element) { - Util.warn('getFirstTextNode is deprecated and will be removed in version 6.0.0'); - return Util._getFirstTextNode(element); - }, - - _getFirstTextNode: function (element) { - if (element.nodeType === 3) { - return element; - } - - for (var i = 0; i < element.childNodes.length; i++) { - var textNode = Util._getFirstTextNode(element.childNodes[i]); - if (textNode !== null) { - return textNode; - } - } - return null; - }, - - ensureUrlHasProtocol: function (url) { - if (url.indexOf('://') === -1) { - return 'http://' + url; - } - return url; - }, - - warn: function () { - if (window.console !== undefined && typeof window.console.warn === 'function') { - window.console.warn.apply(window.console, arguments); - } - }, - - deprecated: function (oldName, newName, version) { - // simple deprecation warning mechanism. - var m = oldName + ' is deprecated, please use ' + newName + ' instead.'; - if (version) { - m += ' Will be removed in ' + version; - } - Util.warn(m); - }, - - deprecatedMethod: function (oldName, newName, args, version) { - // run the replacement and warn when someone calls a deprecated method - Util.deprecated(oldName, newName, version); - if (typeof this[newName] === 'function') { - this[newName].apply(this, args); - } - }, - - cleanupAttrs: function (el, attrs) { - attrs.forEach(function (attr) { - el.removeAttribute(attr); - }); - }, - - cleanupTags: function (el, tags) { - if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) { - el.parentNode.removeChild(el); - } - }, - - unwrapTags: function (el, tags) { - if (tags.indexOf(el.nodeName.toLowerCase()) !== -1) { - MediumEditor.util.unwrap(el, document); - } - }, - - // get the closest parent - getClosestTag: function (el, tag) { - return Util.traverseUp(el, function (element) { - return element.nodeName.toLowerCase() === tag.toLowerCase(); - }); - }, - - unwrap: function (el, doc) { - var fragment = doc.createDocumentFragment(), - nodes = Array.prototype.slice.call(el.childNodes); - - // cast nodeList to array since appending child - // to a different node will alter length of el.childNodes - for (var i = 0; i < nodes.length; i++) { - fragment.appendChild(nodes[i]); - } - - if (fragment.childNodes.length) { - el.parentNode.replaceChild(fragment, el); - } else { - el.parentNode.removeChild(el); - } - }, - - guid: function () { - function _s4() { - return Math - .floor((1 + Math.random()) * 0x10000) - .toString(16) - .substring(1); - } - - return _s4() + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + '-' + _s4() + _s4() + _s4(); - } - }; - - MediumEditor.util = Util; -}(window)); - -(function () { - 'use strict'; - - var Extension = function (options) { - MediumEditor.util.extend(this, options); - }; - - Extension.extend = function (protoProps) { - // magic extender thinger. mostly borrowed from backbone/goog.inherits - // place this function on some thing you want extend-able. - // - // example: - // - // function Thing(args){ - // this.options = args; - // } - // - // Thing.prototype = { foo: "bar" }; - // Thing.extend = extenderify; - // - // var ThingTwo = Thing.extend({ foo: "baz" }); - // - // var thingOne = new Thing(); // foo === "bar" - // var thingTwo = new ThingTwo(); // foo === "baz" - // - // which seems like some simply shallow copy nonsense - // at first, but a lot more is going on there. - // - // passing a `constructor` to the extend props - // will cause the instance to instantiate through that - // instead of the parent's constructor. - - var parent = this, - child; - - // The constructor function for the new subclass is either defined by you - // (the "constructor" property in your `extend` definition), or defaulted - // by us to simply call the parent's constructor. - - if (protoProps && protoProps.hasOwnProperty('constructor')) { - child = protoProps.constructor; - } else { - child = function () { - return parent.apply(this, arguments); - }; - } - - // das statics (.extend comes over, so your subclass can have subclasses too) - MediumEditor.util.extend(child, parent); - - // Set the prototype chain to inherit from `parent`, without calling - // `parent`'s constructor function. - var Surrogate = function () { - this.constructor = child; - }; - Surrogate.prototype = parent.prototype; - child.prototype = new Surrogate(); - - if (protoProps) { - MediumEditor.util.extend(child.prototype, protoProps); - } - - // todo: $super? - - return child; - }; - - Extension.prototype = { - /* init: [function] - * - * Called by MediumEditor during initialization. - * The .base property will already have been set to - * current instance of MediumEditor when this is called. - * All helper methods will exist as well - */ - init: function () {}, - - /* base: [MediumEditor instance] - * - * If not overriden, this will be set to the current instance - * of MediumEditor, before the init method is called - */ - base: undefined, - - /* name: [string] - * - * 'name' of the extension, used for retrieving the extension. - * If not set, MediumEditor will set this to be the key - * used when passing the extension into MediumEditor via the - * 'extensions' option - */ - name: undefined, - - /* checkState: [function (node)] - * - * If implemented, this function will be called one or more times - * the state of the editor & toolbar are updated. - * When the state is updated, the editor does the following: - * - * 1) Find the parent node containing the current selection - * 2) Call checkState on the extension, passing the node as an argument - * 3) Get the parent node of the previous node - * 4) Repeat steps #2 and #3 until we move outside the parent contenteditable - */ - checkState: undefined, - - /* destroy: [function ()] - * - * This method should remove any created html, custom event handlers - * or any other cleanup tasks that should be performed. - * If implemented, this function will be called when MediumEditor's - * destroy method has been called. - */ - destroy: undefined, - - /* As alternatives to checkState, these functions provide a more structured - * path to updating the state of an extension (usually a button) whenever - * the state of the editor & toolbar are updated. - */ - - /* queryCommandState: [function ()] - * - * If implemented, this function will be called once on each extension - * when the state of the editor/toolbar is being updated. - * - * If this function returns a non-null value, the extension will - * be ignored as the code climbs the dom tree. - * - * If this function returns true, and the setActive() function is defined - * setActive() will be called - */ - queryCommandState: undefined, - - /* isActive: [function ()] - * - * If implemented, this function will be called when MediumEditor - * has determined that this extension is 'active' for the current selection. - * This may be called when the editor & toolbar are being updated, - * but only if queryCommandState() or isAlreadyApplied() functions - * are implemented, and when called, return true. - */ - isActive: undefined, - - /* isAlreadyApplied: [function (node)] - * - * If implemented, this function is similar to checkState() in - * that it will be called repeatedly as MediumEditor moves up - * the DOM to update the editor & toolbar after a state change. - * - * NOTE: This function will NOT be called if checkState() has - * been implemented. This function will NOT be called if - * queryCommandState() is implemented and returns a non-null - * value when called - */ - isAlreadyApplied: undefined, - - /* setActive: [function ()] - * - * If implemented, this function is called when MediumEditor knows - * that this extension is currently enabled. Currently, this - * function is called when updating the editor & toolbar, and - * only if queryCommandState() or isAlreadyApplied(node) return - * true when called - */ - setActive: undefined, - - /* setInactive: [function ()] - * - * If implemented, this function is called when MediumEditor knows - * that this extension is currently disabled. Curently, this - * is called at the beginning of each state change for - * the editor & toolbar. After calling this, MediumEditor - * will attempt to update the extension, either via checkState() - * or the combination of queryCommandState(), isAlreadyApplied(node), - * isActive(), and setActive() - */ - setInactive: undefined, - - /* getInteractionElements: [function ()] - * - * If the extension renders any elements that the user can interact with, - * this method should be implemented and return the root element or an array - * containing all of the root elements. MediumEditor will call this function - * during interaction to see if the user clicked on something outside of the editor. - * The elements are used to check if the target element of a click or - * other user event is a descendant of any extension elements. - * This way, the editor can also count user interaction within editor elements as - * interactions with the editor, and thus not trigger 'blur' - */ - getInteractionElements: undefined, - - /************************ Helpers ************************ - * The following are helpers that are either set by MediumEditor - * during initialization, or are helper methods which either - * route calls to the MediumEditor instance or provide common - * functionality for all extensions - *********************************************************/ - - /* window: [Window] - * - * If not overriden, this will be set to the window object - * to be used by MediumEditor and its extensions. This is - * passed via the 'contentWindow' option to MediumEditor - * and is the global 'window' object by default - */ - 'window': undefined, - - /* document: [Document] - * - * If not overriden, this will be set to the document object - * to be used by MediumEditor and its extensions. This is - * passed via the 'ownerDocument' optin to MediumEditor - * and is the global 'document' object by default - */ - 'document': undefined, - - /* getEditorElements: [function ()] - * - * Helper function which returns an array containing - * all the contenteditable elements for this instance - * of MediumEditor - */ - getEditorElements: function () { - return this.base.elements; - }, - - /* getEditorId: [function ()] - * - * Helper function which returns a unique identifier - * for this instance of MediumEditor - */ - getEditorId: function () { - return this.base.id; - }, - - /* getEditorOptions: [function (option)] - * - * Helper function which returns the value of an option - * used to initialize this instance of MediumEditor - */ - getEditorOption: function (option) { - return this.base.options[option]; - } - }; - - /* List of method names to add to the prototype of Extension - * Each of these methods will be defined as helpers that - * just call directly into the MediumEditor instance. - * - * example for 'on' method: - * Extension.prototype.on = function () { - * return this.base.on.apply(this.base, arguments); - * } - */ - [ - // general helpers - 'execAction', - - // event handling - 'on', - 'off', - 'subscribe', - 'trigger' - - ].forEach(function (helper) { - Extension.prototype[helper] = function () { - return this.base[helper].apply(this.base, arguments); - }; - }); - - MediumEditor.Extension = Extension; -})(); - -(function () { - 'use strict'; - - function filterOnlyParentElements(node) { - if (MediumEditor.util.isBlockContainer(node)) { - return NodeFilter.FILTER_ACCEPT; - } else { - return NodeFilter.FILTER_SKIP; - } - } - - var Selection = { - findMatchingSelectionParent: function (testElementFunction, contentWindow) { - var selection = contentWindow.getSelection(), - range, - current; - - if (selection.rangeCount === 0) { - return false; - } - - range = selection.getRangeAt(0); - current = range.commonAncestorContainer; - - return MediumEditor.util.traverseUp(current, testElementFunction); - }, - - getSelectionElement: function (contentWindow) { - return this.findMatchingSelectionParent(function (el) { - return MediumEditor.util.isMediumEditorElement(el); - }, contentWindow); - }, - - // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html - // Tim Down - exportSelection: function (root, doc) { - if (!root) { - return null; - } - - var selectionState = null, - selection = doc.getSelection(); - - if (selection.rangeCount > 0) { - var range = selection.getRangeAt(0), - preSelectionRange = range.cloneRange(), - start; - - preSelectionRange.selectNodeContents(root); - preSelectionRange.setEnd(range.startContainer, range.startOffset); - start = preSelectionRange.toString().length; - - selectionState = { - start: start, - end: start + range.toString().length - }; - - // Check to see if the selection starts with any images - // if so we need to make sure the the beginning of the selection is - // set correctly when importing selection - if (this.doesRangeStartWithImages(range, doc)) { - selectionState.startsWithImage = true; - } - - // Check to see if the selection has any trailing images - // if so, this this means we need to look for them when we import selection - var trailingImageCount = this.getTrailingImageCount(root, selectionState, range.endContainer, range.endOffset); - if (trailingImageCount) { - selectionState.trailingImageCount = trailingImageCount; - } - - // If start = 0 there may still be an empty paragraph before it, but we don't care. - if (start !== 0) { - var emptyBlocksIndex = this.getIndexRelativeToAdjacentEmptyBlocks(doc, root, range.startContainer, range.startOffset); - if (emptyBlocksIndex !== -1) { - selectionState.emptyBlocksIndex = emptyBlocksIndex; - } - } - } - - return selectionState; - }, - - // http://stackoverflow.com/questions/17678843/cant-restore-selection-after-html-modify-even-if-its-the-same-html - // Tim Down - // - // {object} selectionState - the selection to import - // {DOMElement} root - the root element the selection is being restored inside of - // {Document} doc - the document to use for managing selection - // {boolean} [favorLaterSelectionAnchor] - defaults to false. If true, import the cursor immediately - // subsequent to an anchor tag if it would otherwise be placed right at the trailing edge inside the - // anchor. This cursor positioning, even though visually equivalent to the user, can affect behavior - // in MS IE. - importSelection: function (selectionState, root, doc, favorLaterSelectionAnchor) { - if (!selectionState || !root) { - return; - } - - var range = doc.createRange(); - range.setStart(root, 0); - range.collapse(true); - - var node = root, - nodeStack = [], - charIndex = 0, - foundStart = false, - foundEnd = false, - trailingImageCount = 0, - stop = false, - nextCharIndex, - allowRangeToStartAtEndOfNode = false, - lastTextNode = null; - - // When importing selection, the start of the selection may lie at the end of an element - // or at the beginning of an element. Since visually there is no difference between these 2 - // we will try to move the selection to the beginning of an element since this is generally - // what users will expect and it's a more predictable behavior. - // - // However, there are some specific cases when we don't want to do this: - // 1) We're attempting to move the cursor outside of the end of an anchor [favorLaterSelectionAnchor = true] - // 2) The selection starts with an image, which is special since an image doesn't have any 'content' - // as far as selection and ranges are concerned - // 3) The selection starts after a specified number of empty block elements (selectionState.emptyBlocksIndex) - // - // For these cases, we want the selection to start at a very specific location, so we should NOT - // automatically move the cursor to the beginning of the first actual chunk of text - if (favorLaterSelectionAnchor || selectionState.startsWithImage || typeof selectionState.emptyBlocksIndex !== 'undefined') { - allowRangeToStartAtEndOfNode = true; - } - - while (!stop && node) { - // Only iterate over elements and text nodes - if (node.nodeType > 3) { - node = nodeStack.pop(); - continue; - } - - // If we hit a text node, we need to add the amount of characters to the overall count - if (node.nodeType === 3 && !foundEnd) { - nextCharIndex = charIndex + node.length; - // Check if we're at or beyond the start of the selection we're importing - if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) { - // NOTE: We only want to allow a selection to start at the END of an element if - // allowRangeToStartAtEndOfNode is true - if (allowRangeToStartAtEndOfNode || selectionState.start < nextCharIndex) { - range.setStart(node, selectionState.start - charIndex); - foundStart = true; - } - // We're at the end of a text node where the selection could start but we shouldn't - // make the selection start here because allowRangeToStartAtEndOfNode is false. - // However, we should keep a reference to this node in case there aren't any more - // text nodes after this, so that we have somewhere to import the selection to - else { - lastTextNode = node; - } - } - // We've found the start of the selection, check if we're at or beyond the end of the selection we're importing - if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) { - if (!selectionState.trailingImageCount) { - range.setEnd(node, selectionState.end - charIndex); - stop = true; - } else { - foundEnd = true; - } - } - charIndex = nextCharIndex; - } else { - if (selectionState.trailingImageCount && foundEnd) { - if (node.nodeName.toLowerCase() === 'img') { - trailingImageCount++; - } - if (trailingImageCount === selectionState.trailingImageCount) { - // Find which index the image is in its parent's children - var endIndex = 0; - while (node.parentNode.childNodes[endIndex] !== node) { - endIndex++; - } - range.setEnd(node.parentNode, endIndex + 1); - stop = true; - } - } - - if (!stop && node.nodeType === 1) { - // this is an element - // add all its children to the stack - var i = node.childNodes.length - 1; - while (i >= 0) { - nodeStack.push(node.childNodes[i]); - i -= 1; - } - } - } - - if (!stop) { - node = nodeStack.pop(); - } - } - - // If we've gone through the entire text but didn't find the beginning of a text node - // to make the selection start at, we should fall back to starting the selection - // at the END of the last text node we found - if (!foundStart && lastTextNode) { - range.setStart(lastTextNode, lastTextNode.length); - range.setEnd(lastTextNode, lastTextNode.length); - } - - if (typeof selectionState.emptyBlocksIndex !== 'undefined') { - range = this.importSelectionMoveCursorPastBlocks(doc, root, selectionState.emptyBlocksIndex, range); - } - - // If the selection is right at the ending edge of a link, put it outside the anchor tag instead of inside. - if (favorLaterSelectionAnchor) { - range = this.importSelectionMoveCursorPastAnchor(selectionState, range); - } - - this.selectRange(doc, range); - }, - - // Utility method called from importSelection only - importSelectionMoveCursorPastAnchor: function (selectionState, range) { - var nodeInsideAnchorTagFunction = function (node) { - return node.nodeName.toLowerCase() === 'a'; - }; - if (selectionState.start === selectionState.end && - range.startContainer.nodeType === 3 && - range.startOffset === range.startContainer.nodeValue.length && - MediumEditor.util.traverseUp(range.startContainer, nodeInsideAnchorTagFunction)) { - var prevNode = range.startContainer, - currentNode = range.startContainer.parentNode; - while (currentNode !== null && currentNode.nodeName.toLowerCase() !== 'a') { - if (currentNode.childNodes[currentNode.childNodes.length - 1] !== prevNode) { - currentNode = null; - } else { - prevNode = currentNode; - currentNode = currentNode.parentNode; - } - } - if (currentNode !== null && currentNode.nodeName.toLowerCase() === 'a') { - var currentNodeIndex = null; - for (var i = 0; currentNodeIndex === null && i < currentNode.parentNode.childNodes.length; i++) { - if (currentNode.parentNode.childNodes[i] === currentNode) { - currentNodeIndex = i; - } - } - range.setStart(currentNode.parentNode, currentNodeIndex + 1); - range.collapse(true); - } - } - return range; - }, - - // Uses the emptyBlocksIndex calculated by getIndexRelativeToAdjacentEmptyBlocks - // to move the cursor back to the start of the correct paragraph - importSelectionMoveCursorPastBlocks: function (doc, root, index, range) { - var treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false), - startContainer = range.startContainer, - startBlock, - targetNode, - currIndex = 0; - index = index || 1; // If index is 0, we still want to move to the next block - - // Chrome counts newlines and spaces that separate block elements as actual elements. - // If the selection is inside one of these text nodes, and it has a previous sibling - // which is a block element, we want the treewalker to start at the previous sibling - // and NOT at the parent of the textnode - if (startContainer.nodeType === 3 && MediumEditor.util.isBlockContainer(startContainer.previousSibling)) { - startBlock = startContainer.previousSibling; - } else { - startBlock = MediumEditor.util.getClosestBlockContainer(startContainer); - } - - // Skip over empty blocks until we hit the block we want the selection to be in - while (treeWalker.nextNode()) { - if (!targetNode) { - // Loop through all blocks until we hit the starting block element - if (startBlock === treeWalker.currentNode) { - targetNode = treeWalker.currentNode; - } - } else { - targetNode = treeWalker.currentNode; - currIndex++; - // We hit the target index, bail - if (currIndex === index) { - break; - } - // If we find a non-empty block, ignore the emptyBlocksIndex and just put selection here - if (targetNode.textContent.length > 0) { - break; - } - } - } - - if (!targetNode) { - targetNode = startBlock; - } - - // We're selecting a high-level block node, so make sure the cursor gets moved into the deepest - // element at the beginning of the block - range.setStart(MediumEditor.util.getFirstSelectableLeafNode(targetNode), 0); - - return range; - }, - - // Returns -1 unless the cursor is at the beginning of a paragraph/block - // If the paragraph/block is preceeded by empty paragraphs/block (with no text) - // it will return the number of empty paragraphs before the cursor. - // Otherwise, it will return 0, which indicates the cursor is at the beginning - // of a paragraph/block, and not at the end of the paragraph/block before it - getIndexRelativeToAdjacentEmptyBlocks: function (doc, root, cursorContainer, cursorOffset) { - // If there is text in front of the cursor, that means there isn't only empty blocks before it - if (cursorContainer.textContent.length > 0 && cursorOffset > 0) { - return -1; - } - - // Check if the block that contains the cursor has any other text in front of the cursor - var node = cursorContainer; - if (node.nodeType !== 3) { - node = cursorContainer.childNodes[cursorOffset]; - } - if (node) { - // The element isn't at the beginning of a block, so it has content before it - if (!MediumEditor.util.isElementAtBeginningOfBlock(node)) { - return -1; - } - - var previousSibling = MediumEditor.util.findPreviousSibling(node); - // If there is no previous sibling, this is the first text element in the editor - if (!previousSibling) { - return -1; - } - // If the previous sibling has text, then there are no empty blocks before this - else if (previousSibling.nodeValue) { - return -1; - } - } - - // Walk over block elements, counting number of empty blocks between last piece of text - // and the block the cursor is in - var closestBlock = MediumEditor.util.getClosestBlockContainer(cursorContainer), - treeWalker = doc.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, filterOnlyParentElements, false), - emptyBlocksCount = 0; - while (treeWalker.nextNode()) { - var blockIsEmpty = treeWalker.currentNode.textContent === ''; - if (blockIsEmpty || emptyBlocksCount > 0) { - emptyBlocksCount += 1; - } - if (treeWalker.currentNode === closestBlock) { - return emptyBlocksCount; - } - if (!blockIsEmpty) { - emptyBlocksCount = 0; - } - } - - return emptyBlocksCount; - }, - - // Returns true if the selection range begins with an image tag - // Returns false if the range starts with any non empty text nodes - doesRangeStartWithImages: function (range, doc) { - if (range.startOffset !== 0 || range.startContainer.nodeType !== 1) { - return false; - } - - if (range.startContainer.nodeName.toLowerCase() === 'img') { - return true; - } - - var img = range.startContainer.querySelector('img'); - if (!img) { - return false; - } - - var treeWalker = doc.createTreeWalker(range.startContainer, NodeFilter.SHOW_ALL, null, false); - while (treeWalker.nextNode()) { - var next = treeWalker.currentNode; - // If we hit the image, then there isn't any text before the image so - // the image is at the beginning of the range - if (next === img) { - break; - } - // If we haven't hit the iamge, but found text that contains content - // then the range doesn't start with an image - if (next.nodeValue) { - return false; - } - } - - return true; - }, - - getTrailingImageCount: function (root, selectionState, endContainer, endOffset) { - // If the endOffset of a range is 0, the endContainer doesn't contain images - // If the endContainer is a text node, there are no trailing images - if (endOffset === 0 || endContainer.nodeType !== 1) { - return 0; - } - - // If the endContainer isn't an image, and doesn't have an image descendants - // there are no trailing images - if (endContainer.nodeName.toLowerCase() !== 'img' && !endContainer.querySelector('img')) { - return 0; - } - - var lastNode = endContainer.childNodes[endOffset - 1]; - while (lastNode.hasChildNodes()) { - lastNode = lastNode.lastChild; - } - - var node = root, - nodeStack = [], - charIndex = 0, - foundStart = false, - foundEnd = false, - stop = false, - nextCharIndex, - trailingImages = 0; - - while (!stop && node) { - // Only iterate over elements and text nodes - if (node.nodeType > 3) { - node = nodeStack.pop(); - continue; - } - - if (node.nodeType === 3 && !foundEnd) { - trailingImages = 0; - nextCharIndex = charIndex + node.length; - if (!foundStart && selectionState.start >= charIndex && selectionState.start <= nextCharIndex) { - foundStart = true; - } - if (foundStart && selectionState.end >= charIndex && selectionState.end <= nextCharIndex) { - foundEnd = true; - } - charIndex = nextCharIndex; - } else { - if (node.nodeName.toLowerCase() === 'img') { - trailingImages++; - } - - if (node === lastNode) { - stop = true; - } else if (node.nodeType === 1) { - // this is an element - // add all its children to the stack - var i = node.childNodes.length - 1; - while (i >= 0) { - nodeStack.push(node.childNodes[i]); - i -= 1; - } - } - } - - if (!stop) { - node = nodeStack.pop(); - } - } - - return trailingImages; - }, - - // determine if the current selection contains any 'content' - // content being any non-white space text or an image - selectionContainsContent: function (doc) { - var sel = doc.getSelection(); - - // collapsed selection or selection withour range doesn't contain content - if (!sel || sel.isCollapsed || !sel.rangeCount) { - return false; - } - - // if toString() contains any text, the selection contains some content - if (sel.toString().trim() !== '') { - return true; - } - - // if selection contains only image(s), it will return empty for toString() - // so check for an image manually - var selectionNode = this.getSelectedParentElement(sel.getRangeAt(0)); - if (selectionNode) { - if (selectionNode.nodeName.toLowerCase() === 'img' || - (selectionNode.nodeType === 1 && selectionNode.querySelector('img'))) { - return true; - } - } - - return false; - }, - - selectionInContentEditableFalse: function (contentWindow) { - // determine if the current selection is exclusively inside - // a contenteditable="false", though treat the case of an - // explicit contenteditable="true" inside a "false" as false. - var sawtrue, - sawfalse = this.findMatchingSelectionParent(function (el) { - var ce = el && el.getAttribute('contenteditable'); - if (ce === 'true') { - sawtrue = true; - } - return el.nodeName !== '#text' && ce === 'false'; - }, contentWindow); - - return !sawtrue && sawfalse; - }, - - // http://stackoverflow.com/questions/4176923/html-of-selected-text - // by Tim Down - getSelectionHtml: function getSelectionHtml(doc) { - var i, - html = '', - sel = doc.getSelection(), - len, - container; - if (sel.rangeCount) { - container = doc.createElement('div'); - for (i = 0, len = sel.rangeCount; i < len; i += 1) { - container.appendChild(sel.getRangeAt(i).cloneContents()); - } - html = container.innerHTML; - } - return html; - }, - - /** - * Find the caret position within an element irrespective of any inline tags it may contain. - * - * @param {DOMElement} An element containing the cursor to find offsets relative to. - * @param {Range} A Range representing cursor position. Will window.getSelection if none is passed. - * @return {Object} 'left' and 'right' attributes contain offsets from begining and end of Element - */ - getCaretOffsets: function getCaretOffsets(element, range) { - var preCaretRange, postCaretRange; - - if (!range) { - range = window.getSelection().getRangeAt(0); - } - - preCaretRange = range.cloneRange(); - postCaretRange = range.cloneRange(); - - preCaretRange.selectNodeContents(element); - preCaretRange.setEnd(range.endContainer, range.endOffset); - - postCaretRange.selectNodeContents(element); - postCaretRange.setStart(range.endContainer, range.endOffset); - - return { - left: preCaretRange.toString().length, - right: postCaretRange.toString().length - }; - }, - - // http://stackoverflow.com/questions/15867542/range-object-get-selection-parent-node-chrome-vs-firefox - rangeSelectsSingleNode: function (range) { - var startNode = range.startContainer; - return startNode === range.endContainer && - startNode.hasChildNodes() && - range.endOffset === range.startOffset + 1; - }, - - getSelectedParentElement: function (range) { - if (!range) { - return null; - } - - // Selection encompasses a single element - if (this.rangeSelectsSingleNode(range) && range.startContainer.childNodes[range.startOffset].nodeType !== 3) { - return range.startContainer.childNodes[range.startOffset]; - } - - // Selection range starts inside a text node, so get its parent - if (range.startContainer.nodeType === 3) { - return range.startContainer.parentNode; - } - - // Selection starts inside an element - return range.startContainer; - }, - - getSelectedElements: function (doc) { - var selection = doc.getSelection(), - range, - toRet, - currNode; - - if (!selection.rangeCount || selection.isCollapsed || !selection.getRangeAt(0).commonAncestorContainer) { - return []; - } - - range = selection.getRangeAt(0); - - if (range.commonAncestorContainer.nodeType === 3) { - toRet = []; - currNode = range.commonAncestorContainer; - while (currNode.parentNode && currNode.parentNode.childNodes.length === 1) { - toRet.push(currNode.parentNode); - currNode = currNode.parentNode; - } - - return toRet; - } - - return [].filter.call(range.commonAncestorContainer.getElementsByTagName('*'), function (el) { - return (typeof selection.containsNode === 'function') ? selection.containsNode(el, true) : true; - }); - }, - - selectNode: function (node, doc) { - var range = doc.createRange(); - range.selectNodeContents(node); - this.selectRange(doc, range); - }, - - select: function (doc, startNode, startOffset, endNode, endOffset) { - var range = doc.createRange(); - range.setStart(startNode, startOffset); - if (endNode) { - range.setEnd(endNode, endOffset); - } else { - range.collapse(true); - } - this.selectRange(doc, range); - return range; - }, - - /** - * Clear the current highlighted selection and set the caret to the start or the end of that prior selection, defaults to end. - * - * @param {DomDocument} doc Current document - * @param {boolean} moveCursorToStart A boolean representing whether or not to set the caret to the beginning of the prior selection. - */ - clearSelection: function (doc, moveCursorToStart) { - if (moveCursorToStart) { - doc.getSelection().collapseToStart(); - } else { - doc.getSelection().collapseToEnd(); - } - }, - - /** - * Move cursor to the given node with the given offset. - * - * @param {DomDocument} doc Current document - * @param {DomElement} node Element where to jump - * @param {integer} offset Where in the element should we jump, 0 by default - */ - moveCursor: function (doc, node, offset) { - this.select(doc, node, offset); - }, - - getSelectionRange: function (ownerDocument) { - var selection = ownerDocument.getSelection(); - if (selection.rangeCount === 0) { - return null; - } - return selection.getRangeAt(0); - }, - - selectRange: function (ownerDocument, range) { - var selection = ownerDocument.getSelection(); - - selection.removeAllRanges(); - selection.addRange(range); - }, - - // http://stackoverflow.com/questions/1197401/how-can-i-get-the-element-the-caret-is-in-with-javascript-when-using-contentedi - // by You - getSelectionStart: function (ownerDocument) { - var node = ownerDocument.getSelection().anchorNode, - startNode = (node && node.nodeType === 3 ? node.parentNode : node); - - return startNode; - } - }; - - MediumEditor.selection = Selection; -}()); - -(function () { - 'use strict'; - - function isElementDescendantOfExtension(extensions, element) { - return extensions.some(function (extension) { - if (typeof extension.getInteractionElements !== 'function') { - return false; - } - - var extensionElements = extension.getInteractionElements(); - if (!extensionElements) { - return false; - } - - if (!Array.isArray(extensionElements)) { - extensionElements = [extensionElements]; - } - return extensionElements.some(function (el) { - return MediumEditor.util.isDescendant(el, element, true); - }); - }); - } - - var Events = function (instance) { - this.base = instance; - this.options = this.base.options; - this.events = []; - this.disabledEvents = {}; - this.customEvents = {}; - this.listeners = {}; - }; - - Events.prototype = { - InputEventOnContenteditableSupported: !MediumEditor.util.isIE && !MediumEditor.util.isEdge, - - // Helpers for event handling - - attachDOMEvent: function (targets, event, listener, useCapture) { - var win = this.base.options.contentWindow, - doc = this.base.options.ownerDocument; - - targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets; - - Array.prototype.forEach.call(targets, function (target) { - target.addEventListener(event, listener, useCapture); - this.events.push([target, event, listener, useCapture]); - }.bind(this)); - }, - - detachDOMEvent: function (targets, event, listener, useCapture) { - var index, e, - win = this.base.options.contentWindow, - doc = this.base.options.ownerDocument; - - if (targets !== null) { - targets = MediumEditor.util.isElement(targets) || [win, doc].indexOf(targets) > -1 ? [targets] : targets; - - Array.prototype.forEach.call(targets, function (target) { - index = this.indexOfListener(target, event, listener, useCapture); - if (index !== -1) { - e = this.events.splice(index, 1)[0]; - e[0].removeEventListener(e[1], e[2], e[3]); - } - }.bind(this)); - } - }, - - indexOfListener: function (target, event, listener, useCapture) { - var i, n, item; - for (i = 0, n = this.events.length; i < n; i = i + 1) { - item = this.events[i]; - if (item[0] === target && item[1] === event && item[2] === listener && item[3] === useCapture) { - return i; - } - } - return -1; - }, - - detachAllDOMEvents: function () { - var e = this.events.pop(); - while (e) { - e[0].removeEventListener(e[1], e[2], e[3]); - e = this.events.pop(); - } - }, - - detachAllEventsFromElement: function (element) { - var filtered = this.events.filter(function (e) { - return e && e[0].getAttribute && e[0].getAttribute('medium-editor-index') === element.getAttribute('medium-editor-index'); - }); - - for (var i = 0, len = filtered.length; i < len; i++) { - var e = filtered[i]; - this.detachDOMEvent(e[0], e[1], e[2], e[3]); - } - }, - - // Attach all existing handlers to a new element - attachAllEventsToElement: function (element) { - if (this.listeners['editableInput']) { - this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML; - } - - if (this.eventsCache) { - this.eventsCache.forEach(function (e) { - this.attachDOMEvent(element, e['name'], e['handler'].bind(this)); - }, this); - } - }, - - enableCustomEvent: function (event) { - if (this.disabledEvents[event] !== undefined) { - delete this.disabledEvents[event]; - } - }, - - disableCustomEvent: function (event) { - this.disabledEvents[event] = true; - }, - - // custom events - attachCustomEvent: function (event, listener) { - this.setupListener(event); - if (!this.customEvents[event]) { - this.customEvents[event] = []; - } - this.customEvents[event].push(listener); - }, - - detachCustomEvent: function (event, listener) { - var index = this.indexOfCustomListener(event, listener); - if (index !== -1) { - this.customEvents[event].splice(index, 1); - // TODO: If array is empty, should detach internal listeners via destroyListener() - } - }, - - indexOfCustomListener: function (event, listener) { - if (!this.customEvents[event] || !this.customEvents[event].length) { - return -1; - } - - return this.customEvents[event].indexOf(listener); - }, - - detachAllCustomEvents: function () { - this.customEvents = {}; - // TODO: Should detach internal listeners here via destroyListener() - }, - - triggerCustomEvent: function (name, data, editable) { - if (this.customEvents[name] && !this.disabledEvents[name]) { - this.customEvents[name].forEach(function (listener) { - listener(data, editable); - }); - } - }, - - // Cleaning up - - destroy: function () { - this.detachAllDOMEvents(); - this.detachAllCustomEvents(); - this.detachExecCommand(); - - if (this.base.elements) { - this.base.elements.forEach(function (element) { - element.removeAttribute('data-medium-focused'); - }); - } - }, - - // Listening to calls to document.execCommand - - // Attach a listener to be notified when document.execCommand is called - attachToExecCommand: function () { - if (this.execCommandListener) { - return; - } - - // Store an instance of the listener so: - // 1) We only attach to execCommand once - // 2) We can remove the listener later - this.execCommandListener = function (execInfo) { - this.handleDocumentExecCommand(execInfo); - }.bind(this); - - // Ensure that execCommand has been wrapped correctly - this.wrapExecCommand(); - - // Add listener to list of execCommand listeners - this.options.ownerDocument.execCommand.listeners.push(this.execCommandListener); - }, - - // Remove our listener for calls to document.execCommand - detachExecCommand: function () { - var doc = this.options.ownerDocument; - if (!this.execCommandListener || !doc.execCommand.listeners) { - return; - } - - // Find the index of this listener in the array of listeners so it can be removed - var index = doc.execCommand.listeners.indexOf(this.execCommandListener); - if (index !== -1) { - doc.execCommand.listeners.splice(index, 1); - } - - // If the list of listeners is now empty, put execCommand back to its original state - if (!doc.execCommand.listeners.length) { - this.unwrapExecCommand(); - } - }, - - // Wrap document.execCommand in a custom method so we can listen to calls to it - wrapExecCommand: function () { - var doc = this.options.ownerDocument; - - // Ensure all instance of MediumEditor only wrap execCommand once - if (doc.execCommand.listeners) { - return; - } - - // Helper method to call all listeners to execCommand - var callListeners = function (args, result) { - if (doc.execCommand.listeners) { - doc.execCommand.listeners.forEach(function (listener) { - listener({ - command: args[0], - value: args[2], - args: args, - result: result - }); - }); - } - }, - - // Create a wrapper method for execCommand which will: - // 1) Call document.execCommand with the correct arguments - // 2) Loop through any listeners and notify them that execCommand was called - // passing extra info on the call - // 3) Return the result - wrapper = function () { - var result = doc.execCommand.orig.apply(this, arguments); - - if (!doc.execCommand.listeners) { - return result; - } - - var args = Array.prototype.slice.call(arguments); - callListeners(args, result); - - return result; - }; - - // Store a reference to the original execCommand - wrapper.orig = doc.execCommand; - - // Attach an array for storing listeners - wrapper.listeners = []; - - // Helper for notifying listeners - wrapper.callListeners = callListeners; - - // Overwrite execCommand - doc.execCommand = wrapper; - }, - - // Revert document.execCommand back to its original self - unwrapExecCommand: function () { - var doc = this.options.ownerDocument; - if (!doc.execCommand.orig) { - return; - } - - // Use the reference to the original execCommand to revert back - doc.execCommand = doc.execCommand.orig; - }, - - // Listening to browser events to emit events medium-editor cares about - setupListener: function (name) { - if (this.listeners[name]) { - return; - } - - switch (name) { - case 'externalInteraction': - // Detecting when user has interacted with elements outside of MediumEditor - this.attachDOMEvent(this.options.ownerDocument.body, 'mousedown', this.handleBodyMousedown.bind(this), true); - this.attachDOMEvent(this.options.ownerDocument.body, 'click', this.handleBodyClick.bind(this), true); - this.attachDOMEvent(this.options.ownerDocument.body, 'focus', this.handleBodyFocus.bind(this), true); - break; - case 'blur': - // Detecting when focus is lost - this.setupListener('externalInteraction'); - break; - case 'focus': - // Detecting when focus moves into some part of MediumEditor - this.setupListener('externalInteraction'); - break; - case 'editableInput': - // setup cache for knowing when the content has changed - this.contentCache = {}; - this.base.elements.forEach(function (element) { - this.contentCache[element.getAttribute('medium-editor-index')] = element.innerHTML; - }, this); - - // Attach to the 'oninput' event, handled correctly by most browsers - if (this.InputEventOnContenteditableSupported) { - this.attachToEachElement('input', this.handleInput); - } - - // For browsers which don't support the input event on contenteditable (IE) - // we'll attach to 'selectionchange' on the document and 'keypress' on the editables - if (!this.InputEventOnContenteditableSupported) { - this.setupListener('editableKeypress'); - this.keypressUpdateInput = true; - this.attachDOMEvent(document, 'selectionchange', this.handleDocumentSelectionChange.bind(this)); - // Listen to calls to execCommand - this.attachToExecCommand(); - } - break; - case 'editableClick': - // Detecting click in the contenteditables - this.attachToEachElement('click', this.handleClick); - break; - case 'editableBlur': - // Detecting blur in the contenteditables - this.attachToEachElement('blur', this.handleBlur); - break; - case 'editableKeypress': - // Detecting keypress in the contenteditables - this.attachToEachElement('keypress', this.handleKeypress); - break; - case 'editableKeyup': - // Detecting keyup in the contenteditables - this.attachToEachElement('keyup', this.handleKeyup); - break; - case 'editableKeydown': - // Detecting keydown on the contenteditables - this.attachToEachElement('keydown', this.handleKeydown); - break; - case 'editableKeydownSpace': - // Detecting keydown for SPACE on the contenteditables - this.setupListener('editableKeydown'); - break; - case 'editableKeydownEnter': - // Detecting keydown for ENTER on the contenteditables - this.setupListener('editableKeydown'); - break; - case 'editableKeydownTab': - // Detecting keydown for TAB on the contenteditable - this.setupListener('editableKeydown'); - break; - case 'editableKeydownDelete': - // Detecting keydown for DELETE/BACKSPACE on the contenteditables - this.setupListener('editableKeydown'); - break; - case 'editableMouseover': - // Detecting mouseover on the contenteditables - this.attachToEachElement('mouseover', this.handleMouseover); - break; - case 'editableDrag': - // Detecting dragover and dragleave on the contenteditables - this.attachToEachElement('dragover', this.handleDragging); - this.attachToEachElement('dragleave', this.handleDragging); - break; - case 'editableDrop': - // Detecting drop on the contenteditables - this.attachToEachElement('drop', this.handleDrop); - break; - // TODO: We need to have a custom 'paste' event separate from 'editablePaste' - // Need to think about the way to introduce this without breaking folks - case 'editablePaste': - // Detecting paste on the contenteditables - this.attachToEachElement('paste', this.handlePaste); - break; - } - this.listeners[name] = true; - }, - - attachToEachElement: function (name, handler) { - // build our internal cache to know which element got already what handler attached - if (!this.eventsCache) { - this.eventsCache = []; - } - - this.base.elements.forEach(function (element) { - this.attachDOMEvent(element, name, handler.bind(this)); - }, this); - - this.eventsCache.push({ 'name': name, 'handler': handler }); - }, - - cleanupElement: function (element) { - var index = element.getAttribute('medium-editor-index'); - if (index) { - this.detachAllEventsFromElement(element); - if (this.contentCache) { - delete this.contentCache[index]; - } - } - }, - - focusElement: function (element) { - element.focus(); - this.updateFocus(element, { target: element, type: 'focus' }); - }, - - updateFocus: function (target, eventObj) { - var hadFocus = this.base.getFocusedElement(), - toFocus; - - // For clicks, we need to know if the mousedown that caused the click happened inside the existing focused element - // or one of the extension elements. If so, we don't want to focus another element - if (hadFocus && - eventObj.type === 'click' && - this.lastMousedownTarget && - (MediumEditor.util.isDescendant(hadFocus, this.lastMousedownTarget, true) || - isElementDescendantOfExtension(this.base.extensions, this.lastMousedownTarget))) { - toFocus = hadFocus; - } - - if (!toFocus) { - this.base.elements.some(function (element) { - // If the target is part of an editor element, this is the element getting focus - if (!toFocus && (MediumEditor.util.isDescendant(element, target, true))) { - toFocus = element; - } - - // bail if we found an element that's getting focus - return !!toFocus; - }, this); - } - - // Check if the target is external (not part of the editor, toolbar, or any other extension) - var externalEvent = !MediumEditor.util.isDescendant(hadFocus, target, true) && - !isElementDescendantOfExtension(this.base.extensions, target); - - if (toFocus !== hadFocus) { - // If element has focus, and focus is going outside of editor - // Don't blur focused element if clicking on editor, toolbar, or anchorpreview - if (hadFocus && externalEvent) { - // Trigger blur on the editable that has lost focus - hadFocus.removeAttribute('data-medium-focused'); - this.triggerCustomEvent('blur', eventObj, hadFocus); - } - - // If focus is going into an editor element - if (toFocus) { - // Trigger focus on the editable that now has focus - toFocus.setAttribute('data-medium-focused', true); - this.triggerCustomEvent('focus', eventObj, toFocus); - } - } - - if (externalEvent) { - this.triggerCustomEvent('externalInteraction', eventObj); - } - }, - - updateInput: function (target, eventObj) { - if (!this.contentCache) { - return; - } - // An event triggered which signifies that the user may have changed someting - // Look in our cache of input for the contenteditables to see if something changed - var index = target.getAttribute('medium-editor-index'), - html = target.innerHTML; - - if (html !== this.contentCache[index]) { - // The content has changed since the last time we checked, fire the event - this.triggerCustomEvent('editableInput', eventObj, target); - } - this.contentCache[index] = html; - }, - - handleDocumentSelectionChange: function (event) { - // When selectionchange fires, target and current target are set - // to document, since this is where the event is handled - // However, currentTarget will have an 'activeElement' property - // which will point to whatever element has focus. - if (event.currentTarget && event.currentTarget.activeElement) { - var activeElement = event.currentTarget.activeElement, - currentTarget; - // We can look at the 'activeElement' to determine if the selectionchange has - // happened within a contenteditable owned by this instance of MediumEditor - this.base.elements.some(function (element) { - if (MediumEditor.util.isDescendant(element, activeElement, true)) { - currentTarget = element; - return true; - } - return false; - }, this); - - // We know selectionchange fired within one of our contenteditables - if (currentTarget) { - this.updateInput(currentTarget, { target: activeElement, currentTarget: currentTarget }); - } - } - }, - - handleDocumentExecCommand: function () { - // document.execCommand has been called - // If one of our contenteditables currently has focus, we should - // attempt to trigger the 'editableInput' event - var target = this.base.getFocusedElement(); - if (target) { - this.updateInput(target, { target: target, currentTarget: target }); - } - }, - - handleBodyClick: function (event) { - this.updateFocus(event.target, event); - }, - - handleBodyFocus: function (event) { - this.updateFocus(event.target, event); - }, - - handleBodyMousedown: function (event) { - this.lastMousedownTarget = event.target; - }, - - handleInput: function (event) { - this.updateInput(event.currentTarget, event); - }, - - handleClick: function (event) { - this.triggerCustomEvent('editableClick', event, event.currentTarget); - }, - - handleBlur: function (event) { - this.triggerCustomEvent('editableBlur', event, event.currentTarget); - }, - - handleKeypress: function (event) { - this.triggerCustomEvent('editableKeypress', event, event.currentTarget); - - // If we're doing manual detection of the editableInput event we need - // to check for input changes during 'keypress' - if (this.keypressUpdateInput) { - var eventObj = { target: event.target, currentTarget: event.currentTarget }; - - // In IE, we need to let the rest of the event stack complete before we detect - // changes to input, so using setTimeout here - setTimeout(function () { - this.updateInput(eventObj.currentTarget, eventObj); - }.bind(this), 0); - } - }, - - handleKeyup: function (event) { - this.triggerCustomEvent('editableKeyup', event, event.currentTarget); - }, - - handleMouseover: function (event) { - this.triggerCustomEvent('editableMouseover', event, event.currentTarget); - }, - - handleDragging: function (event) { - this.triggerCustomEvent('editableDrag', event, event.currentTarget); - }, - - handleDrop: function (event) { - this.triggerCustomEvent('editableDrop', event, event.currentTarget); - }, - - handlePaste: function (event) { - this.triggerCustomEvent('editablePaste', event, event.currentTarget); - }, - - handleKeydown: function (event) { - - this.triggerCustomEvent('editableKeydown', event, event.currentTarget); - - if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.SPACE)) { - return this.triggerCustomEvent('editableKeydownSpace', event, event.currentTarget); - } - - if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ENTER) || (event.ctrlKey && MediumEditor.util.isKey(event, MediumEditor.util.keyCode.M))) { - return this.triggerCustomEvent('editableKeydownEnter', event, event.currentTarget); - } - - if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.TAB)) { - return this.triggerCustomEvent('editableKeydownTab', event, event.currentTarget); - } - - if (MediumEditor.util.isKey(event, [MediumEditor.util.keyCode.DELETE, MediumEditor.util.keyCode.BACKSPACE])) { - return this.triggerCustomEvent('editableKeydownDelete', event, event.currentTarget); - } - } - }; - - MediumEditor.Events = Events; -}()); - -(function () { - 'use strict'; - - var Button = MediumEditor.Extension.extend({ - - /* Button Options */ - - /* action: [string] - * The action argument to pass to MediumEditor.execAction() - * when the button is clicked - */ - action: undefined, - - /* aria: [string] - * The value to add as the aria-label attribute of the button - * element displayed in the toolbar. - * This is also used as the tooltip for the button - */ - aria: undefined, - - /* tagNames: [Array] - * NOTE: This is not used if useQueryState is set to true. - * - * Array of element tag names that would indicate that this - * button has already been applied. If this action has already - * been applied, the button will be displayed as 'active' in the toolbar - * - * Example: - * For 'bold', if the text is ever within a or - * tag that indicates the text is already bold. So the array - * of tagNames for bold would be: ['b', 'strong'] - */ - tagNames: undefined, - - /* style: [Object] - * NOTE: This is not used if useQueryState is set to true. - * - * A pair of css property & value(s) that indicate that this - * button has already been applied. If this action has already - * been applied, the button will be displayed as 'active' in the toolbar - * Properties of the object: - * prop [String]: name of the css property - * value [String]: value(s) of the css property - * multiple values can be separated by a '|' - * - * Example: - * For 'bold', if the text is ever within an element with a 'font-weight' - * style property set to '700' or 'bold', that indicates the text - * is already bold. So the style object for bold would be: - * { prop: 'font-weight', value: '700|bold' } - */ - style: undefined, - - /* useQueryState: [boolean] - * Enables/disables whether this button should use the built-in - * document.queryCommandState() method to determine whether - * the action has already been applied. If the action has already - * been applied, the button will be displayed as 'active' in the toolbar - * - * Example: - * For 'bold', if this is set to true, the code will call: - * document.queryCommandState('bold') which will return true if the - * browser thinks the text is already bold, and false otherwise - */ - useQueryState: undefined, - - /* contentDefault: [string] - * Default innerHTML to put inside the button - */ - contentDefault: undefined, - - /* contentFA: [string] - * The innerHTML to use for the content of the button - * if the `buttonLabels` option for MediumEditor is set to 'fontawesome' - */ - contentFA: undefined, - - /* classList: [Array] - * An array of classNames (strings) to be added to the button - */ - classList: undefined, - - /* attrs: [object] - * A set of key-value pairs to add to the button as custom attributes - */ - attrs: undefined, - - // The button constructor can optionally accept the name of a built-in button - // (ie 'bold', 'italic', etc.) - // When the name of a button is passed, it will initialize itself with the - // configuration for that button - constructor: function (options) { - if (Button.isBuiltInButton(options)) { - MediumEditor.Extension.call(this, this.defaults[options]); - } else { - MediumEditor.Extension.call(this, options); - } - }, - - init: function () { - MediumEditor.Extension.prototype.init.apply(this, arguments); - - this.button = this.createButton(); - this.on(this.button, 'click', this.handleClick.bind(this)); - }, - - /* getButton: [function ()] - * - * If implemented, this function will be called when - * the toolbar is being created. The DOM Element returned - * by this function will be appended to the toolbar along - * with any other buttons. - */ - getButton: function () { - return this.button; - }, - - getAction: function () { - return (typeof this.action === 'function') ? this.action(this.base.options) : this.action; - }, - - getAria: function () { - return (typeof this.aria === 'function') ? this.aria(this.base.options) : this.aria; - }, - - getTagNames: function () { - return (typeof this.tagNames === 'function') ? this.tagNames(this.base.options) : this.tagNames; - }, - - createButton: function () { - var button = this.document.createElement('button'), - content = this.contentDefault, - ariaLabel = this.getAria(), - buttonLabels = this.getEditorOption('buttonLabels'); - // Add class names - button.classList.add('medium-editor-action'); - button.classList.add('medium-editor-action-' + this.name); - if (this.classList) { - this.classList.forEach(function (className) { - button.classList.add(className); - }); - } - - // Add attributes - button.setAttribute('data-action', this.getAction()); - if (ariaLabel) { - button.setAttribute('title', ariaLabel); - button.setAttribute('aria-label', ariaLabel); - } - if (this.attrs) { - Object.keys(this.attrs).forEach(function (attr) { - button.setAttribute(attr, this.attrs[attr]); - }, this); - } - - if (buttonLabels === 'fontawesome' && this.contentFA) { - content = this.contentFA; - } - button.innerHTML = content; - return button; - }, - - handleClick: function (event) { - event.preventDefault(); - event.stopPropagation(); - - var action = this.getAction(); - - if (action) { - this.execAction(action); - } - }, - - isActive: function () { - return this.button.classList.contains(this.getEditorOption('activeButtonClass')); - }, - - setInactive: function () { - this.button.classList.remove(this.getEditorOption('activeButtonClass')); - delete this.knownState; - }, - - setActive: function () { - this.button.classList.add(this.getEditorOption('activeButtonClass')); - delete this.knownState; - }, - - queryCommandState: function () { - var queryState = null; - if (this.useQueryState) { - queryState = this.base.queryCommandState(this.getAction()); - } - return queryState; - }, - - isAlreadyApplied: function (node) { - var isMatch = false, - tagNames = this.getTagNames(), - styleVals, - computedStyle; - - if (this.knownState === false || this.knownState === true) { - return this.knownState; - } - - if (tagNames && tagNames.length > 0) { - isMatch = tagNames.indexOf(node.nodeName.toLowerCase()) !== -1; - } - - if (!isMatch && this.style) { - styleVals = this.style.value.split('|'); - computedStyle = this.window.getComputedStyle(node, null).getPropertyValue(this.style.prop); - styleVals.forEach(function (val) { - if (!this.knownState) { - isMatch = (computedStyle.indexOf(val) !== -1); - // text-decoration is not inherited by default - // so if the computed style for text-decoration doesn't match - // don't write to knownState so we can fallback to other checks - if (isMatch || this.style.prop !== 'text-decoration') { - this.knownState = isMatch; - } - } - }, this); - } - - return isMatch; - } - }); - - Button.isBuiltInButton = function (name) { - return (typeof name === 'string') && MediumEditor.extensions.button.prototype.defaults.hasOwnProperty(name); - }; - - MediumEditor.extensions.button = Button; -}()); - -(function () { - 'use strict'; - - /* MediumEditor.extensions.button.defaults: [Object] - * Set of default config options for all of the built-in MediumEditor buttons - */ - MediumEditor.extensions.button.prototype.defaults = { - 'bold': { - name: 'bold', - action: 'bold', - aria: 'bold', - tagNames: ['b', 'strong'], - style: { - prop: 'font-weight', - value: '700|bold' - }, - useQueryState: true, - contentDefault: 'B', - contentFA: '' - }, - 'italic': { - name: 'italic', - action: 'italic', - aria: 'italic', - tagNames: ['i', 'em'], - style: { - prop: 'font-style', - value: 'italic' - }, - useQueryState: true, - contentDefault: 'I', - contentFA: '' - }, - 'underline': { - name: 'underline', - action: 'underline', - aria: 'underline', - tagNames: ['u'], - style: { - prop: 'text-decoration', - value: 'underline' - }, - useQueryState: true, - contentDefault: 'U', - contentFA: '' - }, - 'strikethrough': { - name: 'strikethrough', - action: 'strikethrough', - aria: 'strike through', - tagNames: ['strike'], - style: { - prop: 'text-decoration', - value: 'line-through' - }, - useQueryState: true, - contentDefault: 'A', - contentFA: '' - }, - 'superscript': { - name: 'superscript', - action: 'superscript', - aria: 'superscript', - tagNames: ['sup'], - /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for superscript - https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */ - // useQueryState: true - contentDefault: 'x1', - contentFA: '' - }, - 'subscript': { - name: 'subscript', - action: 'subscript', - aria: 'subscript', - tagNames: ['sub'], - /* firefox doesn't behave the way we want it to, so we CAN'T use queryCommandState for subscript - https://github.com/guardian/scribe/blob/master/BROWSERINCONSISTENCIES.md#documentquerycommandstate */ - // useQueryState: true - contentDefault: 'x1', - contentFA: '' - }, - 'image': { - name: 'image', - action: 'image', - aria: 'image', - tagNames: ['img'], - contentDefault: 'image', - contentFA: '' - }, - 'html': { - name: 'html', - action: 'html', - aria: 'evaluate html', - tagNames: ['iframe', 'object'], - contentDefault: 'html', - contentFA: '' - }, - 'orderedlist': { - name: 'orderedlist', - action: 'insertorderedlist', - aria: 'ordered list', - tagNames: ['ol'], - useQueryState: true, - contentDefault: '1.', - contentFA: '' - }, - 'unorderedlist': { - name: 'unorderedlist', - action: 'insertunorderedlist', - aria: 'unordered list', - tagNames: ['ul'], - useQueryState: true, - contentDefault: '', - contentFA: '' - }, - 'indent': { - name: 'indent', - action: 'indent', - aria: 'indent', - tagNames: [], - contentDefault: '', - contentFA: '' - }, - 'outdent': { - name: 'outdent', - action: 'outdent', - aria: 'outdent', - tagNames: [], - contentDefault: '', - contentFA: '' - }, - 'justifyCenter': { - name: 'justifyCenter', - action: 'justifyCenter', - aria: 'center justify', - tagNames: [], - style: { - prop: 'text-align', - value: 'center' - }, - contentDefault: 'C', - contentFA: '' - }, - 'justifyFull': { - name: 'justifyFull', - action: 'justifyFull', - aria: 'full justify', - tagNames: [], - style: { - prop: 'text-align', - value: 'justify' - }, - contentDefault: 'J', - contentFA: '' - }, - 'justifyLeft': { - name: 'justifyLeft', - action: 'justifyLeft', - aria: 'left justify', - tagNames: [], - style: { - prop: 'text-align', - value: 'left' - }, - contentDefault: 'L', - contentFA: '' - }, - 'justifyRight': { - name: 'justifyRight', - action: 'justifyRight', - aria: 'right justify', - tagNames: [], - style: { - prop: 'text-align', - value: 'right' - }, - contentDefault: 'R', - contentFA: '' - }, - // Known inline elements that are not removed, or not removed consistantly across browsers: - // ,