├── .babelrc ├── .github ├── FUNDING.yml └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── LICENSE ├── README.md ├── TODO.md ├── dist ├── grapesjs-blocks-bootstrap4.min.js ├── grapesjs-blocks-bootstrap4.min.js.LICENSE.txt └── index.html ├── index.html ├── package-lock.json ├── package.json ├── src ├── bootstrap-btn-sizes.js ├── bootstrap-contexts.js ├── commands.js ├── components.js ├── components │ ├── Alert.js │ ├── Badge.js │ ├── Button.js │ ├── ButtonGroup.js │ ├── ButtonToolbar.js │ ├── Card.js │ ├── Checkbox.js │ ├── Collapse.js │ ├── Column.js │ ├── ColumnBreak.js │ ├── Container.js │ ├── Default.js │ ├── Dropdown.js │ ├── FileInput.js │ ├── Form.js │ ├── Header.js │ ├── Image.js │ ├── Input.js │ ├── InputGroup.js │ ├── Label.js │ ├── Link.js │ ├── List.js │ ├── MediaObject.js │ ├── Paragraph.js │ ├── Radio.js │ ├── Row.js │ ├── Select.js │ ├── Text.js │ ├── Textarea.js │ ├── tabs │ │ ├── Tab.js │ │ ├── TabPane.js │ │ ├── TabsNavigation.js │ │ ├── TabsPanes.js │ │ └── constants.js │ └── video │ │ ├── Embed.js │ │ └── Video.js ├── devices.js ├── icons │ ├── button.svg │ ├── caret-square-down-regular.svg │ ├── certificate-solid.svg │ ├── check-square-solid.svg │ ├── circle-solid.svg │ ├── columns-solid.svg │ ├── compress-solid.svg │ ├── credit-card-solid.svg │ ├── dot-circle-regular.svg │ ├── ellipsis-h-solid.svg │ ├── equals-solid.svg │ ├── exclamation-triangle-solid.svg │ ├── file-input.svg │ ├── font-solid.svg │ ├── form-group.svg │ ├── form.svg │ ├── heading-solid.svg │ ├── image-light.svg │ ├── image-solid.svg │ ├── input-group.svg │ ├── input.svg │ ├── label.svg │ ├── link-solid.svg │ ├── paragraph-solid.svg │ ├── select-input.svg │ ├── textarea.svg │ ├── window-maximize-solid.svg │ └── youtube-brands.svg ├── index.js ├── traits.js └── utils.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-proposal-object-rest-spread" 7 | ] 8 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: kaoz70 4 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [master, ] 6 | pull_request: 7 | # The branches below must be a subset of the branches above 8 | branches: [master] 9 | schedule: 10 | - cron: '0 23 * * 5' 11 | 12 | jobs: 13 | analyze: 14 | name: Analyze 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v2 20 | with: 21 | # We must fetch at least the immediate parents so that if this is 22 | # a pull request then we can checkout the head. 23 | fetch-depth: 2 24 | 25 | # If this run was triggered by a pull request event, then checkout 26 | # the head of the pull request instead of the merge commit. 27 | - run: git checkout HEAD^2 28 | if: ${{ github.event_name == 'pull_request' }} 29 | 30 | # Initializes the CodeQL tools for scanning. 31 | - name: Initialize CodeQL 32 | uses: github/codeql-action/init@v1 33 | # Override language selection by uncommenting this and choosing your languages 34 | # with: 35 | # languages: go, javascript, csharp, python, cpp, java 36 | 37 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 38 | # If this step fails, then you should remove it and run the build manually (see below) 39 | - name: Autobuild 40 | uses: github/codeql-action/autobuild@v1 41 | 42 | # ℹ️ Command-line programs to run using the OS shell. 43 | # 📚 https://git.io/JvXDl 44 | 45 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 46 | # and modify them (or add more) to build your code if your project 47 | # uses a compiled language 48 | 49 | #- run: | 50 | # make bootstrap 51 | # make release 52 | 53 | - name: Perform CodeQL Analysis 54 | uses: github/codeql-action/analyze@v1 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | private/ 3 | node_modules/ 4 | .eslintrc 5 | *.log 6 | _index.html 7 | .idea 8 | 9 | *~ 10 | *.swp 11 | *.swo 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, (YOUR NAME) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | - Redistributions in binary form must reproduce the above copyright notice, this 10 | list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | - Neither the name "GrapesJS" nor the names of its contributors may be 13 | used to endorse or promote products derived from this software without 14 | specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 20 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 23 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GrapesJS Bootstrap v4 Blocks Plugin 2 | 3 | [![npm](https://img.shields.io/npm/v/grapesjs-blocks-bootstrap4.svg)](https://www.npmjs.com/package/grapesjs-blocks-bootstrap4) 4 | 5 | 6 | 7 | 10 | 11 | ### Bootstrap 5 12 | For a Bootstrap 5 version, you can use the [Bootstrap 5 plugin](https://github.com/MoonriseSoftwareCalifornia/grapesjs-blocks-bootstrap5) from [Eric Dean Kauffman](https://github.com/toiyabe) which is a fork of this project. 13 | 14 | ## Summary 15 | 16 | * Plugin name: `grapesjs-blocks-bootstrap4` 17 | * Components (see Options for list of Blocks) 18 | * Layout 19 | * `container` 20 | * `row` 21 | * `column` 22 | * `column_break` 23 | * `media_object` 24 | * `media_body` 25 | * Components 26 | * `alert` 27 | * `tabs` 28 | * `badge` 29 | * `card` 30 | * `card_container` 31 | * `collapse` 32 | * `dropdown` 33 | * `dropdown_menu` 34 | * Typography 35 | * `text` 36 | * `header` 37 | * `paragraph` 38 | * Media 39 | * `image` 40 | * `video` 41 | * Forms 42 | * `form` 43 | * `button` 44 | * `button_group` 45 | * `button_toolbar` 46 | * `input` 47 | * `input_group` 48 | * `form_group_input` 49 | * `textarea` 50 | * `checkbox` 51 | * `radio` 52 | 53 | 54 | 55 | 56 | 57 | ## Options 58 | 59 | ```js 60 | { 61 | blocks: { 62 | ... 63 | } 64 | blockCategories: { 65 | ... 66 | } 67 | labels: { 68 | ... 69 | } 70 | formPredefinedActions: null, 71 | optionsStringSeparator: '::' 72 | } 73 | ``` 74 | 75 | ### Blocks 76 | 77 | |Option|Description|Default| 78 | |-|-|- 79 | |`default`|Rebuild default component with utility settings|true| 80 | |`text`|Rebuild text component to re-inherit from default|true| 81 | |`link`|Rebuild link component to re-inherit from default and give toggle setting|true| 82 | |`image`|Rebuild image component to re-inherit from default|true| 83 | |`video`|Rebuild video component to re-inherit from default|true| 84 | |`container`|Container (fixed/fluid)|true| 85 | |`row`|Row|true| 86 | |`column`|Columns of all sizes|true| 87 | |`column_break`|Column-break (`div.w-100`)|true| 88 | |`media_object`|Media object|true| 89 | |`alert`||true| 90 | |`tabs`||true| 91 | |`badge`||true| 92 | |`card`|Card with settings for images, image overlay, header, body, & footer components|true| 93 | |`card_container`|Layouts: group, deck, columns|true| 94 | |`collapse`|Collapse component that can be toggled via link component|true| 95 | |`dropdown`|Dropdown|true| 96 | |`header`|H1-H6|true| 97 | |`paragraph`|P tag with "lead" setting|true| 98 | |`form`||true| 99 | |`button`||true| 100 | |`button_group`||true| 101 | |`button_toolbar`||true| 102 | |`input`||true| 103 | |`input_group`||true| 104 | |`form_group_input`||true| 105 | |`textarea`||true| 106 | |`checkbox`||true| 107 | |`radio`||true| 108 | 109 | ### Block Categories 110 | 111 | These are the different categories of blocks as they are grouped in the Blocks sidebar panel. Set a value to false exclude entire groups of blocks (as well as the associated components). 112 | 113 | |Option|Description|Default| 114 | |-|-|- 115 | |`layout`|Container, row, col, col-break, media object|true| 116 | |`components`|_Bootstrap_'s Components--alert, button, card, etc.|true| 117 | |`typography`|Text, header, paragraph, etc.|true| 118 | |`basic`|Link, image, etc.|true| 119 | |`forms`|Form, input, textarea, etc.|true| 120 | 121 | 122 | ### Labels 123 | 124 | Same keys as Blocks, but value is the label for the block. 125 | 126 | |Option|Description|Default| 127 | |-|-|- 128 | |`text`||'Text'| 129 | |`header`||'Header'| 130 | 131 | etc. 132 | 133 | ### Other 134 | 135 | |Option|Description|Default| 136 | |-|-|- 137 | |`gridDevices`|Add devices based on BS grid breakpoints|true| 138 | |`gridDevicesPanel`|Build a panel in the top-left corner with device buttons (use with editor `showDevices`=`false`)|false| 139 | |`formPredefinedActions`|Pass a list of predefined form actions to generate a select menu: [{name: 'Contact', value: '/contact'}, ...], if no list is passed an input box to add the action is shown|null| 140 | |`optionsStringSeparator`|Pass a string to identify the separator of values and labels of the select options: optionValue::optionLabel. This setting WILL BE overridden by the gjs-preset-webpage plugin if enabled|'::'| 141 | 142 | 143 | ## Download 144 | 145 | 147 | * NPM 148 | * `npm i grapesjs-blocks-bootstrap4` 149 | * GIT 150 | * `git clone https://github.com/kaoz70/grapesjs-blocks-bootstrap4.git` 151 | 152 | 153 | 154 | 155 | 156 | ## Usage 157 | 158 | ```html 159 | 160 | 161 | 162 | 163 |
164 | 165 | 196 | ``` 197 | 198 | 199 | 200 | 201 | 202 | ## Development 203 | 204 | Clone the repository 205 | 206 | ```sh 207 | $ git clone https://github.com/kaoz70/grapesjs-blocks-bootstrap4.git 208 | $ cd grapesjs-blocks-bootstrap4 209 | ``` 210 | 211 | Install dependencies 212 | 213 | ```sh 214 | $ npm i 215 | ``` 216 | 217 | The plugin relies on GrapesJS via `peerDependencies` so you have to install it manually (without adding it to package.json) 218 | 219 | ```sh 220 | $ npm i grapesjs --no-save 221 | ``` 222 | 223 | Start the dev server 224 | 225 | ```sh 226 | $ npm start 227 | ``` 228 | 229 | 230 | 231 | 232 | 233 | ## License 234 | 235 | BSD 3-Clause 236 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - flex container component (row will extend this) 2 | - flex item component (column will extend this) 3 | - breadcrumb component 4 | - buttons js plugin (http://getbootstrap.com/docs/4.0/components/buttons/#button-plugin) 5 | - carousel 6 | - automatic ARIA data attrs for link/collapse 7 | - they go on the link, but we need to lookup the collapse with id=link.href to check its "show" state 8 | - search through the components and find the one with that id 9 | - class_select trait needs to somehow listen to changes of classes on selected component and re-render 10 | -------------------------------------------------------------------------------- /dist/grapesjs-blocks-bootstrap4.min.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! grapesjs-blocks-bootstrap4 - 0.2.5 */ 2 | -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GrapesJS Bootstrap v4 Blocks Plugin 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 |
20 | 35 | 39 |
40 |
41 |
42 |
43 |

GrapesJS Bootstrap v4 Blocks Plugin

44 |
45 |
46 |
47 |

Hello!

48 |

This is demo content from index.html. For development, you shouldn't edit this file. Instead, you can copy and rename it to _index.html. The next time the server starts, the new file will be served, and it will be ignored by git.

49 |
50 |
51 |
52 |
53 |
54 |
Col
55 |
Col
56 |
Col
57 |
Col
58 |
Col
59 |
Col
60 |
Col
61 |
Col
62 |
Col
63 |
Col
64 |
Col
65 |
Col
66 |
67 | 68 |
69 |
70 | 84 | 85 | 86 |
87 |
Home
88 |
Profile
89 |
Messages
90 |
Settings
91 |
92 |
93 |
94 |
95 | 96 |
97 |
98 | 99 |
100 | 101 | 102 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GrapesJS Bootstrap v4 Blocks Plugin 6 | 7 | 8 | 9 | 16 | 17 | 18 | 19 |
20 | 35 | 39 |
40 |
41 |
42 |
43 |

GrapesJS Bootstrap v4 Blocks Plugin

44 |
45 |
46 |
47 |

Hello!

48 |

This is demo content from index.html. For development, you shouldn't edit this file. Instead, you can copy and rename it to _index.html. The next time the server starts, the new file will be served, and it will be ignored by git.

49 |
50 |
51 |
52 |
53 |
54 |
Col
55 |
Col
56 |
Col
57 |
Col
58 |
Col
59 |
Col
60 |
Col
61 |
Col
62 |
Col
63 |
Col
64 |
Col
65 |
Col
66 |
67 | 68 |
69 |
70 | 84 | 85 | 86 |
87 |
Home
88 |
Profile
89 |
Messages
90 |
Settings
91 |
92 |
93 |
94 |
95 | 96 |
97 |
98 | 99 |
100 | 101 | 102 | 137 | 138 | 139 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "grapesjs-blocks-bootstrap4", 3 | "version": "0.2.5", 4 | "description": "GrapesJS Bootstrap v4 Blocks Plugin", 5 | "main": "dist/grapesjs-blocks-bootstrap4.min.js", 6 | "scripts": { 7 | "dev": "webpack serve --mode development --progress --env development", 8 | "lint": "eslint src", 9 | "v:patch": "npm version --no-git-tag-version patch", 10 | "v:minor": "npm version --no-git-tag-version minor", 11 | "build": "npm run v:patch && webpack --env production", 12 | "build-dev": "npm run && webpack --env production", 13 | "start": "webpack-cli serve --mode development --progress" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/kaoz70/grapesjs-bootstrap4-blocks.git" 18 | }, 19 | "keywords": [ 20 | "grapesjs", 21 | "plugin", 22 | "bootstrap" 23 | ], 24 | "author": "z1lk", 25 | "license": "BSD-3-Clause", 26 | "peerDependencies": { 27 | "grapesjs": "0.x" 28 | }, 29 | "devDependencies": { 30 | "@babel/core": "^7.12.17", 31 | "@babel/plugin-proposal-object-rest-spread": "^7.13.0", 32 | "@babel/preset-env": "^7.13.5", 33 | "babel-loader": "^8.2.2", 34 | "eslint": "^7.20.0", 35 | "html-webpack-plugin": "^5.6.3", 36 | "raw-loader": "^4.0.2", 37 | "terser-webpack-plugin": "^5.3.10", 38 | "webpack": "^5.96.1", 39 | "webpack-cli": "^5.1.4", 40 | "webpack-dev-server": "^5.1.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/bootstrap-btn-sizes.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'lg': 'Large', 3 | 'sm': 'Small' 4 | }; 5 | -------------------------------------------------------------------------------- /src/bootstrap-contexts.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'primary', 3 | 'secondary', 4 | 'success', 5 | 'info', 6 | 'warning', 7 | 'danger', 8 | 'light', 9 | 'dark', 10 | ]; 11 | -------------------------------------------------------------------------------- /src/commands.js: -------------------------------------------------------------------------------- 1 | export default (editor, config = {}) => { 2 | const commands = editor.Commands; 3 | } 4 | -------------------------------------------------------------------------------- /src/components.js: -------------------------------------------------------------------------------- 1 | import Collapse, {CollapseBlock} from './components/Collapse'; 2 | import Dropdown, {DropDownBlock} from './components/Dropdown'; 3 | import TabsNavigation, {TabsBlock} from "./components/tabs/TabsNavigation"; 4 | import TabsPanes from "./components/tabs/TabsPanes"; 5 | import Tab from "./components/tabs/Tab"; 6 | import TabPane from "./components/tabs/TabPane"; 7 | import Form, {FormBlock} from "./components/Form"; 8 | import Input, {InputBlock} from "./components/Input"; 9 | import InputGroup, {InputGroupBlock} from "./components/InputGroup"; 10 | import Textarea, {TextareaBlock} from "./components/Textarea"; 11 | import Select, {SelectBlock} from "./components/Select"; 12 | import Checkbox, {CheckboxBlock} from "./components/Checkbox"; 13 | import Radio, {RadioBlock} from "./components/Radio"; 14 | import Button, {ButtonBlock} from "./components/Button"; 15 | import ButtonGroup, {ButtonGroupBlock} from "./components/ButtonGroup"; 16 | import ButtonToolbar, {ButtonToolbarBlock} from "./components/ButtonToolbar"; 17 | import Label, {LabelBlock} from "./components/Label"; 18 | import Link, {LinkBlock} from "./components/Link"; 19 | import FileInput, {FileInputBlock} from "./components/FileInput"; 20 | import Image, {ImageBlock} from "./components/Image"; 21 | import Video, {VideoBlock} from "./components/video/Video"; 22 | import Embed from "./components/video/Embed"; 23 | import Paragraph, {ParagraphBlock} from "./components/Paragraph"; 24 | import Header, {HeaderBlock} from "./components/Header"; 25 | import Card, {CardBlock} from "./components/Card"; 26 | import Badge, {BadgeBlock} from "./components/Badge"; 27 | import Alert, {AlertBlock} from "./components/Alert"; 28 | import MediaObject, {MediaObjectBlock} from "./components/MediaObject"; 29 | import ColumnBreak, {ColumnBreakBlock} from "./components/ColumnBreak"; 30 | import Column, {ColumnBlock} from "./components/Column"; 31 | import Row, {RowBlock} from "./components/Row"; 32 | import Container, {ContainerBlock} from "./components/Container"; 33 | import Text, {TextBlock} from "./components/Text"; 34 | import Default from "./components/Default"; 35 | 36 | 37 | export default (editor, config = {}) => { 38 | const c = config; 39 | const domc = editor.DomComponents; 40 | const blocks = c.blocks; 41 | const bm = editor.BlockManager; 42 | const cats = c.blockCategories; 43 | 44 | const traits = { 45 | id: { 46 | name: 'id', 47 | label: c.labels.trait_id, 48 | }, 49 | for: { 50 | name: 'for', 51 | label: c.labels.trait_for, 52 | }, 53 | name: { 54 | name: 'name', 55 | label: c.labels.trait_name, 56 | }, 57 | placeholder: { 58 | name: 'placeholder', 59 | label: c.labels.trait_placeholder, 60 | }, 61 | value: { 62 | name: 'value', 63 | label: c.labels.trait_value, 64 | }, 65 | required: { 66 | type: 'checkbox', 67 | name: 'required', 68 | label: c.labels.trait_required, 69 | }, 70 | checked: { 71 | label: c.labels.trait_checked, 72 | type: 'checkbox', 73 | name: 'checked', 74 | changeProp: 1 75 | } 76 | }; 77 | 78 | if (cats.media) { 79 | if (blocks.image) { 80 | ImageBlock(bm, c.labels.image); 81 | Image(domc); 82 | } 83 | 84 | if (blocks.video) { 85 | Embed(domc); 86 | VideoBlock(bm, c.labels.video); 87 | Video(domc); 88 | } 89 | } 90 | 91 | // Rebuild the default component and add utility settings to it (border, bg, color, etc) 92 | if (cats.basic) { 93 | if (blocks.default) { 94 | Default(domc); 95 | } 96 | 97 | // Rebuild the text component and add display utility setting 98 | if (blocks.text) { 99 | TextBlock(bm, c.labels.text); 100 | Text(domc); 101 | } 102 | 103 | // Rebuild the link component with settings for collapse-control 104 | if (blocks.link) { 105 | LinkBlock(bm, c.labels.link); 106 | Link(editor); 107 | } 108 | 109 | // Basic 110 | /*if (blocks.list) { 111 | ListBlock(bm, c.labels.list) 112 | List(domc); 113 | }*/ 114 | 115 | /*if (blocks.description_list) { 116 | }*/ 117 | 118 | } 119 | 120 | // LAYOUT 121 | if (cats.layout) { 122 | if (blocks.container) { 123 | ContainerBlock(bm, c.labels.container); 124 | Container(domc); 125 | } 126 | if (blocks.row) { 127 | RowBlock(bm, c.labels.row); 128 | Row(domc); 129 | } 130 | if (blocks.column) { 131 | ColumnBlock(bm, c.labels.column); 132 | Column(domc, editor); 133 | 134 | ColumnBreakBlock(bm, c.labels.column_break); 135 | ColumnBreak(domc); 136 | } 137 | // Media object 138 | if (blocks.media_object) { 139 | MediaObjectBlock(bm, c.labels.media_object); 140 | MediaObject(domc); 141 | } 142 | } 143 | 144 | // Bootstrap COMPONENTS 145 | if (cats.components) { 146 | // Alert 147 | if (blocks.alert) { 148 | AlertBlock(bm, c.labels.alert); 149 | Alert(domc); 150 | } 151 | 152 | if (blocks.tabs) { 153 | TabsBlock(bm, c); 154 | TabsNavigation(domc, config); 155 | Tab(domc, config); 156 | TabsPanes(domc, config); 157 | TabPane(domc, config); 158 | } 159 | 160 | // Badge 161 | if (blocks.badge) { 162 | BadgeBlock(bm, c.labels.badge); 163 | Badge(domc); 164 | } 165 | 166 | // Card 167 | if (blocks.card) { 168 | CardBlock(bm, c); 169 | Card(domc, editor); 170 | } 171 | 172 | // Collapse 173 | if (blocks.collapse) { 174 | CollapseBlock(bm, c.labels.collapse); 175 | Collapse(editor); 176 | } 177 | 178 | // Dropdown 179 | if (blocks.dropdown) { 180 | DropDownBlock(bm, c.labels.dropdown); 181 | Dropdown(editor); 182 | } 183 | 184 | } 185 | 186 | // TYPOGRAPHY 187 | if (cats.typography) { 188 | if (blocks.header) { 189 | HeaderBlock(bm, c.labels.header); 190 | Header(domc); 191 | } 192 | if (blocks.paragraph) { 193 | ParagraphBlock(bm, c.labels.paragraph); 194 | Paragraph(domc); 195 | } 196 | } 197 | 198 | if(cats.forms) { 199 | if (blocks.form) { 200 | FormBlock(bm, c.labels.form); 201 | Form(domc, traits, config); 202 | } 203 | 204 | if (blocks.input) { 205 | InputBlock(bm, c.labels.input); 206 | Input(domc, traits, config); 207 | 208 | FileInputBlock(bm, c.labels.file_input); 209 | FileInput(domc, traits, config); 210 | } 211 | 212 | if (blocks.form_group_input) { 213 | InputGroupBlock(bm, c.labels.form_group_input); 214 | InputGroup(domc, traits, config); 215 | } 216 | 217 | if (blocks.textarea) { 218 | TextareaBlock(bm, c.labels.textarea); 219 | Textarea(domc, traits, config); 220 | } 221 | 222 | if (blocks.select) { 223 | SelectBlock(bm, c.labels.select); 224 | Select(editor, domc, traits, config); 225 | } 226 | 227 | if (blocks.checkbox) { 228 | CheckboxBlock(bm, c.labels.checkbox); 229 | Checkbox(domc, traits, config); 230 | } 231 | 232 | if (blocks.radio) { 233 | RadioBlock(bm, c.labels.radio); 234 | Radio(domc, traits, config); 235 | } 236 | 237 | if (blocks.label) { 238 | LabelBlock(bm, c.labels.label); 239 | Label(domc, traits, config); 240 | } 241 | 242 | if (blocks.button) { 243 | ButtonBlock(bm, c.labels.button); 244 | Button(domc); 245 | } 246 | 247 | if (blocks.button_group) { 248 | ButtonGroupBlock(bm, c.labels.button_group); 249 | ButtonGroup(domc); 250 | } 251 | 252 | if (blocks.button_toolbar) { 253 | ButtonToolbarBlock(bm, c.labels.button_toolbar, c); 254 | ButtonToolbar(domc); 255 | } 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /src/components/Alert.js: -------------------------------------------------------------------------------- 1 | import contexts from '../bootstrap-contexts'; 2 | import exclamationIcon from "raw-loader!../icons/exclamation-triangle-solid.svg"; 3 | import { capitalize } from "../utils"; 4 | 5 | export const AlertBlock = (bm, label) => { 6 | bm.add('alert', { 7 | label: ` 8 | ${exclamationIcon} 9 |
${label}
10 | `, 11 | category: 'Components', 12 | content: { 13 | type: 'alert', 14 | content: 'This is an alert—check it out!' 15 | } 16 | }); 17 | }; 18 | 19 | export default (domc) => { 20 | const textType = domc.getType('text'); 21 | const textModel = textType.model; 22 | const textView = textType.view; 23 | 24 | domc.addType('alert', { 25 | extend: 'text', 26 | model: { 27 | defaults: Object.assign({}, textModel.prototype.defaults, { 28 | 'custom-name': 'Alert', 29 | tagName: 'div', 30 | classes: ['alert'], 31 | traits: [ 32 | { 33 | type: 'class_select', 34 | options: [ 35 | { value: '', name: 'None' }, 36 | ...contexts.map(function (v) { return { value: 'alert-' + v, name: capitalize(v) } }) 37 | ], 38 | label: 'Context' 39 | } 40 | ].concat(textModel.prototype.defaults.traits) 41 | }) 42 | }, 43 | isComponent(el) { 44 | if (el && el.classList && el.classList.contains('alert')) { 45 | return { type: 'alert' }; 46 | } 47 | }, 48 | view: textView 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/Badge.js: -------------------------------------------------------------------------------- 1 | import contexts from '../bootstrap-contexts'; 2 | import certificateIcon from "raw-loader!../icons/certificate-solid.svg"; 3 | import { capitalize } from "../utils"; 4 | 5 | export const BadgeBlock = (bm, label) => { 6 | bm.add('badge', { 7 | label: ` 8 | ${certificateIcon} 9 |
${label}
10 | `, 11 | category: 'Components', 12 | content: { 13 | type: 'badge', 14 | content: 'New!' 15 | } 16 | }); 17 | }; 18 | 19 | export default (domc) => { 20 | const textType = domc.getType('text'); 21 | const textModel = textType.model; 22 | const textView = textType.view; 23 | 24 | domc.addType('badge', { 25 | extend: 'text', 26 | model: { 27 | defaults: Object.assign({}, textModel.prototype.defaults, { 28 | 'custom-name': 'Badge', 29 | tagName: 'span', 30 | classes: ['badge'], 31 | traits: [ 32 | { 33 | type: 'class_select', 34 | options: [ 35 | { value: '', name: 'None' }, 36 | ...contexts.map(function (v) { return { value: 'badge-' + v, name: capitalize(v) } }) 37 | ], 38 | label: 'Context' 39 | }, 40 | { 41 | type: 'class_select', 42 | options: [ 43 | { value: '', name: 'Default' }, 44 | { value: 'badge-pill', name: 'Pill' }, 45 | ], 46 | label: 'Shape' 47 | } 48 | ].concat(textModel.prototype.defaults.traits) 49 | }) 50 | }, 51 | isComponent(el) { 52 | if (el && el.classList && el.classList.contains('badge')) { 53 | return { type: 'badge' }; 54 | } 55 | }, 56 | view: textView 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Button.js: -------------------------------------------------------------------------------- 1 | import contexts from '../bootstrap-contexts'; 2 | import sizes from '../bootstrap-btn-sizes'; 3 | import buttonIcon from "raw-loader!../icons/button.svg"; 4 | import { capitalize } from "../utils"; 5 | 6 | export const ButtonBlock = (bm, label) => { 7 | bm.add('button', { 8 | label: `${buttonIcon}
${label}
`, 9 | category: 'Forms', 10 | content: '', 11 | }); 12 | }; 13 | 14 | export default (dc) => { 15 | const defaultType = dc.getType('default'); 16 | const defaultModel = defaultType.model; 17 | const defaultView = defaultType.view; 18 | 19 | dc.addType('button', { 20 | model: { 21 | defaults: { 22 | ...defaultModel.prototype.defaults, 23 | 'custom-name': 'Button', 24 | droppable: false, 25 | attributes: { 26 | role: 'button' 27 | }, 28 | classes: ['btn'], 29 | traits: [ 30 | { 31 | type: 'content', 32 | label: 'Text', 33 | }, 34 | { 35 | label: 'Type', 36 | type: 'select', 37 | name: 'type', 38 | options: [ 39 | { value: 'submit', name: 'Submit' }, 40 | { value: 'reset', name: 'Reset' }, 41 | { value: 'button', name: 'Button' }, 42 | ] 43 | }, 44 | { 45 | type: 'class_select', 46 | options: [ 47 | { value: '', name: 'None' }, 48 | ...contexts.map((v) => { return { value: `btn-${v}`, name: capitalize(v) } }), 49 | ...contexts.map((v) => { return { value: `btn-outline-${v}`, name: capitalize(v) + ' (Outline)' } }) 50 | ], 51 | label: 'Context' 52 | }, 53 | { 54 | type: 'class_select', 55 | options: [ 56 | { value: '', name: 'Default' }, 57 | ...Object.keys(sizes).map((k) => { return { value: `btn-${k}`, name: sizes[k] } }) 58 | ], 59 | label: 'Size' 60 | }, 61 | { 62 | type: 'class_select', 63 | options: [ 64 | { value: '', name: 'Inline' }, 65 | { value: 'btn-block', name: 'Block' } 66 | ], 67 | label: 'Width' 68 | } 69 | ].concat(defaultModel.prototype.defaults.traits) 70 | }, 71 | afterChange(e) { 72 | if (this.attributes.type === 'button') { 73 | if (this.attributes.classes.filter((klass) => { return klass.id === 'btn' }).length === 0) { 74 | this.changeType('link'); 75 | } 76 | } 77 | } 78 | }, 79 | isComponent(el) { 80 | if (el && el.classList && el.classList.contains('btn')) { 81 | return { type: 'button' }; 82 | } 83 | }, 84 | view: { 85 | events: { 86 | 'click': 'handleClick' 87 | }, 88 | 89 | init() { 90 | this.listenTo(this.model, 'change:content', this.updateContent); 91 | }, 92 | 93 | updateContent() { 94 | this.el.innerHTML = this.model.get('content') 95 | }, 96 | 97 | handleClick(e) { 98 | e.preventDefault(); 99 | }, 100 | }, 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /src/components/ButtonGroup.js: -------------------------------------------------------------------------------- 1 | import sizes from '../bootstrap-btn-sizes'; 2 | import buttonIcon from "raw-loader!../icons/button.svg"; 3 | 4 | export const ButtonGroupBlock = (bm, label) => { 5 | bm.add('button_group', { 6 | label: ` 7 | ${buttonIcon} 8 |
${label}
9 | `, 10 | category: 'Forms', 11 | content: { 12 | type: 'button_group' 13 | } 14 | }); 15 | }; 16 | 17 | export default (dc) => { 18 | 19 | const defaultType = dc.getType('default'); 20 | const defaultModel = defaultType.model; 21 | const defaultView = defaultType.view; 22 | 23 | dc.addType('button_group', { 24 | model: { 25 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 26 | 'custom-name': 'Button Group', 27 | tagName: 'div', 28 | classes: ['btn-group'], 29 | droppable: '.btn', 30 | attributes: { 31 | role: 'group' 32 | }, 33 | traits: [ 34 | { 35 | type: 'class_select', 36 | options: [ 37 | { value: '', name: 'Default' }, 38 | ...Object.keys(sizes).map(function (k) { return { value: 'btn-group-' + k, name: sizes[k] } }) 39 | ], 40 | label: 'Size' 41 | }, 42 | { 43 | type: 'class_select', 44 | options: [ 45 | { value: '', name: 'Horizontal' }, 46 | { value: 'btn-group-vertical', name: 'Vertical' }, 47 | ], 48 | label: 'Size' 49 | }, 50 | { 51 | type: 'Text', 52 | label: 'ARIA Label', 53 | name: 'aria-label', 54 | placeholder: 'A group of buttons' 55 | } 56 | ].concat(defaultModel.prototype.defaults.traits) 57 | }) 58 | }, 59 | isComponent(el) { 60 | if (el && el.classList && el.classList.contains('btn-group')) { 61 | return { type: 'button_group' }; 62 | } 63 | }, 64 | view: defaultView 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /src/components/ButtonToolbar.js: -------------------------------------------------------------------------------- 1 | import buttonIcon from "raw-loader!../icons/button.svg"; 2 | 3 | export const ButtonToolbarBlock = (bm, label) => { 4 | bm.add('button_toolbar', { 5 | label: ` 6 | ${buttonIcon} 7 |
${label}
8 | `, 9 | category: 'Forms', 10 | content: { 11 | type: 'button_toolbar' 12 | } 13 | }); 14 | }; 15 | 16 | export default (dc) => { 17 | 18 | const defaultType = dc.getType('default'); 19 | const defaultModel = defaultType.model; 20 | const defaultView = defaultType.view; 21 | 22 | dc.addType('button_toolbar', { 23 | model: { 24 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 25 | 'custom-name': 'Button Toolbar', 26 | tagName: 'div', 27 | classes: ['btn-toolbar'], 28 | droppable: '.btn-group', 29 | attributes: { 30 | role: 'toolbar' 31 | }, 32 | traits: [ 33 | { 34 | type: 'Text', 35 | label: 'ARIA Label', 36 | name: 'aria-label', 37 | placeholder: 'A toolbar of button groups' 38 | } 39 | ].concat(defaultModel.prototype.defaults.traits) 40 | }) 41 | }, 42 | isComponent(el) { 43 | if (el && el.classList && el.classList.contains('btn-toolbar')) { 44 | return { type: 'button_toolbar' }; 45 | } 46 | }, 47 | view: defaultView 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Card.js: -------------------------------------------------------------------------------- 1 | import cardIcon from "raw-loader!../icons/credit-card-solid.svg"; 2 | 3 | export const CardBlock = (bm, c) => { 4 | bm.add('card', { 5 | label: ` 6 | ${cardIcon} 7 |
${c.labels.card}
8 | `, 9 | category: 'Components', 10 | content: { 11 | type: 'card' 12 | } 13 | }); 14 | bm.add('card_container', { 15 | label: ` 16 | ${cardIcon} 17 |
${c.labels.card_container}
18 | `, 19 | category: 'Components', 20 | content: { 21 | type: 'card_container' 22 | } 23 | }); 24 | }; 25 | 26 | export default (domc, editor) => { 27 | const comps = editor.DomComponents; 28 | const defaultType = comps.getType('default'); 29 | const defaultModel = defaultType.model; 30 | const defaultView = defaultType.view; 31 | const imageType = domc.getType('image'); 32 | const imageModel = imageType.model; 33 | const imageView = imageType.view; 34 | 35 | domc.addType('card', { 36 | model: { 37 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 38 | 'custom-name': 'Card', 39 | classes: ['card'], 40 | traits: [ 41 | { 42 | type: 'checkbox', 43 | label: 'Image Top', 44 | name: 'card-img-top', 45 | changeProp: 1 46 | }, 47 | { 48 | type: 'checkbox', 49 | label: 'Header', 50 | name: 'card-header', 51 | changeProp: 1 52 | }, 53 | { 54 | type: 'checkbox', 55 | label: 'Image', 56 | name: 'card-img', 57 | changeProp: 1 58 | }, 59 | { 60 | type: 'checkbox', 61 | label: 'Image Overlay', 62 | name: 'card-img-overlay', 63 | changeProp: 1 64 | }, 65 | { 66 | type: 'checkbox', 67 | label: 'Body', 68 | name: 'card-body', 69 | changeProp: 1 70 | }, 71 | { 72 | type: 'checkbox', 73 | label: 'Footer', 74 | name: 'card-footer', 75 | changeProp: 1 76 | }, 77 | { 78 | type: 'checkbox', 79 | label: 'Image Bottom', 80 | name: 'card-img-bottom', 81 | changeProp: 1 82 | } 83 | ].concat(defaultModel.prototype.defaults.traits) 84 | }), 85 | init2() { 86 | this.listenTo(this, 'change:card-img-top', this.cardImageTop); 87 | this.listenTo(this, 'change:card-header', this.cardHeader); 88 | this.listenTo(this, 'change:card-img', this.cardImage); 89 | this.listenTo(this, 'change:card-img-overlay', this.cardImageOverlay); 90 | this.listenTo(this, 'change:card-body', this.cardBody); 91 | this.listenTo(this, 'change:card-footer', this.cardFooter); 92 | this.listenTo(this, 'change:card-img-bottom', this.cardImageBottom); 93 | this.components().comparator = 'card-order'; 94 | this.set('card-img-top', true); 95 | this.set('card-body', true); 96 | }, 97 | cardImageTop() { this.createCardComponent('card-img-top'); }, 98 | cardHeader() { this.createCardComponent('card-header'); }, 99 | cardImage() { this.createCardComponent('card-img'); }, 100 | cardImageOverlay() { this.createCardComponent('card-img-overlay'); }, 101 | cardBody() { this.createCardComponent('card-body'); }, 102 | cardFooter() { this.createCardComponent('card-footer'); }, 103 | cardImageBottom() { this.createCardComponent('card-img-bottom'); }, 104 | createCardComponent(prop) { 105 | const state = this.get(prop); 106 | const type = prop.replace(/-/g, '_').replace(/img/g, 'image') 107 | let children = this.components(); 108 | let existing = children.filter(function (comp) { 109 | return comp.attributes.type === type; 110 | })[0]; // should only be one of each. 111 | 112 | if (state && !existing) { 113 | var comp = children.add({ 114 | type: type 115 | }); 116 | let comp_children = comp.components(); 117 | if (prop === 'card-header') { 118 | comp_children.add({ 119 | type: 'header', 120 | tagName: 'h4', 121 | style: { 'margin-bottom': '0px' }, 122 | content: 'Card Header' 123 | }); 124 | } 125 | if (prop === 'card-img-overlay') { 126 | comp_children.add({ 127 | type: 'header', 128 | tagName: 'h4', 129 | classes: ['card-title'], 130 | content: 'Card title' 131 | }); 132 | comp_children.add({ 133 | type: 'text', 134 | tagName: 'p', 135 | classes: ['card-text'], 136 | content: "Some quick example text to build on the card title and make up the bulk of the card's content." 137 | }); 138 | } 139 | if (prop === 'card-body') { 140 | comp_children.add({ 141 | type: 'header', 142 | tagName: 'h4', 143 | classes: ['card-title'], 144 | content: 'Card title' 145 | }); 146 | comp_children.add({ 147 | type: 'header', 148 | tagName: 'h6', 149 | classes: ['card-subtitle', 'text-muted', 'mb-2'], 150 | content: 'Card subtitle' 151 | }); 152 | comp_children.add({ 153 | type: 'text', 154 | tagName: 'p', 155 | classes: ['card-text'], 156 | content: "Some quick example text to build on the card title and make up the bulk of the card's content." 157 | }); 158 | comp_children.add({ 159 | type: 'link', 160 | classes: ['card-link'], 161 | href: '#', 162 | content: 'Card link' 163 | }); 164 | comp_children.add({ 165 | type: 'link', 166 | classes: ['card-link'], 167 | href: '#', 168 | content: 'Another link' 169 | }); 170 | } 171 | this.order(); 172 | } else if (!state) { 173 | existing.destroy(); 174 | } 175 | }, 176 | order() { 177 | 178 | } 179 | }, 180 | isComponent(el) { 181 | if (el && el.classList && el.classList.contains('card')) { 182 | return { type: 'card' }; 183 | } 184 | }, 185 | view: defaultView 186 | }); 187 | 188 | domc.addType('card_image_top', { 189 | extend: 'image', 190 | model: { 191 | defaults: Object.assign({}, imageModel.prototype.defaults, { 192 | 'custom-name': 'Card Image Top', 193 | classes: ['card-img-top'], 194 | 'card-order': 1 195 | }) 196 | }, 197 | isComponent(el) { 198 | if (el && el.classList && el.classList.contains('card-img-top')) { 199 | return { type: 'card_image_top' }; 200 | } 201 | }, 202 | view: imageView 203 | }); 204 | 205 | domc.addType('card_header', { 206 | model: { 207 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 208 | 'custom-name': 'Card Header', 209 | classes: ['card-header'], 210 | 'card-order': 2 211 | }) 212 | }, 213 | isComponent(el) { 214 | if (el && el.classList && el.classList.contains('card-header')) { 215 | return { type: 'card_header' }; 216 | } 217 | }, 218 | view: defaultView 219 | }); 220 | 221 | domc.addType('card_image', { 222 | extend: 'image', 223 | model: { 224 | defaults: Object.assign({}, imageModel.prototype.defaults, { 225 | 'custom-name': 'Card Image', 226 | classes: ['card-img'], 227 | 'card-order': 3 228 | }) 229 | }, 230 | isComponent(el) { 231 | if (el && el.classList && el.classList.contains('card-img')) { 232 | return { type: 'card_image' }; 233 | } 234 | }, 235 | view: imageView 236 | }); 237 | 238 | domc.addType('card_image_overlay', { 239 | model: { 240 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 241 | 'custom-name': 'Card Image Overlay', 242 | classes: ['card-img-overlay'], 243 | 'card-order': 4 244 | }) 245 | }, 246 | isComponent(el) { 247 | if (el && el.classList && el.classList.contains('card-img-overlay')) { 248 | return { type: 'card_image_overlay' }; 249 | } 250 | }, 251 | view: defaultView 252 | }); 253 | 254 | domc.addType('card_body', { 255 | model: { 256 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 257 | 'custom-name': 'Card Body', 258 | classes: ['card-body'], 259 | 'card-order': 5 260 | }) 261 | }, 262 | isComponent(el) { 263 | if (el && el.classList && el.classList.contains('card-body')) { 264 | return { type: 'card_body' }; 265 | } 266 | }, 267 | view: defaultView 268 | }); 269 | 270 | domc.addType('card_footer', { 271 | model: { 272 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 273 | 'custom-name': 'Card Footer', 274 | classes: ['card-footer'], 275 | 'card-order': 6 276 | }) 277 | }, 278 | isComponent(el) { 279 | if (el && el.classList && el.classList.contains('card-footer')) { 280 | return { type: 'card_footer' }; 281 | } 282 | }, 283 | view: defaultView 284 | }); 285 | 286 | domc.addType('card_image_bottom', { 287 | extend: 'image', 288 | model: { 289 | defaults: Object.assign({}, imageModel.prototype.defaults, { 290 | 'custom-name': 'Card Image Bottom', 291 | classes: ['card-img-bottom'], 292 | 'card-order': 7 293 | }) 294 | }, 295 | isComponent(el) { 296 | if (el && el.classList && el.classList.contains('card-img-bottom')) { 297 | return { type: 'card_image_bottom' }; 298 | } 299 | }, 300 | view: imageView 301 | }); 302 | 303 | domc.addType('card_container', { 304 | model: { 305 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 306 | 'custom-name': 'Card Container', 307 | classes: ['card-group'], 308 | droppable: '.card', 309 | traits: [ 310 | { 311 | type: 'class_select', 312 | options: [ 313 | { value: 'card-group', name: 'Group' }, 314 | { value: 'card-deck', name: 'Deck' }, 315 | { value: 'card-columns', name: 'Columns' }, 316 | ], 317 | label: 'Layout', 318 | } 319 | ].concat(defaultModel.prototype.defaults.traits) 320 | }) 321 | }, 322 | isComponent(el) { 323 | const css = Array.from(el.classList || []); 324 | const includes = ['card-group', 'card-deck', 'card-columns']; 325 | const intersection = css.filter(x => includes.includes(x)); 326 | 327 | if (el && el.classList && intersection.length) { 328 | return { type: 'card_container' }; 329 | } 330 | }, 331 | view: defaultView 332 | }); 333 | 334 | } 335 | -------------------------------------------------------------------------------- /src/components/Checkbox.js: -------------------------------------------------------------------------------- 1 | import checkIcon from "raw-loader!../icons/check-square-solid.svg"; 2 | 3 | export const CheckboxBlock = (bm, label) => { 4 | bm.add('checkbox', { 5 | label: ` 6 | ${checkIcon} 7 |
${label}
8 | `, 9 | category: 'Forms', 10 | content: ` 11 |
12 | 13 | 16 |
17 | `, 18 | }); 19 | }; 20 | 21 | export default (dc, traits, config = {}) => { 22 | const defaultType = dc.getType('default'); 23 | const defaultModel = defaultType.model; 24 | const defaultView = defaultType.view; 25 | const inputType = dc.getType('input'); 26 | const inputModel = inputType.model; 27 | 28 | dc.addType('checkbox', { 29 | model: { 30 | defaults: { 31 | ...inputModel.prototype.defaults, 32 | 'custom-name': config.labels.checkbox_name, 33 | copyable: false, 34 | droppable: false, 35 | attributes: { type: 'checkbox' }, 36 | traits: [ 37 | traits.id, 38 | traits.name, 39 | traits.value, 40 | traits.required, 41 | traits.checked 42 | ], 43 | }, 44 | 45 | init() { 46 | this.listenTo(this, 'change:checked', this.handleChecked); 47 | }, 48 | 49 | handleChecked() { 50 | let checked = this.get('checked'); 51 | let attrs = this.get('attributes'); 52 | const view = this.view; 53 | 54 | if (checked) { 55 | attrs.checked = true; 56 | } else { 57 | delete attrs.checked; 58 | } 59 | 60 | if (view) { 61 | view.el.checked = checked 62 | } 63 | 64 | this.set('attributes', { ...attrs }); 65 | } 66 | }, 67 | isComponent(el) { 68 | if (el.tagName === 'INPUT' && el.type === 'checkbox') { 69 | return { type: 'checkbox' }; 70 | } 71 | }, 72 | view: { 73 | events: { 74 | 'click': 'handleClick', 75 | }, 76 | 77 | handleClick(e) { 78 | e.preventDefault(); 79 | }, 80 | }, 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /src/components/Collapse.js: -------------------------------------------------------------------------------- 1 | import compressIcon from "raw-loader!../icons/compress-solid.svg"; 2 | 3 | export const CollapseBlock = (bm, label) => { 4 | bm.add('collapse', { 5 | label: ` 6 | ${compressIcon} 7 |
${label}
8 | `, 9 | category: 'Components', 10 | content: { 11 | type: 'collapse' 12 | } 13 | }); 14 | }; 15 | 16 | export default (editor) => { 17 | const comps = editor.DomComponents; 18 | const defaultType = comps.getType('default'); 19 | const defaultModel = defaultType.model; 20 | const defaultView = defaultType.view; 21 | 22 | comps.addType('collapse', { 23 | model: { 24 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 25 | 'custom-name': 'Dropdown', 26 | classes: ['collapse'], 27 | droppable: true, 28 | traits: [ 29 | { 30 | type: 'class_select', 31 | options: [ 32 | { value: '', name: 'Closed' }, 33 | { value: 'show', name: 'Open' } 34 | ], 35 | label: 'Initial state' 36 | } 37 | ].concat(defaultModel.prototype.defaults.traits) 38 | }), 39 | /*init2() { 40 | window.asdf = this; 41 | const toggle = { 42 | type: 'button', 43 | content: 'Click to toggle', 44 | classes: ['btn', 'dropdown-toggle'] 45 | } 46 | const toggle_comp = this.append(toggle)[0]; 47 | const menu = { 48 | type: 'dropdown_menu' 49 | } 50 | const menu_comp = this.append(menu)[0]; 51 | this.setupToggle(null, null, {force: true}); 52 | const comps = this.components(); 53 | comps.bind('add', this.setupToggle.bind(this)); 54 | comps.bind('change', this.setupToggle.bind(this)); 55 | comps.bind('remove', this.setupToggle.bind(this)); 56 | const classes = this.get('classes'); 57 | classes.bind('add', this.setupToggle.bind(this)); 58 | classes.bind('change', this.setupToggle.bind(this)); 59 | classes.bind('remove', this.setupToggle.bind(this)); 60 | }, 61 | setupToggle(a, b, options = {}) { 62 | const toggle = this.components().filter(c => c.getAttributes().class.split(' ').includes('dropdown-toggle'))[0]; 63 | // raise error if toggle not found 64 | const menu = this.components().filter(c => c.getAttributes().class.split(' ').includes('dropdown-menu'))[0]; 65 | // raise error if menu not found 66 | 67 | if(options.force !== true && options.ignore === true) { 68 | return; 69 | } 70 | 71 | if(toggle && menu) { 72 | 73 | function hasEvent(comp) { 74 | let eca = comp._events['change:attributes']; 75 | if(!eca) return false; 76 | return eca.filter(e => e.callback.name == 'setupToggle').length != 0; 77 | } 78 | 79 | // setup event listeners if they aren't set 80 | if(!hasEvent(toggle)) { 81 | this.listenTo(toggle, 'change:attributes', this.setupToggle); 82 | } 83 | if(!hasEvent(menu)) { 84 | this.listenTo(menu, 'change:attributes', this.setupToggle); 85 | } 86 | 87 | // setup toggle 88 | var toggle_attrs = toggle.getAttributes(); 89 | toggle_attrs['role'] = 'button'; // if A 90 | var menu_attrs = menu.getAttributes(); 91 | if(!toggle_attrs.hasOwnProperty('data-toggle')) { 92 | toggle_attrs['data-toggle'] = 'dropdown'; 93 | } 94 | if(!toggle_attrs.hasOwnProperty('aria-haspopup')) { 95 | toggle_attrs['aria-haspopup'] = true; 96 | } 97 | const dropdown_classes = this.getAttributes().class.split(' '); 98 | toggle_attrs['aria-expanded'] = dropdown_classes.includes('show'); 99 | toggle.set('attributes', toggle_attrs, {ignore: true}); 100 | // setup menu 101 | // toggle needs ID for aria-labelled on the menu, could alert here 102 | if(toggle_attrs.hasOwnProperty('id')) { 103 | menu_attrs['aria-labelledby'] = toggle_attrs.id; 104 | } else { 105 | delete menu_attrs['aria-labelledby']; 106 | } 107 | menu.set('attributes', menu_attrs, {ignore: true}); 108 | } 109 | }*/ 110 | }, 111 | isComponent(el) { 112 | if (el && el.classList && el.classList.contains('dropdown')) { 113 | return { type: 'dropdown' }; 114 | } 115 | }, 116 | view: { 117 | /*init() { 118 | this.model.setupToggle 119 | }*/ 120 | } 121 | }); 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/components/Column.js: -------------------------------------------------------------------------------- 1 | import columnsIcon from "raw-loader!../icons/columns-solid.svg"; 2 | 3 | export const ColumnBlock = (bm, label) => { 4 | bm.add('column').set({ 5 | label: ` 6 | ${columnsIcon} 7 |
${label}
8 | `, 9 | category: 'Layout', 10 | content: { 11 | type: 'column', 12 | classes: ['col'] 13 | } 14 | }); 15 | }; 16 | 17 | export default (domc, editor) => { 18 | const defaultType = domc.getType('default'); 19 | const defaultModel = defaultType.model; 20 | const defaultView = defaultType.view; 21 | const spans = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]; 22 | 23 | domc.addType('column', { 24 | model: { 25 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 26 | 'custom-name': 'Column', 27 | draggable: '.row', 28 | droppable: true, 29 | resizable: { 30 | updateTarget: (el, rect, opt) => { 31 | const selected = editor.getSelected(); 32 | if (!selected) { return; } 33 | 34 | //compute the current screen size (bootstrap semantic) 35 | const docWidth = el.getRootNode().body.offsetWidth; 36 | let currentSize = ""; 37 | if (docWidth >= 1200) { 38 | currentSize = "xl"; 39 | } else if (docWidth >= 992) { 40 | currentSize = "lg"; 41 | } else if (docWidth >= 768) { 42 | currentSize = "md"; 43 | } else if (docWidth >= 576) { 44 | currentSize = "sm"; 45 | } 46 | 47 | //compute the threshold when add on remove 1 col span to the element 48 | const row = el.parentElement; 49 | const oneColWidth = row.offsetWidth / 12; 50 | //the threshold is half one column width 51 | const threshold = oneColWidth * 0.5; 52 | 53 | //check if we are growing or shrinking the column 54 | const grow = rect.w > el.offsetWidth + threshold; 55 | const shrink = rect.w < el.offsetWidth - threshold; 56 | if (grow || shrink) { 57 | let testRegexp = new RegExp("^col-" + currentSize + "-\\d{1,2}$"); 58 | if (!currentSize) { 59 | testRegexp = new RegExp("^col-\\d{1,2}$"); 60 | } 61 | let found = false; 62 | let sizesSpans = {}; 63 | let oldSpan = 0; 64 | let oldClass = null; 65 | for (let cl of el.classList) { 66 | if (cl.indexOf("col-") === 0) { 67 | let [c, size, span] = cl.split("-"); 68 | if (!span) { 69 | span = size; 70 | size = ""; 71 | } 72 | sizesSpans[size] = span; 73 | if (size === currentSize) { 74 | //found the col-XX-99 class 75 | oldClass = cl; 76 | oldSpan = span; 77 | found = true; 78 | } 79 | } 80 | } 81 | 82 | if (!found) { 83 | const sizeOrder = ["", "xs", "sm", "md", "lg", "xl"]; 84 | for (let s of sizeOrder) { 85 | if (sizesSpans[s]) { 86 | oldSpan = sizesSpans[s]; 87 | found = true; 88 | } 89 | if (s === currentSize) { 90 | break; 91 | } 92 | } 93 | } 94 | 95 | let newSpan = Number(oldSpan); 96 | if (grow) { 97 | newSpan++; 98 | } else { 99 | newSpan--; 100 | } 101 | if (newSpan > 12) { newSpan = 12; } 102 | if (newSpan < 1) { newSpan = 1; } 103 | 104 | let newClass = "col-" + currentSize + "-" + newSpan; 105 | if (!currentSize) { 106 | newClass = "col-" + newSpan; 107 | } 108 | //update the class 109 | selected.addClass(newClass); 110 | if (oldClass && oldClass !== newClass) { 111 | selected.removeClass(oldClass); 112 | } 113 | //notify the corresponding trait to update its value accordingly 114 | selected.getTrait((currentSize || "xs") + "_width").view.postUpdate(); 115 | } 116 | }, 117 | tl: 0, 118 | tc: 0, 119 | tr: 0, 120 | cl: 0, 121 | cr: 1, 122 | bl: 0, 123 | bc: 0, 124 | br: 0 125 | }, 126 | traits: [ 127 | { 128 | id: "xs_width", 129 | type: 'class_select', 130 | options: [ 131 | { value: 'col', name: 'Equal' }, 132 | { value: 'col-auto', name: 'Variable' }, 133 | ...spans.map(function (i) { return { value: 'col-' + i, name: i + '/12' } }) 134 | ], 135 | label: 'XS Width', 136 | }, 137 | { 138 | id: "sm_width", 139 | type: 'class_select', 140 | options: [ 141 | { value: '', name: 'None' }, 142 | { value: 'col-sm', name: 'Equal' }, 143 | { value: 'col-sm-auto', name: 'Variable' }, 144 | ...spans.map(function (i) { return { value: 'col-sm-' + i, name: i + '/12' } }) 145 | ], 146 | label: 'SM Width', 147 | }, 148 | { 149 | id: "md_width", 150 | type: 'class_select', 151 | options: [ 152 | { value: '', name: 'None' }, 153 | { value: 'col-md', name: 'Equal' }, 154 | { value: 'col-md-auto', name: 'Variable' }, 155 | ...spans.map(function (i) { return { value: 'col-md-' + i, name: i + '/12' } }) 156 | ], 157 | label: 'MD Width', 158 | }, 159 | { 160 | id: "lg_width", 161 | type: 'class_select', 162 | options: [ 163 | { value: '', name: 'None' }, 164 | { value: 'col-lg', name: 'Equal' }, 165 | { value: 'col-lg-auto', name: 'Variable' }, 166 | ...spans.map(function (i) { return { value: 'col-lg-' + i, name: i + '/12' } }) 167 | ], 168 | label: 'LG Width', 169 | }, 170 | { 171 | id: "xl_width", 172 | type: 'class_select', 173 | options: [ 174 | { value: '', name: 'None' }, 175 | { value: 'col-xl', name: 'Equal' }, 176 | { value: 'col-xl-auto', name: 'Variable' }, 177 | ...spans.map(function (i) { return { value: 'col-xl-' + i, name: i + '/12' } }) 178 | ], 179 | label: 'XL Width', 180 | }, 181 | { 182 | type: 'class_select', 183 | options: [ 184 | { value: '', name: 'None' }, 185 | ...spans.map(function (i) { return { value: 'offset-' + i, name: i + '/12' } }) 186 | ], 187 | label: 'XS Offset', 188 | }, 189 | { 190 | type: 'class_select', 191 | options: [ 192 | { value: '', name: 'None' }, 193 | ...spans.map(function (i) { return { value: 'offset-sm-' + i, name: i + '/12' } }) 194 | ], 195 | label: 'SM Offset', 196 | }, 197 | { 198 | type: 'class_select', 199 | options: [ 200 | { value: '', name: 'None' }, 201 | ...spans.map(function (i) { return { value: 'offset-md-' + i, name: i + '/12' } }) 202 | ], 203 | label: 'MD Offset', 204 | }, 205 | { 206 | type: 'class_select', 207 | options: [ 208 | { value: '', name: 'None' }, 209 | ...spans.map(function (i) { return { value: 'offset-lg-' + i, name: i + '/12' } }) 210 | ], 211 | label: 'LG Offset', 212 | }, 213 | { 214 | type: 'class_select', 215 | options: [ 216 | { value: '', name: 'None' }, 217 | ...spans.map(function (i) { return { value: 'offset-xl-' + i, name: i + '/12' } }) 218 | ], 219 | label: 'XL Offset', 220 | }, 221 | ].concat(defaultModel.prototype.defaults.traits) 222 | }), 223 | }, 224 | isComponent(el) { 225 | let match = false; 226 | if (el && el.classList) { 227 | el.classList.forEach(function (klass) { 228 | if (klass == "col" || klass.match(/^col-/)) { 229 | match = true; 230 | } 231 | }); 232 | } 233 | if (match) return { type: 'column' }; 234 | }, 235 | view: defaultView 236 | }); 237 | } 238 | -------------------------------------------------------------------------------- /src/components/ColumnBreak.js: -------------------------------------------------------------------------------- 1 | import equalsIcon from "raw-loader!../icons/equals-solid.svg"; 2 | 3 | export const ColumnBreakBlock = (bm, label) => { 4 | bm.add('column_break').set({ 5 | label: ` 6 | ${equalsIcon} 7 |
${label}
8 | `, 9 | category: 'Layout', 10 | content: { 11 | type: 'column_break' 12 | } 13 | }); 14 | }; 15 | 16 | export default (domc) => { 17 | const defaultType = domc.getType('default'); 18 | const defaultModel = defaultType.model; 19 | const defaultView = defaultType.view; 20 | 21 | domc.addType('column_break', { 22 | model: { 23 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 24 | 'custom-name': 'Column Break', 25 | tagName: 'div', 26 | classes: ['w-100'] 27 | }) 28 | }, 29 | isComponent(el) { 30 | if (el && el.classList && el.classList.contains('w-100')) { // also check if parent is `.row` 31 | return { type: 'column_break' }; 32 | } 33 | }, 34 | view: defaultView 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Container.js: -------------------------------------------------------------------------------- 1 | import windowIcon from "raw-loader!../icons/window-maximize-solid.svg"; 2 | 3 | export const ContainerBlock = (bm, label) => { 4 | bm.add('container').set({ 5 | label: ` 6 | ${windowIcon} 7 |
${label}
8 | `, 9 | category: 'Layout', 10 | content: { 11 | type: 'container', 12 | classes: ['container'] 13 | } 14 | }); 15 | }; 16 | 17 | export default (domc) => { 18 | const defaultType = domc.getType('default'); 19 | const defaultModel = defaultType.model; 20 | const defaultView = defaultType.view; 21 | 22 | domc.addType('container', { 23 | model: { 24 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 25 | 'custom-name': 'Container', 26 | tagName: 'div', 27 | droppable: true, 28 | traits: [ 29 | { 30 | type: 'class_select', 31 | options: [ 32 | { value: 'container', name: 'Fixed' }, 33 | { value: 'container-fluid', name: 'Fluid' } 34 | ], 35 | label: 'Width' 36 | } 37 | ].concat(defaultModel.prototype.defaults.traits) 38 | }) 39 | }, 40 | isComponent(el) { 41 | if (el && el.classList && (el.classList.contains('container') || el.classList.contains('container-fluid'))) { 42 | return { type: 'container' }; 43 | } 44 | }, 45 | view: defaultView 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Default.js: -------------------------------------------------------------------------------- 1 | import contexts from '../bootstrap-contexts'; 2 | import { capitalize } from "../utils"; 3 | 4 | export default (domc) => { 5 | const contexts_w_white = contexts.concat(['white']); 6 | const defaultType = domc.getType('default'); 7 | const defaultModel = defaultType.model; 8 | const defaultView = defaultType.view; 9 | 10 | domc.addType('default', { 11 | model: { 12 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 13 | tagName: 'div', 14 | traits: [ 15 | { 16 | type: 'class_select', 17 | options: [ 18 | { value: '', name: 'Default' }, 19 | ...contexts_w_white.map(function (v) { return { value: 'text-' + v, name: capitalize(v) } }) 20 | ], 21 | label: 'Text color' 22 | }, 23 | { 24 | type: 'class_select', 25 | options: [ 26 | { value: '', name: 'Default' }, 27 | ...contexts_w_white.map(function (v) { return { value: 'bg-' + v, name: capitalize(v) } }) 28 | ], 29 | label: 'Background color' 30 | }, 31 | { 32 | type: 'class_select', 33 | options: [ 34 | { value: '', name: 'Default' }, 35 | { value: 'border', name: 'Full' }, 36 | { value: 'border-top-0', name: 'No top' }, 37 | { value: 'border-right-0', name: 'No right' }, 38 | { value: 'border-bottom-0', name: 'No bottom' }, 39 | { value: 'border-left-0', name: 'No left' }, 40 | { value: 'border-0', name: 'None' } 41 | ], 42 | label: 'Border width' 43 | }, 44 | { 45 | type: 'class_select', 46 | options: [ 47 | { value: '', name: 'Default' }, 48 | ...contexts_w_white.map(function (v) { return { value: 'border border-' + v, name: capitalize(v) } }) 49 | ], 50 | label: 'Border color' 51 | }, 52 | { 53 | type: 'class_select', 54 | options: [ 55 | { value: '', name: 'Default' }, 56 | { value: 'rounded', name: 'Rounded' }, 57 | { value: 'rounded-top', name: 'Rounded top' }, 58 | { value: 'rounded-right', name: 'Rounded right' }, 59 | { value: 'rounded-bottom', name: 'Rounded bottom' }, 60 | { value: 'rounded-left', name: 'Rounded left' }, 61 | { value: 'rounded-circle', name: 'Circle' }, 62 | { value: 'rounded-0', name: 'Square' }, 63 | ], 64 | label: 'Border radius' 65 | }, 66 | { 67 | type: 'text', 68 | label: 'ID', 69 | name: 'id', 70 | placeholder: 'my_element' 71 | }, 72 | { 73 | type: 'text', 74 | label: 'Title', 75 | name: 'title', 76 | placeholder: 'My Element' 77 | } 78 | ] //.concat(defaultModel.prototype.defaults.traits) 79 | }), 80 | init() { 81 | const classes = this.get('classes'); 82 | classes.bind('add', this.classesChanged.bind(this)); 83 | classes.bind('change', this.classesChanged.bind(this)); 84 | classes.bind('remove', this.classesChanged.bind(this)); 85 | this.init2(); 86 | }, 87 | /* BS comps use init2, not init */ 88 | init2() { }, 89 | /* method where we can check if we should changeType */ 90 | classesChanged() { }, 91 | /* replace the comp with a copy of a different type */ 92 | changeType(new_type) { 93 | const coll = this.collection; 94 | const at = coll.indexOf(this); 95 | const button_opts = { 96 | type: new_type, 97 | style: this.getStyle(), 98 | attributes: this.getAttributes(), 99 | content: this.view.el.innerHTML 100 | } 101 | coll.remove(this); 102 | coll.add(button_opts, { at }); 103 | this.destroy(); 104 | } 105 | }, 106 | view: defaultView 107 | }); 108 | } 109 | -------------------------------------------------------------------------------- /src/components/Dropdown.js: -------------------------------------------------------------------------------- 1 | /* 2 | known issues: 3 | - BS dropdown JS isn't attached if you remove the existing toggle and add a new one 4 | */ 5 | 6 | import caretIcon from "raw-loader!../icons/caret-square-down-regular.svg"; 7 | 8 | export const DropDownBlock = (bm, label) => { 9 | bm.add('dropdown', { 10 | label: ` 11 | ${caretIcon} 12 |
${label}
13 | `, 14 | category: 'Components', 15 | content: { 16 | type: 'dropdown' 17 | } 18 | }); 19 | /*bm.add('dropdown_menu', { 20 | label: c.labels.dropdown_menu, 21 | category: 'Components', 22 | attributes: {class:'fa fa-caret-down'}, 23 | content: { 24 | type: 'dropdown_menu' 25 | } 26 | }); 27 | bm.add('dropdown_item', { 28 | label: c.labels.dropdown_item, 29 | category: 'Components', 30 | attributes: {class:'fa fa-link'}, 31 | content: { 32 | type: 'dropdown_item' 33 | } 34 | });*/ 35 | }; 36 | 37 | export default (editor) => { 38 | const comps = editor.DomComponents; 39 | const defaultType = comps.getType('default'); 40 | const defaultModel = defaultType.model; 41 | const defaultView = defaultType.view; 42 | 43 | function hasEvent(comp) { 44 | let eca = comp._events['change:attributes']; 45 | if (!eca) return false; 46 | return eca.filter(e => e.callback.name === 'setupToggle').length !== 0; 47 | } 48 | 49 | comps.addType('dropdown', { 50 | model: { 51 | defaults: { 52 | ...defaultModel.prototype.defaults, 53 | 'custom-name': 'Dropdown', 54 | classes: ['dropdown'], 55 | droppable: 'a, button, .dropdown-menu', 56 | traits: [ 57 | { 58 | type: 'select', 59 | label: 'Initial state', 60 | name: 'initial_state', 61 | options: [ 62 | { value: '', name: 'Closed' }, 63 | { value: 'show', name: 'Open' } 64 | ], 65 | } 66 | ].concat(defaultModel.prototype.defaults.traits), 67 | }, 68 | 69 | init2() { 70 | const toggle = { 71 | type: 'button', 72 | content: 'Click to toggle', 73 | classes: ['btn', 'dropdown-toggle'] 74 | }; 75 | const toggle_comp = this.append(toggle)[0]; 76 | const menu = { 77 | type: 'dropdown_menu' 78 | }; 79 | const menu_comp = this.append(menu)[0]; 80 | this.setupToggle(null, null, { force: true }); 81 | const comps = this.components(); 82 | comps.bind('add', this.setupToggle.bind(this)); 83 | comps.bind('remove', this.setupToggle.bind(this)); 84 | const classes = this.get('classes'); 85 | classes.bind('add', this.setupToggle.bind(this)); 86 | classes.bind('change', this.setupToggle.bind(this)); 87 | classes.bind('remove', this.setupToggle.bind(this)); 88 | }, 89 | 90 | setupToggle(a, b, options = {}) { 91 | const toggle = this.components().filter(c => c.getAttributes().class.split(' ').includes('dropdown-toggle'))[0]; 92 | const menu = this.components().filter(c => c.getAttributes().class.split(' ').includes('dropdown-menu'))[0]; 93 | 94 | if (options.force !== true && options.ignore === true) { 95 | return; 96 | } 97 | 98 | if (toggle && menu) { 99 | 100 | // setup event listeners if they aren't set 101 | if (!hasEvent(toggle)) { 102 | this.listenTo(toggle, 'change:attributes', this.setupToggle); 103 | } 104 | if (!hasEvent(menu)) { 105 | this.listenTo(menu, 'change:attributes', this.setupToggle); 106 | } 107 | 108 | // setup toggle 109 | const toggle_attrs = toggle.getAttributes(); 110 | toggle_attrs['role'] = 'button'; 111 | const menu_attrs = menu.getAttributes(); 112 | if (!toggle_attrs.hasOwnProperty('data-toggle')) { 113 | toggle_attrs['data-toggle'] = 'dropdown'; 114 | } 115 | if (!toggle_attrs.hasOwnProperty('aria-haspopup')) { 116 | toggle_attrs['aria-haspopup'] = true; 117 | } 118 | 119 | toggle.set('attributes', toggle_attrs, { ignore: true }); 120 | 121 | // setup menu 122 | // toggle needs ID for aria-labelled on the menu, could alert here 123 | if (toggle_attrs.hasOwnProperty('id')) { 124 | menu_attrs['aria-labelledby'] = toggle_attrs.id; 125 | } else { 126 | delete menu_attrs['aria-labelledby']; 127 | } 128 | menu.set('attributes', menu_attrs, { ignore: true }); 129 | } 130 | }, 131 | 132 | updated(property, value) { 133 | if (value.hasOwnProperty('initial_state')) { 134 | const menu = this.components().filter(c => c.getAttributes().class.split(' ').includes('dropdown-menu'))[0]; 135 | const attrs = menu.getAttributes(); 136 | const classes = attrs.class.split(' '); 137 | 138 | if (classes.includes('show')) { 139 | // Close the menu 140 | attrs['aria-expanded'] = false; 141 | menu.removeClass('show'); 142 | } else { 143 | // Open the menu 144 | attrs['aria-expanded'] = true; 145 | menu.addClass('show'); 146 | } 147 | } 148 | }, 149 | 150 | }, 151 | isComponent(el) { 152 | if (el && el.classList && el.classList.contains('dropdown')) { 153 | return { type: 'dropdown' }; 154 | } 155 | }, 156 | view: defaultView 157 | }); 158 | 159 | // need aria-labelledby to equal dropdown-toggle id 160 | // need to insert dropdown-item class on links when added 161 | comps.addType('dropdown_menu', { 162 | model: { 163 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 164 | 'custom-name': 'Dropdown Menu', 165 | classes: ['dropdown-menu'], 166 | draggable: '.dropdown', 167 | droppable: true 168 | }), 169 | init2() { 170 | const header = { 171 | type: 'header', 172 | tagName: 'h6', 173 | classes: ['dropdown-header'], 174 | content: 'Dropdown header' 175 | }; 176 | const link = { 177 | type: 'link', 178 | classes: ['dropdown-item'], 179 | content: 'Dropdown item' 180 | }; 181 | const divider = { 182 | type: 'default', 183 | classes: ['dropdown-divider'] 184 | }; 185 | this.append(header); 186 | this.append(link); 187 | this.append(divider); 188 | this.append(link); 189 | } 190 | }, 191 | isComponent(el) { 192 | if (el && el.classList && el.classList.contains('dropdown-menu')) { 193 | return { type: 'dropdown_menu' }; 194 | } 195 | }, 196 | view: defaultView, 197 | }); 198 | 199 | } 200 | -------------------------------------------------------------------------------- /src/components/FileInput.js: -------------------------------------------------------------------------------- 1 | import { elHasClass } from "../utils"; 2 | import fileInputIcon from "raw-loader!../icons/file-input.svg"; 3 | 4 | export const FileInputBlock = (bm, label) => { 5 | bm.add('file-input', { 6 | label: ` 7 | ${fileInputIcon} 8 |
${label}
9 | `, 10 | category: 'Forms', 11 | content: `` 12 | }); 13 | }; 14 | 15 | export default (dc, traits, config = {}) => { 16 | const defaultType = dc.getType('default'); 17 | const defaultModel = defaultType.model; 18 | const defaultView = defaultType.view; 19 | const type = 'file-input'; 20 | 21 | dc.addType(type, { 22 | model: { 23 | defaults: { 24 | ...defaultModel.prototype.defaults, 25 | 'custom-name': config.labels.input, 26 | tagName: 'input', 27 | draggable: 'form .form-group', 28 | droppable: false, 29 | traits: [ 30 | traits.name, 31 | traits.required, 32 | { 33 | type: 'checkbox', 34 | label: config.labels.trait_multiple, 35 | name: 'multiple', 36 | }, 37 | ], 38 | }, 39 | }, 40 | isComponent(el) { 41 | if (el.tagName === 'INPUT' && elHasClass(el, 'form-control-file')) { 42 | return { type }; 43 | } 44 | }, 45 | view: defaultView, 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Form.js: -------------------------------------------------------------------------------- 1 | import formIcon from "raw-loader!../icons/form.svg"; 2 | 3 | export const FormBlock = (bm, label) => { 4 | bm.add('form', { 5 | label: ` 6 | ${formIcon} 7 |
${label}
`, 8 | category: 'Forms', 9 | content: ` 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 |
34 |
35 | `, 36 | }); 37 | }; 38 | 39 | export default (dc, traits, config = {}) => { 40 | const defaultType = dc.getType('default'); 41 | const defaultModel = defaultType.model; 42 | const defaultView = defaultType.view; 43 | let actionTrait; 44 | 45 | // If the formPredefinedActions is set in the config you can add a dropdown menu to the actions trait 46 | if (config.formPredefinedActions && config.formPredefinedActions.length) { 47 | actionTrait = { 48 | type: 'select', 49 | label: config.labels.trait_action, 50 | name: 'action', 51 | options: [], 52 | }; 53 | config.formPredefinedActions.forEach((action) => { 54 | actionTrait.options.push({ value: action.value, name: action.name }) 55 | }); 56 | } else { 57 | actionTrait = { 58 | label: config.labels.trait_action, 59 | name: 'action', 60 | } 61 | } 62 | 63 | dc.addType('form', { 64 | model: { 65 | defaults: { 66 | ...defaultModel.prototype.defaults, 67 | droppable: ':not(form)', 68 | draggable: ':not(form)', 69 | traits: [ 70 | { 71 | type: 'select', 72 | label: config.labels.trait_enctype, 73 | name: 'enctype', 74 | options: [ 75 | { value: 'application/x-www-form-urlencoded', name: 'application/x-www-form-urlencoded (default)' }, 76 | { value: 'multipart/form-data', name: 'multipart/form-data' }, 77 | { value: 'text/plain', name: 'text/plain' }, 78 | ] 79 | }, 80 | { 81 | type: 'select', 82 | label: config.labels.trait_method, 83 | name: 'method', 84 | options: [ 85 | { value: 'post', name: 'POST' }, 86 | { value: 'get', name: 'GET' }, 87 | ] 88 | }, 89 | actionTrait 90 | ], 91 | }, 92 | 93 | init() { 94 | this.listenTo(this, 'change:formState', this.updateFormState); 95 | }, 96 | 97 | updateFormState() { 98 | var state = this.get('formState'); 99 | switch (state) { 100 | case 'success': 101 | this.showState('success'); 102 | break; 103 | case 'error': 104 | this.showState('error'); 105 | break; 106 | default: 107 | this.showState('normal'); 108 | } 109 | }, 110 | 111 | showState(state) { 112 | var st = state || 'normal'; 113 | var failVis, successVis; 114 | if (st === 'success') { 115 | failVis = 'none'; 116 | successVis = 'block'; 117 | } else if (st === 'error') { 118 | failVis = 'block'; 119 | successVis = 'none'; 120 | } else { 121 | failVis = 'none'; 122 | successVis = 'none'; 123 | } 124 | var successModel = this.getStateModel('success'); 125 | var failModel = this.getStateModel('error'); 126 | var successStyle = successModel.getStyle(); 127 | var failStyle = failModel.getStyle(); 128 | successStyle.display = successVis; 129 | failStyle.display = failVis; 130 | successModel.setStyle(successStyle); 131 | failModel.setStyle(failStyle); 132 | }, 133 | 134 | getStateModel(state) { 135 | var st = state || 'success'; 136 | var stateName = 'form-state-' + st; 137 | var stateModel; 138 | var comps = this.get('components'); 139 | for (var i = 0; i < comps.length; i++) { 140 | var model = comps.models[i]; 141 | if (model.get('form-state-type') === st) { 142 | stateModel = model; 143 | break; 144 | } 145 | } 146 | if (!stateModel) { 147 | var contentStr = formMsgSuccess; 148 | if (st === 'error') { 149 | contentStr = formMsgError; 150 | } 151 | stateModel = comps.add({ 152 | 'form-state-type': st, 153 | type: 'text', 154 | removable: false, 155 | copyable: false, 156 | draggable: false, 157 | attributes: { 'data-form-state': st }, 158 | content: contentStr, 159 | }); 160 | } 161 | return stateModel; 162 | }, 163 | }, 164 | isComponent(el) { 165 | if (el.tagName === 'FORM') { 166 | return { type: 'form' }; 167 | } 168 | }, 169 | 170 | view: { 171 | events: { 172 | submit(e) { 173 | e.preventDefault(); 174 | } 175 | } 176 | }, 177 | }); 178 | } 179 | -------------------------------------------------------------------------------- /src/components/Header.js: -------------------------------------------------------------------------------- 1 | import headingIcon from "raw-loader!../icons/heading-solid.svg"; 2 | 3 | export const HeaderBlock = (bm, label) => { 4 | bm.add('header', { 5 | label: ` 6 | ${headingIcon} 7 |
${label}
8 | `, 9 | category: 'Typography', 10 | content: { 11 | type: 'header', 12 | content: 'Bootstrap heading' 13 | } 14 | }); 15 | }; 16 | 17 | export default (domc) => { 18 | const textType = domc.getType('text'); 19 | const textModel = textType.model; 20 | const textView = textType.view; 21 | 22 | domc.addType('header', { 23 | extend: 'text', 24 | model: { 25 | defaults: Object.assign({}, textModel.prototype.defaults, { 26 | 'custom-name': 'Header', 27 | tagName: 'h1', 28 | traits: [ 29 | { 30 | type: 'select', 31 | options: [ 32 | { value: 'h1', name: 'One (largest)' }, 33 | { value: 'h2', name: 'Two' }, 34 | { value: 'h3', name: 'Three' }, 35 | { value: 'h4', name: 'Four' }, 36 | { value: 'h5', name: 'Five' }, 37 | { value: 'h6', name: 'Six (smallest)' }, 38 | ], 39 | label: 'Size', 40 | name: 'tagName', 41 | changeProp: 1 42 | }, 43 | { 44 | type: 'class_select', 45 | options: [ 46 | { value: '', name: 'None' }, 47 | { value: 'display-1', name: 'One (largest)' }, 48 | { value: 'display-2', name: 'Two ' }, 49 | { value: 'display-3', name: 'Three ' }, 50 | { value: 'display-4', name: 'Four (smallest)' } 51 | ], 52 | label: 'Display Heading' 53 | } 54 | ].concat(textModel.prototype.defaults.traits) 55 | }), 56 | 57 | }, 58 | isComponent(el) { 59 | if (el && ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(el.tagName)) { 60 | return { type: 'header' }; 61 | } 62 | }, 63 | view: textView 64 | }); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Image.js: -------------------------------------------------------------------------------- 1 | import imageIcon from "raw-loader!../icons/image-solid.svg"; 2 | 3 | export const ImageBlock = (bm, label) => { 4 | bm.add('bs-image', { 5 | label: ` 6 | ${imageIcon} 7 |
${label}
8 | `, 9 | category: 'Media', 10 | content: { 11 | type: 'bs-image' 12 | } 13 | }); 14 | }; 15 | 16 | export default (domComponent) => { 17 | const img_src_default = 'https://dummyimage.com/800x500/999/222'; 18 | const imageType = domComponent.getType('image'); 19 | const model = imageType.model; 20 | const view = imageType.view; 21 | const type = 'bs-image'; 22 | 23 | domComponent.addType(type, { 24 | extend: 'image', 25 | model: { 26 | defaults: Object.assign({}, model.prototype.defaults, { 27 | 'custom-name': 'Image', 28 | tagName: 'img', 29 | resizable: 1, 30 | attributes: { 31 | src: img_src_default, 32 | }, 33 | classes: ['img-fluid'], 34 | traits: [ 35 | { 36 | type: 'text', 37 | label: 'Source (URL)', 38 | name: 'src' 39 | }, 40 | { 41 | type: 'text', 42 | label: 'Alternate text', 43 | name: 'alt' 44 | } 45 | ].concat(model.prototype.defaults.traits) 46 | }) 47 | }, 48 | isComponent: function (el) { 49 | if (el && el.tagName === 'IMG') { 50 | return { type: type }; 51 | } 52 | }, 53 | view: view 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/components/Input.js: -------------------------------------------------------------------------------- 1 | import inputIcon from "raw-loader!../icons/input.svg"; 2 | 3 | export const InputBlock = (bm, label) => { 4 | bm.add('input', { 5 | label: ` 6 | ${inputIcon} 7 |
${label}
`, 8 | category: 'Forms', 9 | content: '', 10 | }); 11 | }; 12 | 13 | export default (dc, traits, config = {}) => { 14 | const defaultType = dc.getType('default'); 15 | const defaultModel = defaultType.model; 16 | const defaultView = defaultType.view; 17 | 18 | dc.addType('input', { 19 | model: { 20 | defaults: { 21 | ...defaultModel.prototype.defaults, 22 | 'custom-name': config.labels.input, 23 | tagName: 'input', 24 | draggable: 'form .form-group', 25 | droppable: false, 26 | traits: [ 27 | traits.value, 28 | traits.name, 29 | traits.placeholder, { 30 | label: config.labels.trait_type, 31 | type: 'select', 32 | name: 'type', 33 | options: [ 34 | { value: 'text', name: config.labels.type_text }, 35 | { value: 'email', name: config.labels.type_email }, 36 | { value: 'password', name: config.labels.type_password }, 37 | { value: 'number', name: config.labels.type_number }, 38 | { value: 'date', name: config.labels.type_date }, 39 | { value: 'hidden', name: config.labels.type_hidden }, 40 | ] 41 | }, traits.required 42 | ], 43 | }, 44 | }, 45 | isComponent(el) { 46 | if (el.tagName === 'INPUT') { 47 | return { type: 'input' }; 48 | } 49 | }, 50 | view: defaultView, 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/InputGroup.js: -------------------------------------------------------------------------------- 1 | import formGroupIcon from "raw-loader!../icons/form-group.svg"; 2 | import inputGroupIcon from "raw-loader!../icons/input-group.svg"; 3 | 4 | export const InputGroupBlock = (bm, label, c) => { 5 | bm.add('form_group_input', { 6 | label: ` 7 | ${formGroupIcon} 8 |
${label}
`, 9 | category: 'Forms', 10 | content: ` 11 |
12 | 13 | 14 |
15 | `, 16 | }); 17 | 18 | bm.add('input_group', { 19 | label: ` 20 | ${inputGroupIcon} 21 |
${label}
`, 22 | category: 'Forms', 23 | content: ` 24 |
25 |
26 | $ 27 |
28 | 29 |
30 | .00 31 |
32 |
33 | `, 34 | }); 35 | }; 36 | 37 | export default (dc, traits, config = {}) => { 38 | const defaultType = dc.getType('default'); 39 | const defaultModel = defaultType.model; 40 | const defaultView = defaultType.view; 41 | 42 | dc.addType('input_group', { 43 | model: { 44 | defaults: { 45 | ...defaultModel.prototype.defaults, 46 | 'custom-name': config.labels.input_group, 47 | tagName: 'div', 48 | traits: [], 49 | }, 50 | }, 51 | isComponent(el) { 52 | if (el && el.classList && el.classList.contains('form_group_input')) { 53 | return { type: 'form_group_input' }; 54 | } 55 | }, 56 | view: defaultView, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Label.js: -------------------------------------------------------------------------------- 1 | import labelIcon from "raw-loader!../icons/label.svg"; 2 | 3 | export const LabelBlock = (bm, label) => { 4 | bm.add('label', { 5 | label: ` 6 | ${labelIcon} 7 |
${label}
`, 8 | category: 'Forms', 9 | content: '', 10 | }); 11 | }; 12 | 13 | export default (dc, traits, config = {}) => { 14 | 15 | const textType = dc.getType('text'); 16 | const textModel = textType.model; 17 | const textView = textType.view; 18 | 19 | dc.addType('label', { 20 | extend: 'text', 21 | model: { 22 | defaults: { 23 | ...textModel.prototype.defaults, 24 | 'custom-name': config.labels.label, 25 | tagName: 'label', 26 | traits: [traits.for], 27 | }, 28 | }, 29 | isComponent(el) { 30 | if (el.tagName == 'LABEL') { 31 | return { type: 'label' }; 32 | } 33 | }, 34 | view: textView, 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /src/components/Link.js: -------------------------------------------------------------------------------- 1 | /* 2 | known issues: 3 | - BS dropdown JS isn't attached if you remove the existing toggle and add a new one 4 | */ 5 | 6 | import linkIcon from "raw-loader!../icons/link-solid.svg"; 7 | 8 | export const LinkBlock = (bm, label) => { 9 | bm.add('link', { 10 | label: ` 11 | ${linkIcon} 12 |
${label}
13 | `, 14 | category: 'Basic', 15 | content: { 16 | type: 'link', 17 | content: 'Link text' 18 | } 19 | }); 20 | }; 21 | 22 | export default (editor) => { 23 | const comps = editor.DomComponents; 24 | const textType = comps.getType('text'); 25 | const textModel = textType.model; 26 | 27 | const linkType = comps.getType('link'); 28 | const linkView = linkType.view; 29 | 30 | comps.addType('link', { 31 | extend: 'text', 32 | model: { 33 | defaults: Object.assign({}, textModel.prototype.defaults, { 34 | 'custom-name': 'Link', 35 | tagName: 'a', 36 | droppable: true, 37 | editable: true, 38 | traits: [ 39 | { 40 | type: 'text', 41 | label: 'Href', 42 | name: 'href', 43 | placeholder: 'https://www.grapesjs.com' 44 | }, 45 | { 46 | type: 'select', 47 | options: [ 48 | { value: '', name: 'This window' }, 49 | { value: '_blank', name: 'New window' } 50 | ], 51 | label: 'Target', 52 | name: 'target', 53 | }, 54 | { 55 | type: 'select', 56 | options: [ 57 | { value: '', name: 'None' }, 58 | { value: 'button', name: 'Self' }, 59 | { value: 'collapse', name: 'Collapse' }, 60 | { value: 'dropdown', name: 'Dropdown' } 61 | ], 62 | label: 'Toggles', 63 | name: 'data-toggle', 64 | changeProp: 1 65 | } 66 | ].concat(textModel.prototype.defaults.traits) 67 | }), 68 | init2() { 69 | //textModel.prototype.init.call(this); 70 | this.listenTo(this, 'change:data-toggle', this.setupToggle); 71 | this.listenTo(this, 'change:attributes', this.setupToggle); // for when href changes 72 | }, 73 | setupToggle(a, b, options = {}) { // TODO this should be in the dropdown comp and not the link comp 74 | if (options.ignore === true && options.force !== true) { 75 | return; 76 | } 77 | console.log('setup toggle'); 78 | const attrs = this.getAttributes(); 79 | const href = attrs.href; 80 | // old attributes are not removed from DOM even if deleted... 81 | delete attrs['data-toggle']; 82 | delete attrs['aria-expanded']; 83 | delete attrs['aria-controls']; 84 | delete attrs['aria-haspopup']; 85 | if (href && href.length > 0 && href.match(/^#/)) { 86 | console.log('link has href'); 87 | // find the el where id == link href 88 | const els = this.em.get('Editor').DomComponents.getWrapper().find(href); 89 | if (els.length > 0) { 90 | console.log('referenced el found'); 91 | const el = els[0]; // should only be one el with this ID 92 | const el_attrs = el.getAttributes(); 93 | //delete el_attrs['aria-labelledby']; 94 | const el_classes = el_attrs.class; 95 | if (el_classes) { 96 | console.log('el has classes'); 97 | const el_classes_list = el_classes.split(' '); 98 | const includes = ['collapse', 'dropdown-menu']; 99 | const intersection = el_classes_list.filter(x => includes.includes(x)); 100 | 101 | if (intersection.length) { 102 | console.log('link data-toggle matches el class'); 103 | switch (intersection[0]) { 104 | case 'collapse': 105 | attrs['data-toggle'] = 'collapse'; 106 | break; 107 | } 108 | attrs['aria-expanded'] = el_classes_list.includes('show'); 109 | if (intersection[0] === 'collapse') { 110 | attrs['aria-controls'] = href.substring(1); 111 | } 112 | } 113 | } 114 | } 115 | } 116 | this.set('attributes', attrs, { ignore: true }); 117 | }, 118 | classesChanged(e) { 119 | console.log('classes changed'); 120 | if (this.attributes.type === 'link') { 121 | if (this.attributes.classes.filter(function (klass) { 122 | return klass.id === 'btn' 123 | }).length > 0) { 124 | this.changeType('button'); 125 | } 126 | } 127 | } 128 | }, 129 | isComponent(el) { 130 | if (el && el.tagName && el.tagName === 'A') { 131 | return { type: 'link' }; 132 | } 133 | }, 134 | view: linkView 135 | }); 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/components/List.js: -------------------------------------------------------------------------------- 1 | export const ListBlock = (bm, label) => { 2 | bm.add('list', { 3 | label: label, 4 | category: 'Basic', 5 | attributes: { class: 'fa fa-list' }, 6 | content: { 7 | type: 'list' 8 | } 9 | }); 10 | }; 11 | 12 | export default (domc) => { 13 | const defaultType = domc.getType('default'); 14 | const defaultModel = defaultType.model; 15 | const defaultView = defaultType.view; 16 | 17 | domc.addType('list', { 18 | model: { 19 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 20 | 'custom-name': 'List', 21 | tagName: 'ul', 22 | resizable: 1, 23 | traits: [ 24 | { 25 | type: 'select', 26 | options: [ 27 | { value: 'ul', name: 'No' }, 28 | { value: 'ol', name: 'Yes' } 29 | ], 30 | label: 'Ordered?', 31 | name: 'tagName', 32 | changeProp: 1 33 | } 34 | ].concat(defaultModel.prototype.defaults.traits) 35 | }) 36 | }, 37 | isComponent: function (el) { 38 | if (el && ['UL', 'OL'].includes(el.tagName)) { 39 | return { type: 'list' }; 40 | } 41 | }, 42 | view: defaultView 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/MediaObject.js: -------------------------------------------------------------------------------- 1 | import columnsIcon from 'raw-loader!../icons/columns-solid.svg'; 2 | 3 | export const MediaObjectBlock = (bm, label) => { 4 | bm.add('media_object').set({ 5 | label: ` 6 | ${columnsIcon} 7 |
${label}
8 | `, 9 | category: 'Layout', 10 | content: `
11 | 12 |
13 |
Media heading
14 |
Cras sit amet nibh libero, in gravida nulla. Nulla vel metus scelerisque ante sollicitudin. Cras purus odio, vestibulum in vulputate at, tempus viverra turpis. Fusce condimentum nunc ac nisi vulputate fringilla. Donec lacinia congue felis in faucibus.
15 |
16 |
` 17 | }); 18 | }; 19 | 20 | export default (domc) => { 21 | const defaultType = domc.getType('default'); 22 | const defaultModel = defaultType.model; 23 | const defaultView = defaultType.view; 24 | 25 | domc.addType('media_object', { 26 | model: { 27 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 28 | 'custom-name': 'Media Object', 29 | tagName: 'div', 30 | classes: ['media'] 31 | }) 32 | }, 33 | isComponent(el) { 34 | if (el && el.classList && el.classList.contains('media')) { 35 | return { type: 'media' }; 36 | } 37 | }, 38 | view: defaultView 39 | }); 40 | 41 | domc.addType('media_body', { 42 | model: { 43 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 44 | 'custom-name': 'Media Body', 45 | tagName: 'div', 46 | classes: ['media-body'] 47 | }) 48 | }, 49 | isComponent(el) { 50 | if (el && el.classList && el.classList.contains('media-body')) { 51 | return { type: 'media_body' }; 52 | } 53 | }, 54 | view: defaultView 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /src/components/Paragraph.js: -------------------------------------------------------------------------------- 1 | import paragraphIcon from "raw-loader!../icons/paragraph-solid.svg"; 2 | 3 | export const ParagraphBlock = (bm, label) => { 4 | bm.add('paragraph', { 5 | label: ` 6 | ${paragraphIcon} 7 |
${label}
8 | `, 9 | category: 'Typography', 10 | content: { 11 | type: 'paragraph', 12 | content: 'Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Duis mollis, est non commodo luctus.' 13 | } 14 | }); 15 | }; 16 | 17 | export default (domc) => { 18 | const textType = domc.getType('text'); 19 | const textModel = textType.model; 20 | const textView = textType.view; 21 | 22 | domc.addType('paragraph', { 23 | extend: 'text', 24 | model: { 25 | defaults: Object.assign({}, textModel.prototype.defaults, { 26 | 'custom-name': 'Paragraph', 27 | tagName: 'p', 28 | traits: [ 29 | { 30 | type: 'class_select', 31 | options: [ 32 | { value: '', name: 'No' }, 33 | { value: 'lead', name: 'Yes' } 34 | ], 35 | label: 'Lead?' 36 | } 37 | ].concat(textModel.prototype.defaults.traits) 38 | }) 39 | }, 40 | isComponent(el) { 41 | if (el && el.tagName && el.tagName === 'P') { 42 | return { type: 'paragraph' }; 43 | } 44 | }, 45 | view: textView 46 | }); 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Radio.js: -------------------------------------------------------------------------------- 1 | import radioIcon from "raw-loader!../icons/dot-circle-regular.svg"; 2 | 3 | export const RadioBlock = (bm, label) => { 4 | bm.add('radio', { 5 | label: ` 6 | ${radioIcon} 7 |
${label}
8 | `, 9 | category: 'Forms', 10 | content: ` 11 |
12 | 13 | 16 |
17 | `, 18 | }); 19 | }; 20 | 21 | export default (dc, traits, config = {}) => { 22 | const checkType = dc.getType('checkbox'); 23 | 24 | // RADIO 25 | dc.addType('radio', { 26 | extend: 'checkbox', 27 | model: { 28 | defaults: { 29 | ...checkType.model.prototype.defaults, 30 | 'custom-name': config.labels.radio, 31 | attributes: { type: 'radio' }, 32 | }, 33 | }, 34 | isComponent(el) { 35 | if (el.tagName === 'INPUT' && el.type === 'radio') { 36 | return { type: 'radio' }; 37 | } 38 | }, 39 | view: checkType.view, 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Row.js: -------------------------------------------------------------------------------- 1 | import windowIcon from "raw-loader!../icons/window-maximize-solid.svg"; 2 | 3 | export const RowBlock = (bm, label) => { 4 | bm.add('row').set({ 5 | label: ` 6 | ${windowIcon} 7 |
${label}
8 | `, 9 | category: 'Layout', 10 | content: { 11 | type: 'row', 12 | classes: ['row'] 13 | } 14 | }); 15 | }; 16 | 17 | export default (domc) => { 18 | const defaultType = domc.getType('default'); 19 | const defaultModel = defaultType.model; 20 | const defaultView = defaultType.view; 21 | 22 | domc.addType('row', { 23 | model: { 24 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 25 | 'custom-name': 'Row', 26 | tagName: 'div', 27 | draggable: '.container, .container-fluid', 28 | droppable: true, 29 | traits: [ 30 | { 31 | type: 'class_select', 32 | options: [ 33 | { value: '', name: 'Yes' }, 34 | { value: 'no-gutters', name: 'No' } 35 | ], 36 | label: 'Gutters?' 37 | } 38 | ].concat(defaultModel.prototype.defaults.traits) 39 | }) 40 | }, 41 | isComponent(el) { 42 | if (el && el.classList && el.classList.contains('row')) { 43 | return { type: 'row' }; 44 | } 45 | }, 46 | view: defaultView 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Select.js: -------------------------------------------------------------------------------- 1 | import selectIcon from "raw-loader!../icons/select-input.svg"; 2 | 3 | export const SelectBlock = (bm, label) => { 4 | bm.add('select', { 5 | label: ` 6 | ${selectIcon} 7 |
${label}
`, 8 | category: 'Forms', 9 | content: ``, 13 | }); 14 | }; 15 | 16 | export default (editor, dc, traits, config = {}) => { 17 | const defaultType = dc.getType('default'); 18 | const defaultModel = defaultType.model; 19 | const inputType = dc.getType('input'); 20 | const inputModel = inputType.model; 21 | 22 | const preventDefaultClick = () => { 23 | return defaultType.view.extend({ 24 | events: { 25 | 'mousedown': 'handleClick', 26 | }, 27 | 28 | handleClick(e) { 29 | e.preventDefault(); 30 | }, 31 | }); 32 | }; 33 | 34 | // SELECT 35 | dc.addType('select', { 36 | model: { 37 | defaults: { 38 | ...inputModel.prototype.defaults, 39 | 'custom-name': config.labels.select, 40 | tagName: 'select', 41 | traits: [ 42 | traits.name, { 43 | label: config.labels.trait_options, 44 | type: 'select-options' 45 | }, 46 | traits.required 47 | ], 48 | }, 49 | }, 50 | isComponent(el) { 51 | if (el.tagName === 'SELECT') { 52 | return { type: 'select' }; 53 | } 54 | }, 55 | view: preventDefaultClick(), 56 | }); 57 | 58 | const traitManager = editor.TraitManager; 59 | traitManager.addType('select-options', { 60 | events: { 61 | 'keyup': 'onChange', 62 | }, 63 | 64 | onValueChange: function () { 65 | const optionsStr = this.model.get('value').trim(); 66 | const options = optionsStr.split('\n'); 67 | const optComps = []; 68 | 69 | for (let i = 0; i < options.length; i++) { 70 | const optionStr = options[i]; 71 | const option = optionStr.split(config.optionsStringSeparator); 72 | const opt = { 73 | tagName: 'option', 74 | attributes: {} 75 | }; 76 | if (option[1]) { 77 | opt.content = option[1]; 78 | opt.attributes.value = option[0]; 79 | } else { 80 | opt.content = option[0]; 81 | opt.attributes.value = option[0]; 82 | } 83 | optComps.push(opt); 84 | } 85 | 86 | const comps = this.target.get('components'); 87 | comps.reset(optComps); 88 | this.target.view.render(); 89 | }, 90 | 91 | getInputEl: function () { 92 | if (!this.$input) { 93 | const target = this.target; 94 | let optionsStr = ''; 95 | const options = target.get('components'); 96 | 97 | for (let i = 0; i < options.length; i++) { 98 | const option = options.models[i]; 99 | const optAttr = option.get('attributes'); 100 | const optValue = optAttr.value || ''; 101 | optionsStr += `${optValue}${config.optionsStringSeparator}${option.get('content')}\n`; 102 | } 103 | 104 | this.$input = document.createElement('textarea'); 105 | this.$input.value = optionsStr; 106 | } 107 | return this.$input; 108 | }, 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /src/components/Text.js: -------------------------------------------------------------------------------- 1 | import fontIcon from "raw-loader!../icons/font-solid.svg"; 2 | 3 | export const TextBlock = (bm, label) => { 4 | bm.add('text', { 5 | label: ` 6 | ${fontIcon} 7 |
${label}
8 | `, 9 | category: 'Typography', 10 | content: { 11 | type: 'text', 12 | content: 'Insert your text here' 13 | } 14 | }); 15 | }; 16 | 17 | export default (domc) => { 18 | const defaultType = domc.getType('default'); 19 | const defaultModel = defaultType.model; 20 | const textType = domc.getType('text'); 21 | const textView = textType.view; 22 | 23 | domc.addType('text', { 24 | model: { 25 | defaults: Object.assign({}, defaultModel.prototype.defaults, { 26 | 'custom-name': 'Text', 27 | tagName: 'div', 28 | droppable: true, 29 | editable: true 30 | }) 31 | }, 32 | view: textView 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/Textarea.js: -------------------------------------------------------------------------------- 1 | import textareaIcon from "raw-loader!../icons/textarea.svg"; 2 | 3 | export const TextareaBlock = (bm, label) => { 4 | bm.add('textarea', { 5 | label: ` 6 | ${textareaIcon} 7 |
${label}
`, 8 | category: 'Forms', 9 | content: '', 10 | }); 11 | }; 12 | 13 | export default (dc, traits, config = {}) => { 14 | const defaultType = dc.getType('default'); 15 | const defaultView = defaultType.view; 16 | const inputType = dc.getType('input'); 17 | const inputModel = inputType.model; 18 | 19 | // TEXTAREA 20 | dc.addType('textarea', { 21 | extend: 'input', 22 | model: { 23 | defaults: { 24 | ...inputModel.prototype.defaults, 25 | 'custom-name': config.labels.textarea, 26 | tagName: 'textarea', 27 | traits: [ 28 | traits.name, 29 | traits.placeholder, 30 | traits.required 31 | ] 32 | }, 33 | }, 34 | isComponent(el) { 35 | if (el.tagName === 'TEXTAREA') { 36 | return { type: 'textarea' }; 37 | } 38 | }, 39 | view: defaultView, 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/components/tabs/Tab.js: -------------------------------------------------------------------------------- 1 | import constants from './constants'; 2 | import { elHasClass } from '../../utils'; 3 | 4 | export default (dc, config = {}) => { 5 | const defaultType = dc.getType('default'); 6 | const defaultModel = defaultType.model; 7 | const defaultView = defaultType.view; 8 | const { tabName, navigationSelector } = constants; 9 | const classId = config.classTab; 10 | const type = tabName; 11 | 12 | dc.addType(type, { 13 | 14 | 15 | model: { 16 | defaults: { 17 | ...defaultModel.prototype.defaults, 18 | name: 'Tab', 19 | tagName: 'li', 20 | copyable: true, 21 | draggable: navigationSelector, 22 | 23 | }, 24 | 25 | init() { 26 | this.get('classes').pluck('name').indexOf(classId) < 0 && this.addClass(classId); 27 | } 28 | }, 29 | isComponent(el) { 30 | if (elHasClass(el, classId)) return { type }; 31 | }, 32 | 33 | view: { 34 | init() { 35 | const comps = this.model.components(); 36 | 37 | // Add a basic template if it's not yet initialized 38 | if (!comps.length) { 39 | comps.add(` 40 | Tab 41 | `); 42 | } 43 | }, 44 | }, 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/tabs/TabPane.js: -------------------------------------------------------------------------------- 1 | import constants from './constants'; 2 | import { elHasClass } from '../../utils'; 3 | 4 | export default (dc, config = {}) => { 5 | const defaultType = dc.getType('default'); 6 | const defaultModel = defaultType.model; 7 | const defaultView = defaultType.view; 8 | const { tabPaneName, tabPanesSelector } = constants; 9 | const classId = config.classTabPane; 10 | const type = tabPaneName; 11 | 12 | dc.addType(type, { 13 | 14 | model: { 15 | defaults: { 16 | ...defaultModel.prototype.defaults, 17 | name: 'Tab Pane', 18 | copyable: true, 19 | draggable: tabPanesSelector, 20 | 21 | traits: [ 22 | 'id', 23 | { 24 | type: 'class_select', 25 | options: [ 26 | { value: 'fade', name: 'Fade' }, 27 | { value: '', name: 'None' }, 28 | ], 29 | label: 'Animation', 30 | }, 31 | { 32 | type: 'class_select', 33 | options: [ 34 | { value: '', name: 'Inactive' }, 35 | { value: 'active', name: 'Active' }, 36 | ], 37 | label: 'Is Active', 38 | }, 39 | ], 40 | }, 41 | 42 | init() { 43 | this.get('classes').pluck('name').indexOf(classId) < 0 && this.addClass(classId); 44 | } 45 | }, 46 | isComponent(el) { 47 | if (elHasClass(el, classId)) return { type }; 48 | }, 49 | 50 | view: defaultView, 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /src/components/tabs/TabsNavigation.js: -------------------------------------------------------------------------------- 1 | import constants from './constants'; 2 | import { elHasClass } from '../../utils'; 3 | import ellipsisIcon from "raw-loader!../../icons/ellipsis-h-solid.svg"; 4 | import circleIcon from "raw-loader!../../icons/circle-solid.svg"; 5 | import windowIcon from "raw-loader!../../icons/window-maximize-solid.svg"; 6 | 7 | export const TabsBlock = (bm, c) => { 8 | bm.add('tabs', { 9 | label: ` 10 | ${ellipsisIcon} 11 |
${c.labels.tabs}
12 | `, 13 | category: 'Components', 14 | content: ` 15 | 26 |
27 |
28 |
29 |
30 |
31 | ` 32 | }); 33 | bm.add('tabs-tab', { 34 | label: ` 35 | ${circleIcon} 36 |
${c.labels.tab}
37 | `, 38 | category: 'Components', 39 | content: { 40 | type: 'tabs-tab', 41 | } 42 | }); 43 | bm.add('tabs-tab-pane', { 44 | label: ` 45 | ${windowIcon} 46 |
${c.labels.tabPane}
47 | `, 48 | category: 'Components', 49 | content: { 50 | type: 'tabs-tab-pane', 51 | } 52 | }); 53 | }; 54 | 55 | export default (dc, config = {}) => { 56 | const defaultType = dc.getType('default'); 57 | const defaultModel = defaultType.model; 58 | const defaultView = defaultType.view; 59 | const { navigationName, tabSelector } = constants; 60 | const classId = config.classNavigation; 61 | const type = navigationName; 62 | 63 | dc.addType(type, { 64 | 65 | model: { 66 | defaults: { 67 | ...defaultModel.prototype.defaults, 68 | name: 'Tabs Navigation', 69 | copyable: 0, 70 | draggable: true, 71 | droppable: tabSelector, 72 | 73 | traits: [ 74 | { 75 | type: 'class_select', 76 | options: [ 77 | { value: 'nav-tabs', name: 'Tabs' }, 78 | { value: 'nav-pills', name: 'Pills' }, 79 | ], 80 | label: 'Type', 81 | }, 82 | { 83 | type: 'class_select', 84 | options: [ 85 | { value: '', name: 'Left' }, 86 | { value: 'nav-fill', name: 'Fill' }, 87 | { value: 'nav-justified', name: 'Justify' }, 88 | ], 89 | label: 'Layout', 90 | }, 91 | ], 92 | }, 93 | 94 | init() { 95 | this.get('classes').pluck('name').indexOf(classId) < 0 && this.addClass(classId); 96 | } 97 | }, 98 | isComponent(el) { 99 | if (elHasClass(el, classId)) return { type }; 100 | }, 101 | 102 | view: { 103 | init() { 104 | const props = [ 105 | 'type', 106 | 'layout', 107 | ]; 108 | const reactTo = props.map(prop => `change:${prop}`).join(' '); 109 | this.listenTo(this.model, reactTo, this.render); 110 | const comps = this.model.components(); 111 | 112 | // Add a basic template if it's not yet initialized 113 | if (!comps.length) { 114 | comps.add(` 115 | 126 | `); 127 | } 128 | }, 129 | }, 130 | }); 131 | } 132 | -------------------------------------------------------------------------------- /src/components/tabs/TabsPanes.js: -------------------------------------------------------------------------------- 1 | import constants from './constants'; 2 | import { elHasClass } from '../../utils'; 3 | 4 | export default (dc, config = {}) => { 5 | const defaultType = dc.getType('default'); 6 | const defaultModel = defaultType.model; 7 | const defaultView = defaultType.view; 8 | const { tabPanesName, tabPaneSelector } = constants; 9 | const classId = config.classTabPanes; 10 | const type = tabPanesName; 11 | 12 | dc.addType(type, { 13 | 14 | model: { 15 | defaults: { 16 | ...defaultModel.prototype.defaults, 17 | name: 'Tabs Panes', 18 | copyable: 0, 19 | draggable: true, 20 | droppable: tabPaneSelector, 21 | }, 22 | 23 | init() { 24 | this.get('classes').pluck('name').indexOf(classId) < 0 && this.addClass(classId); 25 | } 26 | }, 27 | isComponent(el) { 28 | if (elHasClass(el, classId)) return { type }; 29 | }, 30 | 31 | view: { 32 | init() { 33 | const comps = this.model.components(); 34 | 35 | // Add a basic template if it's not yet initialized 36 | if (!comps.length) { 37 | comps.add(` 38 |
39 |
Tab pane 1
40 |
Tab pane 2
41 |
Tab pane 3
42 |
43 | `); 44 | } 45 | }, 46 | }, 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/components/tabs/constants.js: -------------------------------------------------------------------------------- 1 | const prefix = 'tabs-'; 2 | const containerName = `${prefix}container`; 3 | const navigationName = `${prefix}navigation`; 4 | const tabPanesName = `${prefix}panes`; 5 | const tabName = `${prefix}tab`; 6 | const tabPaneName = `${prefix}tab-pane`; 7 | 8 | export default { 9 | navigationName, 10 | tabPanesName, 11 | tabName, 12 | tabPaneName, 13 | 14 | // Selectors 15 | navigationSelector: `[data-gjs-type="${navigationName}"]`, 16 | tabPanesSelector: `[data-gjs-type="${tabPanesName}"]`, 17 | tabSelector: `[data-gjs-type="${tabName}"]`, 18 | tabPaneSelector: `[data-gjs-type="${tabPaneName}"]`, 19 | 20 | // IDs 21 | containerId: `data-${containerName}`, 22 | navigationId: `data-${navigationName}`, 23 | tabPanesId: `data-${tabPanesName}`, 24 | tabId: `data-${tabName}`, 25 | tabPaneId: `data-${tabPaneName}`, 26 | } 27 | -------------------------------------------------------------------------------- /src/components/video/Embed.js: -------------------------------------------------------------------------------- 1 | export default (domComponent) => { 2 | const src_default = 'https://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4'; 3 | const defaultType = domComponent.getType('default'); 4 | const model = defaultType.model; 5 | const view = defaultType.view; 6 | const type = 'bs-video'; 7 | 8 | domComponent.addType(type, { 9 | model: { 10 | defaults: Object.assign({}, model.prototype.defaults, { 11 | 'custom-name': 'Embed', 12 | tagName: 'div', 13 | resizable: false, 14 | droppable: false, 15 | classes: ['embed-responsive', 'embed-responsive-16by9'], 16 | traits: [ 17 | { 18 | type: 'class_select', 19 | options: [ 20 | { value: 'embed-responsive-21by9', name: '21:9' }, 21 | { value: 'embed-responsive-16by9', name: '16:9' }, 22 | { value: 'embed-responsive-4by3', name: '4:3' }, 23 | { value: 'embed-responsive-1by1', name: '1:1' }, 24 | ], 25 | label: 'Aspect Ratio', 26 | }, 27 | ].concat(model.prototype.defaults.traits) 28 | }) 29 | }, 30 | isComponent: function (el) { 31 | if (el && el.className === 'embed-responsive') { 32 | return { type: type }; 33 | } 34 | }, 35 | view: { 36 | init() { 37 | const props = [ 38 | 'Aspect Ratio', 39 | ]; 40 | const reactTo = props.map(prop => `change:${prop}`).join(' '); 41 | this.listenTo(this.model, reactTo, this.render); 42 | const comps = this.model.components(); 43 | // Add a basic template if it's not yet initialized 44 | if (!comps.length) { 45 | comps.add(``); 46 | } 47 | }, 48 | }, 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/components/video/Video.js: -------------------------------------------------------------------------------- 1 | import videoIcon from "raw-loader!../../icons/youtube-brands.svg"; 2 | 3 | export const VideoBlock = (bm, label) => { 4 | bm.add('bs-video', { 5 | label: ` 6 | ${videoIcon} 7 |
${label}
8 | `, 9 | category: 'Media', 10 | content: { 11 | type: 'bs-video' 12 | } 13 | }); 14 | }; 15 | 16 | export default (domComponent) => { 17 | const videoType = domComponent.getType('video'); 18 | const model = videoType.model; 19 | const view = videoType.view; 20 | const type = 'bs-embed-responsive'; 21 | 22 | domComponent.addType(type, { 23 | extend: 'video', 24 | model: { 25 | defaults: Object.assign({}, model.prototype.defaults, { 26 | 'custom-name': 'Video', 27 | resizable: false, 28 | droppable: false, 29 | draggable: false, 30 | copyable: false, 31 | provider: 'so', 32 | classes: ['embed-responsive-item'], 33 | }) 34 | }, 35 | isComponent: function (el) { 36 | if (el && el.className === 'embed-responsive-item') { 37 | var result = { 38 | provider: 'so', 39 | type: type 40 | }; 41 | var isYtProv = /youtube\.com\/embed/.test(el.src); 42 | var isYtncProv = /youtube-nocookie\.com\/embed/.test(el.src); 43 | var isViProv = /player\.vimeo\.com\/video/.test(el.src); 44 | var isExtProv = isYtProv || isYtncProv || isViProv; 45 | if (el.tagName == 'VIDEO' || (el.tagName == 'IFRAME' && isExtProv)) { 46 | if (el.src) result.src = el.src; 47 | if (isExtProv) { 48 | if (isYtProv) result.provider = 'yt'; 49 | else if (isYtncProv) result.provider = 'ytnc'; 50 | else if (isViProv) result.provider = 'vi'; 51 | } 52 | } 53 | return result; 54 | 55 | } 56 | }, 57 | view: view 58 | }); 59 | } 60 | -------------------------------------------------------------------------------- /src/devices.js: -------------------------------------------------------------------------------- 1 | export default (editor, config = {}) => { 2 | const c = config; 3 | const deviceManager = editor.DeviceManager; 4 | if(c.gridDevices) { 5 | deviceManager.add('Extra Small', '575px'); 6 | deviceManager.add('Small', '767px'); 7 | deviceManager.add('Medium', '991px'); 8 | deviceManager.add('Large', '1199px'); 9 | deviceManager.add('Extra Large'); 10 | 11 | 12 | if(c.gridDevicesPanel) { 13 | const panels = editor.Panels; 14 | const commands = editor.Commands; 15 | var panelDevices = panels.addPanel({id: 'devices-buttons'}); 16 | var deviceBtns = panelDevices.get('buttons'); 17 | deviceBtns.add([{ 18 | id: 'deviceXl', 19 | command: 'set-device-xl', 20 | className: 'fa fa-desktop', 21 | text: 'XL', 22 | attributes: {'title': 'Extra Large'}, 23 | active: 1 24 | },{ 25 | id: 'deviceLg', 26 | command: 'set-device-lg', 27 | className: 'fa fa-desktop', 28 | attributes: {'title': 'Large'} 29 | },{ 30 | id: 'deviceMd', 31 | command: 'set-device-md', 32 | className: 'fa fa-tablet', 33 | attributes: {'title': 'Medium'} 34 | },{ 35 | id: 'deviceSm', 36 | command: 'set-device-sm', 37 | className: 'fa fa-mobile', 38 | attributes: {'title': 'Small'} 39 | },{ 40 | id: 'deviceXs', 41 | command: 'set-device-xs', 42 | className: 'fa fa-mobile', 43 | attributes: {'title': 'Extra Small'} 44 | }]); 45 | 46 | commands.add('set-device-xs', { 47 | run: function(editor) { 48 | editor.setDevice('Extra Small'); 49 | } 50 | }); 51 | commands.add('set-device-sm', { 52 | run: function(editor) { 53 | editor.setDevice('Small'); 54 | } 55 | }); 56 | commands.add('set-device-md', { 57 | run: function(editor) { 58 | editor.setDevice('Medium'); 59 | } 60 | }); 61 | commands.add('set-device-lg', { 62 | run: function(editor) { 63 | editor.setDevice('Large'); 64 | } 65 | }); 66 | commands.add('set-device-xl', { 67 | run: function(editor) { 68 | editor.setDevice('Extra Large'); 69 | } 70 | }); 71 | } 72 | 73 | 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/icons/button.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/caret-square-down-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/certificate-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/check-square-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/circle-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/columns-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/compress-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/credit-card-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/dot-circle-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/ellipsis-h-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/equals-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/exclamation-triangle-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/file-input.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/font-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/form-group.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/form.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/heading-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/image-light.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/image-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/input-group.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/input.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/icons/label.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/link-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/paragraph-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/select-input.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/icons/textarea.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/window-maximize-solid.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/icons/youtube-brands.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import grapesjs from 'grapesjs'; 2 | import loadCommands from './commands'; 3 | import loadTraits from './traits'; 4 | import loadComponents from './components'; 5 | import loadDevices from './devices'; 6 | 7 | const loadCss = editor => { 8 | editor.Config.canvasCss += ` 9 | /* Layout */ 10 | 11 | .gjs-dashed .container, .gjs-dashed .container-fluid, 12 | .gjs-dashed .row, 13 | .gjs-dashed .col, .gjs-dashed [class^="col-"] { 14 | min-height: 1.5rem !important; 15 | } 16 | .gjs-dashed .w-100 { 17 | min-height: .25rem !important; 18 | background-color: rgba(0,0,0,0.1); 19 | } 20 | .gjs-dashed img { 21 | min-width: 25px; 22 | min-height: 25px; 23 | background-color: rgba(0,0,0,0.5); 24 | } 25 | 26 | /* Components */ 27 | 28 | .gjs-dashed .btn-group, 29 | .gjs-dashed .btn-toolbar { 30 | padding-right: 1.5rem !important; 31 | min-height: 1.5rem !important; 32 | } 33 | .gjs-dashed .card, 34 | .gjs-dashed .card-group, .gjs-dashed .card-deck, .gjs-dashed .card-columns { 35 | min-height: 1.5rem !important; 36 | } 37 | .gjs-dashed .collapse { 38 | display: block !important; 39 | min-height: 1.5rem !important; 40 | } 41 | .gjs-dashed .dropdown { 42 | display: block !important; 43 | min-height: 1.5rem !important; 44 | } 45 | .gjs-dashed .dropdown-menu { 46 | min-height: 1.5rem !important; 47 | display: block !important; 48 | } 49 | ` 50 | }; 51 | 52 | export default grapesjs.plugins.add('grapesjs-blocks-bootstrap4', (editor, opts = {}) => { 53 | 54 | window.editor = editor; 55 | 56 | const opts_blocks = opts.blocks || {}; 57 | const opts_labels = opts.labels || {}; 58 | const opts_categories = opts.blockCategories || {}; 59 | delete opts['blocks']; 60 | delete opts['labels']; 61 | delete opts['blockCategories']; 62 | 63 | const default_blocks = { 64 | default: true, 65 | text: true, 66 | link: true, 67 | image: true, 68 | // LAYOUT 69 | container: true, 70 | row: true, 71 | column: true, 72 | column_break: true, 73 | media_object: true, 74 | // COMPONENTS 75 | alert: true, 76 | tabs: true, 77 | badge: true, 78 | button: true, 79 | button_group: true, 80 | button_toolbar: true, 81 | card: true, 82 | card_container: true, 83 | collapse: true, 84 | dropdown: true, 85 | video: true, 86 | // TYPOGRAPHY 87 | header: true, 88 | paragraph: true, 89 | // BASIC 90 | list: true, 91 | // FORMS 92 | form: true, 93 | input: true, 94 | form_group_input: true, 95 | input_group: true, 96 | textarea: true, 97 | select: true, 98 | label: true, 99 | checkbox: true, 100 | radio: true, 101 | }; 102 | 103 | const default_labels = { 104 | // LAYOUT 105 | container: 'Container', 106 | row: 'Row', 107 | column: 'Column', 108 | column_break: 'Column Break', 109 | media_object: 'Media Object', 110 | 111 | // COMPONENTS 112 | alert: 'Alert', 113 | tabs: 'Tabs', 114 | tab: 'Tab', 115 | tabPane: 'Tab Pane', 116 | badge: 'Badge', 117 | button: 'Button', 118 | button_group: 'Button Group', 119 | button_toolbar: 'Button Toolbar', 120 | card: 'Card', 121 | card_container: 'Card Container', 122 | collapse: 'Collapse', 123 | dropdown: 'Dropdown', 124 | dropdown_menu: 'Dropdown Menu', 125 | dropdown_item: 'Dropdown Item', 126 | 127 | // MEDIA 128 | image: 'Image', 129 | video: 'Video', 130 | 131 | // TYPOGRAPHY 132 | text: 'Text', 133 | 134 | // BASIC 135 | header: 'Header', 136 | paragraph: 'Paragraph', 137 | link: 'Link', 138 | list: 'Simple List', 139 | 140 | // FORMS 141 | form: 'Form', 142 | input: 'Input', 143 | file_input: 'File', 144 | form_group_input: 'Form Group', 145 | input_group: 'Input group', 146 | textarea: 'Textarea', 147 | select: 'Select', 148 | select_option: '- Select option -', 149 | option: 'Option', 150 | label: 'Label', 151 | checkbox: 'Checkbox', 152 | radio: 'Radio', 153 | trait_method: 'Method', 154 | trait_enctype: 'Encoding Type', 155 | trait_multiple: 'Multiple', 156 | trait_action: 'Action', 157 | trait_state: 'State', 158 | trait_id: 'ID', 159 | trait_for: 'For', 160 | trait_name: 'Name', 161 | trait_placeholder: 'Placeholder', 162 | trait_value: 'Value', 163 | trait_required: 'Required', 164 | trait_type: 'Type', 165 | trait_options: 'Options', 166 | trait_checked: 'Checked', 167 | type_text: 'Text', 168 | type_email: 'Email', 169 | type_password: 'Password', 170 | type_number: 'Number', 171 | type_date: 'Date', 172 | type_hidden: 'Hidden', 173 | type_submit: 'Submit', 174 | type_reset: 'Reset', 175 | type_button: 'Button', 176 | }; 177 | 178 | const default_categories = { 179 | 'layout': true, 180 | 'media': true, 181 | 'components': true, 182 | 'typography': true, 183 | 'basic': true, 184 | 'forms': true, 185 | }; 186 | 187 | let options = { ...{ 188 | blocks: Object.assign(default_blocks, opts_blocks), 189 | labels: Object.assign(default_labels, opts_labels), 190 | blockCategories: Object.assign(default_categories, opts_categories), 191 | optionsStringSeparator: '::', 192 | gridDevices: true, 193 | gridDevicesPanel: false, 194 | classNavigation: 'nav', 195 | classTabPanes: 'tab-content', 196 | classTabPane: 'tab-pane', 197 | classTab: 'nav-item', 198 | }, ...opts }; 199 | 200 | // Add components 201 | loadCommands(editor, options); 202 | loadTraits(editor, options); 203 | loadComponents(editor, options); 204 | loadDevices(editor, options); 205 | loadCss(editor, options); 206 | }); 207 | -------------------------------------------------------------------------------- /src/traits.js: -------------------------------------------------------------------------------- 1 | export default (editor, config = {}) => { 2 | 3 | const tm = editor.TraitManager; 4 | 5 | // Select trait that maps a class list to the select options. 6 | // The default select option is set if the input has a class, and class list is modified when select value changes. 7 | tm.addType('class_select', { 8 | events: { 9 | 'change': 'onChange' // trigger parent onChange method on input change 10 | }, 11 | createInput({trait}) { 12 | const md = this.model; 13 | const opts = md.get('options') || []; 14 | const input = document.createElement('select'); 15 | const target_view_el = this.target.view.el; 16 | 17 | for (let i = 0; i < opts.length; i++) { 18 | const option = document.createElement('option'); 19 | let value = opts[i].value; 20 | if (value === '') { 21 | value = 'GJS_NO_CLASS'; 22 | } // 'GJS_NO_CLASS' represents no class--empty string does not trigger value change 23 | option.text = opts[i].name; 24 | option.value = value; 25 | 26 | // Convert the Token List to an Array 27 | const css = Array.from(target_view_el.classList); 28 | 29 | const value_a = value.split(' '); 30 | const intersection = css.filter(x => value_a.includes(x)); 31 | 32 | if(intersection.length === value_a.length) { 33 | option.setAttribute('selected', 'selected'); 34 | } 35 | 36 | input.append(option); 37 | } 38 | return input; 39 | }, 40 | onUpdate({elInput, component}) { 41 | const classes = component.getClasses(); 42 | const opts = this.model.get('options') || []; 43 | for (let i = 0; i < opts.length; i++) { 44 | let value = opts[i].value; 45 | if (value && classes.includes(value)) { 46 | elInput.value = value; 47 | return; 48 | } 49 | } 50 | elInput.value = "GJS_NO_CLASS"; 51 | }, 52 | 53 | onEvent({elInput, component, event}) { 54 | const classes = this.model.get('options').map(opt => opt.value); 55 | for (let i = 0; i < classes.length; i++) { 56 | if (classes[i].length > 0) { 57 | const classes_i_a = classes[i].split(' '); 58 | for (let j = 0; j < classes_i_a.length; j++) { 59 | if (classes_i_a[j].length > 0) { 60 | component.removeClass(classes_i_a[j]); 61 | } 62 | } 63 | } 64 | } 65 | const value = this.model.get('value'); 66 | 67 | // This piece of code removes the empty attribute name from attributes list 68 | const elAttributes = component.attributes.attributes; 69 | delete elAttributes[""]; 70 | 71 | if (value.length > 0 && value !== 'GJS_NO_CLASS') { 72 | const value_a = value.split(' '); 73 | for (let i = 0; i < value_a.length; i++) { 74 | component.addClass(value_a[i]); 75 | } 76 | } 77 | component.em.trigger('component:toggled'); 78 | }, 79 | }); 80 | 81 | const textTrait = tm.getType('text'); 82 | 83 | tm.addType('content', { 84 | events: { 85 | 'keyup': 'onChange', 86 | }, 87 | 88 | onValueChange: function () { 89 | const md = this.model; 90 | const target = md.target; 91 | target.set('content', md.get('value')); 92 | }, 93 | 94 | getInputEl: function () { 95 | if (!this.inputEl) { 96 | this.inputEl = textTrait.prototype.getInputEl.bind(this)(); 97 | this.inputEl.value = this.target.get('content'); 98 | } 99 | return this.inputEl; 100 | } 101 | }); 102 | 103 | tm.addType('content', { 104 | events: { 105 | 'keyup': 'onChange', 106 | }, 107 | 108 | onValueChange: function () { 109 | const md = this.model; 110 | const target = md.target; 111 | target.set('content', md.get('value')); 112 | }, 113 | 114 | getInputEl: function () { 115 | if (!this.inputEl) { 116 | this.inputEl = textTrait.prototype.getInputEl.bind(this)(); 117 | this.inputEl.value = this.target.get('content'); 118 | } 119 | return this.inputEl; 120 | } 121 | }); 122 | 123 | } 124 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const elHasClass = (el, toFind) => { 2 | let cls = el.className; 3 | cls = cls && cls.toString(); 4 | if (cls && cls.split(' ').indexOf(toFind) >= 0) return 1; 5 | }; 6 | 7 | const capitalize = (phrase) => { 8 | return phrase 9 | .toLowerCase() 10 | .split(' ') 11 | .map(word => word.charAt(0).toUpperCase() + word.slice(1)) 12 | .join(' '); 13 | }; 14 | 15 | export { 16 | elHasClass, 17 | capitalize, 18 | } 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const TerserPlugin = require("terser-webpack-plugin"); 3 | const pkg = require('./package.json'); 4 | const webpack = require('webpack'); 5 | const fs = require('fs'); 6 | const path = require('path') 7 | const name = pkg.name; 8 | let plugins = []; 9 | let optimization = {}; 10 | 11 | 12 | module.exports = (env = {}) => { 13 | if (env.production) { 14 | optimization.minimizer = [ 15 | new TerserPlugin({ 16 | parallel: true, 17 | }) 18 | ]; 19 | plugins = [ 20 | new webpack.BannerPlugin(`${name} - ${pkg.version}`), 21 | ] 22 | } else { 23 | const index = 'index.html'; 24 | const indexDev = '_' + index; 25 | plugins.push(new HtmlWebpackPlugin({ 26 | template: fs.existsSync(indexDev) ? indexDev : index 27 | })); 28 | } 29 | 30 | return { 31 | mode: env.production ? 'production' : 'development', 32 | entry: './src', 33 | output: { 34 | filename: `./${name}.min.js`, 35 | library: name, 36 | libraryTarget: 'umd', 37 | path: path.resolve(__dirname, 'dist'), 38 | publicPath: '/dist/', 39 | }, 40 | devServer: { 41 | static: path.join(__dirname, 'dist'), 42 | hot: true, 43 | }, 44 | module: { 45 | rules: [ 46 | { 47 | test: /\.js$/, 48 | include: /src/, 49 | use: { 50 | loader: 'babel-loader', 51 | } 52 | }, 53 | ], 54 | }, 55 | externals: {'grapesjs': 'grapesjs'}, 56 | optimization: optimization, 57 | plugins: plugins, 58 | watchOptions: { 59 | ignored: /node_modules/ 60 | } 61 | }; 62 | }; 63 | --------------------------------------------------------------------------------