├── .gitignore
├── .npmignore
├── src
├── consts.js
├── utils
│ ├── index.js
│ ├── repetition.js
│ ├── breadcrumbs.js
│ ├── store.js
│ ├── palette.js
│ └── resizer.js
├── blocks.js
├── panels.js
├── grid.js
├── index.js
├── components.js
└── styles.scss
├── TODO.md
├── package.json
├── LICENSE
├── index.html
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | private/
3 | /locale
4 | node_modules/
5 | *.log
6 | _index.html
7 | /dist
8 | *.svg
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | private/
3 | /locale
4 | node_modules/
5 | *.log
6 | _index.html
7 | TODO.md
8 | *.svg
--------------------------------------------------------------------------------
/src/consts.js:
--------------------------------------------------------------------------------
1 | export const
2 | cmdSave = 'save-page',
3 | gridCompId = 'css-grid',
4 | gridChildId = 'grid-cell';
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | # TODO
2 |
3 | - [x] Color palette generator
4 | - [x] DOM breadcrumbs
5 | - [x] Layer icons map
6 | - [x] Grid Component
7 | - [x] Canvas Resizer
8 | - [x] npm package
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import breadcrumbs from './breadcrumbs';
2 | import palette from './palette';
3 | import store from './store';
4 | import resizer from './resizer';
5 |
6 | export { palette, breadcrumbs, store, resizer };
--------------------------------------------------------------------------------
/src/blocks.js:
--------------------------------------------------------------------------------
1 | import { gridCompId } from './consts'
2 |
3 | export default (editor, opts = {}) => {
4 | const bm = editor.BlockManager;
5 | const { labelGrid, categoryGrid, gridBlock } = opts;
6 |
7 | gridBlock && bm.add(gridCompId, {
8 | label: labelGrid,
9 | media: ' ',
10 | category: categoryGrid,
11 | content: { type: gridCompId },
12 | ...gridBlock
13 | });
14 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "grapesjs-plugin-toolbox",
3 | "version": "1.0.15",
4 | "description": "Grapesjs Plugin Toolbox",
5 | "main": "dist/grapesjs-plugin-toolbox.min.js",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/Ju99ernaut/grapesjs-plugin-toolbox.git"
9 | },
10 | "scripts": {
11 | "start": "grapesjs-cli serve",
12 | "build": "grapesjs-cli build",
13 | "build:css": "node-sass src/styles.scss dist/grapesjs-plugin-toolbox.min.css --output-style compressed",
14 | "bump": "npm version patch -m 'Bump v%s'"
15 | },
16 | "keywords": [
17 | "grapesjs",
18 | "plugin",
19 | "tools"
20 | ],
21 | "devDependencies": {
22 | "grapesjs-cli": "^1.0.12",
23 | "node-sass": "^4.14.1"
24 | },
25 | "author": "Brendon Ngirazi",
26 | "license": "MIT",
27 | "dependencies": {
28 | "colorthief": "^2.3.2"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/repetition.js:
--------------------------------------------------------------------------------
1 | export const groupRepeatedUnits = (templateUnitArray = [{ unit: "1fr" }]) => {
2 | const templateArray = templateUnitArray.map(i => i["unit"]);
3 | const groups = [[templateArray.shift()]];
4 | for (const templateUnit of templateArray) {
5 | const lastGroup = groups[groups.length - 1];
6 | if (lastGroup.indexOf(templateUnit) !== -1) {
7 | lastGroup.push(templateUnit);
8 | } else {
9 | groups.push([templateUnit]);
10 | }
11 | }
12 | return groups;
13 | };
14 |
15 | export const createRepetition = (groups, min, auto, maxRepetition = 1) => {
16 | return auto ? `repeat(auto-fill, minmax(${min}px, 1fr))` : groups
17 | .map(group =>
18 | // If you want to add repetition only when a measure is repeated more than x times,
19 | // change maxRepetition value to x
20 | group.length === maxRepetition ? group.join(" ") : `repeat(${group.length}, ${group[0]})`
21 | )
22 | .join(" ");
23 | };
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License Copyright (c) 2020-current Grapesjs Plugin Toolbox
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Grapesjs Plugin Toolbox
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | This is a demo content from _index.html. You can use this template file for
24 | development purpose. It won't be stored in your git repository
25 |
26 |
27 |
28 |
29 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/utils/breadcrumbs.js:
--------------------------------------------------------------------------------
1 | export default (editor, config) => {
2 | const generateQuerySelector = el => {
3 | let str = el.tagName.toLowerCase();
4 | str += el.id ? '#' + el.id : '';
5 | el.className.length && el.className.split(/\s/).forEach(cls => {
6 | str += (cls != 'gjs-selected' && cls != 'cke_editable' &&
7 | cls != 'cke_editable_inline' && cls != 'cke_contents_ltr' &&
8 | cls != 'cke_show_borders') ? '.' + cls : '';
9 | });
10 | return generateTree(el.parentNode) + `${str} `;
11 | };
12 |
13 | const generateTree = el => {
14 | if (el.tagName.toLowerCase() === 'html')
15 | return ` html `; //?link href to components
16 | return generateTree(el.parentNode) + `${el.tagName.toLowerCase() + (el.id ? '#' + el.id : '')} `;
17 | };
18 |
19 | const $ = editor.$;
20 | const pfx = editor.Config.stylePrefix;
21 |
22 | editor.on('component:selected', model => {
23 | const breadcrumbs = $(`#${pfx}breadcrumbs`);
24 | !breadcrumbs.length && $('body').append($(`
`));
25 | breadcrumbs.html(generateQuerySelector(model.getEl()));
26 | breadcrumbs.find('span').on('click', function(e) {
27 | const doc = editor.Canvas.getDocument();
28 | editor.select(doc.querySelector(e.currentTarget.innerText.trim()));
29 | });
30 | });
31 | }
--------------------------------------------------------------------------------
/src/panels.js:
--------------------------------------------------------------------------------
1 | import { cmdSave } from './consts';
2 |
3 | export default (editor, config) => {
4 | const cm = editor.Commands;
5 | const pn = editor.Panels;
6 | const um = editor.UndoManager;
7 | const pfx = editor.Config.stylePrefix;
8 |
9 | cm.add(cmdSave, e => {
10 | editor.store(res => console.log("Saved..."));
11 | });
12 |
13 | const pnOptions = pn.getPanel('options');
14 | pnOptions.get('buttons').add([{
15 | id: 'undo',
16 | className: 'fa fa-undo', //reply
17 | attributes: { title: 'undo' },
18 | command: e => e.runCommand('core:undo'),
19 | }, {
20 | id: 'redo',
21 | className: 'fa fa-repeat', //share
22 | attributes: { title: 'redo' },
23 | command: e => e.runCommand('core:redo'),
24 | }, {
25 | id: cmdSave,
26 | className: 'fa fa-floppy-o',
27 | attributes: { title: 'save' },
28 | command: e => e.runCommand(cmdSave),
29 | }]);
30 |
31 | const undoBtn = pn.getButton('options', 'undo');
32 | const redoBtn = pn.getButton('options', 'redo');
33 | const saveBtn = pn.getButton('options', cmdSave);
34 |
35 | editor.on('component:update', () => {
36 | um.hasUndo() ? undoBtn.set('className', `${pfx}font-aqua fa fa-undo`) : undoBtn.set('className', 'fa fa-undo');
37 | um.hasRedo() ? redoBtn.set('className', `${pfx}font-aqua fa fa-repeat`) : redoBtn.set('className', 'fa fa-repeat');
38 | editor.getDirtyCount() > 0 ? saveBtn.set('className', `${pfx}font-green fa fa-floppy-o`) : saveBtn.set('className', 'fa fa-floppy-o');
39 | });
40 | }
--------------------------------------------------------------------------------
/src/utils/store.js:
--------------------------------------------------------------------------------
1 | import { groupRepeatedUnits, createRepetition } from '../utils/repetition';
2 |
3 | export default (editor, opts = {}) => {
4 | return {
5 | state: {
6 | auto: false,
7 | min: 200,
8 | columns: 5,
9 | rows: 5,
10 | columngap: 0,
11 | rowgap: 0,
12 | colArr: [],
13 | rowArr: [],
14 | childarea: []
15 | },
16 | getters: {
17 | colTemplate(state) {
18 | const unitGroups = groupRepeatedUnits(state.colArr);
19 | return createRepetition(unitGroups, state.min, state.auto);
20 | },
21 | rowTemplate(state) {
22 | const unitGroups = groupRepeatedUnits(state.rowArr);
23 | return createRepetition(unitGroups, state.min, false);
24 | },
25 | divNum(state) {
26 | return Math.max(state.columns, 0) * Math.max(state.rows, 0);
27 | }
28 | },
29 | mutations: {
30 | initialArrIndex(state, payload) {
31 | if (payload !== '') {
32 | const queryParams = new URLSearchParams(payload);
33 |
34 | for (const stateKey in state) {
35 | const paramIsValid = queryParams.has(stateKey);
36 | const paramType = typeof (state[stateKey]);
37 |
38 | if (paramIsValid && paramType === 'number') {
39 | state[stateKey] = queryParams.get(stateKey);
40 | } else if (paramIsValid && paramType === 'object') {
41 | state[stateKey] = JSON.parse(queryParams.get(stateKey));
42 | }
43 | }
44 | } else {
45 | createArr(state.columns, state.colArr);
46 | createArr(state.rows, state.rowArr);
47 | }
48 | },
49 | adjustArr(state, payload) {
50 | let newVal = Math.max(Number(payload.newVal), 0),
51 | oldVal = Math.max(Number(payload.oldVal), 0);
52 |
53 | if (newVal < oldVal) {
54 | // you'd think that .length would be quicker here, but it doesn't trigger the getter/computed in colTemplate etc.
55 | let difference = oldVal - newVal;
56 | for (let i = 1; i <= difference; i++) {
57 | state[payload.direction].pop();
58 | }
59 | } else {
60 | let difference = newVal - oldVal;
61 | for (let i = 1; i <= difference; i++) {
62 | state[payload.direction].push({ unit: "1fr" });
63 | }
64 | }
65 | },
66 | addChildren(state, payload) {
67 | state.childarea.push(payload);
68 | },
69 | removeChildren(state, payload) {
70 | state.childarea.splice(payload, 1);
71 | },
72 | updateAuto(state, payload) {
73 | state.auto = payload;
74 | },
75 | updateMin(state, payload) {
76 | state.min = payload;
77 | },
78 | updateColumns(state, payload) {
79 | state.columns = payload;
80 | },
81 | updateRows(state, payload) {
82 | state.rows = payload;
83 | },
84 | updateColumnGap(state, payload) {
85 | state.columngap = payload;
86 | },
87 | updateRowGap(state, payload) {
88 | state.rowgap = payload;
89 | },
90 | resetGrid(state, payload) {
91 | state.childarea = [];
92 | }
93 | }
94 | }
95 | };
96 |
97 | //we start off with just a few rows and columns filled with 1fr units
98 | const createArr = (direction, arr) => {
99 | for (let i = 1; i <= direction; i++) {
100 | arr.push({ unit: "1fr" });
101 | }
102 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Grapesjs Plugin Toolbox
2 |
3 | Tools for `grapesjs`
4 |
5 | [DEMO](https://codepen.io/ju99ernaut/pen/Vwaqdpw)
6 |
7 | ### HTML
8 | ```html
9 |
10 |
11 |
12 |
13 |
14 |
15 | ```
16 |
17 | ### JS
18 | ```js
19 | const editor = grapesjs.init({
20 | container: '#gjs',
21 | height: '100%',
22 | fromElement: true,
23 | storageManager: false,
24 | plugins: ['grapesjs-plugin-toolbox'],
25 | });
26 | ```
27 |
28 | ### CSS
29 | ```css
30 | body, html {
31 | margin: 0;
32 | height: 100%;
33 | }
34 | ```
35 |
36 |
37 | ## Summary
38 |
39 | * Tool in this plugin are
40 | * [Css grid layout tool](https://i.imgur.com/jwXI0MS.mp4)
41 | * [Canvas resizer](https://i.imgur.com/GWBJsmI.mp4)
42 | * [Palette from image generator](https://i.imgur.com/eeIqyHO.png)
43 | * Breadcrumbs
44 | * Layer icon mapper
45 |
46 | * Plugin name: `grapesjs-plugin-toolbox`
47 | * Components
48 | * `css-grid`
49 | * `grid-cell`
50 | * Blocks
51 | * `css-grid`
52 | * Commands
53 | * `add-palette`
54 |
55 | ## Grid Component
56 |
57 | >Here `template guides` referes to the dotted lines and colored template areas whereas `grid cells` referes to the actual divs generated from the `template guide`.
58 |
59 | If the grid component is not empty the update button will only update the css to avoid overwriting any content. Check [here](https://i.imgur.com/jwXI0MS.mp4) to see usage example.
60 |
61 | * Traits
62 | * `auto-fill` - generate grid automatically(disables guides)
63 | * `min` - min cell width in px if auto-fill is used
64 | * `columns`
65 | * `rows`
66 | * `columngap`
67 | * `rowgap`
68 | * `toggle` - show/hide template guides if not auto-fill(same function as toolbar icon)
69 | * `reset` - reset colored template areas used to generate grid
70 | * `clear` - remove grid cells
71 | * `update` - generates grid cells or updates css if grid component isn't empty
72 |
73 | ## Options
74 |
75 | | Option | Description | Default |
76 | |-|-|-
77 | | `panels` | use plugin panels | `false` |'
78 | | `traitsInSm` | move traits/settings to styles panel | `true` |
79 | | `resizer` | include canvas resizer | `true` |
80 | | `hideOnZoom` | hide resizer if zoom is not 100 | `true` |
81 | | `breadcrumbs` | include breadcrumbs | `true` |
82 | | `labelGrid` | label for grid block | `Grid` |
83 | | `categoryGrid` | category for grid block | `Basic` |
84 | | `gridBlock` | options to extend grid block | `{}` |
85 | | `gridComponent` | options to extend grid component model | `{}` |
86 | | `gridClass` | class for grid block | `grid` |
87 | | `gridCellClass` | class for grid cell block | `grid-cell` |
88 | | `cellItemClass` | class for cell item block | `cell-item` |
89 | | `labelColors` | label for color palette modal | `Image palette` |
90 | | `labelApply` | label for apply button | `Add` |
91 | | `palleteIcon` | toolbar icon to open palette modal | ` ` |
92 | | `onAdd` | custom logic when palette is added | `0` |
93 | | `refreshPalette` | color pickers to refresh color palettes | `[{sector: 'typography',name: 'Color',property: 'color',type: 'color',defaults: 'black'},...]` |
94 | | `minScreenSize` | minimum value the screen can be resized | `250` |
95 | | `icons` | icons to map to components | `[{type: 'default', icon:' '},...]` |
96 |
97 |
98 | ## Download
99 |
100 | * CDN
101 | * `https://unpkg.com/grapesjs-plugin-toolbox`
102 | * NPM
103 | * `npm i grapesjs-plugin-toolbox`
104 | * GIT
105 | * `git clone https://github.com/Ju99ernaut/grapesjs-plugin-toolbox.git`
106 |
107 |
108 |
109 | ## Usage
110 |
111 | Directly in the browser
112 | ```html
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
130 | ```
131 |
132 | Modern javascript
133 | ```js
134 | import grapesjs from 'grapesjs';
135 | import plugin from 'grapesjs-plugin-toolbox';
136 | import 'grapesjs/dist/css/grapes.min.css';
137 | import 'grapesjs-plugin-toolbox/dist/grapesjs-plugin-toolbox.min.css';
138 |
139 | const editor = grapesjs.init({
140 | container : '#gjs',
141 | // ...
142 | plugins: [plugin],
143 | pluginsOpts: {
144 | [plugin]: { /* options */ }
145 | }
146 | // or
147 | plugins: [
148 | editor => plugin(editor, { /* options */ }),
149 | ],
150 | });
151 | ```
152 |
153 |
154 |
155 | ## Development
156 |
157 | Clone the repository
158 |
159 | ```sh
160 | $ git clone https://github.com/Ju99ernaut/grapesjs-plugin-toolbox.git
161 | $ cd grapesjs-plugin-toolbox
162 | ```
163 |
164 | Install dependencies
165 |
166 | ```sh
167 | $ npm i
168 | ```
169 |
170 | Build css
171 |
172 | ```sh
173 | $ npm run build:css
174 | ```
175 |
176 | Start the dev server
177 |
178 | ```sh
179 | $ npm start
180 | ```
181 |
182 | Build the source
183 |
184 | ```sh
185 | $ npm run build
186 | ```
187 |
188 |
189 |
190 | ## License
191 |
192 | MIT
193 |
--------------------------------------------------------------------------------
/src/utils/palette.js:
--------------------------------------------------------------------------------
1 | import ColorThief from '../../node_modules/colorthief/dist/color-thief.mjs';
2 |
3 | export default (editor, opts = {}) => {
4 | const colorthief = new ColorThief();
5 | const { colorsNum, commandId, labelColors, labelApply, paletteIcon, onAdd } = opts;
6 | const $ = editor.$;
7 | const pfx = editor.Config.stylePrefix;
8 |
9 | // Update image component toolbar
10 | const domc = editor.DomComponents;
11 | const typeImage = domc.getType('image').model;
12 | domc.addType('image', {
13 | model: {
14 | initToolbar() {
15 | typeImage.prototype.initToolbar.apply(this, arguments);
16 | const tb = this.get('toolbar');
17 | const tbExists = tb.some(item => item.command === commandId);
18 |
19 | if (!tbExists) {
20 | tb.unshift({ command: commandId, label: paletteIcon });
21 | this.set('toolbar', tb);
22 | }
23 | }
24 | }
25 | });
26 |
27 | // Add the palette add command
28 | editor.Commands.add(commandId, {
29 | run(ed, s, options = {}) {
30 | const { id } = this;
31 |
32 | this.editor = ed;
33 | this.target = options.target || ed.getSelected();
34 | const colorArr = this.getDominantColor(this.target.getEl());
35 | this.color = `rgb(${colorArr[0]},${colorArr[1]},${colorArr[2]})`;
36 | const paletteArr = this.getPaletteArray(this.target.getEl());
37 | this.palette = this.generateColorsFromArray(paletteArr);
38 | const content = this.createContent(this.color, this.palette);
39 | const title = labelColors;
40 | const btn = content.children[1];
41 | ed.Modal.open({ title, content })
42 | .getModel().once('change:open', () => ed.stopCommand(id));
43 | btn.onclick = () => this.addPalette();
44 | opts.addPalette(btn);
45 | },
46 |
47 | stop(ed) {
48 | //clear modal
49 | ed.Modal.close();
50 | },
51 |
52 | createContent(color, palette) {
53 | const content = $('
');
54 | const colorOutput = `
55 |
56 |
57 |
63 |
64 |
Palette
65 |
66 |
67 | ${palette.map(col => `
`).join('')}
68 |
69 |
70 |
71 |
72 |
`
73 | content.html(`
74 | ${colorOutput}
75 |
86 | ${labelApply}
87 |
88 | `);
89 |
90 | return content.get(0);
91 | },
92 |
93 | addPalette() {
94 | const { target, editor } = this;
95 | const sm = editor.StyleManager;
96 |
97 | !editor.Config.colorPicker && (editor.Config.colorPicker = {
98 | palette: []
99 | });
100 | let pickerPalette = editor.Config.colorPicker.palette;
101 |
102 | if (onAdd) {
103 | onAdd(target);
104 | } else {
105 | //add to colorpicker palette
106 | !(pickerPalette && pickerPalette.push(this.palette));
107 | opts.refreshPalette.forEach(input => this.refreshPickerPalette(sm, input));
108 | editor.stopCommand(commandId);
109 | }
110 | },
111 |
112 | /**
113 | * Refresh an input field i.e color so that the palette updates
114 | * @param {Object} sm StyleManager reference
115 | * @param {Object} input Info about input to refresh
116 | * @returns void
117 | */
118 | refreshPickerPalette(sm, input) {
119 | const coll = sm.getProperties(input.sector).models;
120 | const at = coll.indexOf(sm.getProperty(input.sector, input.property));
121 | sm.removeProperty(input.sector, input.property)
122 | sm.addProperty(input.sector, input, { at });
123 | },
124 |
125 | /**
126 | * Return rgb values for dominant color
127 | * @param {HTMLElement} img image element
128 | * @returns {Array} [Number, Number, Number]
129 | * @private
130 | */
131 | getDominantColor(img) {
132 | try {
133 | return colorthief.getColor(img);
134 | } catch {
135 | return [255, 255, 255];
136 | }
137 | },
138 |
139 | /**
140 | * Returns array rgb values for color palette
141 | * @param {HTMLElement} img image element
142 | * @returns {Array} [[Number, Number, Number], ...]
143 | * @private
144 | */
145 | getPaletteArray(img) {
146 | try {
147 | return colorthief.getPalette(img, colorsNum);
148 | } catch {
149 | return [[0, 0, 0], [255, 255, 255]];
150 | }
151 | },
152 |
153 | /**
154 | * convert palette values to colors
155 | * @param {Array} arr [[Number, Number, Number], ...]
156 | * @returns {Array} ['rgb(Number,Number,Number)', ...]
157 | * @private
158 | */
159 | generateColorsFromArray(arr) {
160 | arr = arr || [[0, 0, 0], [255, 255, 255]];
161 | const cols = [];
162 | arr.forEach(col => cols.push(`rgb(${col[0]},${col[1]},${col[2]})`));
163 | return cols;
164 | }
165 | });
166 | }
--------------------------------------------------------------------------------
/src/utils/resizer.js:
--------------------------------------------------------------------------------
1 | // Adated from https://github.com/artf/grapesjs/issues/1368
2 |
3 | export default (editor, opts = {}) => {
4 | let frame;
5 | let canvas;
6 | const { $ } = editor;
7 | const deviceManager = editor.Devices;
8 | const hints = $(``);
16 |
17 | editor.Commands.add('smoothresize', () => { });
18 |
19 | editor.on('change:device', () => {
20 | if (!frame) frame = $('.gjs-frame-wrapper');
21 | frame.css({
22 | 'width': '',
23 | 'transition': 'width 0.35s ease,height 0.35s ease'
24 | });
25 | hints.find('.dim-indicator').css('display', 'none');
26 | hints.css('display', 'none');
27 | setTimeout(() => {
28 | const device = deviceManager.get(editor.getDevice());
29 | showDeviceResolution(device);
30 | initDeviceEventHandle(device);
31 | getWindowDims();
32 | hints.css('display', 'block')
33 | frame.css('transition', 'none');
34 | }, 800);
35 | });
36 |
37 | editor.on('run:preview', () => hints.css('display', 'none'));
38 | editor.on('stop:preview', () => hints.css('display', 'block'));
39 | editor.Canvas.model.on('change:zoom', () => {
40 | if (opts.hideOnZoom) {
41 | if (editor.Canvas.getZoom() === 100) hints.css('display', 'block');
42 | else hints.css('display', 'none');
43 | }
44 | });
45 |
46 | /**
47 | * This function will receive a screen type and it will prompt the description on the left side of the canvas.
48 | *
49 | * @param device
50 | */
51 | const showDeviceResolution = (device) => {
52 | const glutterResize = hints;
53 |
54 | if (glutterResize.length > 0) {
55 | glutterResize.addClass('hidden');
56 | } else {
57 | // If the div is not created yet inside the iframe then we include it.
58 | if (!frame) frame = $('.gjs-frame-wrapper');
59 | const copyGlutterResize = hints.clone(true, true);
60 | frame.before(copyGlutterResize);
61 | }
62 | // We force to refresh the screen because then we will update all dimensions
63 | setTimeout(function () {
64 | $(window).trigger('resize');
65 | }, 600);
66 | };
67 |
68 |
69 | /**
70 | * This function initializes device glutter handle
71 | *
72 | */
73 | const initDeviceEventHandle = (device) => {
74 |
75 | try {
76 | const maxDeviceSize = parseInt(device.get('widthMedia'), 10);
77 |
78 | let widthIframe = 0; // Current iframe Width
79 | let maxLeftPos = 0;
80 |
81 | draggable(hints.find('.right-handle').get(0), {
82 | axis: 'x',
83 | max: maxDeviceSize,
84 | min: opts.minScreenSize,
85 | start() {
86 | widthIframe = editor.Canvas.getFrameEl().offsetWidth;
87 | },
88 | drag(uleft) {
89 | try {
90 | if (!canvas) canvas = $('.gjs-cv-canvas');
91 | if (canvas.find('.handle-mask').length === 0) {
92 | // We need to create a mask to avoid in the moment that we are dragging to move the pointer over the iframe and losing then the control of the resizing.
93 | canvas.append('
');
94 | }
95 |
96 | // We need to change the iframe width dynamically
97 | const total = uleft - editor.Canvas.getOffset().left - widthIframe;
98 | let width = widthIframe + total * opts.dragDampen;
99 | let left = editor.Canvas.getOffset().left;
100 | let res = true;
101 |
102 | if (width > maxDeviceSize || width < opts.minScreenSize) {
103 | uleft = maxLeftPos;
104 | res = false;
105 | } else {
106 | // Set the iframe width
107 | maxLeftPos = uleft;
108 | if (!frame) frame = $('.gjs-frame-wrapper');
109 | frame.css('width', width);
110 |
111 | // Set the position left of the left handle
112 | hints.find('.left-handle').css('left', left);
113 |
114 | let leftDesc = left - 162; // 162 = the right panel width
115 | hints.find('.device-resolution').css('left', leftDesc);
116 | hints.find('.dim-indicator').html('Screen size ' + Math.round(width) + 'px');
117 | hints.find('.dim-indicator').css('display', 'block');
118 | }
119 | // After dragging we need to refresh the editor to re-calculate the highlight border in the element selected.
120 | editor.refresh();
121 | return res
122 | } catch (err) {
123 | console.error(err);
124 | return false;
125 | }
126 | },
127 | stop() {
128 | try {
129 | $('.handle-mask').remove();
130 | editor.runCommand('smoothresize');
131 | } catch (err) {
132 | console.error(err);
133 | }
134 | }
135 | });
136 | } catch (err) {
137 | console.error(err);
138 | }
139 | };
140 |
141 | editor.on('load', function () {
142 | // Control that the screen size is not too small
143 | $('.gjs-cv-canvas').append(hints);
144 | getWindowDims();
145 | });
146 |
147 | /**
148 | * Function to determine Viewport Size
149 | */
150 | const getWindowDims = () => {
151 | const doc = document,
152 | w = window;
153 | const docEl = (doc.compatMode && doc.compatMode === 'CSS1Compat') ?
154 | doc.documentElement : doc.body;
155 |
156 | let width = docEl.clientWidth;
157 | let height = docEl.clientHeight;
158 |
159 | // mobile zoomed in?
160 | if (w.innerWidth && width > w.innerWidth) {
161 | width = w.innerWidth;
162 | height = w.innerHeight;
163 | }
164 |
165 | // IMPORTANT!!!!
166 | // Glutter Handle information
167 | const glutterHandleObj = hints;
168 | if (glutterHandleObj.length > 0) {
169 |
170 | const leftGlutterHandleBar = glutterHandleObj.find('.left-handle');
171 | const rightGlutterHandleBar = glutterHandleObj.find('.right-handle');
172 |
173 | const leftOffset = editor.Canvas.getOffset().left;
174 | const rightOffset = editor.Canvas.getOffset().left + editor.Canvas.getFrameEl().offsetWidth;
175 |
176 | leftGlutterHandleBar.css('cssText', 'left: ' + leftOffset + 'px !important');
177 | rightGlutterHandleBar.css('cssText', 'left: ' + rightOffset + 'px !important');
178 | glutterHandleObj.removeClass('hidden');
179 | }
180 |
181 | return { width, height };
182 | };
183 | }
184 |
185 | const draggable = (element, opts = {}) => {
186 | let pos1 = 0,
187 | pos2 = 0,
188 | pos3 = 0,
189 | pos4 = 0,
190 | push = 5,
191 | falseCountX = 0,
192 | falseCountY = 0;
193 |
194 | element.addEventListener('mousedown', e => {
195 | e = e || window.event;
196 | e.preventDefault();
197 |
198 | pos3 = e.clientX;
199 | pos4 = e.clientY;
200 | document.onmouseup = closeDragElement;
201 | document.onmousemove = elementDrag;
202 | opts.start();
203 | });
204 |
205 | const elementDrag = (e) => {
206 | e = e || window.event;
207 | e.preventDefault();
208 |
209 | pos1 = pos3 - e.clientX;
210 | pos2 = pos4 - e.clientY;
211 | pos3 = e.clientX;
212 | pos4 = e.clientY;
213 | if (opts.axis === 'x' || opts.axis === 'xy') {
214 | element.style.left = (element.offsetLeft - pos1) + 'px';
215 | if (!opts.drag(element.offsetLeft)) {
216 | if (falseCountX) {
217 | element.style.left = (element.offsetLeft + pos1) + Math.sign(pos1) * push + 'px';
218 | falseCountX = 0;
219 | } else {
220 | element.style.left = (element.offsetLeft + pos1) + 'px'
221 | falseCountX++;
222 | }
223 | };
224 | }
225 | if (opts.axis === 'y' || opts.axis === 'xy') {
226 | element.style.top = (element.offsetTop - pos2) + 'px';
227 | if (!opts.drag(element.offsetTop)) {
228 | if (falseCountY) {
229 | element.style.top = (element.offsetTop + pos2) + Math.sign(pos2) * push + 'px';
230 | falseCountY = 0
231 | } else {
232 | element.style.top = (element.offsetTop + pos2) + 'px';
233 | falseCountY++;
234 | }
235 | };
236 | }
237 | }
238 |
239 | const closeDragElement = () => {
240 | document.onmouseup = null;
241 | document.onmousemove = null;
242 | opts.stop();
243 | }
244 | }
--------------------------------------------------------------------------------
/src/grid.js:
--------------------------------------------------------------------------------
1 | // Based on https://github.com/sdras/cssgridgenerator
2 |
3 | export default (editor, opts = {}) => {
4 | const { $ } = editor;
5 | const pfx = editor.Config.stylePrefix;
6 |
7 | return {
8 | gridEl(store) {
9 | const { width, height, top } = this.dimensions();
10 | const el = $(``);
14 | const gridcontainer = el.find(`#${pfx}gridcontainer`);
15 | el.prepend(this.gridColUnits(store, width, height, top));
16 | el.prepend(this.gridRowUnits(store));
17 | gridcontainer.append(this.gridCanvas(store));
18 | gridcontainer.append(this.gridCanvasChildren(store));
19 | return el;
20 | },
21 | gridColUnits(store, width, height, top) {
22 | const colunits = $(``);
37 |
38 | const inputscol = colunits.find('input');
39 | inputscol.on('change', e => this.validateunit(e));
40 |
41 | return colunits
42 | },
43 | gridRowUnits(store) {
44 | const rowunits = $(``);
59 |
60 | const inputsrow = rowunits.find('input');
61 | inputsrow.on('change', e => this.validateunit(e));
62 |
63 | return rowunits;
64 | },
65 | gridCanvas(store) {
66 | const gridsection = $(`
71 | ${Array(store.getters.divNum(store.state)).fill().map((item, i) => `
75 |
`).join("")}
76 | `);
77 | gridsection.on('ontouchstart', e => {
78 | e.preventDefault();
79 | this.delegatedTouchPlaceChild(e);
80 | });
81 | gridsection.on('ontouchend', e => {
82 | e.preventDefault();
83 | this.delegatedTouchPlaceChild(e);
84 | });
85 | const place = gridsection.find('div');
86 | place.on('mousedown', e => this.placeChild(e, 's'));
87 | place.on('mouseup', e => this.placeChild(e, 'e'));
88 |
89 | return gridsection;
90 | },
91 | gridCanvasChildren(store) {
92 | const gridchild = $(`
97 | ${store.state.childarea.map((child, i) => `
101 | ×
102 |
`).join("")}
103 | `);
104 | const del = gridchild.find('button');
105 | del.on('click', e => this.removeChild(e));
106 |
107 | return gridchild;
108 | },
109 | render(cont, store) {
110 | if (!this.container?.length) {
111 | this.el = this.gridEl(store);
112 | this.container = $(cont);
113 | this.container.append(this.el);
114 | editor.on('styleManager:change:height run:smoothresize run:resize', () => {
115 | const st = editor.getSelected()?.get('store');
116 | st && editor.Grid.visible && editor.Grid.update(st);
117 | });
118 | }
119 | this.rendered = true;
120 | },
121 | getEl() {
122 | return this.el.get(0);
123 | },
124 | select(selected) {
125 | this.selected = selected;
126 | },
127 | update(store) {
128 | this.el = this.gridEl(store);
129 | $(`#${pfx}grid-main`).replaceWith(this.el);
130 | },
131 | updateRows(store) {
132 | $(`.${pfx}rowunits`).replaceWith(this.gridRowUnits(store));
133 | },
134 | updateCols(store) {
135 | const { width, height, top } = this.dimensions();
136 | $(`.${pfx}colunits`).replaceWith(this.gridRowUnits(store, width, height, top));
137 | },
138 | updateCanvas(store) {
139 | $(`.${pfx}gridcanvas`).replaceWith(this.gridCanvas(store));
140 | },
141 | updateChildren(store) {
142 | $(`.${pfx}gridchild`).replaceWith(this.gridCanvasChildren(store));
143 | },
144 | child: {},
145 | widthfull: `${pfx}widthfull`,
146 | widthhalf: `${pfx}widthhalf`,
147 | errors: { col: [], row: [] },
148 | visible: false,
149 | rendered: false,
150 | dimensions() {
151 | return (this.selected && editor.Canvas.getElementPos(this.selected.getEl())) || {
152 | width: 100,
153 | height: 100
154 | }
155 | },
156 | validateunit(e) {
157 | const unit = e.target.value;
158 | const i = e.target.getAttribute('data-key');
159 | const direction = e.target.getAttribute('data-direction');
160 | const check =
161 | /fr$/.test(unit) ||
162 | /px$/.test(unit) ||
163 | /%$/.test(unit) ||
164 | /em$/.test(unit) ||
165 | /rem$/.test(unit) ||
166 | /vw$/.test(unit) ||
167 | /vh$/.test(unit) ||
168 | /vmin$/.test(unit) ||
169 | /q$/.test(unit) ||
170 | /mm$/.test(unit) ||
171 | /cm$/.test(unit) ||
172 | /in$/.test(unit) ||
173 | /pt$/.test(unit) ||
174 | /pc$/.test(unit) ||
175 | /ex$/.test(unit) ||
176 | /ch$/.test(unit) ||
177 | /minmax/.test(unit) || ["auto", "min-content", "max-content"].includes(unit) ||
178 | parseInt(unit, 10) === 0; // allow 0 as a valid value without a unit
179 | if (!check) {
180 | this.errors[direction].push(i);
181 | } else {
182 | const store = this.selected.get('store');
183 | this.errors[direction].splice(this.errors[direction].indexOf(i), 1);
184 | if (direction === 'col') {
185 | store.state.colArr[i].unit = unit;
186 | this.selected.addStyle({
187 | 'grid-template-columns': store.getters.colTemplate(store.state)
188 | });
189 | } else {
190 | store.state.rowArr[i].unit = unit;
191 | this.selected.addStyle({
192 | 'grid-template-rows': store.getters.rowTemplate(store.state)
193 | });
194 | }
195 | this.update(store);
196 | }
197 | },
198 | delegatedTouchPlaceChild(ev) {
199 | const target = document.elementFromPoint(
200 | ev.changedTouches[0].clientX,
201 | ev.changedTouches[0].clientY
202 | );
203 | const startend = ev.type === "touchstart" ? "s" : "e";
204 | this.placeChild(target.dataset.id, startend);
205 | },
206 | placeChild(ev, startend) {
207 | const store = this.selected.get('store');
208 | const item = parseInt(ev.target.getAttribute('data-key')) + 1;
209 | //built an object first because I might use this for something else
210 | this.child[`${startend}row`] = Math.ceil(item / store.state.columns);
211 | this.child[`${startend}col`] =
212 | item - (this.child[`${startend}row`] - 1) * store.state.columns;
213 | //create the children css units as a string
214 | if (startend === "e") {
215 | // flip starts and ends if dragged in the opposite direction
216 | let [startRow, endRow] =
217 | this.child.srow <= this.child.erow ? [this.child.srow, this.child.erow] : [this.child.erow, this.child.srow];
218 | let [startCol, endCol] =
219 | this.child.scol <= this.child.ecol ? [this.child.scol, this.child.ecol] : [this.child.ecol, this.child.scol];
220 | let childstring = `${startRow} / ${startCol} / ${endRow +
221 | 1} / ${endCol + 1}`;
222 | store.mutations.addChildren(store.state, childstring);
223 | }
224 | this.updateChildren(store);
225 | },
226 | removeChild(ev) {
227 | const store = this.selected.get('store');
228 | const index = ev.target.getAttribute('data-key');
229 | store.mutations.removeChildren(store.state, index);
230 | this.updateChildren(store);
231 | }
232 | }
233 | };
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import loadComponents from './components';
2 | import loadBlocks from './blocks';
3 | import loadPanels from './panels';
4 | import grid from './grid';
5 | import { breadcrumbs, palette, resizer } from './utils';
6 |
7 | export default (editor, opts = {}) => {
8 | const options = {
9 | ...{
10 | // use plugin panels
11 | panels: 0,
12 | // show canvas resizer
13 | resizer: 1,
14 | // show breadcrumbs
15 | breadcrumbs: 1,
16 | // label for grid block
17 | labelGrid: 'Grid',
18 | // category for grid block
19 | categoryGrid: 'Basic',
20 | // options to extend grid block
21 | gridBlock: {},
22 | // options to extend grid component model
23 | gridComponent: {},
24 | // options to extend cell component model
25 | cellComponent: {},
26 | // grid class name
27 | gridClass: 'grid',
28 | // grid cell class name
29 | gridCellClass: 'grid-cell',
30 | // cell item class name
31 | cellItemClass: 'cell-item',
32 | // add palette
33 | palette: 1,
34 | // add palette command
35 | commandId: 'add-palette',
36 | // add palette modal label
37 | labelColors: 'Image palette',
38 | // add palette button text
39 | labelApply: 'Add',
40 | // add palette toolbar icon
41 | paletteIcon: ' ',
42 | // custom onAdd
43 | // onAdd: (color, palette) => {
44 | // Custom stuff
45 | // }
46 | onAdd: 0,
47 | // The add button (HTMLElement) will be passed as an argument to this function, once created.
48 | // This will allow you a higher customization.
49 | addPalette: () => { },
50 | // Number of colors to generate for pallete
51 | colorsNum: 9,
52 | // Refresh inputs on palette update
53 | refreshPalette: [{
54 | sector: 'typography',
55 | name: 'Color',
56 | property: 'color',
57 | type: 'color',
58 | defaults: 'black'
59 | }, {
60 | sector: 'decorations',
61 | name: 'Background color',
62 | property: 'background-color',
63 | type: 'color'
64 | }],
65 | // Minimum value the screen can be resized
66 | minScreenSize: 250,
67 | // Dampen the drag speed
68 | dragDampen: 1,
69 | // Hide if zoom is not 100
70 | hideOnZoom: 1,
71 | // Add settings to style manager
72 | traitsInSm: 1,
73 | // Wrapper icon
74 | wrapperIcon: 0,
75 | // Icons Map
76 | icons: [],
77 | defIcons: [{
78 | type: 'body',
79 | icon: ' '
80 | },
81 | {
82 | type: 'wrapper',
83 | icon: ' '
84 | },
85 | {
86 | type: 'custom-code',
87 | icon: ' '
88 | },
89 | {
90 | type: 'script',
91 | icon: ' '
92 | },
93 | {
94 | type: 'comment',
95 | icon: ''
96 | },
97 | {
98 | type: 'text',
99 | icon: ' '
100 | },
101 | {
102 | type: 'textnode',
103 | icon: ' '
104 | },
105 | {
106 | type: 'header',
107 | icon: ''
108 | },
109 | {
110 | type: 'box',
111 | icon: ' '
112 | },
113 | {
114 | type: 'section',
115 | icon: ' '
116 | },
117 | {
118 | type: 'link',
119 | icon: ' '
120 | },
121 | {
122 | type: 'footer',
123 | icon: ' '
124 | },
125 | {
126 | type: 'input',
127 | icon: ' '
128 | },
129 | {
130 | type: 'button',
131 | icon: ' '
132 | },
133 | {
134 | type: 'image',
135 | icon: ' '
136 | },
137 | {
138 | type: 'video',
139 | icon: ' '
140 | },
141 | {
142 | type: 'row',
143 | icon: ' '
144 | },
145 | {
146 | type: 'cell',
147 | icon: ' '
148 | },
149 | {
150 | type: 'table',
151 | icon: ' '
152 | },
153 | {
154 | type: 'thead',
155 | icon: ''
156 | },
157 | {
158 | type: 'tbody',
159 | icon: ' '
160 | },
161 | {
162 | type: 'tfoot',
163 | icon: ' '
164 | },
165 | {
166 | type: 'column',
167 | icon: ' '
168 | },
169 | {
170 | type: 'map',
171 | icon: ' '
172 | },
173 | {
174 | type: 'label',
175 | icon: ' '
176 | },
177 | {
178 | type: 'checkbox',
179 | icon: ' '
180 | },
181 | {
182 | type: 'textarea',
183 | icon: ' '
184 | },
185 | {
186 | type: 'select',
187 | icon: ' '
188 | },
189 | {
190 | type: 'radio',
191 | icon: ' '
192 | },
193 | {
194 | type: 'form',
195 | icon: ' '
196 | },
197 | {
198 | type: 'svg',
199 | icon: ' '
200 | },
201 | {
202 | type: 'svg-in',
203 | icon: ' '
204 | },
205 | {
206 | type: 'nav',
207 | icon: ' '
208 | },
209 | {
210 | type: 'navbar',
211 | icon: ' '
212 | },
213 | {
214 | type: 'navbar-container',
215 | icon: ' '
216 | },
217 | {
218 | type: 'navbar-menu',
219 | icon: ' '
220 | },
221 | {
222 | type: 'burger-menu',
223 | icon: ' '
224 | },
225 | {
226 | type: 'burger-line',
227 | icon: ' '
228 | },
229 | {
230 | type: 'span',
231 | icon: ' '
232 | },
233 | {
234 | type: 'countdown',
235 | icon: ' '
236 | },
237 | {
238 | type: 'twitch',
239 | icon: ' '
240 | },
241 | {
242 | type: 'tooltip',
243 | icon: ''
244 | },
245 | {
246 | type: 'tabs',
247 | icon: ' '
248 | },
249 | {
250 | type: 'tab',
251 | icon: ' '
252 | },
253 | {
254 | type: 'tab-container',
255 | icon: ' '
256 | },
257 | {
258 | type: 'tab-content',
259 | icon: ' '
260 | },
261 | {
262 | type: 'lory-slider',
263 | icon: ' '
264 | },
265 | {
266 | type: 'lory-frame',
267 | icon: ' '
268 | },
269 | {
270 | type: 'lory-slides',
271 | icon: ' '
272 | },
273 | {
274 | type: 'lory-slide',
275 | icon: ' '
276 | },
277 | {
278 | type: 'lory-prev',
279 | icon: ' '
280 | },
281 | {
282 | type: 'lory-next',
283 | icon: ' '
284 | },
285 | {
286 | type: 'typed',
287 | icon: ' '
288 | },
289 | {
290 | type: 'default',
291 | icon: ' '
292 | }
293 | ]
294 | },
295 | ...opts
296 | };
297 |
298 | // Load grid
299 | editor.Grid = grid(editor, options);
300 | // Add components
301 | loadComponents(editor, options);
302 | // Add blocks
303 | loadBlocks(editor, options);
304 | // Add panels
305 | options.panels && loadPanels(editor, options);
306 | // Add breadcrumbs
307 | options.breadcrumbs && breadcrumbs(editor, options);
308 | // Add palette
309 | options.palette && palette(editor, options);
310 | // Load resizer
311 | options.resizer && resizer(editor, options);
312 |
313 | editor.on('load', () => {
314 | const $ = editor.$;
315 | const pn = editor.Panels;
316 | const pfx = editor.Config.stylePrefix;
317 | const cmp = editor.Components;
318 |
319 | //? Map layer icons to components
320 | options.defIcons.forEach(icon => {
321 | try {
322 | cmp.getType(icon.type).model.prototype.defaults.icon = icon.icon;
323 | } catch (error) { }
324 | })
325 | options.icons.forEach(icon => {
326 | try {
327 | cmp.getType(icon.type).model.prototype.defaults.icon = icon.icon;
328 | } catch (error) { }
329 | });
330 |
331 | // Add Settings Sector
332 | if (options.traitsInSm) {
333 | // Load and show settings and style manager
334 | const openTmBtn = pn.getButton('views', 'open-tm');
335 | openTmBtn && openTmBtn.set('active', 1);
336 | const openSm = pn.getButton('views', 'open-sm');
337 | openSm && openSm.set('active', 1);
338 |
339 | const traitsSector = $(``);
340 | const traitsProps = traitsSector.find(`.${pfx}sm-properties`);
341 | traitsProps.append($(`.${pfx}trt-traits`));
342 | $(`.${pfx}sm-sectors`).before(traitsSector);
343 | traitsSector.find(`.${pfx}sm-title`).on('click', function () {
344 | let traitStyle = traitsProps.get(0).style;
345 | let hidden = traitStyle.display == 'none';
346 | if (hidden) {
347 | traitStyle.display = 'block';
348 | } else {
349 | traitStyle.display = 'none';
350 | }
351 | });
352 | pn.removeButton('views', 'open-tm');
353 | $(`.${pfx}pn-views .${pfx}pn-btn`).css('width', `${100 / pn.getPanel('views').buttons.length}%`);
354 | }
355 |
356 | // Body icon
357 | if (options.wrapperIcon) {
358 | const openLm = pn.getButton('views', 'open-layers');
359 | openLm && openLm.set('active', 1);
360 | $(`.${pfx}layer-name`)[0].innerHTML = ' Body';
361 | openSm && openSm.set('active', 1);
362 | }
363 | });
364 | };
--------------------------------------------------------------------------------
/src/components.js:
--------------------------------------------------------------------------------
1 | import { gridCompId, gridChildId } from './consts';
2 | import { store } from './utils';
3 |
4 | export default (editor, opts = {}) => {
5 | const domc = editor.DomComponents;
6 | const pfx = editor.Config.stylePrefix;
7 | const {
8 | gridComponent,
9 | cellComponent,
10 | gridClass,
11 | gridCellClass,
12 | cellItemClass
13 | } = opts;
14 |
15 | const idTrait = { name: 'id', label: 'Id' };
16 |
17 | const titleTrait = { name: 'title', label: 'Title' };
18 |
19 | const privateCls = [`.${gridClass}`, `.${cellItemClass}`, `.${gridCellClass}`];
20 | editor.on(
21 | 'selector:add',
22 | selector =>
23 | privateCls.indexOf(selector.getFullName()) >= 0 &&
24 | selector.set('private', 1)
25 | );
26 |
27 | const showGrid = (ed) => {
28 | if (!ed.getSelected().get('auto')) {
29 | const st = ed.Grid.selected.get('store');
30 | if (!editor.Grid.container?.length) editor.Grid.render(`#${pfx}tools`, st);
31 | ed.Grid.visible = !ed.Grid.visible;
32 | ed.Grid.update(st);
33 | }
34 | }
35 |
36 | const addCellTrait = {
37 | name: 'addCell',
38 | type: 'button',
39 | full: true,
40 | text: 'Add Cell',
41 | command: editor => {
42 | const comp = editor.getSelected();
43 | comp && comp.components().add(`
`);
58 | const cc = editor.Css;
59 | cc.getRule(`.${cellItemClass}`) || cc.setRule(`.${cellItemClass}`, {
60 | 'min-height': '75px',
61 | 'flex-grow': 1,
62 | 'flex-basis': '100%'
63 | });
64 | }
65 | }
66 |
67 | const autoTrait = {
68 | name: 'auto',
69 | label: 'Auto Fill',
70 | type: 'checkbox',
71 | changeProp: 1,
72 | }
73 |
74 | const columnsTrait = {
75 | name: 'columns',
76 | label: 'Columns',
77 | type: 'number',
78 | changeProp: 1,
79 | placeholder: '6',
80 | min: 1,
81 | }
82 |
83 | const rowsTrait = {
84 | name: 'rows',
85 | label: 'Rows',
86 | type: 'number',
87 | changeProp: 1,
88 | placeholder: '6',
89 | min: 1,
90 | }
91 |
92 | const columnGapTrait = {
93 | name: 'columngap',
94 | label: 'Column Gap(px)',
95 | type: 'number',
96 | changeProp: 1,
97 | placeholder: '0',
98 | min: 0,
99 | }
100 |
101 | const rowGapTrait = {
102 | name: 'rowgap',
103 | label: 'Row Gap(px)',
104 | type: 'number',
105 | changeProp: 1,
106 | placeholder: '0',
107 | min: 0,
108 | }
109 |
110 | const minTrait = {
111 | name: 'min',
112 | label: 'Min(px)',
113 | type: 'number',
114 | changeProp: 1,
115 | placeholder: '0',
116 | min: 1,
117 | }
118 |
119 | const toggleTrait = {
120 | name: 'toggle',
121 | label: 'Guides',
122 | type: 'button',
123 | full: true,
124 | text: 'Toggle',
125 | command: e => showGrid(e)
126 | }
127 |
128 | const clearTrait = {
129 | name: 'clear',
130 | label: 'Cells',
131 | type: 'button',
132 | full: true,
133 | text: 'Clear',
134 | command: editor => editor.getSelected().components.reset()
135 | }
136 |
137 | const resetTrait = {
138 | name: 'reset',
139 | label: 'Guides',
140 | type: 'button',
141 | full: true,
142 | text: 'Reset',
143 | command: editor => {
144 | const store = editor.Grid.selected.get('store');
145 | store.mutations.resetGrid(store.state);
146 | editor.Grid.updateChildren(store);
147 | }
148 | }
149 |
150 | const updateTrait = {
151 | name: 'update',
152 | label: 'Cells',
153 | type: 'button',
154 | full: true,
155 | text: 'Update',
156 | command: editor => {
157 | //Generate html and css
158 | const sel = editor.getSelected();
159 | const { state, getters } = sel.get('store');
160 | let grid = state.auto ? Array(state.rows * state.columns).fill().map(i => {
161 | return `
`
162 | }).join("") :
163 | state.childarea.map((area, i) => {
164 | return `
`
165 | }).join("");
166 | const css = state.childarea.map((area, i) => {
167 | return `.${sel.getId() + '-div' + i}{grid-area:${area}}`
168 | }).join("");
169 | !sel.components().length > 0 && sel.components().reset(grid);
170 | editor.addStyle(generateMedia(css));
171 | (editor.Grid.visible = false) || (editor.Grid.getEl().style.display = 'none');
172 | sel.addStyle({
173 | 'grid-template-columns': getters.colTemplate(state)
174 | });
175 | }
176 | }
177 |
178 | const generateMedia = (css) => {
179 | const deviceId = editor.getDevice();
180 | const device = editor.Devices.get(deviceId);
181 | if (device.get('widthMedia')) {
182 | return `@media (${editor.Config.mediaCondition}: ${device.get('widthMedia')}){
183 | ${css}
184 | }`
185 | }
186 | return css;
187 | }
188 |
189 | const toConfigState = (state) => {
190 | return state && Object.keys(state).length ? { state } : undefined;
191 | }
192 |
193 | domc.addType(gridChildId, {
194 | model: {
195 | defaults: {
196 | icon: ' ',
197 | traits: [idTrait, titleTrait, addCellTrait],
198 | },
199 | init() {
200 | const cc = editor.Css;
201 | this.get('classes').pluck('name').indexOf(gridCellClass) < 0 && this.addClass(gridCellClass);
202 | cc.getRule(`.${gridCellClass}`) || cc.setRule(`.${gridCellClass}`, {
203 | display: 'flex',
204 | 'justify-content': 'flex-start',
205 | 'align-items': 'stretch',
206 | padding: '5px'
207 | });
208 | },
209 | ...cellComponent
210 | }
211 | });
212 |
213 | const toolbar = [{
214 | attributes: { class: 'fa fa-table' },
215 | command: e => showGrid(e)
216 | }, {
217 | attributes: { class: 'fa fa-arrow-up' },
218 | command: e => e.runCommand('core:component-exit', { force: 1 })
219 | }, {
220 | attributes: {
221 | class: 'fa fa-arrows gjs-no-touch-actions',
222 | draggable: true
223 | },
224 | command: 'tlb-move'
225 | }, {
226 | attributes: { class: 'fa fa-clone' },
227 | command: 'tlb-clone'
228 | }, {
229 | attributes: { class: 'fa fa-trash-o' },
230 | command: 'tlb-delete'
231 | }]
232 |
233 | domc.addType(gridCompId, {
234 | model: {
235 | defaults: {
236 | // Default props
237 | icon: ' ',
238 | toolbar,
239 | traits: [
240 | idTrait,
241 | titleTrait,
242 | autoTrait,
243 | minTrait,
244 | columnsTrait,
245 | rowsTrait,
246 | columnGapTrait,
247 | rowGapTrait,
248 | toggleTrait,
249 | resetTrait,
250 | clearTrait,
251 | updateTrait
252 | ],
253 | resizable: {
254 | tl: 0,
255 | tc: 0,
256 | tr: 0,
257 | cl: 0,
258 | cr: 0,
259 | bl: 0,
260 | br: 0,
261 | minDim: 5
262 | },
263 | min: 200,
264 | auto: false
265 | },
266 | init() {
267 | const cc = editor.Css;
268 | const storedState = this.get('store')?.state;
269 | const st = {
270 | ...store(opts),
271 | ...toConfigState(storedState || {})
272 | };
273 | !storedState && st.mutations.initialArrIndex(st.state, '');
274 | this.set('rows', st.state.rows);
275 | this.set('columns', st.state.columns);
276 | this.set('rowgap', st.state.rowgap);
277 | this.set('columngap', st.state.columngap);
278 | this.set('store', st);
279 | this.addStyle({
280 | 'grid-template-rows': st.getters.rowTemplate(st.state),
281 | 'grid-template-columns': st.getters.colTemplate(st.state),
282 | });
283 | this.get('classes').pluck('name').indexOf(gridClass) < 0 && this.addClass(gridClass);
284 | cc.getRule(`.${gridClass}`) || cc.setRule(`.${gridClass}`, {
285 | display: 'grid',
286 | padding: '10px',
287 | height: '95%',
288 | width: '100%',
289 | });
290 | this.on("change:auto", this.updateAuto);
291 | this.on("change:rows", this.updateRows);
292 | this.on("change:columns", this.updateColumns);
293 | this.on("change:rowgap", this.updateRowgap);
294 | this.on("change:columngap", this.updateColumngap);
295 | this.on("change:min", this.updateMin);
296 | this.on("change:status", this.onStatusChange);
297 | },
298 | updateRows() {
299 | const rows = parseInt(this.get('rows'));
300 | const store = this.get('store');
301 | const payload = {
302 | newVal: rows,
303 | oldVal: store.state.rows,
304 | direction: "rowArr"
305 | };
306 | store.mutations.updateRows(store.state, rows);
307 | store.mutations.adjustArr(store.state, payload);
308 | editor.Grid.update(store);
309 | this.addStyle({
310 | 'grid-template-rows': store.getters.rowTemplate(store.state)
311 | });
312 | },
313 | updateColumns() {
314 | const columns = parseInt(this.get('columns'));
315 | const store = this.get('store');
316 | const payload = {
317 | newVal: columns,
318 | oldVal: store.state.columns,
319 | direction: "colArr"
320 | };
321 | store.mutations.updateColumns(store.state, columns);
322 | store.mutations.adjustArr(store.state, payload);
323 | editor.Grid.update(store);
324 | this.addStyle({
325 | 'grid-template-columns': store.getters.colTemplate(store.state)
326 | });
327 | },
328 | updateRowgap() {
329 | const rowgap = this.get('rowgap');
330 | const store = this.get('store');
331 | store.mutations.updateRowGap(store.state, parseInt(rowgap));
332 | editor.Grid.updateChildren(store);
333 | this.addStyle({ 'grid-row-gap': `${rowgap}px` });
334 | },
335 | updateColumngap() {
336 | const columngap = this.get('columngap');
337 | const store = this.get('store');
338 | store.mutations.updateColumnGap(store.state, parseInt(columngap));
339 | editor.Grid.updateChildren(store);
340 | this.addStyle({ 'grid-column-gap': `${columngap}px` });
341 | },
342 | updateMin() {
343 | const min = parseInt(this.get('min'));
344 | const store = this.get('store');
345 | store.mutations.updateMin(store.state, min);
346 | },
347 | updateAuto() {
348 | const auto = !!this.get('auto');
349 | const store = this.get('store');
350 | store.mutations.updateAuto(store.state, auto);
351 | if (auto) (editor.Grid.visible = false) || (editor.Grid.getEl().style.display = 'none');
352 | },
353 | onStatusChange() {
354 | const status = this.get('status');
355 | if (!editor.Grid.container?.length) editor.Grid.render(`#${pfx}tools`, this.get('store'));
356 | if (status === 'selected') editor.Grid.select(editor.getSelected());
357 | else (editor.Grid.visible = false) || (editor.Grid.getEl().style.display = 'none');
358 | },
359 | ...gridComponent
360 | }
361 | });
362 | };
363 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* Class name prefixes */
2 | $prefix: 'gjs-' !default;
3 |
4 | /* Colors / Theme */
5 | $breadcrumbsColor1: #3b75f3;
6 | $breadcrumbsColor2: #376de2;
7 | $breadcrumbsHoverColor: #6c8fdb;
8 |
9 | .#{$prefix}font-aqua {
10 | color: #b6e2e5;
11 | }
12 |
13 | .#{$prefix}font-blue {
14 | color: #c9a1d1;
15 | }
16 |
17 | .#{$prefix}font-pink {
18 | color: #d1afc7;
19 | }
20 |
21 | .#{$prefix}font-green {
22 | color: #9ee7a8;
23 | }
24 |
25 | .#{$prefix}font-red {
26 | color: #ca6868;
27 | }
28 |
29 | .#{$prefix}font-orange {
30 | color: #cf9269;
31 | }
32 |
33 | .#{$prefix}sm-property__margin .#{$prefix}sm-properties::after,
34 | .#{$prefix}sm-property__padding .#{$prefix}sm-properties::after {
35 | content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBpZD0iZTkzOWQwODYtNjM4Zi00OTk2LTg3ZmMtYzhmOGUyZTc2ZWQ4IiBkYXRhLW5hbWU9IkxheWVyIDEiIHdpZHRoPSI3MCIgaGVpZ2h0PSI0MCI+PHJlY3Qgd2lkdGg9IjcwIiBoZWlnaHQ9IjQwIiBmaWxsPSJyZ2JhKDAsMCwwLDAuMSkiIHN0cm9rZT0icmdiYSgwLDAsMCwwLjUpIi8+PC9zdmc+");
36 | position: absolute;
37 | left: 30%;
38 | top: 30%;
39 | }
40 |
41 | .#{$prefix}sm-property__margin-top,
42 | .#{$prefix}sm-property__margin-bottom,
43 | .#{$prefix}sm-property__margin-right,
44 | .#{$prefix}sm-property__margin-left,
45 | .#{$prefix}sm-property__padding-top,
46 | .#{$prefix}sm-property__padding-bottom,
47 | .#{$prefix}sm-property__padding-right,
48 | .#{$prefix}sm-property__padding-left {
49 | flex: 999 1 60px;
50 | }
51 |
52 | .#{$prefix}sm-property__margin-top,
53 | .#{$prefix}sm-property__margin-bottom,
54 | .#{$prefix}sm-property__margin-right,
55 | .#{$prefix}sm-property__margin-left,
56 | .#{$prefix}sm-property__padding-top,
57 | .#{$prefix}sm-property__padding-bottom,
58 | .#{$prefix}sm-property__padding-right,
59 | .#{$prefix}sm-property__padding-left {
60 | max-width: 70px;
61 | flex: 999 1 60px;
62 | }
63 |
64 | .#{$prefix}sm-property__top .#{$prefix}sm-label,
65 | .#{$prefix}sm-property__bottom .#{$prefix}sm-label,
66 | .#{$prefix}sm-property__right .#{$prefix}sm-label,
67 | .#{$prefix}sm-property__left .#{$prefix}sm-label,
68 | .#{$prefix}sm-property__margin-top .#{$prefix}sm-label,
69 | .#{$prefix}sm-property__margin-bottom .#{$prefix}sm-label,
70 | .#{$prefix}sm-property__margin-right .#{$prefix}sm-label,
71 | .#{$prefix}sm-property__margin-left .#{$prefix}sm-label,
72 | .#{$prefix}sm-property__padding-top .#{$prefix}sm-label,
73 | .#{$prefix}sm-property__padding-bottom .#{$prefix}sm-label,
74 | .#{$prefix}sm-property__padding-right .#{$prefix}sm-label,
75 | .#{$prefix}sm-property__padding-left .#{$prefix}sm-label {
76 | display: none;
77 | }
78 |
79 | .#{$prefix}sm-property__margin-top .#{$prefix}field,
80 | .#{$prefix}sm-property__margin-bottom .#{$prefix}field,
81 | .#{$prefix}sm-property__margin-right .#{$prefix}field,
82 | .#{$prefix}sm-property__margin-left .#{$prefix}field,
83 | .#{$prefix}sm-property__padding-top .#{$prefix}field,
84 | .#{$prefix}sm-property__padding-bottom .#{$prefix}field,
85 | .#{$prefix}sm-property__padding-right .#{$prefix}field,
86 | .#{$prefix}sm-property__padding-left .#{$prefix}field {
87 | background-color: transparent;
88 | }
89 |
90 | .#{$prefix}sm-property__margin-left,
91 | .#{$prefix}sm-property__padding-left {
92 | margin-left: -15px;
93 | order: 2;
94 | }
95 |
96 | .#{$prefix}sm-property__margin-right,
97 | .#{$prefix}sm-property__padding-right {
98 | margin-left: 54px;
99 | margin-right: -15px;
100 | order: 3;
101 | }
102 |
103 | .#{$prefix}sm-property__bottom,
104 | .#{$prefix}sm-property__margin-bottom,
105 | .#{$prefix}sm-property__padding-bottom {
106 | order: 4;
107 | margin-left: 25%;
108 | margin-right: 25%;
109 | }
110 |
111 | .#{$prefix}sm-property__top,
112 | .#{$prefix}sm-property__margin-top,
113 | .#{$prefix}sm-property__padding-top {
114 | margin-left: 28.5%;
115 | margin-right: 25%;
116 | }
117 |
118 | .#{$prefix}pn-commands {
119 | min-height: 40px;
120 | }
121 |
122 | .#{$prefix}pn-panel.#{$prefix}pn-views {
123 | padding: 0;
124 | }
125 |
126 | .#{$prefix}pn-views .#{$prefix}pn-btn {
127 | margin: 0;
128 | height: 40px;
129 | padding: 10px;
130 | }
131 |
132 | .CodeMirror {
133 | min-height: 450px;
134 | margin-bottom: 8px;
135 | }
136 |
137 | .grp-handler-close {
138 | background-color: transparent;
139 | color: #ddd;
140 | }
141 |
142 | .grp-handler-cp-wrap {
143 | border-color: transparent;
144 | }
145 |
146 | /*Breadcrumbs*/
147 | ##{$prefix}breadcrumbs {
148 | position: absolute;
149 | left: 0px;
150 | bottom: 0px;
151 | display: inline-block;
152 | z-index: 1;
153 | margin: 0;
154 | padding: 0;
155 |
156 | .icon {
157 | font-size: 14px;
158 | }
159 |
160 | li {
161 | float: left;
162 | list-style-type: none;
163 | cursor: pointer;
164 |
165 | a {
166 | color: whitesmoke;
167 | display: block;
168 | background-color: $breadcrumbsColor1;
169 | /*$quaternaryColor;*/
170 | text-decoration: none;
171 | position: relative;
172 | height: 26px;
173 | line-height: 26px;
174 | padding: 0 10px 0 5px;
175 | text-align: center;
176 | margin-right: 23px;
177 | }
178 |
179 | &:nth-child(even) {
180 | a {
181 | background-color: $breadcrumbsColor2;
182 | /*$classColor;*/
183 |
184 | &:before {
185 | border-color: $breadcrumbsColor2;
186 | /*$classColor;*/
187 | border-left-color: transparent;
188 | }
189 |
190 | &:after {
191 | border-left-color: $breadcrumbsColor2;
192 | /*$classColor;*/
193 | }
194 | }
195 | }
196 |
197 | &:first-child {
198 | a {
199 | padding-left: 15px;
200 | /*@include border-radius(4px 0 0 4px);*/
201 | border-radius: 4px 0 0 4px;
202 |
203 | &:before {
204 | border: none;
205 | }
206 | }
207 | }
208 |
209 | &:last-child {
210 | a {
211 | padding-right: 15px;
212 | /*@include border-radius(0 4px 4px 0);*/
213 | border-radius: 0 4px 4px 0;
214 |
215 | &:after {
216 | border: none;
217 | }
218 | }
219 | }
220 |
221 | a {
222 |
223 | &:before,
224 | &:after {
225 | content: "";
226 | position: absolute;
227 | top: 0;
228 | border: 0 solid $breadcrumbsColor1;
229 | /*$quatenaryColor;*/
230 | border-width: 13px 10px;
231 | width: 0;
232 | height: 0;
233 | }
234 |
235 | &:before {
236 | left: -20px;
237 | border-left-color: transparent;
238 | }
239 |
240 | &:after {
241 | left: 100%;
242 | border-color: transparent;
243 | border-left-color: $breadcrumbsColor1;
244 | /*$quatenaryColor;*/
245 | }
246 |
247 | &:hover {
248 | background-color: $breadcrumbsHoverColor;
249 | /*$classColor;*/
250 |
251 | &:before {
252 | border-color: $breadcrumbsHoverColor;
253 | /*$classColor;*/
254 | border-left-color: transparent;
255 | }
256 |
257 | &:after {
258 | border-left-color: $breadcrumbsHoverColor;
259 | /*$classColor;*/
260 | }
261 | }
262 |
263 | &:active {
264 | background-color: $breadcrumbsHoverColor;
265 | /*$classColor;*/
266 |
267 | &:before {
268 | border-color: $breadcrumbsHoverColor;
269 | /*$classColor;*/
270 | border-left-color: transparent;
271 | }
272 |
273 | &:after {
274 | border-left-color: $breadcrumbsHoverColor;
275 | /*$classColor;*/
276 | }
277 | }
278 | }
279 | }
280 | }
281 |
282 | /* Color Palette */
283 | .#{$prefix}color-thief-output {
284 | display: block;
285 | padding: 1.5rem;
286 | background-color: rgba(0, 0, 0, 0.2);
287 | border-top-width: 0;
288 | border-radius: 5px;
289 | }
290 |
291 | .#{$prefix}output-layout {
292 | display: flex;
293 | }
294 |
295 | .#{$prefix}function.#{$prefix}get-color {
296 | flex: 0 1 16rem;
297 | margin-right: 2rem;
298 | }
299 |
300 | .#{$prefix}function-title {
301 | text-align: left;
302 | margin-top: 0;
303 | }
304 |
305 | .#{$prefix}get-color .swatch {
306 | width: 4rem;
307 | height: 4rem;
308 | }
309 |
310 | .#{$prefix}get-palette .swatch {
311 | width: 3rem;
312 | height: 3rem;
313 | }
314 |
315 | .swatch {
316 | display: inline-block;
317 | background: #dddddd;
318 | border-radius: 50%;
319 | }
320 |
321 | ##{$prefix}grid-main {
322 | margin: 3px;
323 | pointer-events: all;
324 | z-index: 1;
325 | }
326 |
327 | @mixin colors($max, $color-frequency) {
328 | $color: 300 / $max;
329 |
330 | // fruit loops!
331 | @for $i from 1 through $max {
332 | div[class*="child"]:nth-child(#{$i}) {
333 | /*background: hsla(($i - 15) * ($color * 1.5), 80%, 30%, 0.5);*/
334 | background: hsla($color-frequency + ($i - 15) * ($color * 1.5), 80%, 30%, 0.5);
335 | border: 1px solid #ddd;
336 | }
337 | }
338 |
339 | div[class*="child"] {
340 | background: hsla(300, 80%, 30%, 0.7);
341 | border: 1px solid #ddd;
342 | }
343 | }
344 |
345 | .#{$prefix}gridchild {
346 | counter-reset: step;
347 |
348 | div {
349 | counter-increment: step;
350 | position: relative;
351 |
352 | &:before {
353 | position: absolute;
354 | content: ".div"counter(step);
355 | display: block;
356 | padding: 0 5px;
357 | text-align: center;
358 | color: white;
359 | }
360 |
361 | button {
362 | position: absolute;
363 | right: 0;
364 | padding: 0 5px;
365 | margin: 0;
366 | color: white;
367 | background-color: transparent;
368 | border: none;
369 | z-index: 99999;
370 | }
371 | }
372 | }
373 |
374 | ##{$prefix}gridcontainer {
375 | border: 2px solid #71db9a;
376 | width: 100%;
377 | height: 100%;
378 | z-index: 0;
379 | position: relative;
380 | box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.15);
381 | }
382 |
383 | .#{$prefix}grid {
384 | width: 100%;
385 | height: 100%;
386 | position: absolute;
387 | display: grid;
388 | grid-auto-flow: row dense;
389 | @include colors(20, 100);
390 |
391 | p {
392 | padding: 0 10px;
393 | }
394 |
395 | div[class*="box"] {
396 | background-image: url("data:image/svg+xml,%3Csvg width='8' height='8' viewBox='0 0 6 6' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%239C92AC' fill-opacity='0.4' fill-rule='evenodd'%3E%3Cpath d='M5 0h1L0 6V5zM6 5v1H5z'/%3E%3C/g%3E%3C/svg%3E");
397 | border: 1px dotted gray;
398 | transition: 0.2s all ease;
399 | cursor: move;
400 | position: relative;
401 | z-index: 1000;
402 | opacity: 0.5;
403 | }
404 | }
405 |
406 | .#{$prefix}rowunits,
407 | .#{$prefix}colunits {
408 | display: grid;
409 | position: absolute;
410 | z-index: 1;
411 |
412 | div {
413 | text-align: center;
414 | position: relative;
415 | }
416 | }
417 |
418 | .#{$prefix}rowunits {
419 | float: left;
420 | height: 100%;
421 |
422 | div {
423 | align-self: center;
424 | }
425 | }
426 |
427 | .#{$prefix}widthfull {
428 | width: 100%;
429 | }
430 |
431 | .#{$prefix}widthhalf {
432 | width: 50%;
433 | }
434 |
435 | .#{$prefix}errors {
436 | position: absolute;
437 | bottom: -5px;
438 | border-radius: 4px;
439 | padding: 8px 12px;
440 | z-index: 1;
441 | font-weight: bold;
442 | width: 150px;
443 | min-height: 50px;
444 | background: #6d1a39;
445 | }
446 |
447 | /* RESEZING GLUTTER */
448 | .iframe-handle-container .handle {
449 | position: absolute;
450 | top: 0;
451 | bottom: 0;
452 | z-index: 14;
453 | }
454 |
455 | .iframe-handle-container .handle:before {
456 | content: "";
457 | position: absolute;
458 | top: 0;
459 | left: -3px;
460 | width: 4px;
461 | height: 100%;
462 | background: #178df7;
463 | display: none;
464 | }
465 |
466 | .iframe-handle-container .right-handle {
467 | left: -1px;
468 | width: 14px;
469 | }
470 |
471 | .iframe-handle-container .left-handle {
472 | left: -1px;
473 | width: 3px;
474 | pointer-events: all;
475 | cursor: col-resize;
476 | }
477 |
478 | .iframe-handle-container .tab-handle {
479 | position: absolute;
480 | top: 50%;
481 | width: 14px;
482 | height: 38px;
483 | margin-top: -19px;
484 | background: #808080;
485 | cursor: col-resize;
486 | pointer-events: all;
487 | border-top-right-radius: 3px;
488 | border-bottom-right-radius: 3px;
489 | }
490 |
491 | .iframe-handle-container .tab-handle:before,
492 | .iframe-handle-container .tab-handle:after {
493 | content: "";
494 | position: absolute;
495 | top: 8px;
496 | bottom: 8px;
497 | width: 1px;
498 | background: #a6a6a6;
499 | }
500 |
501 | .iframe-handle-container .tab-handle:before {
502 | left: 5px;
503 | }
504 |
505 | .iframe-handle-container .tab-handle:after {
506 | left: 8px;
507 | }
508 |
509 | .iframe-handle-container .tab-handle:hover {
510 | background: #178df7;
511 | }
512 |
513 | .iframe-handle-container .tab-handle:hover:before,
514 | .iframe-handle-container .tab-handle:hover:after {
515 | background: #48a5f9;
516 | }
517 |
518 | .iframe-handle-container .gutter-handle {
519 | position: absolute;
520 | top: 0;
521 | left: -3px;
522 | width: 4px;
523 | height: 100%;
524 | cursor: col-resize;
525 | pointer-events: all;
526 | }
527 |
528 | .iframe-handle-container:hover .handle:before {
529 | display: block;
530 | }
531 |
532 | .iframe-handle-container:hover .tab-handle {
533 | background: #178df7;
534 | border-color: #178df7;
535 | }
536 |
537 | .iframe-handle-container:hover .tab-handle:before,
538 | .iframe-handle-container:hover .tab-handle:after {
539 | background: #48a5f9;
540 | }
541 |
542 | .dim-indicator {
543 | position: absolute;
544 | top: 10%;
545 | height: auto;
546 | margin-left: 22px;
547 | pointer-events: all;
548 | font-size: 14px;
549 | display: none;
550 | padding: 2px 9px;
551 | border-radius: 4px !important;
552 | color: #ffffff;
553 | /* font-weight: bold; */
554 | width: 156px;
555 | background-color: rgb(0, 0, 0);
556 | }
--------------------------------------------------------------------------------