├── .editorconfig ├── .eslint.json ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── MIT-LICENCE ├── Readme.md ├── banner.tmp ├── bower.json ├── build ├── e00d9b62b203893a0d3b803f90c52f84.svg ├── sir-trevor.css ├── sir-trevor.js ├── sir-trevor.min.css └── sir-trevor.min.js ├── component.json ├── config ├── jshint.conf.js ├── karma.conf.js └── webpack │ ├── config.js │ ├── dev.js │ ├── dist.js │ ├── test.js │ └── uncompressed.js ├── contributing.md ├── docs └── migrations │ ├── 0.4-0.5.md │ └── 0.5-0.6.md ├── examples ├── blank.html ├── index.html ├── javascript │ ├── example_block.js │ └── limit_chars.js ├── multi.html └── sir-trevor.gif ├── index.js ├── locales ├── de.js ├── es.js ├── fi.js ├── fr.js ├── pt-BR.js ├── pt.js ├── ru.js ├── zh-cn.js └── zh-tw.js ├── package-lock.json ├── package.json ├── public └── images │ └── icons │ └── src │ ├── Bin.svg │ ├── Code.svg │ ├── Competition.svg │ ├── Default.svg │ ├── Embed.svg │ ├── Image.svg │ ├── List.svg │ ├── Poll.svg │ ├── Text.svg │ ├── Tweet.svg │ ├── Video.svg │ ├── add-block.svg │ ├── back.svg │ ├── binopen.svg │ ├── bottom.svg │ ├── bump.svg │ ├── center-align.svg │ ├── cross.svg │ ├── down.svg │ ├── edit.svg │ ├── emphasis.svg │ ├── file.svg │ ├── fmt-bold.svg │ ├── fmt-heading.svg │ ├── fmt-italic.svg │ ├── fmt-link.svg │ ├── fmt-quote.svg │ ├── fmt-quote2.svg │ ├── fmt-superscript.svg │ ├── fmt-unlink.svg │ ├── game.svg │ ├── heading.svg │ ├── iFrame.svg │ ├── icomoon.json │ ├── instagram.svg │ ├── left-align.svg │ ├── link.svg │ ├── middle.svg │ ├── minus.svg │ ├── move-up-down.svg │ ├── next.svg │ ├── plus.svg │ ├── publish.svg │ ├── quote.svg │ ├── republish.svg │ ├── right-align.svg │ ├── rotate.svg │ ├── search.svg │ ├── small-tick.svg │ ├── table.svg │ ├── tick.svg │ ├── top.svg │ ├── user.svg │ └── view.svg ├── release ├── spec ├── app │ └── index.html ├── e2e │ ├── basic.spec.js │ ├── drag_and_drop_helper.js │ ├── format-bar.spec.js │ ├── helpers.js │ ├── selection.spec.js │ └── text-editing.spec.js └── javascripts │ ├── helpers │ ├── shims.js │ └── sir-trevor.js │ └── units │ ├── block-manager │ ├── base.spec.js │ ├── creating-blocks.spec.js │ ├── options.spec.js │ ├── removing-blocks.spec.js │ └── validations.spec.js │ ├── block-mixin │ └── multi-editable.spec.js │ ├── block │ ├── base.spec.js │ ├── controllable.spec.js │ ├── droppable.spec.js │ ├── heading.spec.js │ ├── list.spec.js │ ├── markdown_support.spec.js │ ├── pastable.spec.js │ ├── store.spec.js │ ├── uploadable.spec.js │ ├── validation.spec.js │ └── video.spec.js │ ├── block_controls.spec.js │ ├── block_positioner.spec.js │ ├── editor │ ├── base.spec.js │ ├── options.spec.js │ ├── store.spec.js │ └── submission.spec.js │ ├── format_bar.spec.js │ ├── sir-trevor.spec.js │ ├── submittable.spec.js │ ├── to_html.spec.js │ └── to_markdown.spec.js ├── src ├── block-addition-top.js ├── block-addition.js ├── block-controls.js ├── block-deletion.js ├── block-manager.js ├── block-positioner-select.js ├── block-positioner.js ├── block-reorder.js ├── block-store.js ├── block-validations.js ├── block.js ├── block_mixins │ ├── ajaxable.js │ ├── controllable.js │ ├── droppable.js │ ├── fetchable.js │ ├── index.js │ ├── multi-editable.js │ ├── pastable.js │ ├── textable.js │ └── uploadable.js ├── blocks │ ├── heading.js │ ├── image.js │ ├── index.js │ ├── list.js │ ├── quote.js │ ├── scribe-plugins │ │ ├── scribe-heading-plugin.js │ │ ├── scribe-link-prompt-plugin.js │ │ ├── scribe-list-block-plugin.js │ │ ├── scribe-paste-plugin.js │ │ ├── scribe-quote-plugin.js │ │ ├── scribe-superscript-prompt-plugin.js │ │ ├── scribe-text-block-plugin.js │ │ └── shared.js │ ├── text.js │ ├── tweet.js │ └── video.js ├── config.js ├── editor.js ├── error-handler.js ├── event-bus.js ├── events.js ├── extensions │ ├── editor-store.js │ ├── file-uploader.js │ └── submittable.js ├── form-events.js ├── format-bar.js ├── function-bind.js ├── helpers │ ├── drop-events.js │ └── extend.js ├── icons │ └── sir-trevor-icons.svg ├── index.js ├── locales.js ├── lodash.js ├── mediated-events.js ├── packages │ ├── ajax.js │ ├── cancellable-promise.js │ ├── dom.js │ ├── events.js │ ├── modal.js │ └── uuid.js ├── renderable.js ├── sass │ ├── _icons.scss │ ├── _variables.scss │ ├── base.scss │ ├── block-addition-top.scss │ ├── block-addition.scss │ ├── block-controls.scss │ ├── block-positioner.scss │ ├── block-replacer.scss │ ├── block-ui.scss │ ├── block.scss │ ├── errors.scss │ ├── format-bar.scss │ ├── inputs.scss │ ├── main.scss │ ├── modal.scss │ ├── patterns │ │ └── ui-popup.scss │ └── utils.scss ├── scribe-interface.js ├── selection-handler.js ├── simple-block.js ├── templates │ ├── block-addition-top.js │ ├── block-addition.js │ ├── block-control.js │ ├── block-replacer.js │ ├── block.js │ ├── delete.js │ ├── format-button.js │ └── top-controls.js ├── to-html.js ├── to-markdown.js ├── utils.js └── vendor │ ├── array-includes.js │ ├── custom-event.js │ ├── dom-shims.js │ └── ie-classlist-toggle.js ├── used_underscore_functions.sh └── website ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile ├── config.rb ├── package-lock.json ├── package.json ├── source ├── blank.html.erb ├── docs.html.erb ├── example.html.erb ├── images │ ├── itv.png │ ├── mxm.png │ ├── screen.png │ ├── sir-trevor-icons.svg │ └── sir-trevor.gif ├── index.html.erb ├── javascripts │ ├── all.js │ ├── example.js │ └── jquery.typer.js ├── layouts │ ├── example.erb │ └── layout.erb ├── partials │ ├── _footer.html.erb │ ├── _header.html.erb │ └── docs │ │ ├── _1.html.markdown │ │ ├── _2.html.markdown │ │ ├── _3.html.markdown │ │ ├── _4.html.markdown │ │ ├── _5.html.markdown │ │ ├── _nav.html.erb │ │ └── _uploads.html.markdown ├── setting-up-image-uploading.html.erb └── stylesheets │ ├── _base.scss │ ├── _highlight.scss │ ├── _icons.scss │ ├── all.scss │ ├── example.scss │ ├── modules │ ├── _docs.scss │ ├── _header.scss │ ├── _layout.scss │ ├── _typography.scss │ └── _variables.scss │ └── normalize.css └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 13 | 14 | [**.json] 15 | indent_size = 2 16 | 17 | [**.scss] 18 | indent_size = 2 19 | 20 | [**.js] 21 | indent_size = 2 22 | 23 | [**.css] 24 | indent_size = 2 25 | -------------------------------------------------------------------------------- /.eslint.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madebymany/sir-trevor-js/8b5807a94f6e4486ee00ed815f898c3eb72ef19d/.eslint.json -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .sass-cache/ 3 | .DS_Store 4 | _SpecRunner.html 5 | bower_components/ 6 | components/ 7 | .grunt 8 | yarn-error.log 9 | npm-debug.log 10 | website/.bundle/ 11 | website/.tmp/ 12 | website/bower_components/ 13 | website/build/ 14 | website/node_modules/ 15 | website/vendor/ 16 | build/**/*.debug.* 17 | build/**/*.test.* 18 | .vscode 19 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | spec/ 2 | src/ 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | // prettier.config.js or .prettierrc.js 2 | module.exports = { 3 | trailingComma: "none", 4 | tabWidth: 2, 5 | semi: true, 6 | singleQuote: false, 7 | }; 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - 10.15.1 5 | branches: 6 | only: 7 | - master 8 | before_script: 9 | - export DISPLAY=:99.0 10 | - sh -e /etc/init.d/xvfb start 11 | cache: 12 | directories: 13 | - node_modules 14 | env: 15 | global: 16 | - secure: WewULMIX4C7/27Xflf4/LxfN7VQJt3wOHtGoRVKBK5tfMZtaWXM0ub1hb/exGx+rTSC8TddbfD6v4nXJJKV7Y2+03NCAj8fxrUmlo3SP+XnJSmc16SH+JW4WXb6M+8ZBnlnA4HPEtWNT7yLx/fBAhGSB/arBqBNG4he/gFzz2c0= 17 | - secure: MsR/jN4Ij621RWEN4OwY1p+KWlE1uMapPHgqyrfFCwZ5WF7C40ZvlxphiU26owkgQoUsvRefshY6RXqAy1iGQvg4new5tWKzyXgR0itkzLKexdBIS314uFpwrzAn3RBdLRaMG+SeBcCsFcniWAl28YUwbKcoOGBpa9DNp4a8wb8= 18 | matrix: 19 | - BROWSER_NAME='chrome' BROWSER_VERSION='70' PLATFORM='OSX 10.14' 20 | - BROWSER_NAME='chrome' BROWSER_VERSION='70' PLATFORM='Windows 7' 21 | - BROWSER_NAME='firefox' BROWSER_VERSION='63' PLATFORM='OSX 10.14' 22 | - BROWSER_NAME='firefox' BROWSER_VERSION='63' PLATFORM='Windows 7' 23 | addons: 24 | sauce_connect: true 25 | 26 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /* global module:false */ 2 | 3 | require("es5-shim"); 4 | require("es6-shim"); 5 | 6 | module.exports = function(grunt) { 7 | grunt.loadNpmTasks("grunt-karma"); 8 | grunt.loadNpmTasks("grunt-contrib-jshint"); 9 | grunt.loadNpmTasks("grunt-contrib-uglify"); 10 | grunt.loadNpmTasks("grunt-contrib-watch"); 11 | grunt.loadNpmTasks("grunt-webpack"); 12 | grunt.loadNpmTasks("grunt-contrib-connect"); 13 | grunt.loadNpmTasks("grunt-jasmine-nodejs"); 14 | grunt.loadNpmTasks("grunt-contrib-clean"); 15 | 16 | grunt.initConfig({ 17 | pkg: grunt.file.readJSON("package.json"), 18 | 19 | webpack: { 20 | dist: require("./config/webpack/dist"), 21 | test: require("./config/webpack/test"), 22 | uncompressed: require("./config/webpack/uncompressed") 23 | }, 24 | 25 | "webpack-dev-server": { 26 | start: { 27 | webpack: require("./config/webpack/dev"), 28 | keepalive: true, 29 | hot: true, 30 | contentBase: "./", 31 | inline: true, 32 | host: "localhost", 33 | port: 8080, 34 | disableHostCheck: true 35 | } 36 | }, 37 | 38 | karma: { 39 | test: { 40 | configFile: "./config/karma.conf.js" 41 | } 42 | }, 43 | 44 | jshint: require("./config/jshint.conf"), 45 | 46 | connect: { 47 | server: { 48 | options: { 49 | port: 8000, 50 | hostname: "localhost" 51 | } 52 | } 53 | }, 54 | 55 | jasmine_nodejs: { 56 | options: { 57 | specNameSuffix: "spec.js", 58 | useHelpers: false, 59 | stopOnFailure: false, 60 | reporters: { 61 | console: { 62 | colors: true, 63 | cleanStack: 1, 64 | verbosity: 1, 65 | listStyle: "flat", 66 | activity: true 67 | } 68 | } 69 | }, 70 | test: { 71 | specs: ["spec/e2e/*.spec.js"] 72 | } 73 | }, 74 | 75 | clean: { 76 | all: ["build/*.*"] 77 | } 78 | }); 79 | 80 | grunt.registerTask("default", ["webpack:uncompressed", "webpack:dist"]); 81 | grunt.registerTask("test", ["clean:all", "karma", "test-integration"]); 82 | grunt.registerTask("test-integration", [ 83 | "webpack:test", 84 | "connect", 85 | "jasmine_nodejs" 86 | ]); 87 | grunt.registerTask("dev", ["webpack-dev-server:start"]); 88 | }; 89 | -------------------------------------------------------------------------------- /MIT-LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2012 by ITV plc - http://www.itv.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /banner.tmp: -------------------------------------------------------------------------------- 1 | /* 2 | * Sir Trevor JS v0.5.0-beta3 3 | * 4 | * Released under the MIT license 5 | * www.opensource.org/licenses/MIT 6 | * 7 | * 2015-04-08 8 | */ 9 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sir-trevor-js", 3 | "ignore": [ 4 | "**/.*", 5 | "node_modules", 6 | "components", 7 | "src/!(blocks)", 8 | "spec", 9 | "examples", 10 | "website", 11 | "BrowserRunner.html" 12 | ], 13 | "main": [ 14 | "build/sir-trevor.js", 15 | "build/sir-trevor.css", 16 | "build/sir-trevor-icons.svg" 17 | ], 18 | "dependencies": { 19 | "es5-shim": "~v4.0.3", 20 | "es6-shim": "~0.21.0", 21 | "Eventable": "~1.0.1" 22 | }, 23 | "version": "0.9.0-beta9" 24 | } 25 | -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sir-trevor-js", 3 | "version": "0.9.0-beta9", 4 | "description": "", 5 | "keywords": [], 6 | "repo": "https://github.com/madebymany/sir-trevor-js", 7 | "local": [], 8 | "main": "sir-trevor.js", 9 | "scripts": [ 10 | "sir-trevor.js" 11 | ], 12 | "styles": [ 13 | "sir-trevor.css", 14 | "sir-trevor-icons.css" 15 | ], 16 | "files": [ 17 | "public/images/icons/src/Add.svg", 18 | "public/images/icons/src/Bin-open.svg", 19 | "public/images/icons/src/Bin.svg", 20 | "public/images/icons/src/Competition.svg", 21 | "public/images/icons/src/Embed.svg", 22 | "public/images/icons/src/Header.svg", 23 | "public/images/icons/src/iFrame.svg", 24 | "public/images/icons/src/Image.svg", 25 | "public/images/icons/src/link.svg", 26 | "public/images/icons/src/List.svg", 27 | "public/images/icons/src/Move.svg", 28 | "public/images/icons/src/Poll.svg", 29 | "public/images/icons/src/Quote.svg", 30 | "public/images/icons/src/SirTrev.json", 31 | "public/images/icons/src/Text.svg", 32 | "public/images/icons/src/Tweet.svg", 33 | "public/images/icons/src/Video.svg" 34 | ], 35 | "license": "MIT", 36 | "remotes": [], 37 | "dependencies": { 38 | "components/jquery": "*", 39 | "thinkerous/eventable": "*" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /config/jshint.conf.js: -------------------------------------------------------------------------------- 1 | var jsHintDefaultOptions = { 2 | // Errors 3 | bitwise: true, 4 | camelcase: false, 5 | curly: true, 6 | eqeqeq: true, 7 | forin: true, 8 | freeze: true, 9 | immed: true, 10 | indent: 2, 11 | latedef: true, 12 | newcap: true, 13 | noarg: true, 14 | nonbsp: true, 15 | nonew: true, 16 | strict: true, 17 | maxparams: 5, 18 | maxdepth: 3, 19 | maxcomplexity: 12, 20 | undef: true, 21 | unused: "vars", 22 | 23 | // Relax 24 | eqnull: true, 25 | 26 | // Envs 27 | browser: true, 28 | jquery: true, 29 | node: true, 30 | 31 | esnext: true 32 | }; 33 | 34 | module.exports = { 35 | lib: { 36 | src: ["index.js", "src/**/*.js"], 37 | options: Object.assign({}, jsHintDefaultOptions, { 38 | jquery: false, 39 | globals: { 40 | i18n: true, 41 | webkitURL: true 42 | } 43 | }) 44 | }, 45 | 46 | tests: { 47 | src: ["spec/**/*.js"], 48 | options: Object.assign({}, jsHintDefaultOptions, { 49 | globals: { 50 | _: true, 51 | SirTrevor: true, 52 | i18n: true, 53 | webkitURL: true, 54 | jasmine: true, 55 | describe: true, 56 | expect: true, 57 | it: true, 58 | spyOn: true, 59 | beforeEach: true, 60 | afterEach: true, 61 | beforeAll: true, 62 | afterAll: true, 63 | xit: true 64 | } 65 | }) 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /config/webpack/config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | module.exports = { 4 | entry: "./index.js", 5 | output: { 6 | library: "SirTrevor", 7 | libraryTarget: "umd", 8 | path: "./build" 9 | }, 10 | externals: { 11 | jquery: { 12 | root: "jQuery", 13 | commonjs: "jquery", 14 | commonjs2: "jquery", 15 | amd: "jquery" 16 | } 17 | }, 18 | module: { 19 | rules: [ 20 | { 21 | test: /\.js?$/, 22 | exclude: function(modulePath) { 23 | return /node_modules/.test(modulePath) && 24 | !/node_modules\/micromodal/.test(modulePath); 25 | }, 26 | loader: "babel-loader", 27 | options: { 28 | "presets": ["@babel/preset-env"] 29 | } 30 | } 31 | ] 32 | }, 33 | plugins: [] 34 | }; 35 | -------------------------------------------------------------------------------- /config/webpack/dev.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var webpackConfigMerger = require("webpack-config-merger"); 3 | var ExtractTextPlugin = require("extract-text-webpack-plugin"); 4 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 5 | 6 | var config = webpackConfigMerger(require("./config"), { 7 | devtool: "source-map", 8 | mode: "development", 9 | output: { 10 | filename: "sir-trevor.debug.js" 11 | }, 12 | plugins: [new MiniCssExtractPlugin({ filename: "sir-trevor.debug.css" })], 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.scss$/, 17 | use: [ 18 | MiniCssExtractPlugin.loader, 19 | { 20 | loader: "css-loader", 21 | options: { sourceMap: true } 22 | }, 23 | { 24 | loader: "sass-loader", 25 | options: { 26 | sourceMap: true, 27 | sassOptions: { outputStyle: "expanded" } 28 | } 29 | } 30 | ] 31 | }, 32 | { 33 | test: /\.svg$/, 34 | use: [ 35 | { 36 | loader: "file-loader", 37 | options: {} 38 | } 39 | ] 40 | } 41 | //loader: ExtractTextPlugin.extract("file?name=[name].debug.[ext]") 42 | ] 43 | } 44 | }); 45 | 46 | module.exports = config; 47 | -------------------------------------------------------------------------------- /config/webpack/dist.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var webpackConfigMerger = require("webpack-config-merger"); 3 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | var banner = [ 6 | "/*!", 7 | " * Sir Trevor JS v<%= pkg.version %>", 8 | " *", 9 | " * Released under the MIT license", 10 | " * www.opensource.org/licenses/MIT", 11 | " *", 12 | ' * <%= grunt.template.today("yyyy-mm-dd") %>', 13 | " */\n\n" 14 | ].join("\n"); 15 | 16 | module.exports = webpackConfigMerger(require("./config"), { 17 | mode: "production", 18 | optimization: { 19 | minimize: true 20 | }, 21 | output: { 22 | filename: "sir-trevor.min.js" 23 | }, 24 | plugins: [ 25 | new MiniCssExtractPlugin({ filename: "sir-trevor.min.css" }), 26 | new webpack.BannerPlugin({ banner: banner, raw: true }) 27 | ], 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.scss$/, 32 | use: [ 33 | MiniCssExtractPlugin.loader, 34 | { 35 | loader: "css-loader", 36 | options: {} 37 | }, 38 | { 39 | loader: "sass-loader", 40 | options: { sassOptions: { outputStyle: "compressed" } } 41 | } 42 | ] 43 | }, 44 | { 45 | test: /\.svg$/, 46 | use: [ 47 | { 48 | loader: "file-loader", 49 | options: {} 50 | } 51 | ] 52 | } 53 | //loader: ExtractTextPlugin.extract("file?name=[name].[ext]") 54 | ] 55 | } 56 | }); 57 | -------------------------------------------------------------------------------- /config/webpack/test.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var webpackConfigMerger = require("webpack-config-merger"); 3 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | module.exports = webpackConfigMerger(require("./config"), { 6 | mode: "development", 7 | output: { 8 | filename: "sir-trevor.test.js" 9 | }, 10 | plugins: [new MiniCssExtractPlugin({ filename: "sir-trevor.test.css" })], 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.scss$/, 15 | use: [ 16 | MiniCssExtractPlugin.loader, 17 | { 18 | loader: "css-loader", 19 | options: {} 20 | }, 21 | { 22 | loader: "sass-loader", 23 | options: { sassOptions: { outputStyle: "compressed" } } 24 | } 25 | ] 26 | }, 27 | { 28 | test: /\.svg$/, 29 | use: [ 30 | { 31 | loader: "file-loader", 32 | options: { name: "[path][name].test.[ext]" } 33 | } 34 | ] 35 | } 36 | //loader: ExtractTextPlugin.extract("file?name=[name].debug.[ext]") 37 | ] 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /config/webpack/uncompressed.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | var webpackConfigMerger = require("webpack-config-merger"); 3 | var MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | var config = webpackConfigMerger(require("./config"), { 6 | mode: "production", 7 | optimization: { 8 | minimize: false 9 | }, 10 | output: { 11 | filename: "sir-trevor.js" 12 | }, 13 | plugins: [new MiniCssExtractPlugin({ filename: "sir-trevor.css" })], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.scss$/, 18 | use: [ 19 | MiniCssExtractPlugin.loader, 20 | { 21 | loader: "css-loader", 22 | options: {} 23 | }, 24 | { 25 | loader: "sass-loader", 26 | options: { sassOptions: { outputStyle: "expanded" } } 27 | } 28 | ] 29 | }, 30 | { 31 | test: /\.svg$/, 32 | use: [ 33 | { 34 | loader: "file-loader", 35 | options: {} 36 | } 37 | ] 38 | } 39 | ] 40 | } 41 | }); 42 | 43 | module.exports = config; 44 | -------------------------------------------------------------------------------- /docs/migrations/0.4-0.5.md: -------------------------------------------------------------------------------- 1 | # From 0.4 to 0.5 2 | 3 | ## Important changes 4 | 5 | - Sir Trevor textareas now output html rather than markdown 6 | - Scribe is now used for text formatting 7 | 8 | ## Sir Trevor textareas now output html rather than markdown 9 | 10 | When a block is saved SirTrevor will no longer save text as markdown, but will output valid html. 11 | 12 | ``` 13 | # Old 14 | 15 | { 16 | "type": "text", 17 | "data": { 18 | "text": "One _two_ **three** [four](http://example.com)" 19 | } 20 | } 21 | 22 | # New 23 | 24 | { 25 | "type": "text", 26 | "data": { 27 | "text": "One two three four", 28 | "format": "html" 29 | } 30 | } 31 | ``` 32 | 33 | Sir Trevor will still load existing blocks formatted in Markdown, but will migrate the block data on save. 34 | 35 | **You'll need to make sure your markdown parser accepts html or provide an alternative filter when the html format property is present.** 36 | 37 | An example of how we deal with this in the sir-trevor-rails can be found here: 38 | https://github.com/madebymany/sir-trevor-rails/blob/master/lib/sir_trevor_rails/helpers/view_helper.rb#L9 39 | 40 | ## Scribe is now used for text formatting 41 | 42 | We now use Scribe to edit text rather than a mix of html/markdown. If you have built custom formatters for the existing codebase then these will need to be migrated accordingly. 43 | 44 | For more info on scribe go here: https://github.com/guardian/scribe 45 | -------------------------------------------------------------------------------- /examples/javascript/limit_chars.js: -------------------------------------------------------------------------------- 1 | /* Soft character limits on inputs and textareas */ 2 | 3 | (function($){ 4 | 5 | $.fn.limit_chars = function() { 6 | 7 | if (this.length===0) return; 8 | 9 | // Remove browser maxlength, add soft limit 10 | if(this.attr('maxlength')) { 11 | this.attr('data-maxlength',this.attr('maxlength')); 12 | this.removeAttr('maxlength'); 13 | } 14 | 15 | if(this.parents('.extended_input').length === 0) { 16 | 17 | count = (this.chars()'+this.chars()+''; 18 | 19 | // Build UI 20 | this.wrap($('
',{ 21 | "class": "extended_input" 22 | })).after($('', { 23 | "class": "count", 24 | html: count+' of '+this.attr('data-maxlength') 25 | })); 26 | 27 | // Attach event 28 | this.bind('keydown keyup paste',function(ev){ 29 | count = ($(this).chars()<$(this).attr('data-maxlength')) ? $(this).chars() : ''+$(this).chars()+''; 30 | $(this).parent().find('.count').html(count+' of '+$(this).attr('data-maxlength')); 31 | }); 32 | 33 | } 34 | 35 | }; 36 | 37 | $.fn.chars = function() { 38 | count = (this.attr('contenteditable')!==undefined) ? this.text().length : this.val().length; 39 | return count; 40 | }; 41 | 42 | $.fn.too_long = function() { 43 | return this.chars() > this.attr('data-maxlength'); 44 | }; 45 | 46 | })(jQuery); -------------------------------------------------------------------------------- /examples/sir-trevor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madebymany/sir-trevor-js/8b5807a94f6e4486ee00ed815f898c3eb72ef19d/examples/sir-trevor.gif -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/'); 2 | 3 | require('./src/sass/main.scss'); 4 | -------------------------------------------------------------------------------- /locales/de.js: -------------------------------------------------------------------------------- 1 | SirTrevor.Locales.de = { 2 | general: { 3 | delete: "Löschen?", 4 | drop: "__block__ hier ablegen", 5 | paste: "Oder Adresse hier einfügen", 6 | upload: "...oder Datei auswählen", 7 | close: "Schließen", 8 | position: "Position", 9 | wait: "Bitte warten...", 10 | yes: "Ja ", 11 | no: "Nein" 12 | }, 13 | errors: { 14 | title: "Die folgenden Fehler sind aufgetreten:", 15 | validation_fail: "Block __type__ ist ungültig", 16 | block_empty: "__name__ darf nicht leer sein", 17 | type_missing: "Blöcke mit Typ __type__ sind hier nicht zulässig", 18 | required_type_empty: "Angeforderter Block-Typ __type__ ist leer", 19 | load_fail: 20 | "Es wurde ein Problem beim Laden des Dokumentinhalts festgestellt", 21 | link_empty: "This link appears to be empty", 22 | link_invalid: "The link is not valid" 23 | }, 24 | formatters: { 25 | link: { 26 | prompt: "Link eintragen", 27 | new_tab: "Opens in a new tab", 28 | message: 29 | "The URL you entered appears to be __type__. Do you want to add the required “__prefix__” prefix?", 30 | types: { 31 | email: "an email address", 32 | telephone: "a telephone number", 33 | url: "a link" 34 | } 35 | }, 36 | superscript: { 37 | prompt: "Titelattribut (optional)" 38 | } 39 | }, 40 | blocks: { 41 | text: { 42 | title: "Text" 43 | }, 44 | list: { 45 | title: "Liste (unsortiert)" 46 | }, 47 | quote: { 48 | title: "Zitat", 49 | credit_field: "Quelle" 50 | }, 51 | image: { 52 | title: "Bild", 53 | upload_error: "Es wurde ein Problem beim Upload festgestellt" 54 | }, 55 | video: { 56 | title: "Video", 57 | drop_title: "Video URL" 58 | }, 59 | tweet: { 60 | title: "Tweet", 61 | drop_title: "Tweet URL", 62 | fetch_error: "Es wurde ein Problem beim Laden des Tweets festgestellt" 63 | }, 64 | embedly: { 65 | title: "Embedly", 66 | fetch_error: "There was a problem fetching your embed", 67 | key_missing: "An Embedly API key must be present" 68 | }, 69 | heading: { 70 | title: "Überschrift" 71 | } 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /locales/es.js: -------------------------------------------------------------------------------- 1 | SirTrevor.Locales.es = { 2 | general: { 3 | delete: "¿Eliminar?", 4 | drop: "Arrastrar __block__ ici", 5 | paste: "O copie el enlace aquí", 6 | upload: "...o seleccione un fichero", 7 | close: "cerrar", 8 | position: "Posición", 9 | wait: "Por favor, espere..." 10 | }, 11 | errors: { 12 | title: "Aparecerá el siguiente error :", 13 | validation_fail: "Bloque __type__ inválido", 14 | block_empty: "__name__ no debe estar vacío", 15 | type_missing: "Debe tene un tipo de bloque __type__", 16 | required_type_empty: "Un bloque obligatorio de tipo __type__ está vacío", 17 | load_fail: "Hay un problema con la carga de datos", 18 | link_empty: "This link appears to be empty", 19 | link_invalid: "The link is not valid" 20 | }, 21 | formatters: { 22 | link: { 23 | prompt: "Introduce un link", 24 | new_tab: "Opens in a new tab", 25 | message: 26 | "The URL you entered appears to be __type__. Do you want to add the required “__prefix__” prefix?", 27 | types: { 28 | email: "an email address", 29 | telephone: "a telephone number", 30 | url: "a link" 31 | } 32 | }, 33 | superscript: { 34 | prompt: "Atributo de título (opcional)" 35 | } 36 | }, 37 | blocks: { 38 | text: { 39 | title: "Texto" 40 | }, 41 | list: { 42 | title: "Lista" 43 | }, 44 | quote: { 45 | title: "Cita", 46 | credit_field: "Autor" 47 | }, 48 | image: { 49 | title: "Imagen", 50 | upload_error: "Hay un problema con la subida" 51 | }, 52 | video: { 53 | title: "Vídeo", 54 | drop_title: "URL del Vídeo" 55 | }, 56 | tweet: { 57 | title: "Tweet", 58 | drop_title: "Tweet URL", 59 | fetch_error: "Se produjo un problema al recuperar su tweet" 60 | }, 61 | embedly: { 62 | title: "Embebido", 63 | fetch_error: "Se produjo un problema al recuperar su video", 64 | key_missing: "Debe tener asociada una clave API" 65 | }, 66 | heading: { 67 | title: "Título" 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /locales/fi.js: -------------------------------------------------------------------------------- 1 | SirTrevor.Locales.fi = { 2 | general: { 3 | delete: "Poista?", 4 | drop: "Raahaa __block__ tähän", 5 | paste: "Tai lisää osoite tähän", 6 | upload: "...tai valitse tiedosto", 7 | close: "sulje", 8 | position: "Sijainti", 9 | wait: "Odota hetki..." 10 | }, 11 | errors: { 12 | title: "Seuraavat virheet:", 13 | validation_fail: "__type__-lohko on epäkelpo", 14 | block_empty: "__name__ ei saa olla tyhjä", 15 | type_missing: "__type__ on pakollinen lohko", 16 | required_type_empty: "Pakollinen __type__-lohko on tyhjä", 17 | load_fail: "Sisällön lataamisessa on ongelma", 18 | link_empty: "This link appears to be empty", 19 | link_invalid: "The link is not valid" 20 | }, 21 | formatters: { 22 | link: { 23 | prompt: "Kirjoita osoite", 24 | new_tab: "Opens in a new tab", 25 | message: 26 | "The URL you entered appears to be __type__. Do you want to add the required “__prefix__” prefix?", 27 | types: { 28 | email: "an email address", 29 | telephone: "a telephone number", 30 | url: "a link" 31 | } 32 | }, 33 | superscript: { 34 | prompt: "Otsikon määrite (valinnainen)" 35 | } 36 | }, 37 | blocks: { 38 | text: { 39 | title: "Teksti" 40 | }, 41 | list: { 42 | title: "Lista" 43 | }, 44 | quote: { 45 | title: "Lainaus", 46 | credit_field: "Lähde" 47 | }, 48 | image: { 49 | title: "Kuva", 50 | upload_error: "Tiedoston lataamisessa oli ongelma" 51 | }, 52 | video: { 53 | title: "Video", 54 | drop_title: "Videon URL-osoite" 55 | }, 56 | tweet: { 57 | title: "Tweet", 58 | drop_title: "Tweet URL-osoite", 59 | fetch_error: "Viestin hakemisessa oli ongelma" 60 | }, 61 | embedly: { 62 | title: "Embedly", 63 | fetch_error: "Sisällön hakemisessa oli ongelma", 64 | key_missing: "Embedly vaatii API-avaimen" 65 | }, 66 | heading: { 67 | title: "Otsikko" 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /locales/fr.js: -------------------------------------------------------------------------------- 1 | SirTrevor.Locales.fr = { 2 | general: { 3 | delete: "Suppression ?", 4 | drop: "Glissez __block__ ici", 5 | paste: "Ou copiez le lien ici", 6 | upload: "...ou choisissez un fichier", 7 | close: "fermer", 8 | position: "Position", 9 | wait: "Veuillez patienter...", 10 | yes: "Oui", 11 | no: "Non" 12 | }, 13 | errors: { 14 | title: "Vous avez l'erreur suivante :", 15 | validation_fail: "Bloc __type__ est invalide", 16 | block_empty: "__name__ ne doit pas être vide", 17 | type_missing: "Vous devez avoir un bloc de type __type__", 18 | required_type_empty: "Un bloc requis de type __type__ est vide", 19 | load_fail: "Il y a un problème avec le chargement des données du document", 20 | link_empty: "This link appears to be empty", 21 | link_invalid: "The link is not valid" 22 | }, 23 | formatters: { 24 | link: { 25 | prompt: "Entrez un lien", 26 | new_tab: "Opens in a new tab", 27 | message: 28 | "The URL you entered appears to be __type__. Do you want to add the required “__prefix__” prefix?", 29 | types: { 30 | email: "an email address", 31 | telephone: "a telephone number", 32 | url: "a link" 33 | } 34 | }, 35 | superscript: { 36 | prompt: "Attribut de titre (facultatif)" 37 | } 38 | }, 39 | blocks: { 40 | text: { 41 | title: "Texte" 42 | }, 43 | list: { 44 | title: "Liste" 45 | }, 46 | quote: { 47 | title: "Citation", 48 | credit_field: "Auteur" 49 | }, 50 | image: { 51 | title: "Image", 52 | upload_error: "Il y a un problème avec votre téléchargement" 53 | }, 54 | video: { 55 | title: "Vidéo", 56 | drop_title: "URL de la Vidéo" 57 | }, 58 | tweet: { 59 | title: "Tweet", 60 | drop_title: "URL de Tweet", 61 | fetch_error: 62 | "Un problème est survenu lors de la récupération de votre tweet" 63 | }, 64 | embedly: { 65 | title: "Embedly", 66 | fetch_error: 67 | "Un problème est survenu lors de la récupération de votre embed", 68 | key_missing: "Une clé API pour Embedly doit être présente" 69 | }, 70 | heading: { 71 | title: "Titre" 72 | } 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /locales/pt-BR.js: -------------------------------------------------------------------------------- 1 | SirTrevor.Locales.pt_BR = { 2 | general: { 3 | 'delete': 'Deletar?', 4 | 'drop': 'Arraste __block__ aqui', 5 | 'paste': 'Ou cole o link aqui', 6 | 'upload': '...ou escolha um arquivo', 7 | 'close': 'fechar', 8 | 'position': 'Posição', 9 | 'wait': 'Aguarde...' 10 | }, 11 | errors: { 12 | 'title': "Você tem os seguintes erros:", 13 | 'validation_fail': "o bloco __type__ é inválido", 14 | 'block_empty': "__name__ não pode ser vazio", 15 | 'type_missing': "Você deve ter um bloco do tipo __type__", 16 | 'required_type_empty': "Um bloco obrigatório do tipo __type__ está vazio", 17 | 'load_fail': "Houve um problema ao carregar o conteúdo do documento", 18 | 'link_empty': "This link appears to be empty", 19 | 'link_invalid': "The link is not valid" 20 | }, 21 | formatters: { 22 | link: { 23 | 'prompt': "Insira o link", 24 | 'new_tab': "Opens in a new tab", 25 | 'message': "The URL you entered appears to be __type__. Do you want to add the required “__prefix__” prefix?", 26 | types: { 27 | 'email': 'an email address', 28 | 'telephone': 'a telephone number', 29 | 'url': 'a link' 30 | } 31 | }, 32 | superscript: { 33 | prompt: "Atributo de título (opcional)" 34 | } 35 | }, 36 | blocks: { 37 | text: { 38 | 'title': "Texto" 39 | }, 40 | list: { 41 | 'title': "Lista" 42 | }, 43 | quote: { 44 | 'title': "Citação", 45 | 'credit_field': "Crédito" 46 | }, 47 | image: { 48 | 'title': "Imagem", 49 | 'upload_error': "Houve um problema com o seu upload" 50 | }, 51 | video: { 52 | 'title': "Video", 53 | 'drop_title': "URL do Vídeo" 54 | }, 55 | tweet: { 56 | 'title': "Tweet", 57 | 'drop_title': "URL do Tweet" 58 | 'fetch_error': "Houve um problema ao carregar seu tweet" 59 | }, 60 | embedly: { 61 | 'title': "Embedly", 62 | 'fetch_error': "Houve um problema ao carregar seu embed", 63 | 'key_missing': "Uma API key da Embedly precisa estar presente" 64 | }, 65 | heading: { 66 | 'title': "Título" 67 | } 68 | } 69 | }; 70 | -------------------------------------------------------------------------------- /locales/pt.js: -------------------------------------------------------------------------------- 1 | SirTrevor.Locales.pt = { 2 | general: { 3 | delete: "Elimina?", 4 | drop: "Arrastar __block__ aqui", 5 | paste: "Ou cola o URL aquí", 6 | upload: "...ou selecionar um fichero", 7 | close: "Fechar", 8 | position: "Posicionar", 9 | wait: "Por favor, espere..." 10 | }, 11 | errors: { 12 | title: "Sugerio os seguientes erros :", 13 | validation_fail: "Bloque __type__ inválido", 14 | block_empty: "__name__ no debe estar vacío", 15 | type_missing: "Necessitas um bloque de __type__", 16 | required_type_empty: "Um bloque obligatorio de tipo __type__ está vazio", 17 | load_fail: "Sugerio um problema a cargar os dados do documento", 18 | link_empty: "This link appears to be empty", 19 | link_invalid: "The link is not valid" 20 | }, 21 | formatters: { 22 | link: { 23 | prompt: "Introduz um link", 24 | new_tab: "Opens in a new tab", 25 | message: 26 | "The URL you entered appears to be __type__. Do you want to add the required “__prefix__” prefix?", 27 | types: { 28 | email: "an email address", 29 | telephone: "a telephone number", 30 | url: "a link" 31 | } 32 | }, 33 | superscript: { 34 | prompt: "Atributo de título (opcional)" 35 | } 36 | }, 37 | blocks: { 38 | text: { 39 | title: "Texto" 40 | }, 41 | list: { 42 | title: "Lista" 43 | }, 44 | quote: { 45 | title: "Cita", 46 | credit_field: "Autor" 47 | }, 48 | image: { 49 | title: "Imagem", 50 | upload_error: "Sugerio um problema com o upload da imagem" 51 | }, 52 | video: { 53 | title: "Vídeo", 54 | drop_title: "URL do Vídeo" 55 | }, 56 | tweet: { 57 | title: "Tweet", 58 | drop_title: "URL do Tweet", 59 | fetch_error: "Sugerio um problema na busca da sua tweet" 60 | }, 61 | embedly: { 62 | title: "Embebido", 63 | fetch_error: "Sugerio um problema na busca do video", 64 | key_missing: "Necessitamos a chave do API Embedly" 65 | }, 66 | heading: { 67 | title: "Título" 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /locales/ru.js: -------------------------------------------------------------------------------- 1 | SirTrevor.Locales.ru = { 2 | general: { 3 | delete: "Удалить?", 4 | drop: "Перетащите __block__ сюда", 5 | paste: "Или введите URL здесь", 6 | upload: "... или выберите файл", 7 | close: "Закрыть", 8 | position: "Позиция", 9 | wait: "Пожалуйста, подождите ..." 10 | }, 11 | errors: { 12 | title: "У вас есть следующие ошибки:", 13 | validation_fail: "__type__ блок является недопустимым", 14 | block_empty: "__name__ не должно быть пустым", 15 | type_missing: "Вы должны иметь блок типа __type__", 16 | required_type_empty: "Нужный тип блока __type__ пуст", 17 | load_fail: "Есть проблема при загрузке содержимого документа", 18 | link_empty: "This link appears to be empty", 19 | link_invalid: "The link is not valid" 20 | }, 21 | formatters: { 22 | link: { 23 | prompt: "Введите ссылку", 24 | new_tab: "Opens in a new tab", 25 | message: 26 | "The URL you entered appears to be __type__. Do you want to add the required “__prefix__” prefix?", 27 | types: { 28 | email: "an email address", 29 | telephone: "a telephone number", 30 | url: "a link" 31 | } 32 | }, 33 | superscript: { 34 | prompt: "Атрибут заголовка (необязательно)" 35 | } 36 | }, 37 | blocks: { 38 | text: { 39 | title: "Текст" 40 | }, 41 | list: { 42 | title: "Cписок" 43 | }, 44 | quote: { 45 | title: "Цитата", 46 | credit_field: "Слова" 47 | }, 48 | image: { 49 | title: "Изображение", 50 | upload_error: "Есть проблемы с загрузкой" 51 | }, 52 | video: { 53 | title: "Видео", 54 | drop_title: "URL видео" 55 | }, 56 | tweet: { 57 | title: "Твит", 58 | drop_title: "URL Твит", 59 | fetch_error: "Есть проблемы с загрузкой" 60 | }, 61 | embedly: { 62 | title: "Вставка", 63 | fetch_error: "Есть проблемы с вставлением", 64 | key_missing: "API должен присутствовать" 65 | }, 66 | heading: { 67 | title: "Заголовок" 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /locales/zh-cn.js: -------------------------------------------------------------------------------- 1 | SirTrevor.Locales.cn = { 2 | general: { 3 | 'delete': '删除', 4 | 'drop': '把目标__block__拖拽到这里', 5 | 'paste': '或者粘贴一个URL', 6 | 'upload': '...或者点击上传', 7 | 'close': '关闭', 8 | 'position': '位置', 9 | 'wait': '处理中,请稍候...' 10 | }, 11 | errors: { 12 | 'title': "发生了以下的错误:", 13 | 'validation_fail': "__type__ 模块不是有效模块", 14 | 'block_empty': "__name__ 不能为空", 15 | 'type_missing': "你必须至少有一个 __type__ 类型的模块", 16 | 'required_type_empty': "一个必须不为空的模块 __type__ 目前为空", 17 | 'load_fail': "载入文档内容失败", 18 | 'link_empty': "This link appears to be empty", 19 | 'link_invalid': "The link is not valid" 20 | }, 21 | formatters: { 22 | link: { 23 | 'prompt': 请填写要插入的链接", 24 | 'new_tab': "Opens in a new tab", 25 | 'message': "The URL you entered appears to be __type__. Do you want to add the required “__prefix__” prefix?", 26 | types: { 27 | 'email': 'an email address', 28 | 'telephone': 'a telephone number', 29 | 'url': 'a link' 30 | } 31 | }, 32 | superscript: { 33 | prompt: "标题属性(可选)" 34 | } 35 | }, 36 | blocks: { 37 | text: { 38 | 'title': "文字" 39 | }, 40 | list: { 41 | 'title': "列表" 42 | }, 43 | quote: { 44 | 'title': "引用", 45 | 'credit_field': "出处" 46 | }, 47 | image: { 48 | 'title': "图片", 49 | 'upload_error': "载入图片失败" 50 | }, 51 | video: { 52 | 'title': "视频", 53 | 'drop_title': "影片网址" 54 | }, 55 | tweet: { 56 | 'title': "Tweet", 57 | 'drop_title': "鸣叫网址", 58 | 'fetch_error': "获取tweet失败" 59 | }, 60 | embedly: { 61 | 'title': "内嵌内容", 62 | 'fetch_error': "内嵌内容载入失败", 63 | 'key_missing': "嵌入内容的API没有合法的Key" 64 | }, 65 | heading: { 66 | 'title': '标题' 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /locales/zh-tw.js: -------------------------------------------------------------------------------- 1 | SirTrevor.Locales.cn_TW = { 2 | general: { 3 | delete: "刪除", 4 | drop: "把目標__block__ 拖拉到這里", 5 | paste: "或者粘貼一個URL", 6 | upload: "...或者點擊上傳", 7 | close: "關閉", 8 | position: "位置", 9 | wait: "處理中,請稍候..." 10 | }, 11 | errors: { 12 | title: "發生了以下的錯誤:", 13 | validation_fail: "__type__ 模塊不是有效模塊", 14 | block_empty: "__name__ 不能為空", 15 | type_missing: "你必須至少有一個 __type__ 類型的模塊", 16 | required_type_empty: "一個必須不為空的模塊 __type__ 目前為空", 17 | load_fail: "載入文檔內容失敗", 18 | link_empty: "This link appears to be empty", 19 | link_invalid: "The link is not valid" 20 | }, 21 | formatters: { 22 | link: { 23 | prompt: "請填寫要插入的鏈結", 24 | new_tab: "Opens in a new tab", 25 | message: 26 | "The URL you entered appears to be __type__. Do you want to add the required “__prefix__” prefix?", 27 | types: { 28 | email: "an email address", 29 | telephone: "a telephone number", 30 | url: "a link" 31 | } 32 | }, 33 | superscript: { 34 | prompt: "标题属性(可选)" 35 | } 36 | }, 37 | blocks: { 38 | text: { 39 | title: "文字" 40 | }, 41 | list: { 42 | title: "列表" 43 | }, 44 | quote: { 45 | title: "引用", 46 | credit_field: "出處" 47 | }, 48 | image: { 49 | title: "圖片", 50 | upload_error: "載入圖片失敗" 51 | }, 52 | video: { 53 | title: "視頻", 54 | drop_title: "影片網址" 55 | }, 56 | tweet: { 57 | title: "Tweet", 58 | drop_title: "鳴叫網址", 59 | fetch_error: "獲取tweet失敗" 60 | }, 61 | embedly: { 62 | title: "內嵌內容", 63 | fetch_error: "內嵌內容失敗", 64 | key_missing: "嵌入內容的API沒有合法的Key" 65 | }, 66 | heading: { 67 | title: "標題" 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /public/images/icons/src/Bin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/Code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/images/icons/src/Competition.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/Default.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/images/icons/src/Embed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/Image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/List.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/Poll.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/Text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/Tweet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/Video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/add-block.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/images/icons/src/back.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/binopen.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/bottom.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/images/icons/src/bump.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/center-align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/icons/src/cross.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/emphasis.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/images/icons/src/file.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/fmt-bold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/fmt-heading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/fmt-italic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/fmt-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/fmt-quote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/fmt-quote2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/fmt-superscript.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | superscript 4 | 5 | 6 | -------------------------------------------------------------------------------- /public/images/icons/src/fmt-unlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/images/icons/src/game.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/heading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/iFrame.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/instagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/images/icons/src/left-align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/icons/src/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/middle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/images/icons/src/minus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/move-up-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/next.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/images/icons/src/publish.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/quote.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/republish.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/right-align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /public/images/icons/src/rotate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /public/images/icons/src/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/small-tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/table.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/images/icons/src/tick.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/top.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/images/icons/src/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/images/icons/src/view.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /spec/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /spec/e2e/drag_and_drop_helper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (function() { 4 | 5 | var SimulateDragDrop = function(elem, options) { 6 | this.options = options || {}; 7 | this.options.dataTransfer = Object.assign({ 8 | data: {}, 9 | setData: function(type, val) { 10 | this.data[type] = val; 11 | }, 12 | getData: function(type) { 13 | return this.data[type]; 14 | }, 15 | setDragImage: function(el, x, y) {} 16 | }, this.options.dataTransfer || {}); 17 | 18 | this.simulateEvent(elem); 19 | }; 20 | 21 | SimulateDragDrop.prototype.simulateEvent = function(elem) { 22 | var type, event; 23 | 24 | if (elem) { 25 | /*Simulating drag start*/ 26 | type = 'dragstart'; 27 | event = this.createEvent(type, this.options.dataTransfer); 28 | this.dispatchEvent(elem, type, event); 29 | } else { 30 | event = this.createEvent('custom', this.options.dataTransfer); 31 | } 32 | 33 | /*Simulating drop*/ 34 | type = 'drop'; 35 | var dropEvent = this.createEvent(type); 36 | this.dispatchEvent(this.options.dropTarget, type, dropEvent); 37 | 38 | if (elem) { 39 | /*Simulating drag end*/ 40 | type = 'dragend'; 41 | var dragEndEvent = this.createEvent(type); 42 | this.dispatchEvent(elem, type, dragEndEvent); 43 | } 44 | }; 45 | 46 | SimulateDragDrop.prototype.createEvent = function(type) { 47 | var event = document.createEvent("CustomEvent"); 48 | event.initCustomEvent(type, true, true, null); 49 | event.dataTransfer = this.options.dataTransfer; 50 | return event; 51 | }; 52 | 53 | SimulateDragDrop.prototype.dispatchEvent = function(elem, type, event) { 54 | if (elem.dispatchEvent) { 55 | elem.dispatchEvent(event); 56 | } else if(elem.fireEvent) { 57 | elem.fireEvent("on"+type, event); 58 | } 59 | }; 60 | 61 | window.simulateDragDrop = function(element, options) { 62 | new SimulateDragDrop(element, options); // jshint ignore:line 63 | }; 64 | 65 | })(); -------------------------------------------------------------------------------- /spec/javascripts/helpers/shims.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require('es5-shim'); 4 | // require('es6-shim'); // should be bundled into SirTrevor for now 5 | -------------------------------------------------------------------------------- /spec/javascripts/helpers/sir-trevor.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | global.SirTrevor = require('../../../'); 4 | 5 | global.createBaseElement = function() { 6 | var form = document.createElement("form"); 7 | var element = document.createElement("textarea"); 8 | form.appendChild(element); 9 | return element; 10 | }; 11 | -------------------------------------------------------------------------------- /spec/javascripts/units/block-manager/options.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("BlockManager::Options", function(){ 4 | 5 | var manager, mediator; 6 | 7 | beforeEach(function(){ 8 | mediator = Object.assign({}, SirTrevor.Events); 9 | }); 10 | 11 | describe("setting required blocks", function(){ 12 | 13 | beforeEach(function(){ 14 | managerWithOptions({ required: ['Text'] }); 15 | }); 16 | 17 | it("has an array of required blocks", function(){ 18 | expect(manager.required).not.toBe(false); 19 | }); 20 | 21 | it("has the options passed in the array", function(){ 22 | expect(manager.required).toContain('Text'); 23 | }); 24 | 25 | }); 26 | 27 | describe("setting available block types", function(){ 28 | 29 | beforeEach(function(){ 30 | managerWithOptions({ blockTypes: ['Text'] }); 31 | }); 32 | 33 | it("sets the object to the specified option", function(){ 34 | expect(manager.blockTypes).toEqual(["Text"]); 35 | }); 36 | 37 | it("won't be the default set of blocks", function(){ 38 | expect(manager.blockTypes).not.toBe(SirTrevor.Blocks); 39 | }); 40 | 41 | }); 42 | 43 | describe("setting the block type limits", function(){ 44 | 45 | beforeEach(function(){ 46 | managerWithOptions({ blockTypeLimits: { 'Text': 1 } }); 47 | }); 48 | 49 | it("sets the options to the specified value", function(){ 50 | expect(manager.options.blockTypeLimits.Text).toBe(1); 51 | }); 52 | 53 | }); 54 | 55 | describe("setting the block limit", function(){ 56 | 57 | beforeEach(function(){ 58 | managerWithOptions({ 59 | blockLimit: 1 60 | }); 61 | }); 62 | 63 | it("sets the limit to the specified option", function(){ 64 | expect(manager.options.blockLimit).toBe(1); 65 | }); 66 | 67 | }); 68 | 69 | function managerWithOptions(options) { 70 | var element = global.createBaseElement(); 71 | var editor = new SirTrevor.Editor(Object.assign({ 72 | el: element, 73 | blockTypes: ["Text"] 74 | }, options)); 75 | manager = editor.blockManager; 76 | } 77 | 78 | }); 79 | -------------------------------------------------------------------------------- /spec/javascripts/units/block-manager/removing-blocks.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("BlockManager::Removing blocks", function(){ 4 | 5 | var manager; 6 | 7 | beforeEach(function(){ 8 | var element = global.createBaseElement(); 9 | var editor = new SirTrevor.Editor({ 10 | el: element, 11 | blockTypes: ["Text"] 12 | }); 13 | manager = editor.blockManager; 14 | manager.createBlock('Text'); 15 | }); 16 | 17 | it("removes the block from the blocks array", function(){ 18 | manager.removeBlock(manager.blocks[0].blockID); 19 | expect(manager.blocks.length).toBe(0); 20 | }); 21 | 22 | it("decrements the block type count", function(){ 23 | manager.removeBlock(manager.blocks[0].blockID); 24 | expect(manager.blockCounts.Text).toBe(0); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /spec/javascripts/units/block-manager/validations.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("BlockManager::Validations", function(){ 4 | 5 | var manager; 6 | 7 | describe("required block types", function(){ 8 | 9 | beforeEach(function(){ 10 | var element = global.createBaseElement(); 11 | var editor = new SirTrevor.Editor({ 12 | el: element, 13 | blockTypes: ["Text"], 14 | defaultType: false, 15 | required: ['Text'] 16 | }); 17 | manager = editor.blockManager; 18 | spyOn(manager.mediator, 'trigger'); 19 | }); 20 | 21 | it("will emit an error if the block type is missing", function(){ 22 | manager.validateBlockTypesExist(true); 23 | expect(manager.mediator.trigger).toHaveBeenCalledWith('errors:add', 24 | { text : i18n.t("errors:type_missing", { type: "Text" }) }); 25 | }); 26 | 27 | xit("will error if a required block is empty", function(){ 28 | createBlock(); 29 | manager.validateBlockTypesExist(true); 30 | expect(manager.mediator.trigger).toHaveBeenCalledWith('errors:add', 31 | { text : i18n.t("errors:required_type_empty", { type: "Text" }) }); 32 | }); 33 | 34 | it("won't error if a required block has text", function(){ 35 | createBlock({ text: 'YOLO' }); 36 | manager.validateBlockTypesExist(true); 37 | expect(manager.mediator.trigger).not.toHaveBeenCalledWith('errors:add', 38 | { text : i18n.t("errors:required_type_empty", { type: "Text" }) }); 39 | }); 40 | 41 | }); 42 | 43 | function createBlock(data) { 44 | manager.createBlock('Text', data || {}); 45 | return manager.blocks[manager.blocks.length - 1]; 46 | } 47 | 48 | }); 49 | -------------------------------------------------------------------------------- /spec/javascripts/units/block/controllable.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("Controllable Block", function(){ 4 | 5 | var element, editor, block, testHandler; 6 | 7 | beforeEach(function(){ 8 | element = global.createBaseElement(); 9 | editor = new SirTrevor.Editor({ 10 | el: element, 11 | blockTypes: ["Text"] 12 | }); 13 | 14 | testHandler = jasmine.createSpy(); 15 | 16 | SirTrevor.Blocks.ControllableBlock = SirTrevor.Block.extend({ 17 | controllable: true, 18 | controls: { 19 | 'test': testHandler 20 | } 21 | }); 22 | 23 | block = new SirTrevor.Blocks.ControllableBlock({}, editor.ID, editor.mediator); 24 | }); 25 | 26 | afterEach(function(){ 27 | delete SirTrevor.Blocks.ControllableBlock; 28 | }); 29 | 30 | describe("render", function(){ 31 | 32 | beforeEach(function(){ 33 | spyOn(block, 'withMixin').and.callThrough(); 34 | block.render(); 35 | }); 36 | 37 | it("gets the controllable mixin", function(){ 38 | expect(block.withMixin) 39 | .toHaveBeenCalledWith(SirTrevor.BlockMixins.Controllable); 40 | }); 41 | 42 | it("adds an element to control_ui", function(){ 43 | expect(block.control_ui.querySelectorAll('.st-block-control-ui-btn').length) 44 | .toBe(1); 45 | }); 46 | 47 | xit("runs the handler on click", function(){ 48 | var event = new MouseEvent("click"); 49 | 50 | block.control_ui.querySelector('.st-block-control-ui-btn').dispatchEvent(event); 51 | expect(testHandler) 52 | .toHaveBeenCalled(); 53 | }); 54 | 55 | }); 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /spec/javascripts/units/block/droppable.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("Block:Droppable Block", function(){ 4 | 5 | var element, editor, block; 6 | 7 | beforeEach(function(){ 8 | element = global.createBaseElement(); 9 | editor = new SirTrevor.Editor({ el: element }); 10 | 11 | SirTrevor.Blocks.DroppableBlock = SirTrevor.Block.extend({ 12 | droppable: true 13 | }); 14 | 15 | block = new SirTrevor.Blocks.DroppableBlock({}, editor.ID, editor.mediator); 16 | }); 17 | 18 | afterEach(function(){ 19 | delete SirTrevor.Blocks.DroppableBlock; 20 | }); 21 | 22 | describe("render", function(){ 23 | 24 | beforeEach(function(){ 25 | spyOn(block, 'withMixin').and.callThrough(); 26 | block = block.render(); 27 | }); 28 | 29 | it("gets the droppable mixin", function(){ 30 | expect(block.withMixin) 31 | .toHaveBeenCalledWith(SirTrevor.BlockMixins.Droppable); 32 | }); 33 | 34 | it("adds a droppable class to inner", function(){ 35 | expect(block.inner.classList.contains('st-block__inner--droppable')); 36 | }); 37 | 38 | it("creates an inputs element", function(){ 39 | expect(block.inputs) 40 | .not.toBe(undefined); 41 | }); 42 | 43 | it("appends the html to the inputs element", function(){ 44 | expect(block.inputs.querySelectorAll('.st-block__dropzone').length) 45 | .toBe(1); 46 | }); 47 | 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /spec/javascripts/units/block/heading.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("Blocks: Heading block", function() { 4 | var block; 5 | 6 | var getSerializedData = function(data) { 7 | var element = global.createBaseElement(); 8 | var editor = new SirTrevor.Editor({ el: element }); 9 | var options = editor.blockManager.blockOptions; 10 | block = new SirTrevor.Blocks.Heading( 11 | data, 12 | editor.id, 13 | editor.mediator, 14 | options, 15 | editor.options 16 | ); 17 | block.render(); 18 | return block.getBlockData(); 19 | }; 20 | 21 | it("doesn't allow block level elements", function() { 22 | getSerializedData({ text: "Test Heading" }); 23 | expect(block._scribe.options.allowBlockElements).toBe(false); 24 | }); 25 | 26 | it("doesn't wrap content in

tags", function() { 27 | var data = getSerializedData({ text: "Test Heading" }); 28 | expect(data.text).not.toContain("

"); 29 | }); 30 | 31 | it("doesn't save
at the end of the text", function() { 32 | var data = getSerializedData({ text: "Test Heading" }); 33 | expect(data.text).toEqual("Test Heading"); 34 | }); 35 | 36 | it("converts markdown inline styling to html", function() { 37 | var data = getSerializedData({ text: "**Test** _Heading_" }); 38 | expect(data.text).toEqual("Test Heading"); 39 | }); 40 | 41 | it("doesn't strip HTML style tags", function() { 42 | var blockData = { text: "Test Heading", format: "html" }; 43 | var data = getSerializedData(blockData); 44 | expect(data.text).toEqual("Test Heading"); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /spec/javascripts/units/block/pastable.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("Block:Pastable Block", function(){ 4 | 5 | var element, editor, block; 6 | 7 | beforeEach(function(){ 8 | element = global.createBaseElement(); 9 | editor = new SirTrevor.Editor({ el: element }); 10 | 11 | SirTrevor.Blocks.PastableBlock = SirTrevor.Block.extend({ 12 | pastable: true 13 | }); 14 | 15 | block = new SirTrevor.Blocks.PastableBlock({}, editor.ID, editor.mediator); 16 | }); 17 | 18 | afterEach(function(){ 19 | delete SirTrevor.Blocks.PastableBlock; 20 | }); 21 | 22 | describe("render", function(){ 23 | 24 | beforeEach(function(){ 25 | spyOn(block, 'withMixin').and.callThrough(); 26 | block = block.render(); 27 | }); 28 | 29 | it("gets the pastable mixin", function(){ 30 | expect(block.withMixin) 31 | .toHaveBeenCalledWith(SirTrevor.BlockMixins.Pastable); 32 | }); 33 | 34 | it("creates an inputs element", function(){ 35 | expect(block.inputs).not.toBe(undefined); 36 | }); 37 | 38 | it("appends the html to the inputs element", function(){ 39 | expect(block.inputs.querySelectorAll('.st-paste-block').length) 40 | .toBe(1); 41 | }); 42 | 43 | }); 44 | 45 | }); 46 | -------------------------------------------------------------------------------- /spec/javascripts/units/block/store.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("Block:Store", function(){ 4 | 5 | var element, editor, block; 6 | 7 | beforeEach(function(){ 8 | element = global.createBaseElement(); 9 | editor = new SirTrevor.Editor({ el: element }); 10 | }); 11 | 12 | describe("create", function(){ 13 | 14 | beforeEach(function(){ 15 | block = new SirTrevor.Blocks.Text({ text: 'Test' }, editor.ID, editor.mediator); 16 | }); 17 | 18 | it("creates the data in the block store", function(){ 19 | expect(block.blockStorage).toBeDefined(); 20 | }); 21 | 22 | it("should have the data provided on intialization", function(){ 23 | expect(block.blockStorage.data.text).toBe('Test'); 24 | }); 25 | 26 | }); 27 | 28 | describe("_getData", function(){ 29 | 30 | beforeEach(function(){ 31 | block = new SirTrevor.Blocks.Text({ text: 'Test' }, editor.ID, editor.mediator); 32 | }); 33 | 34 | it("should retrieve the data", function(){ 35 | expect(block._getData().text).toBe("Test"); 36 | }); 37 | 38 | }); 39 | 40 | describe("setData", function(){ 41 | 42 | beforeEach(function(){ 43 | block = new SirTrevor.Blocks.Text({ text: 'Test' }, editor.ID, editor.mediator); 44 | }); 45 | 46 | it("should set the data on the block", function(){ 47 | block.setData({ text: "Boom" }); 48 | expect(block._getData().text).toBe("Boom"); 49 | }); 50 | 51 | it("should save the data, even if none is given", function(){ 52 | block.setData({ text: '' }); 53 | expect(block._getData().text).toBe(''); 54 | }); 55 | 56 | }); 57 | 58 | describe("save", function(){ 59 | 60 | beforeEach(function() { 61 | block = new SirTrevor.Blocks.Text({ text: 'Test' }, editor.ID, editor.mediator); 62 | spyOn(block, '_serializeData'); 63 | }); 64 | 65 | it("calls _serializeData on save", function(){ 66 | block.save(); 67 | expect(block._serializeData).toHaveBeenCalled(); 68 | }); 69 | 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /spec/javascripts/units/block/uploadable.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("Block:Uploadable Block", function(){ 4 | 5 | var element, editor, block; 6 | 7 | beforeEach(function(){ 8 | element = global.createBaseElement(); 9 | editor = new SirTrevor.Editor({ el: element }); 10 | 11 | SirTrevor.Blocks.UploadableBlock = SirTrevor.Block.extend({ 12 | uploadable: true 13 | }); 14 | 15 | block = new SirTrevor.Blocks.UploadableBlock({}, editor.ID, editor.mediator); 16 | }); 17 | 18 | afterEach(function(){ 19 | delete SirTrevor.Blocks.UploadableBlock; 20 | }); 21 | 22 | describe("render", function(){ 23 | 24 | beforeEach(function(){ 25 | spyOn(block, 'withMixin').and.callThrough(); 26 | 27 | block = block.render(); 28 | }); 29 | 30 | it("gets the uploadable mixin", function(){ 31 | expect(block.withMixin) 32 | .toHaveBeenCalledWith(SirTrevor.BlockMixins.Uploadable); 33 | }); 34 | 35 | it("creates an inputs element", function(){ 36 | expect(block.inputs) 37 | .not.toBe(undefined); 38 | }); 39 | 40 | it("appends the html to the inputs element", function(){ 41 | expect(block.inputs.querySelectorAll('.st-block__upload-container').length) 42 | .toBe(1); 43 | }); 44 | 45 | }); 46 | 47 | }); 48 | -------------------------------------------------------------------------------- /spec/javascripts/units/block/validation.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("Block:Validation", function(){ 4 | var element, editor, block; 5 | 6 | beforeEach(function(){ 7 | element = global.createBaseElement(); 8 | editor = new SirTrevor.Editor({ el: element }); 9 | block = new SirTrevor.Blocks.Text({}, editor.ID, editor.mediator); 10 | }); 11 | 12 | describe("valid", function(){ 13 | 14 | beforeEach(function(){ 15 | block.performValidations = function(){}; 16 | 17 | spyOn(block, "performValidations"); 18 | }); 19 | 20 | it("will return true if there are no errors", function(){ 21 | expect(block.valid()).toBe(true); 22 | }); 23 | 24 | it("will return false if there are errors", function(){ 25 | block.errors.push(1); 26 | expect(block.valid()).toBe(false); 27 | }); 28 | 29 | it("will call the performValidations method on calling valid", function(){ 30 | block.valid(); 31 | expect(block.performValidations).toHaveBeenCalled(); 32 | }); 33 | 34 | }); 35 | 36 | describe("performValidations", function(){ 37 | 38 | beforeEach(function(){ 39 | block.validations = ['testValidator']; 40 | block.testValidator = function(){}; 41 | block.resetErrors = function(){}; 42 | spyOn(block, "testValidator"); 43 | spyOn(block, "resetErrors"); 44 | 45 | block.performValidations(); 46 | }); 47 | 48 | it("will call any custom validators in the validations array", function(){ 49 | expect(block.testValidator).toHaveBeenCalled(); 50 | }); 51 | 52 | it("will call resetErrors", function(){ 53 | expect(block.resetErrors).toHaveBeenCalled(); 54 | }); 55 | 56 | }); 57 | 58 | describe("validateField", function(){ 59 | 60 | var field; 61 | 62 | beforeEach(function(){ 63 | field = document.createElement("input"); 64 | block.setError = function(field, reason){}; 65 | spyOn(block, "setError"); 66 | }); 67 | 68 | it("will call setError if the field has no content", function(){ 69 | block.validateField(field); 70 | expect(block.setError).toHaveBeenCalled(); 71 | }); 72 | 73 | it("will won't call setError if the field has content", function(){ 74 | field.value = "Test"; 75 | block.validateField(field); 76 | expect(block.setError).not.toHaveBeenCalled(); 77 | }); 78 | 79 | }); 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /spec/javascripts/units/block/video.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var utils = require('../../../../src/utils'); 4 | 5 | describe('Blocks: Video block', function() { 6 | 7 | var createBlock = function(type, data) { 8 | var element = global.createBaseElement(); 9 | var editor = new SirTrevor.Editor({ el: element }); 10 | var options = editor.blockManager.blockOptions; 11 | var Klass = SirTrevor.Blocks[utils.classify(type)]; 12 | var block = new Klass(data, editor.id, editor.mediator, options); 13 | editor.blockManager.renderBlock(block); 14 | 15 | return block; 16 | }; 17 | 18 | describe('initialize', function() { 19 | it('creates a video block', function() { 20 | var block = createBlock('video'); 21 | expect(block).not.toBe(undefined); 22 | }); 23 | 24 | it('test URL formats', function() { 25 | 26 | var urls = { 27 | youtube: [ 28 | 'http://www.youtube.com/watch?v=-wtIMTCHWuI', 29 | 'http://www.youtube.com/v/-wtIMTCHWuI?version=3&autohide=1', 30 | 'http://youtu.be/-wtIMTCHWuI', 31 | 'https://www.youtube.com/watch?v=-wtIMTCHWuI&feature=youtu.be', 32 | 'https://youtu.be/-wtIMTCHWuI?t=35', 33 | 'http://www.youtube.com/v/PjDw3azfZWI&hl=en_US&start=1868' 34 | ], 35 | vimeo: [ 36 | 'https://vimeo.com/11111111', 37 | 'http://vimeo.com/11111111', 38 | 'https://www.vimeo.com/11111111', 39 | 'http://www.vimeo.com/11111111', 40 | 'https://vimeo.com/channels/11111111', 41 | 'http://vimeo.com/channels/11111111', 42 | 'https://vimeo.com/groups/name/videos/11111111', 43 | 'http://vimeo.com/groups/name/videos/11111111', 44 | 'https://vimeo.com/album/2222222/video/11111111', 45 | 'http://vimeo.com/album/2222222/video/11111111', 46 | 'https://vimeo.com/11111111?param=test', 47 | 'http://vimeo.com/11111111?param=test' 48 | ] 49 | }; 50 | 51 | var block = createBlock('video', urls[0]); 52 | 53 | for (var provider in block.providers) { 54 | if (typeof urls[provider] !== 'undefined') { 55 | var regex = block.providers[provider].regex; 56 | for (var i = 0; i < urls[provider].length; i++) { 57 | expect(urls[provider][i]).toMatch(regex); 58 | } 59 | } 60 | } 61 | 62 | }); 63 | 64 | }); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /spec/javascripts/units/block_controls.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("BlockControls", function(){ 4 | 5 | describe("creating a new instance", function(){ 6 | 7 | var blockControls, editor; 8 | 9 | beforeEach(function(){ 10 | var element = global.createBaseElement(); 11 | editor = new SirTrevor.Editor({ 12 | el: element 13 | }); 14 | blockControls = editor.blockControls; 15 | }); 16 | 17 | it("can be created", function(){ 18 | expect(blockControls).toBeDefined(); 19 | }); 20 | 21 | it("creates an el", function(){ 22 | expect(blockControls.el).toBeDefined(); 23 | }); 24 | 25 | it("sets the available types", function(){ 26 | 27 | editor.blockManager.blockTypes.forEach(function(blockType){ 28 | 29 | if (SirTrevor.Blocks[blockType].prototype.toolbarEnabled) { 30 | expect( 31 | blockControls.el.querySelector("[data-type=" + blockType.toLowerCase() + "]") 32 | ).not.toBe(null); 33 | } else { 34 | expect( 35 | blockControls.el.querySelector("[data-type=" + blockType.toLowerCase() + "]") 36 | ).toBe(null); 37 | } 38 | }); 39 | }); 40 | 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /spec/javascripts/units/block_positioner.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("BlockPositioner", function(){ 4 | 5 | var positioner, positionerSelect, el, mediator; 6 | 7 | beforeEach(function() { 8 | el = document.createElement("div"); 9 | mediator = Object.assign({}, SirTrevor.Events); 10 | positioner = new SirTrevor.BlockPositioner(el, mediator); 11 | positionerSelect = new SirTrevor.BlockPositionerSelect(mediator); 12 | positionerSelect.positioner = positioner; 13 | }); 14 | 15 | describe("onSelectChange", function() { 16 | beforeEach(function() { 17 | spyOn(positioner.mediator, "trigger"); 18 | }); 19 | 20 | it("triggers a block:changePosition when the select value isn't 0", function() { 21 | positionerSelect.select = {value: 2}; 22 | positionerSelect.onSelectChange(); 23 | 24 | expect(positionerSelect.mediator.trigger) 25 | .toHaveBeenCalledWith("block:changePosition", el, 2, 'after'); 26 | }); 27 | 28 | it("triggers a block:changePosition with the type set as before when the value is 1", function() { 29 | positionerSelect.select = {value: 1}; 30 | positionerSelect.onSelectChange(); 31 | 32 | expect(positionerSelect.mediator.trigger) 33 | .toHaveBeenCalledWith("block:changePosition", el, 1, 'before'); 34 | }); 35 | }); 36 | 37 | }); 38 | -------------------------------------------------------------------------------- /spec/javascripts/units/editor/options.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("Editor:Editor with options", function(){ 4 | 5 | var element, editor; 6 | 7 | beforeEach(function(){ 8 | SirTrevor.config.instances = []; 9 | element = global.createBaseElement(); 10 | }); 11 | 12 | describe("setting the block limit", function(){ 13 | 14 | beforeEach(function(){ 15 | editor = new SirTrevor.Editor({ 16 | el: element, 17 | blockLimit: 1 18 | }); 19 | }); 20 | 21 | it("sets the limit to the specified option", function(){ 22 | expect(editor.options.blockLimit).toBe(1); 23 | }); 24 | 25 | }); 26 | 27 | describe("setting the defaultType", function(){ 28 | 29 | beforeEach(function(){ 30 | editor = new SirTrevor.Editor({ 31 | el: element, 32 | defaultType: 'Text' 33 | }); 34 | spyOn(editor.mediator, 'trigger'); 35 | }); 36 | 37 | it("is not false", function(){ 38 | expect(editor.options.defaultType).not.toBe(false); 39 | }); 40 | 41 | it("creates a default block of a type specified", function(){ 42 | editor.createBlocks(); 43 | expect(editor.mediator.trigger).toHaveBeenCalledWith( 44 | 'block:create', 'Text', {}); 45 | }); 46 | 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /spec/javascripts/units/editor/submission.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("Editor:Submission", function(){ 4 | 5 | var element, editor; 6 | 7 | beforeEach(function(){ 8 | SirTrevor.instances = []; 9 | element = global.createBaseElement(); 10 | editor = new SirTrevor.Editor({ 11 | el: element, defaultType: false 12 | }); 13 | }); 14 | 15 | it("calls reset and save on the store", function(){ 16 | spyOn(editor.store, "reset"); 17 | editor.onFormSubmit(); 18 | expect(editor.store.reset).toHaveBeenCalled(); 19 | }); 20 | 21 | it("calls the validateBlocks method", function(){ 22 | spyOn(editor, "validateBlocks"); 23 | editor.onFormSubmit(); 24 | expect(editor.validateBlocks).toHaveBeenCalled(); 25 | }); 26 | 27 | it("calls the validateBlockTypesExist method", function(){ 28 | spyOn(editor.blockManager, "validateBlockTypesExist"); 29 | editor.onFormSubmit(); 30 | expect(editor.blockManager.validateBlockTypesExist).toHaveBeenCalled(); 31 | }); 32 | 33 | it("calls toString on the store", function(){ 34 | spyOn(editor.store, "toString"); 35 | editor.onFormSubmit(); 36 | // Store gets called twice 37 | expect(editor.store.toString).toHaveBeenCalled(); 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /spec/javascripts/units/format_bar.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("FormatBar", function(){ 4 | }); 5 | -------------------------------------------------------------------------------- /spec/javascripts/units/sir-trevor.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | describe("SirTrevor", function(){ 4 | 5 | describe("setBlockOptions", function(){ 6 | 7 | var block; 8 | 9 | beforeEach(function(){ 10 | SirTrevor.Blocks.Test = SirTrevor.Block.extend({ 11 | test: true 12 | }); 13 | 14 | SirTrevor.setBlockOptions("Test", { test: false }); 15 | }); 16 | 17 | afterEach(function(){ 18 | delete SirTrevor.Blocks.Test; 19 | }); 20 | 21 | it("can set an option on a specific block", function(){ 22 | expect(SirTrevor.Blocks.Test.prototype.test).toBe(false); 23 | }); 24 | 25 | it("has the property set when we instantiate a block", function(){ 26 | block = new SirTrevor.Blocks.Test(); 27 | expect(block.test).toBe(false); 28 | }); 29 | 30 | }); 31 | 32 | describe("getInstance", function(){ 33 | 34 | beforeEach(function(){ 35 | SirTrevor.config.instances = [ 36 | { ID: '123' }, 37 | { ID: '456' } 38 | ]; 39 | }); 40 | 41 | it("retrieves the first instance if no params are given", function(){ 42 | var instance = SirTrevor.getInstance(); 43 | expect(instance.ID).toBe('123'); 44 | }); 45 | 46 | it("retrieves the instance by ID if a string is provided", function(){ 47 | var instance = SirTrevor.getInstance('456'); 48 | expect(instance.ID).toBe('456'); 49 | }); 50 | 51 | it("retrieves the instance by position if an integer is provided", function(){ 52 | var instance = SirTrevor.getInstance(0); 53 | expect(instance.ID).toBe('123'); 54 | }); 55 | 56 | }); 57 | 58 | }); 59 | -------------------------------------------------------------------------------- /spec/javascripts/units/submittable.spec.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var utils = require('../../../src/packages/dom'); 4 | 5 | describe("SirTrevor.Submittable", function() { 6 | 7 | var submittable; 8 | var formTemplate = "

"; 9 | 10 | beforeEach(function() { 11 | submittable = new SirTrevor.Submittable(utils.createDocumentFragmentFromString(formTemplate)); 12 | }); 13 | 14 | describe("submitBtn", function() { 15 | 16 | it("should be an input btn", function() { 17 | expect(submittable.submitBtns[0].nodeName).toBe("INPUT"); 18 | }); 19 | 20 | }); 21 | 22 | describe("submitBtnTitles", function() { 23 | 24 | it("has the initial title for the submitBtn", function() { 25 | expect(submittable.submitBtnTitles).toContain("Go!"); 26 | }); 27 | 28 | }); 29 | 30 | describe("_disableSubmitButton", function() { 31 | 32 | beforeEach(function(){ 33 | submittable._disableSubmitButton(); 34 | }); 35 | 36 | it("should set a disabled attribute", function() { 37 | expect(submittable.submitBtns[0].getAttribute('disabled')).toBe('disabled'); 38 | }); 39 | 40 | it("should set a disabled class", function() { 41 | expect(submittable.submitBtns[0].classList.contains('disabled')).toBe(true); 42 | }); 43 | 44 | }); 45 | 46 | describe("_enableSubmitButton", function() { 47 | 48 | beforeEach(function(){ 49 | submittable._disableSubmitButton(); 50 | submittable._enableSubmitButton(); 51 | }); 52 | 53 | it("shouldn't set a disabled attribute", function() { 54 | expect(submittable.submitBtns[0].getAttribute('disabled')).toBe(null); 55 | }); 56 | 57 | it("shouldn't have a disabled class", function() { 58 | expect(submittable.submitBtns[0].classList.contains('disabled')).toBe(false); 59 | }); 60 | 61 | }); 62 | 63 | describe("setSubmitButton", function() { 64 | 65 | it("Adds the title provided", function() { 66 | submittable.setSubmitButton(null, "YOLO"); 67 | expect(submittable.submitBtns[0].value).toBe('YOLO'); 68 | }); 69 | 70 | }); 71 | 72 | 73 | describe("resetSubmitButton", function() { 74 | 75 | beforeEach(function() { 76 | submittable.setSubmitButton(null, "YOLO"); 77 | submittable.resetSubmitButton(); 78 | }); 79 | 80 | it("should reset the title back to its previous state", function() { 81 | expect(submittable.submitBtns[0].value).toContain("Go!"); 82 | }); 83 | 84 | }); 85 | 86 | }); 87 | -------------------------------------------------------------------------------- /src/block-addition-top.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * SirTrevor Block Controls 5 | * -- 6 | * Gives an interface for adding new Sir Trevor blocks. 7 | */ 8 | 9 | const Events = require("./packages/events"); 10 | 11 | module.exports.create = function(SirTrevor) { 12 | 13 | function createBlock(e) { 14 | // REFACTOR: mediator so that we can trigger events directly on instance? 15 | // REFACTOR: block create event expects data as second argument. 16 | /*jshint validthis:true */ 17 | SirTrevor.mediator.trigger( 18 | "block:create", SirTrevor.options.defaultType || "Text", null, this.parentNode.parentNode.previousSibling, { autoFocus: true } 19 | ); 20 | } 21 | 22 | function hide() {} 23 | 24 | // Public 25 | function destroy() { 26 | SirTrevor = null; 27 | } 28 | 29 | Events.delegate( 30 | SirTrevor.wrapper, ".st-block-addition-top__button", "click", createBlock 31 | ); 32 | 33 | Events.delegate( 34 | SirTrevor.wrapper, ".st-block-addition-top__icon", "click", createBlock 35 | ); 36 | 37 | return {destroy, hide}; 38 | }; 39 | -------------------------------------------------------------------------------- /src/block-addition.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * SirTrevor Block Controls 5 | * -- 6 | * Gives an interface for adding new Sir Trevor blocks. 7 | */ 8 | 9 | const dropEvents = require('./helpers/drop-events'); 10 | 11 | const EventBus = require('./event-bus'); 12 | 13 | const Dom = require('./packages/dom'); 14 | const Events = require("./packages/events"); 15 | 16 | const TOP_CONTROLS_TEMPLATE = require("./templates/top-controls"); 17 | 18 | module.exports.create = function(SirTrevor) { 19 | 20 | function createBlock(e) { 21 | // REFACTOR: mediator so that we can trigger events directly on instance? 22 | // REFACTOR: block create event expects data as second argument. 23 | /*jshint validthis:true */ 24 | SirTrevor.mediator.trigger( 25 | "block:create", SirTrevor.options.defaultType || "Text", null, this.parentNode.parentNode.id ? this.parentNode.parentNode : this.parentNode 26 | ); 27 | } 28 | 29 | function hide() {} 30 | 31 | // Public 32 | function destroy() { 33 | SirTrevor = null; 34 | } 35 | 36 | SirTrevor.wrapper.insertAdjacentHTML("beforeend", TOP_CONTROLS_TEMPLATE()); 37 | 38 | const topControls = SirTrevor.wrapper.querySelector('.st-top-controls'); 39 | 40 | function onDrop(ev) { 41 | ev.preventDefault(); 42 | 43 | var dropped_on = topControls, 44 | item_id = ev.dataTransfer.getData("text/plain"), 45 | block = document.querySelector('#' + item_id); 46 | 47 | if (!!item_id, !!block, dropped_on.id !== item_id) { 48 | Dom.insertAfter(block, dropped_on); 49 | } 50 | SirTrevor.mediator.trigger("block:rerender", item_id); 51 | EventBus.trigger("block:reorder:dropped", item_id); 52 | } 53 | 54 | dropEvents.dropArea(topControls); 55 | topControls.addEventListener('drop', onDrop); 56 | 57 | Events.delegate( 58 | SirTrevor.wrapper, ".st-block-addition", "click", createBlock 59 | ); 60 | 61 | return {destroy, hide}; 62 | }; 63 | -------------------------------------------------------------------------------- /src/block-deletion.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var config = require('./config'); 4 | 5 | var BlockDeletion = function() { 6 | this._ensureElement(); 7 | this._bindFunctions(); 8 | }; 9 | 10 | Object.assign(BlockDeletion.prototype, require('./function-bind'), require('./renderable'), { 11 | 12 | tagName: 'a', 13 | className: 'st-block-ui-btn__delete', 14 | 15 | attributes: { 16 | html: () => ` 17 | 18 | `, 19 | 'data-icon': 'close' 20 | } 21 | 22 | }); 23 | 24 | module.exports = BlockDeletion; 25 | -------------------------------------------------------------------------------- /src/block-positioner-select.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Dom = require('./packages/dom'); 4 | 5 | var template = [ 6 | "", 7 | "" 8 | ].join("\n"); 9 | 10 | var BlockPositionerSelect = function(mediator) { 11 | this.mediator = mediator; 12 | 13 | this._ensureElement(); 14 | this._bindFunctions(); 15 | 16 | this.initialize(); 17 | }; 18 | 19 | Object.assign(BlockPositionerSelect.prototype, require('./function-bind'), require('./renderable'), { 20 | 21 | total_blocks: 0, 22 | 23 | bound: ['onBlockCountChange', 'onSelectChange'], 24 | 25 | className: 'st-block-positioner__inner', 26 | 27 | initialize: function(){ 28 | this.el.insertAdjacentHTML("beforeend", template); 29 | this.select = this.el.querySelector('.st-block-positioner__select'); 30 | this.positioner = null; 31 | 32 | this.select.addEventListener('change', this.onSelectChange); 33 | }, 34 | 35 | onBlockCountChange: function(new_count) { 36 | if (new_count !== this.total_blocks) { 37 | this.total_blocks = new_count; 38 | this.renderPositionList(); 39 | } 40 | }, 41 | 42 | onSelectChange: function() { 43 | var val = this.select.value; 44 | if (val !== 0) { 45 | this.mediator.trigger( 46 | "block:changePosition", this.positioner.block, val, 47 | (val === 1 ? 'before' : 'after')); 48 | this.positioner.toggle(); 49 | } 50 | }, 51 | 52 | renderPositionList: function() { 53 | var inner = ""; 54 | for(var i = 1; i <= this.total_blocks; i++) { 55 | inner += ""; 56 | } 57 | this.select.innerHTML = inner; 58 | }, 59 | 60 | renderInBlock: function(positioner) { 61 | // hide old 62 | if (this.positioner && this.positioner !== positioner) { 63 | this.positioner.hide(); 64 | } 65 | 66 | // add new 67 | this.positioner = positioner; 68 | this.select.value = 0; 69 | Dom.remove(this.el); 70 | positioner.el.appendChild(this.el); 71 | } 72 | 73 | }); 74 | 75 | module.exports = BlockPositionerSelect; 76 | -------------------------------------------------------------------------------- /src/block-positioner.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var BlockPositioner = function(block, mediator) { 4 | this.mediator = mediator; 5 | this.block = block; 6 | 7 | this._ensureElement(); 8 | this._bindFunctions(); 9 | 10 | this.initialize(); 11 | }; 12 | 13 | Object.assign(BlockPositioner.prototype, require('./function-bind'), require('./renderable'), { 14 | 15 | bound: ['toggle', 'show', 'hide'], 16 | 17 | className: 'st-block-positioner', 18 | visibleClass: 'active', 19 | 20 | initialize: function(){}, 21 | 22 | toggle: function() { 23 | this.mediator.trigger('block-positioner-select:render', this); 24 | this.el.classList.toggle(this.visibleClass); 25 | }, 26 | 27 | show: function(){ 28 | this.el.classList.add(this.visibleClass); 29 | }, 30 | 31 | hide: function(){ 32 | this.el.classList.remove(this.visibleClass); 33 | } 34 | 35 | }); 36 | 37 | module.exports = BlockPositioner; 38 | -------------------------------------------------------------------------------- /src/block-store.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('./lodash'); 4 | var utils = require('./utils'); 5 | 6 | var EventBus = require('./event-bus'); 7 | 8 | module.exports = { 9 | 10 | /** 11 | * Internal storage object for the block 12 | */ 13 | blockStorage: {}, 14 | 15 | /** 16 | * Initialize the store, including the block type 17 | */ 18 | createStore: function(blockData) { 19 | this.blockStorage = { 20 | type: utils.underscored(this.type), 21 | data: blockData || {} 22 | }; 23 | }, 24 | 25 | /** 26 | * Serialize the block and save the data into the store 27 | */ 28 | save: function() { 29 | var data = this._serializeData(); 30 | 31 | if (!_.isEmpty(data)) { 32 | this.setData(data); 33 | } 34 | }, 35 | 36 | getData: function() { 37 | this.save(); 38 | return this.blockStorage; 39 | }, 40 | 41 | getBlockData: function() { 42 | this.save(); 43 | return this.blockStorage.data; 44 | }, 45 | 46 | _getData: function() { 47 | return this.blockStorage.data; 48 | }, 49 | 50 | /** 51 | * Set the block data. 52 | * This is used by the save() method. 53 | */ 54 | setData: function(blockData) { 55 | utils.log("Setting data for block " + this.blockID); 56 | Object.assign(this.blockStorage.data, blockData || {}); 57 | }, 58 | 59 | setAndLoadData: function(blockData) { 60 | this.setData(blockData); 61 | this.beforeLoadingData(); 62 | }, 63 | 64 | _serializeData: function() {}, 65 | loadData: function() {}, 66 | 67 | beforeLoadingData: function() { 68 | utils.log("loadData for " + this.blockID); 69 | EventBus.trigger("editor/block/loadData"); 70 | this.loadData(this._getData()); 71 | }, 72 | 73 | _loadData: function() { 74 | utils.log("_loadData is deprecated and will be removed in the future. Please use beforeLoadingData instead."); 75 | this.beforeLoadingData(); 76 | }, 77 | 78 | checkAndLoadData: function() { 79 | if (!_.isEmpty(this._getData())) { 80 | this.beforeLoadingData(); 81 | } 82 | } 83 | 84 | }; 85 | -------------------------------------------------------------------------------- /src/block-validations.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('./lodash'); 4 | var utils = require('./utils'); 5 | 6 | var bestNameFromField = function(field) { 7 | var msg = field.getAttribute("data-st-name") || field.getAttribute("name"); 8 | 9 | if (!msg) { 10 | msg = 'Field'; 11 | } 12 | 13 | return utils.capitalize(msg); 14 | }; 15 | 16 | module.exports = { 17 | 18 | errors: [], 19 | 20 | valid: function(){ 21 | this.performValidations(); 22 | return this.errors.length === 0; 23 | }, 24 | 25 | // This method actually does the leg work 26 | // of running our validators and custom validators 27 | performValidations: function() { 28 | this.resetErrors(); 29 | 30 | var required_fields = this.$('.st-required'); 31 | Array.prototype.forEach.call(required_fields, function (f, i) { 32 | this.validateField(f); 33 | }.bind(this)); 34 | this.validations.forEach(this.runValidator, this); 35 | 36 | this.el.classList.toggle('st-block--with-errors', this.errors.length > 0); 37 | }, 38 | 39 | // Everything in here should be a function that returns true or false 40 | validations: [], 41 | 42 | validateField: function(field) { 43 | 44 | var content = field.getAttribute('contenteditable') ? field.textContent : field.value; 45 | 46 | if (content.length === 0) { 47 | this.setError(field, i18n.t("errors:block_empty", 48 | { name: bestNameFromField(field) })); 49 | } 50 | }, 51 | 52 | runValidator: function(validator) { 53 | if (!_.isUndefined(this[validator])) { 54 | this[validator].call(this); 55 | } 56 | }, 57 | 58 | setError: function(field, reason) { 59 | var msg = this.addMessage(reason, "st-msg--error"); 60 | field.classList.add('st-error'); 61 | 62 | this.errors.push({ field: field, reason: reason, msg: msg }); 63 | }, 64 | 65 | resetErrors: function() { 66 | this.errors.forEach(function(error){ 67 | error.field.classList.remove('st-error'); 68 | error.msg.remove(); 69 | }); 70 | 71 | this.messages.classList.remove("st-block__messages--is-visible"); 72 | this.errors = []; 73 | } 74 | 75 | }; 76 | -------------------------------------------------------------------------------- /src/block_mixins/ajaxable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var utils = require('../utils'); 4 | 5 | module.exports = { 6 | 7 | mixinName: "Ajaxable", 8 | 9 | ajaxable: true, 10 | 11 | initializeAjaxable: function(){ 12 | this._queued = []; 13 | }, 14 | 15 | addQueuedItem: function(name, deferred) { 16 | utils.log("Adding queued item for " + this.blockID + " called " + name); 17 | 18 | this._queued.push({ name: name, deferred: deferred }); 19 | }, 20 | 21 | removeQueuedItem: function(name) { 22 | utils.log("Removing queued item for " + this.blockID + " called " + name); 23 | 24 | this._queued = this._queued.filter(function(queued) { 25 | return queued.name !== name; 26 | }); 27 | }, 28 | 29 | hasItemsInQueue: function() { 30 | return this._queued.length > 0; 31 | }, 32 | 33 | resolveAllInQueue: function() { 34 | this._queued.forEach(function(item){ 35 | utils.log("Aborting queued request: " + item.name); 36 | item.deferred.cancel(); 37 | }, this); 38 | } 39 | 40 | }; 41 | -------------------------------------------------------------------------------- /src/block_mixins/controllable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var utils = require('../utils'); 4 | var config = require('../config'); 5 | var Dom = require('../packages/dom'); 6 | var Events = require('../packages/events'); 7 | 8 | module.exports = { 9 | 10 | mixinName: "Controllable", 11 | 12 | initializeControllable: function() { 13 | utils.log("Adding controllable to block " + this.blockID); 14 | this.inner.classList.add('st-block__inner--controllable'); 15 | this.control_ui = Dom.createElement('div', {'class': 'st-block__control-ui'}); 16 | Object.keys(this.controls).forEach( 17 | function(cmd) { 18 | // Bind configured handler to current block context 19 | this.addUiControl(cmd, this.controls[cmd].bind(this)); 20 | }, 21 | this 22 | ); 23 | this.inner.appendChild(this.control_ui); 24 | }, 25 | 26 | getControlTemplate: function(cmd) { 27 | return Dom.createElement("a", { 28 | 'data-icon': cmd, 29 | 'class': 'st-icon st-block-control-ui-btn st-block-control-ui-btn--' + cmd, 30 | 'html': ` 31 | 32 | ` 33 | }); 34 | }, 35 | 36 | addUiControl: function(cmd, handler) { 37 | this.control_ui.appendChild(this.getControlTemplate(cmd)); 38 | Events.delegate(this.control_ui, '.st-block-control-ui-btn--' + cmd, 'click', (e) => { 39 | this.selectUiControl(cmd); 40 | handler(e); 41 | }); 42 | }, 43 | 44 | selectUiControl: function(cmd) { 45 | var selectedClass = 'st-block-control-ui-btn--selected'; 46 | Object.keys(this.controls).forEach(control => { 47 | this.getControlUiBtn(control).classList.remove(selectedClass); 48 | }); 49 | this.getControlUiBtn(cmd).classList.add(selectedClass); 50 | }, 51 | 52 | getControlUiBtn: function(cmd) { 53 | return this.control_ui.querySelector('.st-block-control-ui-btn--' + cmd); 54 | } 55 | }; 56 | -------------------------------------------------------------------------------- /src/block_mixins/fetchable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('../lodash'); 4 | var Ajax = require('../packages/ajax'); 5 | 6 | module.exports = { 7 | 8 | mixinName: "Fetchable", 9 | 10 | initializeFetchable: function(){ 11 | this.withMixin(require('./ajaxable')); 12 | }, 13 | 14 | fetch: function(url, options, success, failure){ 15 | var uid = _.uniqueId(this.blockID + "_fetch"), 16 | xhr = Ajax.fetch(url, options); 17 | 18 | this.resetMessages(); 19 | this.addQueuedItem(uid, xhr); 20 | 21 | function alwaysFunc(func, arg) { 22 | /*jshint validthis: true */ 23 | func.call(this, arg); 24 | this.removeQueuedItem(uid); 25 | } 26 | 27 | if(!_.isUndefined(success)) { 28 | xhr.then(alwaysFunc.bind(this, success)); 29 | } 30 | 31 | if(!_.isUndefined(failure)) { 32 | xhr.catch(alwaysFunc.bind(this, failure)); 33 | } 34 | 35 | return xhr; 36 | } 37 | 38 | }; 39 | -------------------------------------------------------------------------------- /src/block_mixins/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | Ajaxable: require('./ajaxable.js'), 5 | Controllable: require('./controllable.js'), 6 | Droppable: require('./droppable.js'), 7 | Fetchable: require('./fetchable.js'), 8 | Pastable: require('./pastable.js'), 9 | Uploadable: require('./uploadable.js'), 10 | MultiEditable: require('./multi-editable.js'), 11 | Textable: require('./textable.js') 12 | }; 13 | -------------------------------------------------------------------------------- /src/block_mixins/pastable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('../lodash'); 4 | var config = require('../config'); 5 | var utils = require('../utils'); 6 | 7 | module.exports = { 8 | 9 | mixinName: "Pastable", 10 | requireInputs: true, 11 | 12 | initializePastable: function() { 13 | utils.log("Adding pastable to block " + this.blockID); 14 | 15 | this.paste_options = Object.assign( 16 | {}, config.defaults.Block.paste_options, this.paste_options); 17 | 18 | this.inputs.insertAdjacentHTML("beforeend", _.template(this.paste_options.html, this)); 19 | 20 | Array.prototype.forEach.call(this.$('.st-paste-block'), (el) => { 21 | el.addEventListener('click', function() { 22 | var event = document.createEvent('HTMLEvents'); 23 | event.initEvent('select', true, false); 24 | this.dispatchEvent(event); 25 | }); 26 | el.addEventListener('paste', this._handleContentPaste); 27 | el.addEventListener('submit', this._handleContentPaste); 28 | }); 29 | } 30 | 31 | }; 32 | -------------------------------------------------------------------------------- /src/block_mixins/uploadable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('../lodash'); 4 | var config = require('../config'); 5 | var utils = require('../utils'); 6 | 7 | var fileUploader = require('../extensions/file-uploader'); 8 | 9 | module.exports = { 10 | 11 | mixinName: "Uploadable", 12 | 13 | uploadsCount: 0, 14 | requireInputs: true, 15 | 16 | initializeUploadable: function() { 17 | utils.log("Adding uploadable to block " + this.blockID); 18 | this.withMixin(require('./ajaxable')); 19 | 20 | this.upload_options = Object.assign({}, config.defaults.Block.upload_options, this.upload_options); 21 | this.inputs.insertAdjacentHTML("beforeend", _.template(this.upload_options.html, this)); 22 | 23 | Array.prototype.forEach.call(this.inputs.querySelectorAll('button'), function(button) { 24 | button.addEventListener('click', function(ev){ ev.preventDefault(); }); 25 | }); 26 | Array.prototype.forEach.call(this.inputs.querySelectorAll('input'), function(input) { 27 | input.addEventListener('change', (function(ev) { 28 | this.onDrop(ev.currentTarget); 29 | }).bind(this)); 30 | }.bind(this)); 31 | }, 32 | 33 | uploader: function(file, success, failure){ 34 | return fileUploader(this, file, success, failure); 35 | } 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /src/blocks/heading.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | Heading Block 5 | */ 6 | 7 | var Block = require('../block'); 8 | var stToHTML = require('../to-html'); 9 | 10 | var ScribeTextBlockPlugin = require('./scribe-plugins/scribe-text-block-plugin'); 11 | var ScribeQuotePlugin = require('./scribe-plugins/scribe-quote-plugin'); 12 | var ScribeHeadingPlugin = require('./scribe-plugins/scribe-heading-plugin'); 13 | 14 | module.exports = Block.extend({ 15 | 16 | type: 'heading', 17 | 18 | editorHTML: '

', 19 | 20 | configureScribe: function(scribe) { 21 | scribe.use(new ScribeHeadingPlugin(this)); 22 | scribe.use(new ScribeTextBlockPlugin(this)); 23 | scribe.use(new ScribeQuotePlugin(this)); 24 | 25 | scribe.on('content-changed', this.toggleEmptyClass.bind(this)); 26 | }, 27 | 28 | mergeable: true, 29 | textable: true, 30 | toolbarEnabled: false, 31 | 32 | scribeOptions: { 33 | allowBlockElements: false, 34 | tags: { 35 | p: false 36 | } 37 | }, 38 | 39 | icon_name: 'heading', 40 | 41 | loadData: function(data) { 42 | if (this.options.convertFromMarkdown && data.format !== "html") { 43 | this.setTextBlockHTML(stToHTML(data.text, this.type)); 44 | } else { 45 | this.setTextBlockHTML(data.text); 46 | } 47 | 48 | const level = data.level || this.editorOptions.defaultHeadingLevel; 49 | 50 | this.setData({ level }); 51 | this.el.dataset.level = level; 52 | }, 53 | 54 | onBlockRender: function() { 55 | this.toggleEmptyClass(); 56 | }, 57 | 58 | toggleEmptyClass: function() { 59 | this.el.classList.toggle('st-block--empty', this._scribe.getTextContent().length === 0); 60 | }, 61 | 62 | asClipboardHTML: function() { 63 | var data = this.getBlockData(); 64 | return `${data.text}`; 65 | } 66 | }); 67 | -------------------------------------------------------------------------------- /src/blocks/image.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Dom = require('../packages/dom'); 4 | var Block = require('../block'); 5 | 6 | module.exports = Block.extend({ 7 | 8 | type: "image", 9 | 10 | droppable: true, 11 | uploadable: true, 12 | 13 | icon_name: 'image', 14 | 15 | loadData: function(data){ 16 | // Create our image tag 17 | this.editor.innerHTML = ''; 18 | this.editor.appendChild(Dom.createElement('img', { src: data.file.url })); 19 | }, 20 | 21 | onDrop: function(transferData){ 22 | var file = transferData.files[0], 23 | urlAPI = (typeof URL !== "undefined") ? URL : (typeof webkitURL !== "undefined") ? webkitURL : null; 24 | 25 | // Handle one upload at a time 26 | if (/image/.test(file.type)) { 27 | this.loading(); 28 | // Show this image on here 29 | Dom.hide(this.inputs); 30 | this.editor.innerHTML = ''; 31 | this.editor.appendChild(Dom.createElement('img', { src: urlAPI.createObjectURL(file) })); 32 | Dom.show(this.editor); 33 | 34 | this.uploader( 35 | file, 36 | function(data) { 37 | this.setData(data); 38 | this.ready(); 39 | }, 40 | function(error) { 41 | this.addMessage(i18n.t('blocks:image:upload_error')); 42 | this.ready(); 43 | } 44 | ); 45 | } 46 | }, 47 | 48 | asClipboardHTML: function() { 49 | var data = this.getBlockData(); 50 | var url = data.file && data.file.url; 51 | if (!url) return; 52 | return `${url}`; 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /src/blocks/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | Text: require('./text'), 5 | Quote: require('./quote'), 6 | Image: require('./image'), 7 | Heading: require('./heading'), 8 | List: require('./list'), 9 | Tweet: require('./tweet'), 10 | Video: require('./video'), 11 | }; 12 | -------------------------------------------------------------------------------- /src/blocks/quote.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | Block Quote 5 | */ 6 | 7 | var _ = require('../lodash'); 8 | 9 | var Block = require('../block'); 10 | var stToHTML = require('../to-html'); 11 | var ScribeHeadingPlugin = require('./scribe-plugins/scribe-heading-plugin'); 12 | var ScribeQuotePlugin = require('./scribe-plugins/scribe-quote-plugin'); 13 | 14 | var template = _.template([ 15 | '
', 16 | '', 17 | '"', 18 | ' class="st-input-string js-cite-input" type="text" />' 19 | ].join("\n")); 20 | 21 | module.exports = Block.extend({ 22 | 23 | type: "quote", 24 | 25 | icon_name: 'quote', 26 | 27 | mergeable: true, 28 | textable: true, 29 | toolbarEnabled: false, 30 | 31 | editorHTML: function() { 32 | return template(this); 33 | }, 34 | 35 | configureScribe: function(scribe) { 36 | scribe.use(new ScribeHeadingPlugin(this)); 37 | scribe.use(new ScribeQuotePlugin(this)); 38 | }, 39 | 40 | loadData: function(data){ 41 | if (this.options.convertFromMarkdown && data.format !== "html") { 42 | this.setTextBlockHTML(stToHTML(data.text, this.type)); 43 | } else { 44 | this.setTextBlockHTML(data.text); 45 | } 46 | 47 | if (data.cite) { 48 | this.$('.js-cite-input')[0].value = data.cite; 49 | } 50 | }, 51 | 52 | asClipboardHTML: function() { 53 | var data = this.getBlockData(); 54 | 55 | return `
${data.text}- ${data.cite}
`; 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /src/blocks/scribe-plugins/scribe-heading-plugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var scribeHeadingPlugin = function(block) { 4 | return function(scribe) { 5 | let { defaultHeadingLevel, headingLevels } = block.editorOptions; 6 | headingLevels = headingLevels.sort(); 7 | const minHeadingLevel = headingLevels[0]; 8 | const maxHeadingLevel = headingLevels[headingLevels.length - 1]; 9 | 10 | const headingCommand = new scribe.api.Command(`heading`); 11 | headingCommand.queryEnabled = () => { 12 | return block.inline_editable; 13 | }; 14 | headingCommand.queryState = () => { 15 | if (block.type === 'heading') { 16 | return block.getBlockData().level || defaultHeadingLevel || minHeadingLevel; 17 | } else { 18 | return false; 19 | } 20 | }; 21 | 22 | headingCommand.execute = function headingCommandExecute(value) { 23 | const nextIndex = headingLevels.indexOf(block.getBlockData().level) + 1; 24 | const level = headingLevels[nextIndex]; 25 | const blockType = level ? 'Heading' : 'Text'; 26 | 27 | var data = { 28 | format: 'html', 29 | level: level, 30 | text: block.getScribeInnerContent() 31 | }; 32 | 33 | block.mediator.trigger("block:replace", block.el, blockType, data); 34 | }; 35 | 36 | scribe.commands.heading = headingCommand; 37 | }; 38 | }; 39 | 40 | module.exports = scribeHeadingPlugin; 41 | -------------------------------------------------------------------------------- /src/blocks/scribe-plugins/scribe-quote-plugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var scribeQuotePlugin = function(block) { 4 | return function(scribe) { 5 | 6 | const quoteCommand = new scribe.api.Command('quote'); 7 | quoteCommand.queryEnabled = () => { 8 | return block.inline_editable; 9 | }; 10 | quoteCommand.queryState = () => { 11 | return block.type === 'quote'; 12 | }; 13 | 14 | const getBlockType = function() { 15 | return quoteCommand.queryState() ? 'Text' : 'Quote'; 16 | }; 17 | 18 | quoteCommand.execute = function quoteCommandExecute(value) { 19 | var data = { 20 | format: 'html', 21 | text: block.getScribeInnerContent() 22 | }; 23 | 24 | block.mediator.trigger("block:replace", block.el, getBlockType(), data); 25 | }; 26 | 27 | scribe.commands.quote = quoteCommand; 28 | }; 29 | }; 30 | 31 | module.exports = scribeQuotePlugin; 32 | -------------------------------------------------------------------------------- /src/blocks/text.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | Text Block 5 | */ 6 | 7 | var Block = require('../block'); 8 | var stToHTML = require('../to-html'); 9 | 10 | var ScribeTextBlockPlugin = require('./scribe-plugins/scribe-text-block-plugin'); 11 | var ScribePastePlugin = require('./scribe-plugins/scribe-paste-plugin'); 12 | var ScribeHeadingPlugin = require('./scribe-plugins/scribe-heading-plugin'); 13 | var ScribeLinkPromptPlugin = require('./scribe-plugins/scribe-link-prompt-plugin'); 14 | var ScribeQuotePlugin = require('./scribe-plugins/scribe-quote-plugin'); 15 | var ScribeSuperscriptPromptPlugin = require('./scribe-plugins/scribe-superscript-prompt-plugin'); 16 | 17 | module.exports = Block.extend({ 18 | 19 | type: "text", 20 | 21 | editorHTML: '
', 22 | 23 | icon_name: 'text', 24 | 25 | mergeable: true, 26 | textable: true, 27 | toolbarEnabled: false, 28 | 29 | configureScribe: function(scribe) { 30 | scribe.use(new ScribeTextBlockPlugin(this)); 31 | scribe.use(new ScribePastePlugin(this)); 32 | scribe.use(new ScribeHeadingPlugin(this)); 33 | scribe.use(new ScribeLinkPromptPlugin(this)); 34 | scribe.use(new ScribeQuotePlugin(this)); 35 | scribe.use(new ScribeSuperscriptPromptPlugin(this)); 36 | 37 | scribe.on('content-changed', this.toggleEmptyClass.bind(this)); 38 | }, 39 | 40 | scribeOptions: { 41 | allowBlockElements: true, 42 | tags: { 43 | p: true 44 | } 45 | }, 46 | 47 | loadData: function(data){ 48 | if (this.options.convertFromMarkdown && data.format !== "html") { 49 | this.setTextBlockHTML(stToHTML(data.text, this.type)); 50 | } else { 51 | this.setTextBlockHTML(data.text); 52 | } 53 | }, 54 | 55 | onBlockRender: function() { 56 | this.toggleEmptyClass(); 57 | }, 58 | 59 | toggleEmptyClass: function() { 60 | this.el.classList.toggle('st-block--empty', this.isEmpty()); 61 | }, 62 | 63 | isEmpty: function() { 64 | return this._scribe.getTextContent() === ''; 65 | }, 66 | 67 | asClipboardHTML: function() { 68 | var data = this.getBlockData(); 69 | return `${data.text}`; 70 | } 71 | }); 72 | -------------------------------------------------------------------------------- /src/error-handler.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('./lodash'); 4 | var Dom = require('./packages/dom'); 5 | 6 | var ErrorHandler = function(wrapper, mediator, container) { 7 | this.wrapper = wrapper; 8 | this.mediator = mediator; 9 | this.el = container; 10 | 11 | if (_.isUndefined(this.el)) { 12 | this._ensureElement(); 13 | this.wrapper.insertBefore(this.el, this.wrapper.firstChild); 14 | } 15 | 16 | Dom.hide(this.el); 17 | 18 | this._bindFunctions(); 19 | this._bindMediatedEvents(); 20 | 21 | this.initialize(); 22 | }; 23 | 24 | Object.assign(ErrorHandler.prototype, require('./function-bind'), require('./mediated-events'), require('./renderable'), { 25 | 26 | errors: [], 27 | className: "st-errors", 28 | eventNamespace: 'errors', 29 | 30 | mediatedEvents: { 31 | 'reset': 'reset', 32 | 'add': 'addMessage', 33 | 'render': 'render' 34 | }, 35 | 36 | initialize: function() { 37 | var list = document.createElement("ul"); 38 | var p = document.createElement("p"); 39 | p.innerHTML = i18n.t("errors:title"); 40 | 41 | this.el.appendChild(p) 42 | .appendChild(list); 43 | this.list = list; 44 | }, 45 | 46 | render: function() { 47 | if (this.errors.length === 0) { return false; } 48 | this.errors.forEach(this.createErrorItem, this); 49 | Dom.show(this.el); 50 | }, 51 | 52 | createErrorItem: function(errorObj) { 53 | var error = document.createElement("li"); 54 | error.classList.add("st-errors__msg"); 55 | error.innerHTML = errorObj.text; 56 | this.list.appendChild(error); 57 | }, 58 | 59 | addMessage: function(error) { 60 | this.errors.push(error); 61 | }, 62 | 63 | reset: function() { 64 | if (this.errors.length === 0) { return false; } 65 | this.errors = []; 66 | this.list.innerHTML = ''; 67 | Dom.hide(this.el); 68 | } 69 | 70 | }); 71 | 72 | module.exports = ErrorHandler; 73 | 74 | -------------------------------------------------------------------------------- /src/event-bus.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = Object.assign({}, require('./events')); 4 | -------------------------------------------------------------------------------- /src/events.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = require('eventablejs'); 4 | -------------------------------------------------------------------------------- /src/extensions/editor-store.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * Sir Trevor Editor Store 5 | * By default we store the complete data on the instances $el 6 | * We can easily extend this and store it on some server or something 7 | */ 8 | 9 | var _ = require('../lodash'); 10 | var utils = require('../utils'); 11 | 12 | 13 | var EditorStore = function(data, mediator) { 14 | this.mediator = mediator; 15 | this.initialize(data ? data.trim() : ''); 16 | }; 17 | 18 | Object.assign(EditorStore.prototype, { 19 | 20 | initialize: function(data) { 21 | this.store = this._parseData(data) || { data: [] }; 22 | }, 23 | 24 | retrieve: function() { 25 | return this.store; 26 | }, 27 | 28 | toString: function(space) { 29 | return JSON.stringify(this.store, undefined, space); 30 | }, 31 | 32 | reset: function() { 33 | utils.log("Resetting the EditorStore"); 34 | this.store = { data: [] }; 35 | }, 36 | 37 | addData: function(data) { 38 | this.store.data.push(data); 39 | return this.store; 40 | }, 41 | 42 | _parseData: function(data) { 43 | var result; 44 | 45 | if (data.length === 0) { return result; } 46 | 47 | try { 48 | // Ensure the JSON string has a data element that's an array 49 | var jsonStr = JSON.parse(data); 50 | if (!_.isUndefined(jsonStr.data)) { 51 | result = jsonStr; 52 | } 53 | } catch(e) { 54 | this.mediator.trigger( 55 | 'errors:add', 56 | { text: i18n.t("errors:load_fail") }); 57 | 58 | this.mediator.trigger('errors:render'); 59 | 60 | console.log('Sorry there has been a problem with parsing the JSON'); 61 | console.log(e); 62 | } 63 | 64 | return result; 65 | } 66 | 67 | }); 68 | 69 | module.exports = EditorStore; 70 | -------------------------------------------------------------------------------- /src/extensions/file-uploader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * Sir Trevor Uploader 5 | * Generic Upload implementation that can be extended for blocks 6 | */ 7 | 8 | var _ = require('../lodash'); 9 | var config = require('../config'); 10 | var utils = require('../utils'); 11 | var Ajax = require('../packages/ajax'); 12 | 13 | var EventBus = require('../event-bus'); 14 | 15 | module.exports = function(block, file, success, error) { 16 | var uid = [block.blockID, (new Date()).getTime(), 'raw'].join('-'); 17 | var data = new FormData(); 18 | var attachmentName = block.attachmentName || config.defaults.attachmentName; 19 | var attachmentFile = block.attachmentFile || config.defaults.attachmentFile; 20 | var attachmentUid = block.attachmentUid || config.defaults.attachmentUid; 21 | 22 | data.append(attachmentName, file.name); 23 | data.append(attachmentFile, file); 24 | data.append(attachmentUid, uid); 25 | 26 | EventBus.trigger('onUploadStart', data); 27 | 28 | block.resetMessages(); 29 | 30 | var callbackSuccess = function(data) { 31 | utils.log('Upload callback called'); 32 | EventBus.trigger('onUploadStop', data); 33 | 34 | if (!_.isUndefined(success) && _.isFunction(success)) { 35 | success.apply(block, arguments, data); 36 | } 37 | 38 | block.removeQueuedItem(uid); 39 | }; 40 | 41 | var callbackError = function(jqXHR, status, errorThrown) { 42 | utils.log('Upload callback error called'); 43 | EventBus.trigger('onUploadStop', undefined, errorThrown, status, jqXHR); 44 | 45 | if (!_.isUndefined(error) && _.isFunction(error)) { 46 | error.call(block, status); 47 | } 48 | 49 | block.removeQueuedItem(uid); 50 | }; 51 | 52 | var url = block.uploadUrl || config.defaults.uploadUrl; 53 | 54 | var xhr = Ajax.fetch(url, { 55 | body: data, 56 | method: 'POST', 57 | dataType: 'json' 58 | }); 59 | 60 | block.addQueuedItem(uid, xhr); 61 | 62 | xhr.then(callbackSuccess) 63 | .catch(callbackError); 64 | 65 | return xhr; 66 | }; 67 | -------------------------------------------------------------------------------- /src/form-events.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var config = require('./config'); 4 | var utils = require('./utils'); 5 | 6 | var EventBus = require('./event-bus'); 7 | var Submittable = require('./extensions/submittable'); 8 | 9 | var formBound = false; // Flag to tell us once we've bound our submit event 10 | 11 | var FormEvents = { 12 | bindFormSubmit: function(form) { 13 | if (!formBound) { 14 | // XXX: should we have a formBound and submittable per-editor? 15 | // telling JSHint to ignore as it'll complain we shouldn't be creating 16 | // a new object, but otherwise `this` won't be set in the Submittable 17 | // initialiser. Bit weird. 18 | new Submittable(form); // jshint ignore:line 19 | form.addEventListener('submit', this.onFormSubmit); 20 | formBound = true; 21 | } 22 | }, 23 | 24 | onBeforeSubmit: function(shouldValidate) { 25 | // Loop through all of our instances and do our form submits on them 26 | var errors = 0; 27 | config.instances.forEach(function(inst, i) { 28 | errors += inst.onFormSubmit(shouldValidate); 29 | }); 30 | utils.log("Total errors: " + errors); 31 | 32 | return errors; 33 | }, 34 | 35 | onFormSubmit: function(ev) { 36 | var errors = FormEvents.onBeforeSubmit(); 37 | 38 | if(errors > 0) { 39 | EventBus.trigger("onError"); 40 | ev.preventDefault(); 41 | } 42 | }, 43 | }; 44 | 45 | module.exports = FormEvents; 46 | -------------------------------------------------------------------------------- /src/function-bind.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* Generic function binding utility, used by lots of our classes */ 4 | 5 | module.exports = { 6 | bound: [], 7 | _bindFunctions: function(){ 8 | this.bound.forEach(function(f) { 9 | this[f] = this[f].bind(this); 10 | }, this); 11 | } 12 | }; 13 | -------------------------------------------------------------------------------- /src/helpers/drop-events.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function dragEnter(e) { 4 | e.preventDefault(); 5 | e.stopPropagation(); 6 | } 7 | 8 | function dragOver(e) { 9 | e.dataTransfer.dropEffect = "copy"; 10 | e.currentTarget.classList.add('st-drag-over'); 11 | e.preventDefault(); 12 | e.stopPropagation(); 13 | } 14 | 15 | function dragLeave(e) { 16 | e.currentTarget.classList.remove('st-drag-over'); 17 | e.preventDefault(); 18 | e.stopPropagation(); 19 | } 20 | 21 | module.exports = { 22 | 23 | dropArea: function(el) { 24 | el.addEventListener("dragenter", dragEnter); 25 | el.addEventListener("dragover", dragOver); 26 | el.addEventListener("dragleave", dragLeave); 27 | return el; 28 | }, 29 | 30 | noDropArea: function(el) { 31 | el.removeEventListener("dragenter"); 32 | el.removeEventListener("dragover"); 33 | el.removeEventListener("dragleave"); 34 | return el; 35 | } 36 | 37 | }; -------------------------------------------------------------------------------- /src/helpers/extend.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | Backbone Inheritence 5 | -- 6 | From: https://github.com/documentcloud/backbone/blob/master/backbone.js 7 | Backbone.js 0.9.2 8 | (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. 9 | */ 10 | 11 | module.exports = function(protoProps, staticProps) { 12 | var parent = this; 13 | var child; 14 | 15 | // The constructor function for the new subclass is either defined by you 16 | // (the "constructor" property in your `extend` definition), or defaulted 17 | // by us to simply call the parent's constructor. 18 | if (protoProps && protoProps.hasOwnProperty('constructor')) { 19 | child = protoProps.constructor; 20 | } else { 21 | child = function(){ return parent.apply(this, arguments); }; 22 | } 23 | 24 | // Add static properties to the constructor function, if supplied. 25 | Object.assign(child, parent, staticProps); 26 | 27 | // Set the prototype chain to inherit from `parent`, without calling 28 | // `parent`'s constructor function. 29 | var Surrogate = function(){ this.constructor = child; }; 30 | Surrogate.prototype = parent.prototype; 31 | child.prototype = new Surrogate; // jshint ignore:line 32 | 33 | // Add prototype properties (instance properties) to the subclass, 34 | // if supplied. 35 | if (protoProps) { 36 | Object.assign(child.prototype, protoProps); 37 | } 38 | 39 | // Set a convenience property in case the parent's prototype is needed 40 | // later. 41 | child.__super__ = parent.prototype; 42 | 43 | return child; 44 | }; 45 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require("./icons/sir-trevor-icons.svg"); 4 | 5 | // ES6 shims 6 | require('object.assign').shim(); 7 | require('array.prototype.find').shim(); 8 | require('./vendor/array-includes'); // shims ES7 Array.prototype.includes 9 | require('es6-promise').polyfill(); 10 | 11 | // Old IE support 12 | require('./vendor/custom-event') 13 | require('./vendor/ie-classlist-toggle'); 14 | require('./vendor/dom-shims'); 15 | 16 | var utils = require('./utils'); 17 | 18 | var SirTrevor = { 19 | 20 | config: require('./config'), 21 | 22 | log: utils.log, 23 | 24 | Locales: require('./locales'), 25 | 26 | Events: require('./events'), 27 | EventBus: require('./event-bus'), 28 | 29 | EditorStore: require('./extensions/editor-store'), 30 | Submittable: require('./extensions/submittable'), 31 | FileUploader: require('./extensions/file-uploader'), 32 | 33 | BlockMixins: require('./block_mixins'), 34 | BlockPositioner: require('./block-positioner'), 35 | BlockPositionerSelect: require('./block-positioner-select'), 36 | BlockReorder: require('./block-reorder'), 37 | BlockDeletion: require('./block-deletion'), 38 | BlockValidations: require('./block-validations'), 39 | BlockStore: require('./block-store'), 40 | BlockManager: require('./block-manager'), 41 | 42 | SimpleBlock: require('./simple-block'), 43 | Block: require('./block'), 44 | 45 | Blocks: require('./blocks'), 46 | 47 | FormatBar: require('./format-bar'), 48 | Editor: require('./editor'), 49 | 50 | toMarkdown: require('./to-markdown'), 51 | toHTML: require('./to-html'), 52 | 53 | setDefaults: function(options) { 54 | Object.assign(SirTrevor.config.defaults, options || {}); 55 | }, 56 | 57 | getInstance: utils.getInstance, 58 | 59 | setBlockOptions: function(type, options) { 60 | var block = SirTrevor.Blocks[type]; 61 | 62 | if (typeof block === "undefined") { 63 | return; 64 | } 65 | 66 | Object.assign(block.prototype, options || {}); 67 | }, 68 | 69 | runOnAllInstances: function(method) { 70 | if (SirTrevor.Editor.prototype.hasOwnProperty(method)) { 71 | var methodArgs = Array.prototype.slice.call(arguments, 1); 72 | Array.prototype.forEach.call(SirTrevor.config.instances, function(i) { 73 | i[method].apply(null, methodArgs); 74 | }); 75 | } else { 76 | SirTrevor.log("method doesn't exist"); 77 | } 78 | }, 79 | 80 | }; 81 | 82 | Object.assign(SirTrevor, require('./form-events')); 83 | 84 | 85 | module.exports = SirTrevor; 86 | -------------------------------------------------------------------------------- /src/lodash.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.isEmpty = require('lodash.isempty'); 4 | exports.isFunction = require('lodash.isfunction'); 5 | exports.isObject = require('lodash.isobject'); 6 | exports.isString = require('lodash.isstring'); 7 | exports.isUndefined = require('lodash.isundefined'); 8 | exports.result = require('lodash.result'); 9 | exports.template = require('lodash.template'); 10 | exports.uniqueId = require('lodash.uniqueid'); 11 | -------------------------------------------------------------------------------- /src/mediated-events.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = { 4 | mediatedEvents: {}, 5 | eventNamespace: null, 6 | _bindMediatedEvents: function() { 7 | Object.keys(this.mediatedEvents).forEach(function(eventName){ 8 | var cb = this.mediatedEvents[eventName]; 9 | eventName = this.eventNamespace ? 10 | this.eventNamespace + ':' + eventName : 11 | eventName; 12 | this.mediator.on(eventName, this[cb].bind(this)); 13 | }, this); 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /src/packages/ajax.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | require('whatwg-fetch'); 4 | var fetchJsonP = require('jsonp-promise'); 5 | var cancellablePromise = require('./cancellable-promise'); 6 | var config = require('../config'); 7 | 8 | let Ajax = Object.create(null); 9 | 10 | Ajax.fetch = (url, options = {}) => { 11 | 12 | options = Object.assign({}, config.defaults.ajaxOptions, options); 13 | 14 | var promise; 15 | if (options.jsonp) { 16 | promise = fetchJsonP(url).promise; 17 | } else { 18 | promise = fetch(url, options).then( function(response) { 19 | if (options.dataType === 'json') { 20 | return response.json(); 21 | } 22 | return response.text(); 23 | }); 24 | } 25 | return cancellablePromise(promise); 26 | }; 27 | 28 | module.exports = Ajax; -------------------------------------------------------------------------------- /src/packages/cancellable-promise.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var cancellablePromise = function(promise) { 4 | var resolve, reject; 5 | 6 | var proxyPromise = new Promise(function(res, rej) { 7 | resolve = res; 8 | reject = rej; 9 | }); 10 | 11 | promise.then( 12 | function(value) { 13 | if(!proxyPromise.cancelled) { 14 | resolve(value); 15 | } 16 | }, 17 | function(value) { 18 | if(!proxyPromise.cancelled) { 19 | reject(value); 20 | } 21 | } 22 | ); 23 | 24 | proxyPromise.cancel = function() { 25 | this.cancelled = true; 26 | }; 27 | 28 | return proxyPromise; 29 | }; 30 | 31 | module.exports = cancellablePromise; 32 | -------------------------------------------------------------------------------- /src/packages/events.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var Dom = require("./dom"); 4 | 5 | var fixEvent = function(e, target) { 6 | var obj = {}; 7 | 8 | // Events don't work as normal objects, so need to copy properties directly. 9 | // List and matchers taken from jQuery.Event.fix. 10 | // For other properties refer to the originalEvent object. 11 | 12 | var props = { 13 | shared: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + 14 | "metaKey relatedTarget shiftKey target timeStamp view which" ).split(" "), 15 | mouseEvent: ( "button buttons clientX clientY offsetX offsetY pageX pageY " + 16 | "screenX screenY toElement" ).split(" "), 17 | keyEvent: "char charCode key keyCode".split(" ") 18 | }; 19 | 20 | var rkeyEvent = /^key/, 21 | rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/; 22 | 23 | var propsToCopy = 24 | rmouseEvent.test( e.type ) ? props.shared.concat(props.mouseEvent) : 25 | rkeyEvent.test( e.type ) ? props.shared.concat(props.keyEvent) : 26 | props.shared; 27 | 28 | var prop; 29 | for(var i = 0; i < propsToCopy.length; i++) { 30 | prop = propsToCopy[i]; 31 | obj[prop] = e[prop]; 32 | } 33 | 34 | obj.currentTarget = target; 35 | obj.originalEvent = e; 36 | 37 | obj.preventDefault = function() { 38 | if ( this.originalEvent ) { 39 | this.originalEvent.preventDefault(); 40 | } 41 | }; 42 | 43 | obj.stopPropagation = function() { 44 | if ( this.originalEvent ) { 45 | this.originalEvent.stopPropagation(); 46 | } 47 | }; 48 | 49 | return obj; 50 | }; 51 | 52 | module.exports.delegate = 53 | function delegate(el, selector, event, fn, useCapture = false) { 54 | el.addEventListener(event, (e) => { 55 | var target = e.target; 56 | for (target; target && target !== el; target = target.parentNode) { 57 | if (Dom.matches(target, selector)) { 58 | fn.call(target, fixEvent(e, target)); 59 | break; 60 | } 61 | } 62 | target = null; 63 | }, useCapture); 64 | }; 65 | -------------------------------------------------------------------------------- /src/packages/uuid.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // jshint ignore: start 4 | module.exports = function uuid(a,b){ 5 | for(b=a='';a++<36;b+=a*51&52?(a^15?8^Math.random()*(a^20?16:4):4).toString(16):'-'); 6 | return b; 7 | }; 8 | // jshint ignore: end 9 | -------------------------------------------------------------------------------- /src/renderable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('./lodash'); 4 | var Dom = require('./packages/dom'); 5 | 6 | module.exports = { 7 | tagName: 'div', 8 | className: 'sir-trevor__view', 9 | attributes: {}, 10 | 11 | $: function(selector) { 12 | return this.el.querySelectorAll(selector); 13 | }, 14 | 15 | render: function() { 16 | return this; 17 | }, 18 | 19 | destroy: function() { 20 | if (!_.isUndefined(this.stopListening)) { this.stopListening(); } 21 | Dom.remove(this.el); 22 | }, 23 | 24 | _ensureElement: function() { 25 | if (!this.el) { 26 | var attrs = Object.assign({}, _.result(this, 'attributes')); 27 | if (this.id) { attrs.id = this.id; } 28 | if (this.className) { attrs['class'] = this.className; } 29 | 30 | var el = Dom.createElement(this.tagName, attrs); 31 | this._setElement(el); 32 | } else { 33 | this._setElement(this.el); 34 | } 35 | }, 36 | 37 | _setElement: function(element) { 38 | this.el = element; 39 | return this; 40 | } 41 | }; 42 | 43 | -------------------------------------------------------------------------------- /src/sass/_icons.scss: -------------------------------------------------------------------------------- 1 | .st-icon { 2 | cursor:pointer; 3 | width: 1em; 4 | height: 1em; 5 | fill: currentColor; 6 | } -------------------------------------------------------------------------------- /src/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | $text-block-font-size: 1.275em !default; 2 | 3 | $text-block-font-color: #282d31 !default; 4 | $accent-color: #17bb75 !default; 5 | $base-ui-color: #42474b !default; 6 | $light-ui-color: #ccc !default; 7 | $blocks-control-bg-color: #f7f7f7 !default; 8 | $blocks-control-border-color: #ddd !default; 9 | $error-color: #d70014 !default; 10 | $selection-color: #b1f2d6 !default; 11 | 12 | $inner-block-padding: 30px 30px 48px !default; 13 | 14 | $block-controls-height: 164px !default; 15 | $block-add-height: 40px !default; 16 | $block-controls-color: #9b9b9b !default; 17 | 18 | $border-size: 2px !default; 19 | $border-style: solid !default; 20 | $default-border: $border-size $border-style $accent-color !default; 21 | -------------------------------------------------------------------------------- /src/sass/base.scss: -------------------------------------------------------------------------------- 1 | .st-outer { 2 | font-size: 16px; 3 | position: relative; 4 | background:#fff; 5 | } 6 | 7 | .st-icon { 8 | cursor:pointer; 9 | width: 100%; 10 | height: 100%; 11 | fill: currentColor; 12 | } 13 | 14 | .st-block__inner { 15 | ::-moz-selection { 16 | background: $selection-color; 17 | text-shadow: none; 18 | } 19 | ::selection { 20 | background: $selection-color; 21 | text-shadow: none; 22 | } 23 | } 24 | 25 | .st-spinner { 26 | position: absolute!important; 27 | left: 50%; top: 50%; 28 | } 29 | -------------------------------------------------------------------------------- /src/sass/block-addition-top.scss: -------------------------------------------------------------------------------- 1 | .st-block-addition-top { 2 | transition: all $animation-speed 0.2s ease-in-out; 3 | text-align: left; 4 | outline:none; 5 | border:none; 6 | width:100%; 7 | background-color:transparent; 8 | padding:0; 9 | z-index: 2; 10 | position: relative; 11 | cursor: text; 12 | display: none; 13 | 14 | &::-moz-focus-inner { 15 | padding:0; 16 | margin:0; 17 | margin-left:-1px; 18 | } 19 | 20 | position: absolute; 21 | top: -2em; 22 | height: 30px; 23 | opacity: 0; 24 | display: block; 25 | 26 | &:before { 27 | transition: all $animation-speed 0.1s ease-in-out; 28 | background: $accent-color; 29 | position: absolute; 30 | height: 2px; 31 | top: 50%; 32 | left: 110px; 33 | right: 110px; 34 | content: ""; 35 | display: block; 36 | transform: translateY(-50%) translateZ(0); 37 | } 38 | 39 | .st-block--empty & { 40 | display: none !important; 41 | } 42 | 43 | &:hover { 44 | opacity: 1; 45 | } 46 | 47 | .st-block--textable &, 48 | .st-block[data-type="list"] & { 49 | top: -1.5em; 50 | } 51 | 52 | .st-block:nth-child(3) & { 53 | display: none; 54 | } 55 | 56 | .st-block--empty + .st-block & { 57 | display: none; 58 | } 59 | } 60 | 61 | .st-block-addition-top__icon { 62 | transition: all $animation-speed 0.1s ease-in-out; 63 | border: 1px solid transparent; 64 | color: #444444; 65 | position:absolute; 66 | top: 50%; 67 | box-sizing:border-box; 68 | padding-left: 35px; 69 | display: inline-block; 70 | margin: 0 auto; 71 | cursor: pointer; 72 | opacity: 0.2; 73 | transform: translateY(-50%) translateZ(0); 74 | 75 | .st-icon { 76 | width: 41px; 77 | height: 41px; 78 | color: inherit; 79 | } 80 | 81 | .st-block-addition-top:hover &, 82 | .st-block--active & { 83 | color: $accent-color; 84 | opacity: 1; 85 | } 86 | } 87 | 88 | .st-block-addition-top__button { 89 | background: none; 90 | border:none; 91 | outline:none; 92 | position: absolute; 93 | top: 0; 94 | left: 76px; 95 | right: 0; 96 | bottom: 0; 97 | display: inline-block; 98 | } 99 | -------------------------------------------------------------------------------- /src/sass/block-addition.scss: -------------------------------------------------------------------------------- 1 | 2 | $animation-speed: 0.2s; 3 | 4 | .st-top-controls { 5 | min-height: 1.750em; 6 | position: relative; 7 | z-index: 2; 8 | } 9 | 10 | .st-block-addition { 11 | text-align: center; 12 | outline:none; 13 | border:none; 14 | display:none; 15 | width:100%; 16 | background-color:transparent; 17 | padding:0; 18 | z-index: 2; 19 | 20 | &::-moz-focus-inner { 21 | padding:0; 22 | margin:0; 23 | margin-left:-1px; 24 | } 25 | 26 | .st-top-controls &, 27 | .st-block:last-child:not(.st-block--empty):not(.st-block--textable) &, 28 | .st-block[data-type="quote"]:last-child & { 29 | display: block; 30 | } 31 | } 32 | 33 | .st--hide-top-controls { 34 | .st-top-controls .st-block-addition { 35 | display: none; 36 | } 37 | } 38 | 39 | .st--block-limit-reached { 40 | .st-top-controls, 41 | .st-block-addition { 42 | display: none; 43 | } 44 | } 45 | 46 | .st-block-addition__button { 47 | transition: all $animation-speed 0.3s ease-in-out; 48 | border: 1px solid transparent; 49 | color: #444444; 50 | position:relative; 51 | box-sizing:border-box; 52 | padding:.2em; 53 | display: inline-block; 54 | margin: 0 auto; 55 | width: 100%; 56 | cursor: pointer; 57 | opacity: 0.2; 58 | cursor: pointer; 59 | 60 | transform:translateZ(0); 61 | 62 | .st-icon { 63 | height: 20px; 64 | width: 20px; 65 | color: inherit; 66 | } 67 | 68 | .st-block-addition:hover &, 69 | .st-block--active & { 70 | color: $accent-color; 71 | opacity: 1; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/sass/block-controls.scss: -------------------------------------------------------------------------------- 1 | // Block Controls 2 | 3 | .st-block-controls { 4 | position: absolute; 5 | left: 0; 6 | top: 50%; 7 | right: 0; 8 | font-size:0.8em; 9 | padding: 0px 110px; 10 | transform: translateY(-50%); 11 | z-index: 1; 12 | } 13 | 14 | .st-block-controls__buttons { 15 | text-align: center; 16 | } 17 | 18 | .st-block-controls__button { 19 | border:none; 20 | background:transparent; 21 | font-size: 12px; 22 | text-transform: uppercase; 23 | display: inline-block; 24 | cursor: pointer; 25 | margin: 0.5em; 26 | text-transform: uppercase; 27 | } 28 | 29 | .st-block-controls__button .st-icon { 30 | margin: 0 0 10px 0; 31 | display: block; 32 | width: 42px; 33 | height: 42px; 34 | } 35 | 36 | .st-block-controls__button:hover { 37 | color: $accent-color; 38 | } 39 | -------------------------------------------------------------------------------- /src/sass/block-positioner.scss: -------------------------------------------------------------------------------- 1 | .st-block-positioner { 2 | @include ui-popup; 3 | right: 4em; 4 | top: 0; 5 | &:before { 6 | left: auto; 7 | right: -0.3em; 8 | } 9 | } 10 | 11 | .st-block-positioner__inner { 12 | background: #fff; 13 | position: relative; 14 | z-index: 2; 15 | padding: 0.3em 0.5em; 16 | } 17 | 18 | .st-block-positioner__select { 19 | display:block; 20 | } 21 | 22 | .st-block--with-errors, .st-block--delete-active { 23 | & > .st-block__inner { 24 | 25 | & > .st-block__ui { 26 | .st-block-positioner { 27 | border-color: $error-color; 28 | &:after { 29 | border-color: $error-color; 30 | } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/sass/block-replacer.scss: -------------------------------------------------------------------------------- 1 | .st-block-replacer { 2 | position: absolute; 3 | text-align: center; 4 | transform: translateY(-50%); 5 | outline:none; 6 | border:none; 7 | font-size:1.3em; 8 | display:none; 9 | background-color:transparent; 10 | padding:0; 11 | z-index: 2; 12 | top: 50%; 13 | left: 35px; 14 | 15 | &::-moz-focus-inner { 16 | padding:0; 17 | margin:0; 18 | margin-left:-1px; 19 | } 20 | 21 | .st-block--textable.st-block--empty & { 22 | display: block; 23 | } 24 | 25 | } 26 | 27 | .st-block-replacer__button { 28 | transition: all $animation-speed ease-in-out; 29 | color: $accent-color; 30 | position:relative; 31 | box-sizing:border-box; 32 | display: inline-block; 33 | margin: 0 auto; 34 | width: 41px; 35 | height: 41px; 36 | cursor: pointer; 37 | 38 | transform:translateZ(0); 39 | 40 | .st-block--controls-active & { 41 | transform: rotate(45deg); 42 | } 43 | } -------------------------------------------------------------------------------- /src/sass/errors.scss: -------------------------------------------------------------------------------- 1 | .st-errors { 2 | background-color: lighten($error-color, 52%); 3 | padding: 2em; 4 | color: $error-color; 5 | margin-bottom: 2em; 6 | } 7 | 8 | .st-errors p, 9 | .st-errors ul { 10 | margin: 0; 11 | } 12 | 13 | .st-errors ul { 14 | padding-left: 1em; 15 | } 16 | 17 | .st-errors p { 18 | margin-bottom: 0.5em; 19 | font-weight: 700; 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/sass/format-bar.scss: -------------------------------------------------------------------------------- 1 | .st-format-bar { 2 | top: 0; 3 | position: absolute; 4 | margin: -4.5em 0 0 0; 5 | background: #252525; 6 | opacity: 0; 7 | visibility: hidden; 8 | z-index: 10; 9 | border-radius: 0.4em; 10 | padding: 0; 11 | 12 | transition: opacity 0.2s ease-in-out; 13 | } 14 | 15 | .st-format-bar:before { 16 | content: ''; 17 | display: block; 18 | position: absolute; 19 | left: 50%; 20 | top: 3.3em; 21 | width: 0; 22 | height: 0; 23 | border-left: 0.875em solid transparent; 24 | border-right: 0.875em solid transparent; 25 | border-top: 0.875em solid #252525; 26 | margin-left: -0.875em; 27 | } 28 | 29 | .st-format-bar--is-ready { 30 | visibility: visible; 31 | opacity: 1; 32 | } 33 | 34 | .st-format-btn { 35 | background: transparent; 36 | border: 0; 37 | color: #fff; 38 | font-size: 1em; 39 | line-height: 1; 40 | padding: 0.5em 0.6em; 41 | vertical-align: middle; 42 | text-align: center; 43 | 44 | &:focus { 45 | outline: 0; 46 | } 47 | } 48 | .st-format-btn .st-icon { 49 | width: 35px; 50 | height: 35px; 51 | } 52 | 53 | .st-format-btn--Heading, 54 | .st-format-btn--Quote { 55 | border-left: 1px solid #3e4245; 56 | } 57 | 58 | .st-format-btn:hover, 59 | .st-format-btn--is-active { 60 | color:$accent-color; 61 | } 62 | 63 | .st-format-btn--Unlink { 64 | text-decoration: line-through; 65 | } 66 | 67 | .st-format-btn--Heading[data-state] { 68 | position: relative; 69 | 70 | &::after { 71 | content: attr(data-state); 72 | font-weight: 800; 73 | position: absolute; 74 | right: 12px; 75 | bottom: 17px; 76 | } 77 | 78 | &[data-state="false"]::after { 79 | content: "_"; 80 | } 81 | } 82 | 83 | -------------------------------------------------------------------------------- /src/sass/inputs.scss: -------------------------------------------------------------------------------- 1 | // Reset all our input styles 2 | .st-input-label { 3 | display: block; 4 | margin-bottom: 0.5em; 5 | font-size: 13px; 6 | text-transform: uppercase; 7 | } 8 | 9 | // Generic styles 10 | @mixin st-input { 11 | font-size: inherit; 12 | margin: 0; 13 | } 14 | 15 | .st-block input[type="text"], 16 | .st-block textarea { 17 | @include st-input; 18 | } 19 | 20 | // Focus / Active styles 21 | @mixin st-input-active { 22 | outline: none; 23 | border: none; 24 | } 25 | 26 | @mixin st-input-styled { 27 | color: $base-ui-color; 28 | border: 0.1em solid #d4d4d4; 29 | padding: 0.6em; 30 | } 31 | 32 | .st-block [contenteditable="true"], 33 | .st-block [contenteditable="true"]:active, 34 | .st-block [contenteditable="true"]:focus, 35 | .st-block input[type="text"], 36 | .st-block input[type="text"]:active, 37 | .st-block input[type="text"]:focus, 38 | .st-block textarea, 39 | .st-block textarea:hover, 40 | .st-block textarea:active { 41 | @include st-input-active; 42 | } 43 | 44 | .st-block input[type="text"], 45 | .st-block input[type="text"]:active, 46 | .st-block input[type="text"]:focus { 47 | @include st-input-styled; 48 | } 49 | 50 | // Generic styles 51 | @mixin st-btn-hover { 52 | background: $accent-color; 53 | color: #fff; 54 | } 55 | 56 | @mixin st-btn { 57 | border: 0; 58 | background: $base-ui-color; 59 | border-radius: 0.2em; 60 | padding: 0.35em 1em; 61 | font-size: 1.125em; 62 | cursor: pointer; 63 | color: #fff; 64 | position: relative; 65 | z-index: 10; 66 | 67 | &:hover { 68 | @include st-btn-hover; 69 | } 70 | } 71 | 72 | .st-block__upload-container:hover .st-upload-btn { 73 | background: $accent-color; 74 | color: #fff; 75 | } 76 | -------------------------------------------------------------------------------- /src/sass/main.scss: -------------------------------------------------------------------------------- 1 | @import '_variables'; 2 | @import '_icons'; 3 | 4 | 5 | @import 'patterns/ui-popup'; 6 | @import 'utils'; 7 | @import 'base'; 8 | @import 'inputs'; 9 | 10 | @import 'errors'; 11 | 12 | // Block Controls 13 | @import 'block-addition'; 14 | @import 'block-addition-top'; 15 | @import 'block-controls'; 16 | @import 'block-replacer'; 17 | 18 | // Blocks 19 | @import 'block'; 20 | @import 'block-positioner'; 21 | @import 'block-ui'; 22 | 23 | // Format Bar 24 | @import 'format-bar'; 25 | 26 | // Modal 27 | @import 'modal'; 28 | -------------------------------------------------------------------------------- /src/sass/patterns/ui-popup.scss: -------------------------------------------------------------------------------- 1 | @mixin ui-popup { 2 | border: $default-border; 3 | position: absolute; 4 | z-index: 2; 5 | background: #fff; 6 | display:none; 7 | 8 | &.active { 9 | display:block; 10 | } 11 | 12 | &:after { 13 | content: ''; 14 | display: block; 15 | background-color:#fff; 16 | position:absolute; 17 | top:0; 18 | right:0; 19 | bottom:0; 20 | left:0; 21 | z-index: -1; 22 | } 23 | 24 | &:before { 25 | content: ''; 26 | display: block; 27 | width: 0.4em; 28 | height: 0.4em; 29 | position: absolute; 30 | left: -0.3em; 31 | top: 50%; 32 | z-index: -1; 33 | border: $default-border; 34 | background: #fff; 35 | transform: translateY(-50%) rotate(45deg); 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/sass/utils.scss: -------------------------------------------------------------------------------- 1 | .st-utils__hidden { 2 | border: 0; 3 | clip: rect(0 0 0 0); 4 | height: 1px; 5 | margin: -1px; 6 | overflow: hidden; 7 | padding: 0; 8 | position: absolute; 9 | width: 1px; 10 | } 11 | -------------------------------------------------------------------------------- /src/scribe-interface.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('./lodash'); 4 | var Scribe = require('scribe-editor'); 5 | var config = require('./config'); 6 | 7 | var scribePluginFormatterPlainTextConvertNewLinesToHTML = require('scribe-plugin-formatter-plain-text-convert-new-lines-to-html'); 8 | var scribePluginLinkPromptCommand = require('./blocks/scribe-plugins/scribe-link-prompt-plugin'); 9 | var scribePluginSanitizer = require('scribe-plugin-sanitizer'); 10 | 11 | var sanitizeDefaults = { 12 | p: true, 13 | a: { 14 | href: true, 15 | target: '_blank', 16 | rel: true 17 | }, 18 | i: true, 19 | b: true, 20 | strong: true, 21 | em: true, 22 | sup: true 23 | }; 24 | 25 | module.exports = { 26 | 27 | initScribeInstance: function(el, scribeOptions, configureScribe, editorOptions) { 28 | 29 | scribeOptions = scribeOptions || {}; 30 | 31 | var scribeConfig = {debug: config.scribeDebug}; 32 | var tags = sanitizeDefaults; 33 | 34 | if (_.isObject(scribeOptions)) { 35 | scribeConfig = Object.assign(scribeConfig, scribeOptions); 36 | } 37 | 38 | var scribe = new Scribe(el, scribeConfig); 39 | 40 | if (scribeOptions.hasOwnProperty("tags")) { 41 | tags = Object.assign(sanitizeDefaults, scribeOptions.tags); 42 | } 43 | 44 | scribe.use(scribePluginFormatterPlainTextConvertNewLinesToHTML()); 45 | scribe.use(scribePluginLinkPromptCommand({ editorOptions })); 46 | scribe.use(scribePluginSanitizer({tags: tags})); 47 | 48 | if (_.isFunction(configureScribe)) { 49 | configureScribe.call(this, scribe); 50 | } 51 | 52 | return scribe; 53 | }, 54 | 55 | execTextBlockCommand: function(scribeInstance, cmdName) { 56 | if (_.isUndefined(scribeInstance)) { 57 | throw "No Scribe instance found to query command"; 58 | } 59 | 60 | var cmd = scribeInstance.getCommand(cmdName); 61 | scribeInstance.el.focus(); 62 | return cmd.execute(); 63 | }, 64 | 65 | queryTextBlockCommandState: function(scribeInstance, cmdName) { 66 | if (_.isUndefined(scribeInstance)) { 67 | throw "No Scribe instance found to query command"; 68 | } 69 | 70 | var cmd = scribeInstance.getCommand(cmdName), 71 | sel = new scribeInstance.api.Selection(); 72 | return sel.range && cmd.queryState(); 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /src/templates/block-addition-top.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var config = require('../config'); 4 | 5 | module.exports = () => { 6 | return ` 7 |
8 |
9 |
10 | 11 | 12 | 13 |
14 |
15 | `; 16 | }; 17 | -------------------------------------------------------------------------------- /src/templates/block-addition.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var config = require('../config'); 4 | 5 | module.exports = () => { 6 | return ` 7 | 14 | `; 15 | }; 16 | -------------------------------------------------------------------------------- /src/templates/block-control.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var config = require('../config'); 4 | 5 | module.exports = (block) => { 6 | return ` 7 | 13 | `; 14 | }; 15 | -------------------------------------------------------------------------------- /src/templates/block-replacer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var config = require('../config'); 4 | 5 | module.exports = () => { 6 | return ` 7 | 14 | `; 15 | }; 16 | -------------------------------------------------------------------------------- /src/templates/block.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BLOCK_ADDITION_TOP_TEMPLATE = require("./block-addition-top"); 4 | const BLOCK_ADDITION_TEMPLATE = require("./block-addition"); 5 | const BLOCK_REPLACER_TEMPLATE = require("./block-replacer"); 6 | 7 | module.exports = (editor_html) => { 8 | return ` 9 |
10 | ${ editor_html } 11 |
12 | ${ BLOCK_REPLACER_TEMPLATE() } 13 | ${ BLOCK_ADDITION_TOP_TEMPLATE() } 14 | ${ BLOCK_ADDITION_TEMPLATE() } 15 | `; 16 | }; 17 | -------------------------------------------------------------------------------- /src/templates/delete.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = () => { 4 | return ` 5 |
6 | 9 | 12 | 15 |
16 | `; 17 | }; 18 | -------------------------------------------------------------------------------- /src/templates/format-button.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var config = require('../config'); 4 | 5 | module.exports = function({name, text, cmd, iconName}) { 6 | return ` 7 | 12 | `; 13 | }; 14 | -------------------------------------------------------------------------------- /src/templates/top-controls.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const BLOCK_ADDITION_TEMPLATE = require("./block-addition"); 4 | 5 | module.exports = () => { 6 | return ` 7 |
8 | ${BLOCK_ADDITION_TEMPLATE()} 9 |
10 | `; 11 | }; 12 | -------------------------------------------------------------------------------- /src/vendor/array-includes.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // jshint freeze: false, maxcomplexity: 11 4 | 5 | if (!Array.prototype.includes) { 6 | Array.prototype.includes = function(searchElement /*, fromIndex*/ ) { 7 | var O = Object(this); 8 | var len = parseInt(O.length) || 0; 9 | if (len === 0) { 10 | return false; 11 | } 12 | var n = parseInt(arguments[1]) || 0; 13 | var k; 14 | if (n >= 0) { 15 | k = n; 16 | } else { 17 | k = len + n; 18 | if (k < 0) {k = 0;} 19 | } 20 | var currentElement; 21 | while (k < len) { 22 | currentElement = O[k]; 23 | if (searchElement === currentElement || 24 | (searchElement !== searchElement && currentElement !== currentElement)) { 25 | return true; 26 | } 27 | k++; 28 | } 29 | return false; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /src/vendor/custom-event.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | if ( typeof window.CustomEvent === "function" ) return false; 5 | 6 | function CustomEvent ( event, params ) { 7 | params = params || { bubbles: false, cancelable: false, detail: undefined }; 8 | var evt = document.createEvent( 'CustomEvent' ); 9 | evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); 10 | return evt; 11 | } 12 | 13 | CustomEvent.prototype = window.Event.prototype; 14 | 15 | window.CustomEvent = CustomEvent; 16 | }()); -------------------------------------------------------------------------------- /src/vendor/dom-shims.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | // The IE's "contains" method does not work when a text node is passed as argument. 5 | if (/Trident/.test(navigator.userAgent)) { 6 | Object.defineProperty(HTMLElement.prototype, 'contains', { 7 | writable: true, 8 | enumerable: false, 9 | configurable: true, 10 | value: function(node) { 11 | if (!node) return false; 12 | return this === node || !!(this.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_CONTAINED_BY); 13 | } 14 | }); 15 | } 16 | 17 | // IE does not implement `Document.prototype.contains` 18 | if (typeof Document.prototype.contains !== 'function') { 19 | Object.defineProperty(Document.prototype, 'contains', { 20 | writable: true, 21 | enumerable: false, 22 | configurable: true, 23 | value: function(el) { 24 | if (!el) return false; 25 | return this.documentElement.contains(el); 26 | } 27 | }); 28 | } 29 | 30 | // IE does not implement `Range.prototype.intersectsNode` 31 | if (typeof Range.prototype.intersectsNode !== 'function') { 32 | Object.defineProperty(Range.prototype, 'intersectsNode', { 33 | writable: true, 34 | enumerable: false, 35 | configurable: true, 36 | value: function(node) { 37 | if (!node) { 38 | throw new TypeError("Failed to execute 'intersectsNode' on 'Range': 1 argument required, but only 0 present."); 39 | } 40 | if (this.startContainer.ownerDocument !== node.ownerDocument) return false; 41 | if (!node.parentNode) return true; 42 | 43 | var targetRange = document.createRange(); 44 | targetRange.selectNode(node); 45 | var startEnd = this.compareBoundaryPoints(Range.START_TO_END, targetRange); 46 | var endStart = this.compareBoundaryPoints(Range.END_TO_START, targetRange); 47 | 48 | return startEnd === 1 && endStart === -1; 49 | } 50 | }); 51 | } 52 | }()); -------------------------------------------------------------------------------- /src/vendor/ie-classlist-toggle.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | "use strict"; 3 | 4 | var testElement = document.createElement("_"); 5 | 6 | testElement.classList.add("c1", "c2"); 7 | 8 | // Polyfill for IE 10/11 and Firefox <26, where classList.add and 9 | // classList.remove exist but support only one argument at a time. 10 | if (!testElement.classList.contains("c2")) { 11 | var createMethod = function(method) { 12 | var original = DOMTokenList.prototype[method]; 13 | 14 | DOMTokenList.prototype[method] = function(token) { 15 | var i, len = arguments.length; 16 | 17 | for (i = 0; i < len; i++) { 18 | token = arguments[i]; 19 | original.call(this, token); 20 | } 21 | }; 22 | }; 23 | createMethod('add'); 24 | createMethod('remove'); 25 | } 26 | 27 | testElement.classList.toggle("c3", false); 28 | 29 | // Polyfill for IE 10 and Firefox <24, where classList.toggle does not 30 | // support the second argument. 31 | if (testElement.classList.contains("c3")) { 32 | var _toggle = DOMTokenList.prototype.toggle; 33 | 34 | DOMTokenList.prototype.toggle = function(token, force) { 35 | if (1 in arguments && !this.contains(token) === !force) { 36 | return force; 37 | } else { 38 | return _toggle.call(this, token); 39 | } 40 | }; 41 | 42 | } 43 | 44 | testElement = null; 45 | }()); -------------------------------------------------------------------------------- /used_underscore_functions.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | egrep -Rho "\b_\.\w+" src | sort | uniq 6 | -------------------------------------------------------------------------------- /website/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.1 2 | -------------------------------------------------------------------------------- /website/Gemfile: -------------------------------------------------------------------------------- 1 | # If you have OpenSSL installed, we recommend updating 2 | # the following line to use "https" 3 | source 'http://rubygems.org' 4 | 5 | gem "middleman", "~>4.3.0" 6 | gem 'middleman-gh-pages' 7 | 8 | # Live-reloading plugin 9 | gem "middleman-livereload" 10 | 11 | gem "redcarpet" 12 | 13 | # For faster file watcher updates: 14 | # gem "wdm", "~> 0.1.0") # Windows 15 | 16 | # Cross-templating language block fix for Ruby 1.8 17 | platforms :mri_18 do 18 | gem "ruby18_source_location" 19 | end -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Sir Trevor JS Website 2 | 3 | The website is created with Middleman and deployed to Github pages. 4 | 5 | To update the website you're going to need Ruby and bundler installed. You can read a full guide to [getting started with Middleman here](http://12devs.co.uk/articles/204/). 6 | 7 | The `build` folder points to an orphaned git branch that goes up to Github pages on the `gh-pages` branch. 8 | 9 | First off init the submodule (assuming you're in the website directory) 10 | 11 | cd ../ && git submodule init 12 | 13 | Next, make changes to the website **in the `source` directory** and commit these changes. Then, when you're ready to push run: 14 | 15 | bundle exec middleman build 16 | 17 | Change into the `website/build` directory push your changes: 18 | 19 | git push origin gh-pages 20 | 21 | Then, switch back to the website folder and commit your changes: 22 | 23 | cd ../ && git push origin master 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /website/Rakefile: -------------------------------------------------------------------------------- 1 | require 'middleman-gh-pages' -------------------------------------------------------------------------------- /website/config.rb: -------------------------------------------------------------------------------- 1 | ### 2 | # Compass 3 | ### 4 | 5 | # Change Compass configuration 6 | # compass_config do |config| 7 | # config.output_style = :compact 8 | # end 9 | 10 | ### 11 | # Page options, layouts, aliases and proxies 12 | ### 13 | 14 | # Per-page layout changes: 15 | # 16 | # With no layout 17 | # page "/path/to/file.html", :layout => false 18 | # 19 | # With alternative layout 20 | # page "/path/to/file.html", :layout => :otherlayout 21 | # 22 | # A path which all have the same layout 23 | # with_layout :admin do 24 | # page "/admin/*" 25 | # end 26 | 27 | # Proxy pages (http://middlemanapp.com/dynamic-pages/) 28 | # proxy "/this-page-has-no-template.html", "/template-file.html", :locals => { 29 | # :which_fake_page => "Rendering a fake page with a local variable" } 30 | 31 | ### 32 | # Helpers 33 | ### 34 | 35 | # Automatic image dimensions on image_tag helper 36 | # activate :automatic_image_sizes 37 | 38 | # Reload the browser automatically whenever files change 39 | # activate :livereload 40 | 41 | # Methods defined in the helpers block are available in templates 42 | # helpers do 43 | # def some_helper 44 | # "Helping" 45 | # end 46 | # end 47 | 48 | set :css_dir, 'stylesheets' 49 | set :js_dir, 'javascripts' 50 | set :images_dir, 'images' 51 | set :relative_links, true 52 | 53 | activate :external_pipeline, 54 | name: :webpack, 55 | command: build? ? 56 | "./node_modules/webpack/bin/webpack.js --bail -p" : 57 | "./node_modules/webpack/bin/webpack.js --watch -d --progress --color", 58 | source: ".tmp/dist", 59 | latency: 1 60 | 61 | set :markdown_engine, :redcarpet 62 | set :markdown, 63 | :hard_wrap => true, 64 | :fenced_code_blocks => true, 65 | :smartypants => true, 66 | :layout_engine => :erb, 67 | :autolink => true 68 | 69 | # Build-specific configuration 70 | configure :build do 71 | activate :minify_css 72 | activate :minify_javascript 73 | #activate :asset_hash 74 | activate :relative_assets 75 | 76 | # Or use a different image path 77 | # set :http_path, "/Content/images/" 78 | end 79 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sir-trevor-js-docs", 3 | "version": "1.0.0", 4 | "description": "The website is created with Middleman and deployed to Github pages.", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "dev": "webpack --mode development", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@webpack-cli/migrate": "^0.1.5", 14 | "mini-css-extract-plugin": "^0.5.0", 15 | "node-sass": "^4.12.0", 16 | "sass-loader": "^7.1.0", 17 | "sir-trevor": "^0.8.2", 18 | "webpack": "^4.28.4", 19 | "webpack-cli": "^3.2.1" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /website/source/blank.html.erb: -------------------------------------------------------------------------------- 1 | --- 2 | title: Blank | Sir Trevor JS | Made by Many 3 | layout: example 4 | --- 5 | 6 | <%= partial "partials/header" %> 7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 | <%= partial "partials/footer" %> -------------------------------------------------------------------------------- /website/source/docs.html.erb: -------------------------------------------------------------------------------- 1 | --- 2 | title: Docs | Sir Trevor JS | Made by Many 3 | --- 4 | 5 | <%= partial "partials/header" %> 6 | 7 |
8 | <%= partial "partials/docs/nav" %> 9 | 10 |
11 | <%= partial "partials/docs/1" %> 12 | <%= partial "partials/docs/2" %> 13 | <%= partial "partials/docs/3" %> 14 | <%= partial "partials/docs/4" %> 15 | <%= partial "partials/docs/5" %> 16 |
17 |
18 | 19 | <%= partial "partials/footer" %> -------------------------------------------------------------------------------- /website/source/example.html.erb: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example | Sir Trevor JS | Made by Many 3 | layout: example 4 | --- 5 | 6 | <%= partial "partials/header" %> 7 | 8 |
9 |
10 | 11 |
12 |
13 | 14 | <%= partial "partials/footer" %> -------------------------------------------------------------------------------- /website/source/images/itv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madebymany/sir-trevor-js/8b5807a94f6e4486ee00ed815f898c3eb72ef19d/website/source/images/itv.png -------------------------------------------------------------------------------- /website/source/images/mxm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madebymany/sir-trevor-js/8b5807a94f6e4486ee00ed815f898c3eb72ef19d/website/source/images/mxm.png -------------------------------------------------------------------------------- /website/source/images/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madebymany/sir-trevor-js/8b5807a94f6e4486ee00ed815f898c3eb72ef19d/website/source/images/screen.png -------------------------------------------------------------------------------- /website/source/images/sir-trevor-icons.svg: -------------------------------------------------------------------------------- 1 | ../../node_modules/sir-trevor/build/sir-trevor-icons.svg -------------------------------------------------------------------------------- /website/source/images/sir-trevor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madebymany/sir-trevor-js/8b5807a94f6e4486ee00ed815f898c3eb72ef19d/website/source/images/sir-trevor.gif -------------------------------------------------------------------------------- /website/source/javascripts/all.js: -------------------------------------------------------------------------------- 1 | require('../stylesheets/all.scss'); 2 | require('jquery.typer'); 3 | 4 | $(function(){ 5 | var $typer = $('.typer-target'); 6 | 7 | $.typer.options.highlightSpeed = 10; 8 | $.typer.options.typeSpeed = 75; 9 | $.typer.options.typeDelay = 75; 10 | 11 | $typer.typer(); 12 | }); 13 | -------------------------------------------------------------------------------- /website/source/javascripts/example.js: -------------------------------------------------------------------------------- 1 | var SirTrevor = require('../../node_modules/sir-trevor'); 2 | require('../stylesheets/example.scss'); 3 | 4 | (function() { 5 | 6 | SirTrevor.setDefaults({ 7 | iconUrl: document.body.getAttribute('icon-url') 8 | }); 9 | 10 | new SirTrevor.Editor({ 11 | el: document.querySelector(".js-sir-trevor-instance"), 12 | blockTypes: ["Text", "List", "Video", "Quote", "Iframe"], 13 | defaultType: ['Text'] 14 | }); 15 | })(); -------------------------------------------------------------------------------- /website/source/layouts/example.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <%= current_page.data.title %> 12 | <%= stylesheet_link_tag "example" %> 13 | 14 | "> 15 | 16 | <%= yield %> 17 | 18 | <%= javascript_include_tag "example" %> 19 | 20 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /website/source/layouts/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | <%= current_page.data.title %> 12 | <%= stylesheet_link_tag "normalize", "all" %> 13 | 14 | 15 | 16 | <%= yield %> 17 | 18 | 19 | <%= javascript_include_tag "all" %> 20 | 21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /website/source/partials/_footer.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 13 |
-------------------------------------------------------------------------------- /website/source/partials/_header.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/source/partials/docs/_2.html.markdown: -------------------------------------------------------------------------------- 1 | 2 | # Options 3 | 4 | The editor accepts the following options. Options are passed to the editor on initialisation. 5 | 6 | **`blockTypes`** 7 | Specify an array of block types to use with the editor. 8 | *Defaults to all block types*. 9 | 10 | ```js 11 | { 12 | blockTypes: ["Text", "Tweet", "Image"] 13 | } 14 | ``` 15 | 16 | **`defaultType`** 17 | Specify a default block to start the editor with. 18 | *Defaults to no block*. 19 | 20 | ```js 21 | { 22 | defaultType: "Text" 23 | } 24 | ``` 25 | 26 | **`blockLimit`** 27 | Set an overall total number of blocks that can be displayed. 28 | *Defaults to 0 (infinite)*. 29 | 30 | ```js 31 | { 32 | blockLimit: 1 33 | } 34 | ``` 35 | 36 | **`blockTypeLimits`** 37 | Set a limit on the number of blocks that can be displayed by its type. 38 | *Defaults to {}*. 39 | 40 | ```js 41 | { 42 | blockTypeLimits: { 43 | "Text": 2, 44 | "Image": 1 45 | } 46 | } 47 | ``` 48 | 49 | **`required`** 50 | Specify which block types are required for validatation. 51 | *Defaults to none*. 52 | 53 | ```js 54 | { 55 | required: ["Text", "Image"] 56 | } 57 | ``` 58 | 59 | **`onEditorRender`** 60 | Call a function once the Editor has rendered. 61 | *Defaults to undefined*. 62 | 63 | ```js 64 | { 65 | onEditorRender: function() { 66 | alert('Do something'); 67 | } 68 | } 69 | ``` 70 | 71 | **`headingLevels`** 72 | Set the heading levels that can be set with the heading block 73 | *Defaults to [2]*. 74 | 75 | ```js 76 | { 77 | headingLevels: [1,2,3,4,5,6] 78 | } 79 | ``` 80 | 81 | 82 | **`defaultHeadingLevel`** 83 | Set the default heading level when a heading block is added 84 | *Defaults to 2*. 85 | 86 | ```js 87 | { 88 | defaultHeadingLevel: 2 89 | } 90 | ``` 91 | 92 | 93 | ## Block Options 94 | 95 | You can set specific options for blocks by using the `setBlockOptions` method. 96 | 97 | ```js 98 | SirTrevor.setBlockOptions('Tweet', { 99 | someValue: true 100 | }); 101 | ``` 102 | 103 | 104 | ## Global Options 105 | 106 | You can also set options *globally* for all Sir Trevor instances using the `setDefaults` method. 107 | 108 | ```js 109 | SirTrevor.setDefaults({ 110 | required: ["Text"] 111 | }); 112 | ``` 113 | -------------------------------------------------------------------------------- /website/source/partials/docs/_5.html.markdown: -------------------------------------------------------------------------------- 1 | 2 | # Text editing 3 | 4 | 5 | ## Scribe 6 | 7 | Sir Trevor uses [Scribe](https://github.com/guardian/scribe) for text editing. The document that is available can be found here https://github.com/guardian/scribe/wiki. 8 | 9 | 10 | ## Text Formatting 11 | 12 | The main reason for using Scribe is to provide text formatting and make content editable cross browser compatible. 13 | 14 | Formatting has been enabled using various plugins that can be found here: 15 | https://github.com/madebymany/sir-trevor-js/blob/master/src/scribe-interface.js 16 | 17 | To add your own formatting you'll want to [install a plugin](https://github.com/guardian/scribe/wiki/Plugins) or create your own. 18 | 19 | 20 | ## Create a Scribe plugin 21 | 22 | If you want to create your own plugins then follow the steps below: 23 | 24 | **Step 1.** 25 | 26 | Define your toolbar commands 27 | https://github.com/madebymany/sir-trevor-js/blob/master/src/config.js#L66-L112 28 | 29 | **Step 2.** 30 | 31 | Modify the linkPrompt plugin 32 | https://github.com/guardian/scribe-plugin-link-prompt-command 33 | 34 | The main file is 35 | https://github.com/guardian/scribe-plugin-link-prompt-command/blob/master/src/scribe-plugin-link-prompt-command.js 36 | 37 | You'll find this assigns `scribe.commands.linkPrompt` which is the `cmd` set in the config. 38 | So what you'll do is write your own plugin and then assign it to something like `scribe.commands.customLinkPrompt` then modify the `formatBar.commands` in your config override. 39 | 40 | **Step 3.** 41 | 42 | You'll need to find a way to call `scribe.use`for the plugin. 43 | 44 | If it needs to be on all blocks then you'll need to patch something in: 45 | https://github.com/madebymany/sir-trevor-js/blob/master/src/scribe-interface.js#L43 46 | 47 | Per block you can define `configureScribe` 48 | https://github.com/madebymany/sir-trevor-js/blob/master/src/blocks/text.js#L26 49 | -------------------------------------------------------------------------------- /website/source/partials/docs/_nav.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/source/setting-up-image-uploading.html.erb: -------------------------------------------------------------------------------- 1 | --- 2 | title: How to set up image uploading | Docs | Sir Trevor JS | Made by Many 3 | --- 4 | 5 | <%= partial "partials/header" %> 6 | 7 |
8 | <%= partial "partials/docs/nav" %> 9 | 10 |
11 | <%= partial "partials/docs/uploads" %> 12 |
13 |
14 | 15 | <%= partial "partials/footer" %> 16 | -------------------------------------------------------------------------------- /website/source/stylesheets/all.scss: -------------------------------------------------------------------------------- 1 | @import 'icons'; 2 | @import 'modules/variables'; 3 | @import 'modules/typography'; 4 | @import 'modules/layout'; 5 | @import 'modules/header'; 6 | @import 'modules/docs'; 7 | @import 'base'; 8 | @import 'highlight'; -------------------------------------------------------------------------------- /website/source/stylesheets/example.scss: -------------------------------------------------------------------------------- 1 | @import "normalize"; 2 | 3 | @import 'modules/variables'; 4 | @import 'modules/typography'; 5 | @import 'modules/layout'; 6 | @import 'modules/header'; 7 | @import 'base'; 8 | 9 | @import "../../node_modules/sir-trevor/build/sir-trevor"; 10 | -------------------------------------------------------------------------------- /website/source/stylesheets/modules/_docs.scss: -------------------------------------------------------------------------------- 1 | code, pre { 2 | background-color: darken(#fff, 5%); 3 | color: lighten(#000, 20%); 4 | font-size: 16px; 5 | } 6 | 7 | pre { 8 | padding: .5em; 9 | border-left: 3px solid darken(#f2f2f2, 5%); 10 | margin-bottom: 20px; 11 | } 12 | 13 | .docs { 14 | overflow: hidden; 15 | } 16 | 17 | .docs__content { 18 | font-size: 18px; 19 | width: 75%; 20 | float: left; 21 | padding-bottom: 30px; 22 | 23 | h1, h2, h3 { 24 | color: lighten(#000, 20%); 25 | } 26 | 27 | h1 { 28 | padding-bottom: 10px; 29 | border-bottom: 1px solid #ccc; 30 | } 31 | } 32 | 33 | .docs__navigation { 34 | width: 20%; 35 | margin: 40px 5% 0 0; 36 | float: left; 37 | 38 | ul { 39 | margin: 0; padding: 0; 40 | list-style: none; 41 | } 42 | 43 | ul ul a { 44 | padding-left: 20px; 45 | font-weight: normal; 46 | } 47 | 48 | a { 49 | display: block; 50 | padding: .3em .5em; 51 | font-size: 18px; 52 | border-bottom: 1px solid #ccc; 53 | color: #333; 54 | text-decoration: none; 55 | font-weight: bold; 56 | 57 | &:hover, 58 | &:focus { 59 | background-color: #f2f2f2; 60 | } 61 | } 62 | } 63 | 64 | @include breakpoint($medium-break) { 65 | .docs__navigation { 66 | display: none; 67 | } 68 | 69 | .docs__content { 70 | float: none; 71 | width: auto; 72 | 73 | h1 { font-size: 1.5em; } 74 | } 75 | } -------------------------------------------------------------------------------- /website/source/stylesheets/modules/_header.scss: -------------------------------------------------------------------------------- 1 | .site-logo { 2 | display: inline-block; 3 | margin: px-to-em(20px) 0; 4 | padding-left: 1.25em; 5 | font-size: px-to-em(28px); 6 | color: #fff; 7 | text-decoration: none; 8 | font-weight: 600; 9 | position: relative; 10 | 11 | &:before { 12 | color: $accent-color; 13 | position: absolute; 14 | left: 0; top: .15em; 15 | } 16 | 17 | &:hover { 18 | color: $accent-color; 19 | } 20 | 21 | @include breakpoint($small-break) { 22 | padding-left: 1.1em; 23 | } 24 | } 25 | 26 | .site-header__info-container { 27 | padding: px-to-em(20px) 0; 28 | position: relative; 29 | } 30 | 31 | .site-header__video { 32 | position: absolute; 33 | bottom: 0; 34 | right: 0; 35 | 36 | @include breakpoint(px-to-em(1400px)) { 37 | display: none; 38 | } 39 | } 40 | 41 | .site-nav { 42 | margin: 1.75em 0 0 0; 43 | float: right; 44 | } 45 | 46 | .site-nav__items { 47 | margin: 0; padding: 0; 48 | list-style: none; 49 | } 50 | 51 | .site-nav__item { 52 | margin-left: 1.5em; 53 | float: left; 54 | 55 | a { 56 | color: $accent-color; 57 | text-decoration: none; 58 | } 59 | 60 | @include breakpoint($small-break) { 61 | margin-left: .5em; 62 | } 63 | } -------------------------------------------------------------------------------- /website/source/stylesheets/modules/_layout.scss: -------------------------------------------------------------------------------- 1 | .site-width { 2 | width: $large-break; 3 | margin: 0pt auto; 4 | overflow: hidden; 5 | 6 | @include breakpoint($large-break + 20) { 7 | margin: 0 5%; 8 | width: auto; 9 | } 10 | } 11 | 12 | .example-container { 13 | padding: 50px 0; 14 | } -------------------------------------------------------------------------------- /website/source/stylesheets/modules/_typography.scss: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600); 2 | 3 | html { 4 | font-family: 'Source Sans Pro', sans-serif; 5 | font-size: 100%; 6 | line-height: 1.3; 7 | -webkit-font-smoothing: antialiased; 8 | } 9 | 10 | body { 11 | font-size: $base-font-size; 12 | color: #707477; 13 | 14 | @include breakpoint($medium-break) { 15 | font-size: 16px; 16 | } 17 | } -------------------------------------------------------------------------------- /website/source/stylesheets/modules/_variables.scss: -------------------------------------------------------------------------------- 1 | $base-font-size: 20px; 2 | 3 | @function px-to-em($px, $base: $base-font-size) { 4 | @return ($px / $base) * 1em; 5 | } 6 | 7 | $large-break: px-to-em(960px); 8 | $medium-break: px-to-em(760px); 9 | $small-break: px-to-em(480px); 10 | 11 | $accent-color: #34E0C2; 12 | $dark-grey: #42474B; 13 | 14 | @mixin breakpoint($point) { 15 | @media (max-width: $point) { @content; } 16 | } 17 | 18 | @function grid-calc($n, $g: 12) { 19 | @return ($n / $g) * 100%; 20 | } 21 | 22 | @for $i from 1 through 12 { 23 | %grid-#{$i}-of-12 { 24 | @extend %grid; 25 | width: grid-calc($i); 26 | } 27 | } 28 | 29 | %grid { 30 | float: left; 31 | } 32 | 33 | %grid-half { 34 | @extend %grid; 35 | width: 50%; 36 | 37 | @include breakpoint($medium-break) { 38 | float: none; 39 | width: auto; 40 | } 41 | } 42 | 43 | %grid-five { 44 | @extend %grid; 45 | width: grid-calc(3,15); 46 | 47 | @include breakpoint($medium-break) { 48 | width: grid-calc(4); 49 | } 50 | } 51 | 52 | %grid-one-third { 53 | @extend %grid; 54 | width: grid-calc(4); 55 | 56 | @include breakpoint($medium-break) { 57 | float: none; 58 | width: auto; 59 | } 60 | } 61 | 62 | %grid-two-thirds { 63 | @extend %grid; 64 | width: grid-calc(8); 65 | 66 | @include breakpoint($medium-break) { 67 | float: none; 68 | width: auto; 69 | } 70 | } 71 | 72 | p a, 73 | li a { 74 | color: $accent-color; 75 | } -------------------------------------------------------------------------------- /website/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); 4 | 5 | module.exports = { 6 | entry: { 7 | all: "./source/javascripts/all.js", 8 | example: "./source/javascripts/example.js" 9 | }, 10 | 11 | module: { 12 | rules: [ 13 | { 14 | test: /\.js$/, 15 | exclude: /node_modules/, 16 | use: { 17 | loader: "babel-loader" 18 | } 19 | }, 20 | { 21 | test: /\.scss$/, 22 | use: [ 23 | "style-loader", 24 | { 25 | loader: MiniCssExtractPlugin.loader, 26 | options: { 27 | publicPath: "../" 28 | } 29 | }, 30 | "css-loader", 31 | "sass-loader" 32 | ] 33 | }, 34 | { 35 | test: /\.(jpe?g|png|gif|mp3)$/i, 36 | use: [ 37 | { 38 | loader: "file-loader", 39 | options: { 40 | name: "images/[name].[ext]" 41 | } 42 | } 43 | ] 44 | } 45 | ] 46 | }, 47 | 48 | resolve: { 49 | modules: [__dirname + "/source/javascripts"] 50 | }, 51 | 52 | output: { 53 | path: __dirname + "/.tmp/dist", 54 | filename: "javascripts/[name].js" 55 | }, 56 | 57 | plugins: [ 58 | new MiniCssExtractPlugin({ 59 | // Options similar to the same options in webpackOptions.output 60 | // both options are optional 61 | filename: "stylesheets/[name].css", 62 | chunkFilename: "[id].css" 63 | }) 64 | ] 65 | }; 66 | --------------------------------------------------------------------------------