├── .npmignore ├── .gitignore ├── assets ├── logo.jpg ├── play_mov.gif ├── screen_1.jpg ├── screen_2.jpg ├── screen_3.jpg └── screen_4.jpg ├── docs ├── build │ ├── sp-ico.png │ ├── bg-space.png │ ├── sp-ico@2x.png │ └── PhotoLayoutEditor.js.LICENSE.txt ├── index.css └── index.html ├── src ├── PhotoLayoutEditor │ ├── style │ │ ├── images │ │ │ ├── bg-space.png │ │ │ ├── sp-ico.png │ │ │ └── sp-ico@2x.png │ │ ├── app.scss │ │ ├── components │ │ │ ├── base.scss │ │ │ ├── grid.scss │ │ │ ├── cropper.scss │ │ │ ├── toolbar.scss │ │ │ └── side.scss │ │ ├── lib.scss │ │ └── icons.scss │ ├── lib │ │ ├── object.js │ │ ├── color.js │ │ ├── index.js │ │ ├── Canvas.js │ │ ├── number.js │ │ ├── Keyboard.js │ │ ├── resamplingImage.js │ │ ├── util.js │ │ └── uploader.js │ ├── actions │ │ ├── core.js │ │ ├── index.js │ │ ├── cropper.js │ │ ├── side.js │ │ ├── types.js │ │ └── body.js │ ├── API │ │ ├── index.js │ │ ├── Cropper.js │ │ ├── Util.js │ │ └── Grid.js │ ├── Container │ │ ├── Body │ │ │ ├── index.js │ │ │ ├── Toolbar │ │ │ │ ├── Button.js │ │ │ │ └── EditLayoutSetting.js │ │ │ └── GridLayout │ │ │ │ └── index.js │ │ ├── Side │ │ │ ├── ToggleSideButton │ │ │ │ └── index.js │ │ │ ├── Items │ │ │ │ ├── Item.js │ │ │ │ └── index.js │ │ │ ├── Navigation │ │ │ │ └── index.js │ │ │ ├── selectItems.js │ │ │ └── index.js │ │ ├── index.js │ │ └── Cropper │ │ │ └── index.js │ ├── reducers │ │ ├── defaults.js │ │ ├── index.js │ │ ├── cropper.js │ │ ├── core.js │ │ ├── side.js │ │ └── body.js │ └── index.js ├── production │ └── index.js └── development │ ├── index.html │ ├── index.scss │ └── util.js ├── babel.config.js ├── lib ├── lib │ ├── object.js │ ├── color.js │ ├── Canvas.js │ ├── number.js │ ├── index.js │ ├── util.js │ ├── resamplingImage.js │ ├── uploader.js │ └── Keyboard.js ├── API │ ├── index.js │ └── Cropper.js ├── reducers │ ├── defaults.js │ ├── index.js │ ├── cropper.js │ ├── side.js │ └── core.js ├── Container │ ├── Body │ │ ├── Toolbar │ │ │ └── Button.js │ │ └── index.js │ └── Side │ │ ├── selectItems.js │ │ ├── ToggleSideButton │ │ └── index.js │ │ ├── Items │ │ ├── index.js │ │ └── Item.js │ │ └── Navigation │ │ └── index.js ├── actions │ ├── core.js │ ├── cropper.js │ ├── index.js │ ├── types.js │ ├── side.js │ └── body.js └── index.js ├── LICENSE ├── package.json ├── webpack.config.js └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | assets/ 3 | docs/ 4 | 5 | .DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | 4 | .DS_Store 5 | yarn.lock -------------------------------------------------------------------------------- /assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/assets/logo.jpg -------------------------------------------------------------------------------- /assets/play_mov.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/assets/play_mov.gif -------------------------------------------------------------------------------- /assets/screen_1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/assets/screen_1.jpg -------------------------------------------------------------------------------- /assets/screen_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/assets/screen_2.jpg -------------------------------------------------------------------------------- /assets/screen_3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/assets/screen_3.jpg -------------------------------------------------------------------------------- /assets/screen_4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/assets/screen_4.jpg -------------------------------------------------------------------------------- /docs/build/sp-ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/docs/build/sp-ico.png -------------------------------------------------------------------------------- /docs/build/bg-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/docs/build/bg-space.png -------------------------------------------------------------------------------- /docs/build/sp-ico@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/docs/build/sp-ico@2x.png -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/images/bg-space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/src/PhotoLayoutEditor/style/images/bg-space.png -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/images/sp-ico.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/src/PhotoLayoutEditor/style/images/sp-ico.png -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/images/sp-ico@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/HEAD/src/PhotoLayoutEditor/style/images/sp-ico@2x.png -------------------------------------------------------------------------------- /src/production/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PhotoLayoutEditor from '../PhotoLayoutEditor'; 3 | import '../PhotoLayoutEditor/style/app.scss'; 4 | 5 | export default PhotoLayoutEditor; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/lib/object.js: -------------------------------------------------------------------------------- 1 | /** 2 | * is array 3 | * 4 | * @param {Array} arr 5 | * @return {Boolean} 6 | */ 7 | export function isArray(arr=[]) 8 | { 9 | return !!(arr && arr.length); 10 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/actions/core.js: -------------------------------------------------------------------------------- 1 | import * as types from './types'; 2 | 3 | 4 | export function init(api, preference, element) 5 | { 6 | return { 7 | type: types.INIT_PLE, 8 | api, 9 | preference, 10 | element, 11 | }; 12 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | 4 | const presets = [ 5 | '@babel/preset-env', 6 | '@babel/preset-react', 7 | ]; 8 | const plugins = []; 9 | 10 | return { 11 | presets, 12 | plugins, 13 | }; 14 | }; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/actions/index.js: -------------------------------------------------------------------------------- 1 | import * as core from './core'; 2 | import * as body from './body'; 3 | import * as cropper from './cropper'; 4 | import * as side from './side'; 5 | import * as types from './types'; 6 | 7 | 8 | export { 9 | core, 10 | cropper, 11 | side, 12 | body, 13 | types, 14 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/app.scss: -------------------------------------------------------------------------------- 1 | @import '~react-grid-layout/css/styles.css'; 2 | @import '~react-simple-colorpicker/src/ColorPicker/index.scss'; 3 | 4 | @import "lib"; 5 | 6 | @import "components/base"; 7 | @import "components/grid"; 8 | @import "components/toolbar"; 9 | @import "components/side"; 10 | @import "components/cropper"; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/actions/cropper.js: -------------------------------------------------------------------------------- 1 | import * as types from './types'; 2 | 3 | 4 | export function open(key, item) 5 | { 6 | return { 7 | type: types.CROPPER_OPEN, 8 | item, 9 | key 10 | }; 11 | } 12 | 13 | export function close(key, position, size) 14 | { 15 | return { 16 | type: types.CROPPER_CLOSE, 17 | key, 18 | position, 19 | size 20 | }; 21 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/API/index.js: -------------------------------------------------------------------------------- 1 | import Side from './Side'; 2 | import Grid from './Grid'; 3 | import Cropper from './Cropper'; 4 | import Util from './Util'; 5 | 6 | 7 | function API(store) { 8 | this.side = new Side(store); 9 | this.grid = new Grid(store); 10 | this.cropper = new Cropper(store); 11 | this.util = new Util(store); 12 | } 13 | 14 | 15 | export default API; -------------------------------------------------------------------------------- /lib/lib/object.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.isArray = isArray; 7 | 8 | /** 9 | * is array 10 | * 11 | * @param {Array} arr 12 | * @return {Boolean} 13 | */ 14 | function isArray() { 15 | var arr = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 16 | return !!(arr && arr.length); 17 | } -------------------------------------------------------------------------------- /src/development/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Photo layout editor development page 9 | 10 | 11 |
12 | 13 | -------------------------------------------------------------------------------- /docs/build/PhotoLayoutEditor.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | Copyright (c) 2018 Jed Watson. 3 | Licensed under the MIT License (MIT), see 4 | http://jedwatson.github.io/classnames 5 | */ 6 | 7 | /** @license React v16.13.1 8 | * react-is.production.min.js 9 | * 10 | * Copyright (c) Facebook, Inc. and its affiliates. 11 | * 12 | * This source code is licensed under the MIT license found in the 13 | * LICENSE file in the root directory of this source tree. 14 | */ 15 | -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/lib/color.js: -------------------------------------------------------------------------------- 1 | /** 2 | * rgba to hex 3 | * 4 | * @param {String} color 5 | * @return {String} 6 | */ 7 | export function rgbToHex(color) 8 | { 9 | color = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); 10 | return (color && color.length === 4) ? "#" + 11 | ("0" + parseInt(color[1],10).toString(16)).slice(-2) + 12 | ("0" + parseInt(color[2],10).toString(16)).slice(-2) + 13 | ("0" + parseInt(color[3],10).toString(16)).slice(-2) : ''; 14 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/lib/index.js: -------------------------------------------------------------------------------- 1 | import * as util from './util'; 2 | import * as number from './number'; 3 | import * as object from './object'; 4 | import * as color from './color'; 5 | import uploader from './uploader'; 6 | import makingImage from './makingImage'; 7 | import Canvas from './Canvas'; 8 | import resamplingImage from './resamplingImage'; 9 | 10 | 11 | export { 12 | util, 13 | number, 14 | color, 15 | object, 16 | uploader, 17 | makingImage, 18 | Canvas, 19 | resamplingImage 20 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Body/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import GridLayout from "./GridLayout"; 3 | import Toolbar from './Toolbar'; 4 | 5 | 6 | class Body extends React.Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | } 11 | 12 | render() { 13 | return ( 14 |
15 |
16 | 17 | 18 |
19 |
20 | ); 21 | } 22 | 23 | } 24 | Body.displayName = 'Body'; 25 | 26 | 27 | export default Body; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/lib/Canvas.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Canvas 3 | */ 4 | export default class Canvas { 5 | 6 | /** 7 | * constructor 8 | * 9 | * @param {Number} width 10 | * @param {Number} height 11 | * @param {String} bgColor 12 | */ 13 | constructor(width=150, height=100, bgColor='#ffffff') 14 | { 15 | this.el = document.createElement('canvas'); 16 | this.ctx = this.el.getContext('2d'); 17 | 18 | this.el.width = width; 19 | this.el.height = height; 20 | 21 | this.ctx.fillStyle = bgColor; 22 | this.ctx.fillRect(0, 0, width, height); 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /lib/lib/color.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.rgbToHex = rgbToHex; 7 | 8 | /** 9 | * rgba to hex 10 | * 11 | * @param {String} color 12 | * @return {String} 13 | */ 14 | function rgbToHex(color) { 15 | color = color.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i); 16 | return color && color.length === 4 ? "#" + ("0" + parseInt(color[1], 10).toString(16)).slice(-2) + ("0" + parseInt(color[2], 10).toString(16)).slice(-2) + ("0" + parseInt(color[3], 10).toString(16)).slice(-2) : ''; 17 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Body/Toolbar/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | 5 | const Button = (props) => { 6 | return ( 7 |
8 | 11 | {!!props.children && ( 12 |
{props.children}
13 | )} 14 |
15 | ); 16 | }; 17 | 18 | 19 | export default Button; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/components/base.scss: -------------------------------------------------------------------------------- 1 | /* popup mode */ 2 | html.ple-popup-mode {overflow: hidden;} 3 | 4 | 5 | .ple-editor { 6 | position: relative; 7 | min-height: 420px; 8 | min-width: 640px; 9 | *, *:before, *:after { 10 | -webkit-box-sizing: border-box; 11 | -moz-box-sizing: border-box; 12 | box-sizing: border-box; 13 | } 14 | 15 | .ple-container { 16 | margin: 0; 17 | padding: 0; 18 | -webkit-transition: margin-right 1s ease-out; 19 | transition: margin-right .3s ease-out; 20 | } 21 | .ple-body {} 22 | 23 | &.ple-side-active { 24 | .ple-container { 25 | margin-right: $sidebar-width; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/reducers/defaults.js: -------------------------------------------------------------------------------- 1 | export const setting = { 2 | base: { 3 | uploadScript: null, 4 | uploadParamsConvertFunc: null, 5 | updateStoreFunc: null, 6 | callback: {}, 7 | }, 8 | body: { 9 | setting: { 10 | width: 100, 11 | height: 100, 12 | column: 5, 13 | outerMargin: 10, 14 | innerMargin: 10, 15 | bgColor: 'rgba(255,255,255,1)', 16 | }, 17 | blockColor: 'rgba(211,211,211,1)', 18 | grid: [], 19 | }, 20 | side: { 21 | files: [], 22 | visible: true, 23 | //progressPercent: null, 24 | }, 25 | }; 26 | 27 | 28 | export const side = { 29 | files: {}, 30 | visible: true, 31 | progressPercent: null, 32 | }; -------------------------------------------------------------------------------- /lib/API/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _Side = _interopRequireDefault(require("./Side")); 9 | 10 | var _Grid = _interopRequireDefault(require("./Grid")); 11 | 12 | var _Cropper = _interopRequireDefault(require("./Cropper")); 13 | 14 | var _Util = _interopRequireDefault(require("./Util")); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 17 | 18 | function API(store) { 19 | this.side = new _Side["default"](store); 20 | this.grid = new _Grid["default"](store); 21 | this.cropper = new _Cropper["default"](store); 22 | this.util = new _Util["default"](store); 23 | } 24 | 25 | var _default = API; 26 | exports["default"] = _default; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | 3 | import * as core from './core'; 4 | import * as body from './body'; 5 | import side from './side'; 6 | import * as cropper from './cropper'; 7 | 8 | 9 | export default combineReducers({ 10 | 11 | // settings 12 | setting: core.setting, 13 | 14 | // api 15 | api: core.api, 16 | 17 | // keyboard event 18 | keyboard: core.keyboard, 19 | 20 | // element 21 | element: core.element, 22 | 23 | // data tree 24 | tree: combineReducers({ 25 | side, 26 | body: combineReducers({ 27 | setting: body.setting, 28 | grid: body.grid, 29 | activeBlock: body.activeBlock 30 | }), 31 | cropper: combineReducers({ 32 | visible: cropper.visible, 33 | item: cropper.item, 34 | key: cropper.key 35 | }) 36 | }) 37 | 38 | }); -------------------------------------------------------------------------------- /lib/reducers/defaults.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.side = exports.setting = void 0; 7 | var setting = { 8 | base: { 9 | uploadScript: null, 10 | uploadParamsConvertFunc: null, 11 | updateStoreFunc: null, 12 | callback: {} 13 | }, 14 | body: { 15 | setting: { 16 | width: 100, 17 | height: 100, 18 | column: 5, 19 | outerMargin: 10, 20 | innerMargin: 10, 21 | bgColor: 'rgba(255,255,255,1)' 22 | }, 23 | blockColor: 'rgba(211,211,211,1)', 24 | grid: [] 25 | }, 26 | side: { 27 | files: [], 28 | visible: true //progressPercent: null, 29 | 30 | } 31 | }; 32 | exports.setting = setting; 33 | var side = { 34 | files: {}, 35 | visible: true, 36 | progressPercent: null 37 | }; 38 | exports.side = side; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/reducers/cropper.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/types'; 2 | 3 | 4 | export function visible(state=false, action) 5 | { 6 | switch(action.type) 7 | { 8 | case types.CROPPER_OPEN: 9 | return true; 10 | 11 | case types.CROPPER_CLOSE: 12 | return false; 13 | 14 | default: 15 | return state; 16 | } 17 | } 18 | 19 | export function item(state=null, action) 20 | { 21 | switch(action.type) 22 | { 23 | case types.CROPPER_OPEN: 24 | return action.item; 25 | 26 | case types.CROPPER_CLOSE: 27 | return null; 28 | 29 | default: 30 | return state; 31 | } 32 | } 33 | 34 | export function key(state=null, action) 35 | { 36 | switch(action.type) 37 | { 38 | case types.CROPPER_OPEN: 39 | return action.key; 40 | 41 | case types.CROPPER_CLOSE: 42 | return null; 43 | 44 | default: 45 | return state; 46 | } 47 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Side/ToggleSideButton/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | 5 | class ToggleSideButton extends Component { 6 | 7 | render() { 8 | const { props } = this; 9 | 10 | return ( 11 | 25 | ); 26 | } 27 | 28 | } 29 | ToggleSideButton.displayName = 'ToggleSideButton'; 30 | ToggleSideButton.defaultProps = { 31 | show: false, 32 | onClick: function() {} 33 | }; 34 | 35 | 36 | export default ToggleSideButton; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/actions/side.js: -------------------------------------------------------------------------------- 1 | import * as types from './types'; 2 | 3 | 4 | // control visible side bar 5 | export function visible(sw) 6 | { 7 | return { 8 | type: types.SIDE_VISIBLE, 9 | value: sw, 10 | }; 11 | } 12 | 13 | // toggle side bar 14 | export function toggle() 15 | { 16 | return { 17 | type: types.SIDE_TOGGLE, 18 | }; 19 | } 20 | 21 | export function addFiles(files=[]) 22 | { 23 | return { 24 | type: types.SIDE_ADD_FILES, 25 | files, 26 | }; 27 | } 28 | 29 | export function removeFiles(keys=[]) 30 | { 31 | return { 32 | type: types.SIDE_REMOVE_FILES, 33 | keys, 34 | }; 35 | } 36 | 37 | export function updateSelected(value) 38 | { 39 | return { 40 | type: types.SIDE_UPDATE_SELECTED, 41 | value 42 | }; 43 | } 44 | 45 | export function updateProgress(percent) 46 | { 47 | return { 48 | type: types.SIDE_UPDATE_PROGRESS, 49 | value: percent 50 | }; 51 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/API/Cropper.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | 4 | class Cropper { 5 | 6 | constructor(store) 7 | { 8 | this.store = store; 9 | } 10 | 11 | /** 12 | * open cropper 13 | * 14 | * @param {Number} key 15 | */ 16 | open(key) 17 | { 18 | const state = this.store.getState(); 19 | let item = null; 20 | 21 | try { 22 | item = state.tree.body.grid[key]; 23 | if (!item.image) throw 'Not found image in item'; 24 | } catch(e) { 25 | alert(e); 26 | return; 27 | } 28 | 29 | this.store.dispatch(actions.cropper.open(key, item)); 30 | }; 31 | 32 | /** 33 | * close cropper 34 | * 35 | * @param {Number} key 36 | * @param {String} position 37 | * @param {String} size 38 | */ 39 | close(key, position, size) 40 | { 41 | this.store.dispatch(actions.cropper.close(key, position, size)); 42 | } 43 | 44 | } 45 | 46 | 47 | export default Cropper; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/lib.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | $font-eng: 'Arial', sans-serif; 3 | 4 | $col-key: #626164; 5 | $col-blur: #999999; 6 | $col-bg-blur: #eeeeee; 7 | $col-active: #20B292; 8 | 9 | $sidebar-width: 240px; 10 | $sidebar-toggle-speed : .3s; 11 | 12 | 13 | // icons 14 | @import "icons"; 15 | 16 | 17 | // placeholders 18 | %blind { 19 | position: absolute; overflow: hidden; visibility: hidden; 20 | width: 0; height: 0; font-size: 0; line-height: 0; 21 | } 22 | %icon-close { 23 | position: relative; 24 | display: inline-block; 25 | text-indent: -9999px; 26 | transform: rotate(45deg); 27 | &:before, &:after { 28 | content: ''; position: absolute; display: block; 29 | background: #222; 30 | } 31 | &:before {width: 2px; height: 100%; margin-left: -1px; left: 50%; top: 0;} 32 | &:after {height: 2px; width: 100%; margin-top: -1px; top: 50%; left: 0;} 33 | } 34 | %transparent-background { 35 | background: $col-bg-blur url('./images/bg-space.png') 0 0 repeat; 36 | } -------------------------------------------------------------------------------- /src/development/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 15px; 3 | font-family: sans-serif; 4 | } 5 | 6 | // control 7 | .api-control { 8 | margin: 30px 0 0; 9 | border-top: 1px solid #999; 10 | > section { 11 | margin: 20px 0; 12 | > h1 { 13 | font-size: 20px; 14 | color: #222; 15 | margin: 0 0 5px; 16 | padding: 0 0 0 5px; 17 | } 18 | } 19 | 20 | nav { 21 | font-size: 14px; 22 | padding: 3px 10px; 23 | background: #f1f1f1; 24 | p { 25 | margin: 8px 0; 26 | } 27 | button { 28 | padding: 8px 12px; 29 | margin: 0 5px 4px 0; 30 | font-size: 11px; 31 | background: #999; 32 | font-family: 'Helvetica', Arial, sans-serif; 33 | color: #fff; 34 | font-weight: 400; 35 | border: none; 36 | text-transform: uppercase; 37 | cursor: pointer; 38 | border-radius: 0; 39 | } 40 | } 41 | } 42 | 43 | 44 | #makeImageArea { 45 | margin: 0; 46 | padding: 15px; 47 | min-height: 100px; 48 | border: 1px dashed #20B292; 49 | canvas, img { 50 | display: block; 51 | margin: 0 auto; 52 | border: 1px solid rgba(0,0,0,.05); 53 | } 54 | } -------------------------------------------------------------------------------- /docs/index.css: -------------------------------------------------------------------------------- 1 | body {margin: 0; font-family: sans-serif;} 2 | 3 | /* header */ 4 | .layout-header {padding: 20px 15px 30px;} 5 | .layout-header h1 {margin: 0;} 6 | .layout-header p {margin: 0; font-size: 13px;} 7 | 8 | /* control */ 9 | .api-control {margin: 30px 15px 0; border-top: 1px solid #999;} 10 | .api-control > h1 {font-size: 20px; color: #222; margin: 0 0 5px; padding: 0 0 0 5px;} 11 | .api-control nav {font-size: 14px; padding: 3px 10px; background: #f1f1f1;} 12 | .api-control nav p {margin: 8px 0;} 13 | .api-control nav button { 14 | padding: 8px 12px; margin: 0 5px 4px 0; 15 | font-family: 'Helvetica', Arial, sans-serif; font-size: 11px; color: #fff; font-weight: 400; 16 | border: none; text-transform: uppercase; cursor: pointer; background: #999; border-radius: 0; 17 | } 18 | .api-control > section {margin: 20px 0;} 19 | 20 | /* make image */ 21 | #makeImageArea {margin: 0; padding: 15px; min-height: 100px; border: 1px dashed #20B292;} 22 | #makeImageArea canvas, 23 | #makeImageArea img { 24 | display: block; margin: 0 auto; border: 1px solid rgba(0,0,0,.05); 25 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createStore } from 'redux'; 3 | import { Provider } from 'react-redux'; 4 | import Container from './Container'; 5 | import reducers from './reducers'; 6 | import API from './API'; 7 | 8 | 9 | class PhotoLayoutEditor extends React.Component { 10 | 11 | constructor(props) 12 | { 13 | super(props); 14 | // set store 15 | this.store = createStore(reducers); 16 | // set api 17 | this.api = new API(this.store); 18 | } 19 | 20 | render() 21 | { 22 | const { props } = this; 23 | 24 | return ( 25 | 26 | 27 | 28 | ); 29 | } 30 | 31 | } 32 | 33 | PhotoLayoutEditor.displayName = 'PhotoLayoutEditor'; 34 | PhotoLayoutEditor.defaultProps = { 35 | body: { 36 | setting: {}, 37 | blockColor: '#dddddd', 38 | grid: [] 39 | }, 40 | side: { 41 | files: [], 42 | visible: true 43 | }, 44 | uploadScript: null, 45 | uploadParamsConvertFunc: null 46 | }; 47 | 48 | export default PhotoLayoutEditor; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/actions/types.js: -------------------------------------------------------------------------------- 1 | export const INIT_PLE = 'INIT_PLE'; 2 | 3 | export const SIDE_VISIBLE = 'SIDE_VISIBLE'; 4 | export const SIDE_ADD_FILES = 'SIDE_ADD_FILES'; 5 | export const SIDE_REMOVE_FILES = 'SIDE_REMOVE_FILES'; 6 | export const SIDE_UPDATE_SELECTED = 'SIDE_UPDATE_SELECTED'; 7 | export const SIDE_TOGGLE = 'SIDE_TOGGLE'; 8 | export const SIDE_UPDATE_PROGRESS = 'SIDE_UPDATE_PROGRESS'; 9 | 10 | export const GRID_ATTACH_IMAGES = 'GRID_ATTACH_IMAGES'; 11 | export const GRID_ATTACH_IMAGE = 'GRID_ATTACH_IMAGE'; 12 | 13 | export const GRID_ADD_BLOCK = 'GRID_ADD_BLOCK'; 14 | export const GRID_REMOVE_BLOCK = 'GRID_REMOVE_BLOCK'; 15 | export const GRID_SHUFFLE_BLOCKS = 'GRID_SHUFFLE_BLOCKS'; 16 | export const GRID_DUPLICATE_BLOCK = 'GRID_DUPLICATE_BLOCK'; 17 | export const GRID_UPDATE_BLOCKS = 'GRID_UPDATE_BLOCKS'; 18 | export const GRID_ACTIVE_BLOCK = 'GRID_ACTIVE_BLOCK'; 19 | export const GRID_SETTING_UPDATE = 'GRID_SETTING_UPDATE'; 20 | export const GRID_CHANGE_COLOR = 'GRID_CHANGE_COLOR'; 21 | 22 | export const CROPPER_OPEN = 'CROPPER_OPEN'; 23 | export const CROPPER_CLOSE = 'CROPPER_CLOSE'; -------------------------------------------------------------------------------- /lib/lib/Canvas.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 9 | 10 | /** 11 | * Canvas 12 | */ 13 | var Canvas = 14 | /** 15 | * constructor 16 | * 17 | * @param {Number} width 18 | * @param {Number} height 19 | * @param {String} bgColor 20 | */ 21 | function Canvas() { 22 | var width = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 150; 23 | var height = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100; 24 | var bgColor = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : '#ffffff'; 25 | 26 | _classCallCheck(this, Canvas); 27 | 28 | this.el = document.createElement('canvas'); 29 | this.ctx = this.el.getContext('2d'); 30 | this.el.width = width; 31 | this.el.height = height; 32 | this.ctx.fillStyle = bgColor; 33 | this.ctx.fillRect(0, 0, width, height); 34 | }; 35 | 36 | exports["default"] = Canvas; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Redgoose 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/Container/Body/Toolbar/Button.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | 8 | var _react = _interopRequireDefault(require("react")); 9 | 10 | var _classnames = _interopRequireDefault(require("classnames")); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 13 | 14 | var Button = function Button(props) { 15 | return /*#__PURE__*/_react["default"].createElement("div", { 16 | className: (0, _classnames["default"])('ple-toolbar__block', props.className) 17 | }, /*#__PURE__*/_react["default"].createElement("button", { 18 | type: "button", 19 | title: props.title, 20 | onClick: props.onClick 21 | }, /*#__PURE__*/_react["default"].createElement("i", { 22 | className: (0, _classnames["default"])('ple-sp-ico', 'ple-abs', props.iconClass) 23 | }, props.title)), !!props.children && /*#__PURE__*/_react["default"].createElement("div", { 24 | className: "ple-toolbar__pop" 25 | }, props.children)); 26 | }; 27 | 28 | var _default = Button; 29 | exports["default"] = _default; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/lib/number.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Random range 3 | * min~max 사이의 랜덤 정수를 반환한다. 4 | * 5 | * @param {Number} min 6 | * @param {Number} max 7 | * @param {Boolean} useDecimal 8 | * @return {Number} 9 | */ 10 | export function randomRange(min, max, useDecimal=false) 11 | { 12 | if (useDecimal) 13 | { 14 | return Math.random() * (max - min) + min; 15 | } 16 | else 17 | { 18 | return Math.floor(Math.random() * (max - min + 1)) + min; 19 | } 20 | } 21 | 22 | 23 | /** 24 | * get ratio for resize 25 | * max와 min값의 차이의 비율을 가져온다. 26 | * 27 | * @param {Number} min 28 | * @param {Number} max 29 | * @return {Number} 30 | */ 31 | export function getRatioForResize(min, max) 32 | { 33 | return (max > min) ? min / max : max / min; 34 | } 35 | 36 | 37 | /** 38 | * Get timestamp 39 | * 40 | * @return {Number} 41 | */ 42 | export function getTimeStamp() 43 | { 44 | return Math.round(+new Date()/1000); 45 | } 46 | 47 | 48 | /** 49 | * get ratio 50 | * 51 | * @param {Number} w 52 | * @param {Number} h 53 | * @return {Number} 54 | */ 55 | export function getRatio(w, h) 56 | { 57 | let result = parseInt(w) / parseInt(h); 58 | result = Math.round(result * 1000) / 1000; 59 | return result; 60 | } 61 | -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Side/Items/Item.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import * as lib from '../../../lib'; 4 | 5 | 6 | class Item extends React.Component { 7 | 8 | render() { 9 | const { props } = this; 10 | 11 | let attr = Object.assign({}, 12 | lib.util.isTouchDevice() ? { 13 | onTouchStart: props.onTouchStart, 14 | onTouchMove: props.onTouchMove, 15 | onTouchEnd: props.onTouchEnd 16 | } : { 17 | onDragStart: props.onDragStart, 18 | onDragEnd: props.onDragEnd 19 | }); 20 | 21 | return ( 22 |
  • 23 | 32 |
  • 33 | ); 34 | } 35 | } 36 | Item.displayName = 'Item'; 37 | Item.defaultProps = { 38 | image: null, // image 39 | id: null, // id 40 | active: null, // active item 41 | onClick: null, // on click item 42 | onDragStart: null, // on drag start 43 | onDragEnd: null, // on drag end 44 | onTouchStart: null, // on touch start 45 | onTouchMove: null, // on touch move 46 | onTouchEnd: null, // on touch end 47 | }; 48 | 49 | 50 | export default Item; 51 | -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/components/grid.scss: -------------------------------------------------------------------------------- 1 | /* Grid */ 2 | .ple-grid { 3 | background: #fff; 4 | margin: 0 auto; 5 | box-shadow: 0 2px 4px rgba(0,0,0,.2); 6 | min-height: 40px; 7 | &__wrap { 8 | padding: 30px 30px; 9 | @extend %transparent-background; 10 | } 11 | &__body { 12 | position: relative; 13 | margin: 0 auto; 14 | } 15 | .react-grid-item { 16 | cursor: pointer; overflow: hidden; 17 | &.ple-grid__item-active { 18 | position: relative; 19 | &:after { 20 | content: ''; position: absolute; 21 | left: 0; right: 0; top: 0; bottom: 0; 22 | border: 4px solid $col-active; 23 | pointer-events: none; 24 | } 25 | } 26 | &.ple-grid__item-hover { 27 | position: relative; 28 | &:after { 29 | content: ''; position: absolute; 30 | left: 0; right: 0; top: 0; bottom: 0; 31 | background: $col-active; 32 | opacity: .3; 33 | pointer-events: none; 34 | border: 5px solid red; 35 | } 36 | } 37 | figure { 38 | margin: 0; 39 | width: 100%; height: 100%; 40 | background-repeat: no-repeat; 41 | } 42 | &.react-grid-placeholder { 43 | background: $col-active; 44 | } 45 | & .react-resizable-handle { 46 | &:after { 47 | right: 5px; 48 | bottom: 5px; 49 | width: 8px; 50 | height: 8px; 51 | border-right: 2px solid rgba(0, 0, 0, 0.4); 52 | border-bottom: 2px solid rgba(0, 0, 0, 0.4); 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /lib/lib/number.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.randomRange = randomRange; 7 | exports.getRatioForResize = getRatioForResize; 8 | exports.getTimeStamp = getTimeStamp; 9 | exports.getRatio = getRatio; 10 | 11 | /** 12 | * Random range 13 | * min~max 사이의 랜덤 정수를 반환한다. 14 | * 15 | * @param {Number} min 16 | * @param {Number} max 17 | * @param {Boolean} useDecimal 18 | * @return {Number} 19 | */ 20 | function randomRange(min, max) { 21 | var useDecimal = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 22 | 23 | if (useDecimal) { 24 | return Math.random() * (max - min) + min; 25 | } else { 26 | return Math.floor(Math.random() * (max - min + 1)) + min; 27 | } 28 | } 29 | /** 30 | * get ratio for resize 31 | * max와 min값의 차이의 비율을 가져온다. 32 | * 33 | * @param {Number} min 34 | * @param {Number} max 35 | * @return {Number} 36 | */ 37 | 38 | 39 | function getRatioForResize(min, max) { 40 | return max > min ? min / max : max / min; 41 | } 42 | /** 43 | * Get timestamp 44 | * 45 | * @return {Number} 46 | */ 47 | 48 | 49 | function getTimeStamp() { 50 | return Math.round(+new Date() / 1000); 51 | } 52 | /** 53 | * get ratio 54 | * 55 | * @param {Number} w 56 | * @param {Number} h 57 | * @return {Number} 58 | */ 59 | 60 | 61 | function getRatio(w, h) { 62 | var result = parseInt(w) / parseInt(h); 63 | result = Math.round(result * 1000) / 1000; 64 | return result; 65 | } -------------------------------------------------------------------------------- /src/development/util.js: -------------------------------------------------------------------------------- 1 | export function pickImages(count=3) 2 | { 3 | const sampleImages = [ 4 | 'https://images.unsplash.com/photo-1561271657-fbad0db4caaa?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2292&q=60', 5 | 'https://images.unsplash.com/photo-1561214380-cdcaa684cf52?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1400&q=60', 6 | 'https://images.unsplash.com/photo-1561187273-0d2494d76346?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1000&q=60', 7 | 'https://images.unsplash.com/photo-1561266569-ffd2ec021489?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2234&q=60', 8 | 'https://images.unsplash.com/photo-1561087867-203d3c5344d6?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2250&q=60', 9 | 'https://images.unsplash.com/photo-1561148755-03553117df6d?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2251&q=60', 10 | 'https://images.unsplash.com/photo-1561155654-20461b26ee4b?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=934&q=60', 11 | 'https://images.unsplash.com/photo-1561084195-ee7372303a19?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=2251&q=60', 12 | ]; 13 | let images = Object.assign([], sampleImages); 14 | let result = []; 15 | 16 | (function get() { 17 | if (count <= 0) return; 18 | result.push(images.splice(Math.floor(Math.random() * images.length), 1)[0]); 19 | count--; 20 | get(); 21 | if (count >= 1) get(); 22 | })(); 23 | 24 | return result; 25 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/actions/body.js: -------------------------------------------------------------------------------- 1 | import * as types from './types'; 2 | 3 | 4 | export function addBlock(value) 5 | { 6 | return { 7 | type: types.GRID_ADD_BLOCK, 8 | value: value, 9 | }; 10 | } 11 | 12 | export function removeBlock(keys) { 13 | return { 14 | type: types.GRID_REMOVE_BLOCK, 15 | keys, 16 | }; 17 | } 18 | 19 | export function shuffleBlocks(options) 20 | { 21 | return { 22 | type: types.GRID_SHUFFLE_BLOCKS, 23 | value: options, 24 | } 25 | } 26 | 27 | export function duplicateBlock(keys) 28 | { 29 | return { 30 | type: types.GRID_DUPLICATE_BLOCK, 31 | keys 32 | }; 33 | } 34 | 35 | export function updateBlocks(blocks) 36 | { 37 | return { 38 | type: types.GRID_UPDATE_BLOCKS, 39 | value: blocks, 40 | } 41 | } 42 | 43 | export function activeBlock(keys) 44 | { 45 | return { 46 | type: types.GRID_ACTIVE_BLOCK, 47 | value: keys 48 | }; 49 | } 50 | 51 | export function changeColorBlock(keys, color) 52 | { 53 | return { 54 | type: types.GRID_CHANGE_COLOR, 55 | keys, 56 | color, 57 | } 58 | } 59 | 60 | export function updateSetting(value) 61 | { 62 | return { 63 | type: types.GRID_SETTING_UPDATE, 64 | value: value, 65 | }; 66 | } 67 | 68 | export function attachImages(images, cols, activeBlocks) 69 | { 70 | return { 71 | type: types.GRID_ATTACH_IMAGES, 72 | value: images, 73 | columns: cols, 74 | activeBlocks: activeBlocks, 75 | } 76 | } 77 | 78 | export function attachImage(keys, image) 79 | { 80 | return { 81 | type: types.GRID_ATTACH_IMAGE, 82 | keys, 83 | image, 84 | }; 85 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/lib/Keyboard.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery/dist/jquery.slim'; 2 | 3 | import * as libs from '../lib'; 4 | 5 | 6 | export default class Keyboard { 7 | 8 | constructor() 9 | { 10 | this.eventName = `PLE_${libs.number.getTimeStamp()}`; 11 | this.code = null; 12 | this.keyName = null; 13 | this.names = { 14 | 17: 'CTRL', 15 | 18: 'ALT', 16 | 91: 'CMD', 17 | 93: 'CMD', 18 | 16: 'SHIFT', 19 | }; 20 | 21 | // init key down event 22 | $(window).on(`keydown.${this.eventName}`, (e) => this._keyDown(e)); 23 | } 24 | 25 | /** 26 | * apply 27 | * 28 | * @param {Number} code 29 | */ 30 | apply(code) 31 | { 32 | this.code = code; 33 | this.keyName = this.names[this.code] || null; 34 | } 35 | 36 | /** 37 | * key down event 38 | * 39 | * @param {Event} e 40 | */ 41 | _keyDown(e) 42 | { 43 | // apply keyCode 44 | this.apply(e.keyCode); 45 | 46 | // set events 47 | $(window) 48 | .on(`keyup.${this.eventName}`, () => this._keyUp()) 49 | .on(`contextmenu.${this.eventName}`, () => this._keyUp()) 50 | .on(`blur.${this.eventName}`, () => this._keyUp()) 51 | .off(`keydown.${this.eventName}`); 52 | } 53 | 54 | /** 55 | * key up event 56 | * 57 | */ 58 | _keyUp() 59 | { 60 | // apply keyCode 61 | this.apply(null); 62 | 63 | // set events 64 | $(window) 65 | .on(`keydown.${this.eventName}`, (e) => this._keyDown(e)) 66 | .off(`contextmenu.${this.eventName} keyup.${this.eventName} blur.${this.eventName}`); 67 | } 68 | 69 | /** 70 | * destroy event 71 | * 72 | */ 73 | destroy() 74 | { 75 | $(window).off(`keydown.${this.eventName} contextmenu.${this.eventName} keyup.${this.eventName} blur.${this.eventName}`); 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /lib/actions/core.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.init = init; 9 | 10 | var types = _interopRequireWildcard(require("./types")); 11 | 12 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 13 | 14 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 15 | 16 | function init(api, preference, element) { 17 | return { 18 | type: types.INIT_PLE, 19 | api: api, 20 | preference: preference, 21 | element: element 22 | }; 23 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Side/Items/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Item from './Item'; 3 | 4 | 5 | class Items extends React.Component { 6 | 7 | render() { 8 | const { props } = this; 9 | 10 | return ( 11 |
    12 |
    13 | 40 |
    41 |
    42 | ); 43 | } 44 | 45 | } 46 | Items.displayName = 'Items'; 47 | Items.defaultProps = { 48 | files: [], // files 49 | onSelect: (key) => {}, // on select event 50 | onDragStart: null, // on drag start 51 | onDragEnd: null, // on drag end 52 | onTouchStart: null, // on touch start 53 | onTouchMove: null, // on touch move 54 | onTouchEnd: null, // on touch end 55 | progress: null, // on progress number 56 | }; 57 | 58 | 59 | export default Items; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/reducers/core.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/types'; 2 | import Keyboard from '../lib/Keyboard'; 3 | import * as defaults from './defaults'; 4 | 5 | 6 | export function setting(state=null, action) 7 | { 8 | let newState = Object.assign({}, state); 9 | 10 | switch (action.type) 11 | { 12 | case types.INIT_PLE: 13 | newState = Object.assign({}, { 14 | base: { 15 | ...defaults.setting.base, 16 | uploadScript: action.preference.uploadScript || defaults.setting.base.uploadScript, 17 | uploadParamsConvertFunc: action.preference.uploadParamsConvertFunc || defaults.setting.base.uploadParamsConvertFunc, 18 | updateStoreFunc: action.preference.updateStoreFunc || defaults.setting.base.updateStoreFunc, 19 | callback: action.preference.callback || defaults.setting.base.callback, 20 | }, 21 | side: { 22 | ...defaults.setting.side, 23 | ...action.preference.side 24 | }, 25 | body: { 26 | ...defaults.setting.body, 27 | ...action.preference.body, 28 | } 29 | }); 30 | return newState; 31 | 32 | default: 33 | return state; 34 | } 35 | } 36 | 37 | export function api(state=null, action) 38 | { 39 | switch (action.type) 40 | { 41 | case types.INIT_PLE: 42 | return action.api; 43 | 44 | default: 45 | return state; 46 | } 47 | } 48 | 49 | export function keyboard(state=null, action) 50 | { 51 | switch (action.type) 52 | { 53 | case types.INIT_PLE: 54 | return new Keyboard(); 55 | 56 | default: 57 | return state; 58 | } 59 | } 60 | 61 | export function element(state=null, action) 62 | { 63 | switch (action.type) 64 | { 65 | case types.INIT_PLE: 66 | return action.element; 67 | 68 | default: 69 | return state; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/actions/cropper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.open = open; 9 | exports.close = close; 10 | 11 | var types = _interopRequireWildcard(require("./types")); 12 | 13 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 14 | 15 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 16 | 17 | function open(key, item) { 18 | return { 19 | type: types.CROPPER_OPEN, 20 | item: item, 21 | key: key 22 | }; 23 | } 24 | 25 | function close(key, position, size) { 26 | return { 27 | type: types.CROPPER_CLOSE, 28 | key: key, 29 | position: position, 30 | size: size 31 | }; 32 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Side/Navigation/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | 4 | class SideNavigation extends React.Component { 5 | 6 | constructor(props) { 7 | super(props); 8 | 9 | this.comps = { 10 | inputFile: null, 11 | }; 12 | this.state = { 13 | timestamp : Date.now(), 14 | }; 15 | } 16 | 17 | /** 18 | * Upload images 19 | * 20 | * @param {Event} e 21 | */ 22 | upload(e) { 23 | this.props.onUpload(e.target.files); 24 | 25 | this.setState({ 26 | timestamp : Date.now() 27 | }); 28 | } 29 | 30 | render() { 31 | const { props, state, comps } = this; 32 | 33 | return ( 34 | 54 | ); 55 | } 56 | 57 | } 58 | SideNavigation.displayName = 'Navigation'; 59 | SideNavigation.defaultProps = { 60 | onRemove: function() {}, 61 | onToggleSelect: function() {}, 62 | onAttach: function() {}, 63 | onUpload: function() {}, 64 | }; 65 | 66 | 67 | export default SideNavigation; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/reducers/side.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/types'; 2 | import * as defaults from './defaults'; 3 | 4 | 5 | let nextFileId = 0; 6 | 7 | 8 | /** 9 | * Reducers 10 | */ 11 | 12 | // base 13 | export default function base(state=defaults.side, action) 14 | { 15 | let newState = Object.assign({}, state); 16 | let files = {}; 17 | 18 | switch (action.type) { 19 | case types.INIT_PLE: 20 | try { 21 | action.preference.side.files.forEach(o => { 22 | files[nextFileId++] = { image: o, active: false } 23 | }); 24 | return { 25 | ...state, 26 | ...action.preference.side, 27 | files: { 28 | ...state.files, 29 | ...files, 30 | }, 31 | }; 32 | } catch(e) { 33 | return state; 34 | } 35 | 36 | case types.SIDE_VISIBLE: 37 | return { 38 | ...state, 39 | visible: action.value, 40 | }; 41 | 42 | case types.SIDE_TOGGLE: 43 | return { 44 | ...state, 45 | visible: !state.visible, 46 | }; 47 | 48 | case types.SIDE_ADD_FILES: 49 | action.files.forEach((o) => { 50 | files[nextFileId++] = { image: o, active: false } 51 | }); 52 | return { 53 | ...newState, 54 | files: { 55 | ...newState.files, 56 | ...files, 57 | } 58 | }; 59 | 60 | case types.SIDE_REMOVE_FILES: 61 | if (!action.keys.length) return state; 62 | action.keys.forEach(o => { 63 | delete newState.files[o]; 64 | }); 65 | return newState; 66 | 67 | case types.SIDE_UPDATE_SELECTED: 68 | if (!(action.value && Object.keys(action.value).length)) return state; 69 | Object.keys(action.value).forEach(k => { 70 | if (!newState.files[k]) return; 71 | newState.files[k].active = action.value[k].active; 72 | }); 73 | return newState; 74 | 75 | case types.SIDE_UPDATE_PROGRESS: 76 | return { 77 | ...state, 78 | progressPercent: action.value 79 | }; 80 | 81 | default: 82 | return state; 83 | } 84 | } -------------------------------------------------------------------------------- /lib/actions/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.types = exports.side = exports.cropper = exports.body = exports.core = void 0; 9 | 10 | var core = _interopRequireWildcard(require("./core")); 11 | 12 | exports.core = core; 13 | 14 | var body = _interopRequireWildcard(require("./body")); 15 | 16 | exports.body = body; 17 | 18 | var cropper = _interopRequireWildcard(require("./cropper")); 19 | 20 | exports.cropper = cropper; 21 | 22 | var side = _interopRequireWildcard(require("./side")); 23 | 24 | exports.side = side; 25 | 26 | var types = _interopRequireWildcard(require("./types")); 27 | 28 | exports.types = types; 29 | 30 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 31 | 32 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import classNames from 'classnames'; 4 | import * as actions from '../actions'; 5 | import Body from './Body'; 6 | import Side from './Side'; 7 | 8 | 9 | let ready = false; 10 | 11 | 12 | class Container extends React.Component { 13 | 14 | constructor(props) 15 | { 16 | super(props); 17 | this.el = null; 18 | } 19 | 20 | componentDidUpdate(prevProps, prevState, snapshot) 21 | { 22 | const { props } = this; 23 | if ((!!props.setting.base.updateStoreFunc && typeof props.setting.base.updateStoreFunc === 'function') && ( 24 | prevProps.tree.side.visible !== props.tree.side.visible || 25 | prevProps.tree.side.files !== props.tree.side.files || 26 | prevProps.tree.body.setting !== props.tree.body.setting || 27 | prevProps.tree.body.grid !== props.tree.body.grid 28 | )) { 29 | props.setting.base.updateStoreFunc(); 30 | } 31 | if (!ready && !!props.setting.base.callback.init && typeof props.setting.base.callback.init === 'function') 32 | { 33 | props.setting.base.callback.init(); 34 | ready = true; 35 | } 36 | } 37 | 38 | componentDidMount() 39 | { 40 | const { props } = this; 41 | 42 | props.dispatch(actions.core.init( 43 | props.parent.api, 44 | props.parent.preference || { side: {}, body: {} }, 45 | this.el 46 | )); 47 | } 48 | 49 | render() 50 | { 51 | const { props } = this; 52 | 53 | return ( 54 |
    { this.el = r; }} 56 | className={classNames('ple-editor', { 'ple-side-active': props.tree.side.visible })}> 57 | {props.setting && ( 58 |
    59 | 60 | 61 |
    62 | )} 63 |
    64 | ); 65 | } 66 | 67 | } 68 | Container.displayName = 'Container'; 69 | Container.defaultProps = { 70 | parent: {}, 71 | dispatch: null, 72 | tree: {}, 73 | setting: null, 74 | }; 75 | 76 | 77 | export default connect((state) => Object.assign({}, state, {}))(Container); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-photo-layout-editor", 3 | "version": "1.2.8", 4 | "description": "Photo layout editor for react", 5 | "main": "lib/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/redgoose-dev/react-photo-layout-editor.git" 9 | }, 10 | "author": { 11 | "name": "redgoose", 12 | "email": "scripter@me.com", 13 | "url": "https://redgoose.me" 14 | }, 15 | "license": "MIT", 16 | "keywords": [ 17 | "Photo", 18 | "layout", 19 | "editor", 20 | "redgoose", 21 | "goose" 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/redgoose-dev/react-photo-layout-editor/issues" 25 | }, 26 | "homepage": "https://github.com/redgoose-dev/react-photo-layout-editor", 27 | "scripts": { 28 | "dev": "webpack serve --inline --progress --mode development", 29 | "prebuild": "rm -rf docs/build/ && rm -rf build/ && babel ./src/PhotoLayoutEditor --out-dir ./lib", 30 | "build": "webpack --mode production", 31 | "postbuild": "cp -r build/ docs/build/", 32 | "version-patch": "npm version patch" 33 | }, 34 | "dependencies": { 35 | "classnames": "^2.3.1", 36 | "jquery": "^3.6.0", 37 | "prop-types": "^15.7.2", 38 | "react": "^17.0.2", 39 | "react-dom": "^17.0.2", 40 | "react-grid-layout": "^1.2.4", 41 | "react-redux": "^7.2.3", 42 | "react-simple-colorpicker": "github:redgoose-dev/react-simple-colorpicker", 43 | "redux": "^4.0.5" 44 | }, 45 | "devDependencies": { 46 | "@babel/cli": "^7.13.14", 47 | "@babel/core": "^7.13.15", 48 | "@babel/preset-env": "^7.13.15", 49 | "@babel/preset-react": "^7.13.13", 50 | "babel-loader": "^8.2.2", 51 | "css-loader": "^5.2.1", 52 | "css-minimizer-webpack-plugin": "^2.0.0", 53 | "file-loader": "^6.2.0", 54 | "html-loader": "^2.1.2", 55 | "html-webpack-plugin": "^5.3.1", 56 | "mini-css-extract-plugin": "^1.4.1", 57 | "node-sass": "^5.0.0", 58 | "optimize-css-assets-webpack-plugin": "^5.0.4", 59 | "sass-loader": "^10.1.1", 60 | "terser-webpack-plugin": "^5.1.1", 61 | "webpack": "^5.31.2", 62 | "webpack-cli": "^4.6.0", 63 | "webpack-dev-server": "^3.11.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/actions/types.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.CROPPER_CLOSE = exports.CROPPER_OPEN = exports.GRID_CHANGE_COLOR = exports.GRID_SETTING_UPDATE = exports.GRID_ACTIVE_BLOCK = exports.GRID_UPDATE_BLOCKS = exports.GRID_DUPLICATE_BLOCK = exports.GRID_SHUFFLE_BLOCKS = exports.GRID_REMOVE_BLOCK = exports.GRID_ADD_BLOCK = exports.GRID_ATTACH_IMAGE = exports.GRID_ATTACH_IMAGES = exports.SIDE_UPDATE_PROGRESS = exports.SIDE_TOGGLE = exports.SIDE_UPDATE_SELECTED = exports.SIDE_REMOVE_FILES = exports.SIDE_ADD_FILES = exports.SIDE_VISIBLE = exports.INIT_PLE = void 0; 7 | var INIT_PLE = 'INIT_PLE'; 8 | exports.INIT_PLE = INIT_PLE; 9 | var SIDE_VISIBLE = 'SIDE_VISIBLE'; 10 | exports.SIDE_VISIBLE = SIDE_VISIBLE; 11 | var SIDE_ADD_FILES = 'SIDE_ADD_FILES'; 12 | exports.SIDE_ADD_FILES = SIDE_ADD_FILES; 13 | var SIDE_REMOVE_FILES = 'SIDE_REMOVE_FILES'; 14 | exports.SIDE_REMOVE_FILES = SIDE_REMOVE_FILES; 15 | var SIDE_UPDATE_SELECTED = 'SIDE_UPDATE_SELECTED'; 16 | exports.SIDE_UPDATE_SELECTED = SIDE_UPDATE_SELECTED; 17 | var SIDE_TOGGLE = 'SIDE_TOGGLE'; 18 | exports.SIDE_TOGGLE = SIDE_TOGGLE; 19 | var SIDE_UPDATE_PROGRESS = 'SIDE_UPDATE_PROGRESS'; 20 | exports.SIDE_UPDATE_PROGRESS = SIDE_UPDATE_PROGRESS; 21 | var GRID_ATTACH_IMAGES = 'GRID_ATTACH_IMAGES'; 22 | exports.GRID_ATTACH_IMAGES = GRID_ATTACH_IMAGES; 23 | var GRID_ATTACH_IMAGE = 'GRID_ATTACH_IMAGE'; 24 | exports.GRID_ATTACH_IMAGE = GRID_ATTACH_IMAGE; 25 | var GRID_ADD_BLOCK = 'GRID_ADD_BLOCK'; 26 | exports.GRID_ADD_BLOCK = GRID_ADD_BLOCK; 27 | var GRID_REMOVE_BLOCK = 'GRID_REMOVE_BLOCK'; 28 | exports.GRID_REMOVE_BLOCK = GRID_REMOVE_BLOCK; 29 | var GRID_SHUFFLE_BLOCKS = 'GRID_SHUFFLE_BLOCKS'; 30 | exports.GRID_SHUFFLE_BLOCKS = GRID_SHUFFLE_BLOCKS; 31 | var GRID_DUPLICATE_BLOCK = 'GRID_DUPLICATE_BLOCK'; 32 | exports.GRID_DUPLICATE_BLOCK = GRID_DUPLICATE_BLOCK; 33 | var GRID_UPDATE_BLOCKS = 'GRID_UPDATE_BLOCKS'; 34 | exports.GRID_UPDATE_BLOCKS = GRID_UPDATE_BLOCKS; 35 | var GRID_ACTIVE_BLOCK = 'GRID_ACTIVE_BLOCK'; 36 | exports.GRID_ACTIVE_BLOCK = GRID_ACTIVE_BLOCK; 37 | var GRID_SETTING_UPDATE = 'GRID_SETTING_UPDATE'; 38 | exports.GRID_SETTING_UPDATE = GRID_SETTING_UPDATE; 39 | var GRID_CHANGE_COLOR = 'GRID_CHANGE_COLOR'; 40 | exports.GRID_CHANGE_COLOR = GRID_CHANGE_COLOR; 41 | var CROPPER_OPEN = 'CROPPER_OPEN'; 42 | exports.CROPPER_OPEN = CROPPER_OPEN; 43 | var CROPPER_CLOSE = 'CROPPER_CLOSE'; 44 | exports.CROPPER_CLOSE = CROPPER_CLOSE; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Side/selectItems.js: -------------------------------------------------------------------------------- 1 | let firstSelectIdx = null; 2 | 3 | 4 | /** 5 | * Change active 6 | * 7 | * @param {Number} key 8 | * @param {Object} item 9 | * @param {Number} start 10 | * @param {Number} end 11 | * @param {String} type press key name and select type 12 | * @return {Object} 13 | */ 14 | function changeActive(key, item, start, end, type) 15 | { 16 | switch(type) 17 | { 18 | case 'add': 19 | return { 20 | key: key, 21 | active: (key === end) ? !item.active : item.active 22 | }; 23 | case 'range': 24 | start = start || 0; 25 | if (start < end) 26 | { 27 | return { 28 | key: key, 29 | active: key >= start && key <= end 30 | }; 31 | } 32 | else 33 | { 34 | return { 35 | key: key, 36 | active: key <= start && key >= end 37 | }; 38 | } 39 | return item; 40 | } 41 | 42 | // not found type 43 | if (key === end) 44 | { 45 | return { key: key, active: !item.active }; 46 | } 47 | else 48 | { 49 | return { key: key, active: false }; 50 | } 51 | } 52 | 53 | 54 | /** 55 | * select items 56 | * 57 | * @param {Object} props 58 | * @param {Number} key 59 | * @return {Object} 60 | */ 61 | function selectItems(props, key) 62 | { 63 | const { keyName } = props.keyboard; 64 | const { files } = props.tree.side; 65 | let type = null; 66 | let selected = false; 67 | let items = {}; 68 | 69 | if (keyName !== 'SHIFT') 70 | { 71 | let currentItem = null; 72 | Object.keys(props.tree.side.files).forEach(k => { 73 | let obj = props.tree.side.files[k]; 74 | if (parseInt(k) === key) 75 | { 76 | currentItem = obj; 77 | return false; 78 | } 79 | }); 80 | firstSelectIdx = (currentItem.active === true) ? null : key; 81 | if (!firstSelectIdx && currentItem.active === true) 82 | { 83 | firstSelectIdx = currentItem.key; 84 | } 85 | } 86 | 87 | switch (keyName) { 88 | case 'CTRL': 89 | case 'CMD': 90 | type = 'add'; 91 | break; 92 | case 'SHIFT': 93 | type = 'range'; 94 | } 95 | 96 | Object.keys(files).forEach(k => { 97 | let obj = changeActive(parseInt(k), files[k], firstSelectIdx, key, type); 98 | if (obj.active) 99 | { 100 | selected = true; 101 | } 102 | items[k] = obj; 103 | }); 104 | 105 | if (!selected) 106 | { 107 | firstSelectIdx = null; 108 | } 109 | 110 | return items; 111 | } 112 | 113 | 114 | export default selectItems; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/icons.scss: -------------------------------------------------------------------------------- 1 | // init icon 2 | %sp-ico { 3 | background: transparent url('./images/sp-ico.png') no-repeat; 4 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi), (min-resolution: 1.33dppx) { 5 | background-image: url('./images/sp-ico@2x.png'); 6 | background-size: 200px 100px; 7 | } 8 | } 9 | .ple-sp-ico { 10 | display: inline-block; text-align: left; text-indent: -9999px; overflow: hidden; 11 | @extend %sp-ico; 12 | } 13 | 14 | 15 | @mixin sp-size($width, $height) { 16 | width: $width; height: $height; 17 | &.ple-abs {margin-left: -#{$width/2}; margin-top: -#{$height/2};} 18 | } 19 | 20 | 21 | // set icon classes 22 | .ple-ico-reply { 23 | background-position: 0 0; 24 | @include sp-size(16px, 13px); 25 | } 26 | .ple-ico-select { 27 | background-position: -26px 0; 28 | @include sp-size(16px, 16px); 29 | } 30 | .ple-ico-upload { 31 | background-position: -52px 0; 32 | @include sp-size(12px, 15px); 33 | } 34 | .ple-ico-trash { 35 | background-position: -74px 0; 36 | @include sp-size(12px, 16px); 37 | } 38 | .ple-ico-arrow-left { 39 | background-position: -96px 0; 40 | @include sp-size(7px, 12px); 41 | } 42 | .ple-ico-arrow-right { 43 | background-position: -112px 0; 44 | @include sp-size(7px, 12px); 45 | } 46 | .ple-ico-empty { 47 | background-position: -130px 0; 48 | @include sp-size(16px, 16px); 49 | } 50 | .ple-ico-palette { 51 | background-position: -156px 0; 52 | @include sp-size(18px, 18px); 53 | } 54 | 55 | .ple-ico-setting { 56 | background-position: 0 -36px; 57 | @include sp-size(16px, 16px); 58 | } 59 | .ple-ico-arrow-random { 60 | background-position: -26px -36px; 61 | @include sp-size(16px, 16px); 62 | } 63 | .ple-ico-plus { 64 | background-position: -52px -36px; 65 | @include sp-size(16px, 16px); 66 | } 67 | .ple-ico-pencil { 68 | background-position: -78px -36px; 69 | @include sp-size(16px, 16px); 70 | } 71 | .ple-ico-duplicate { 72 | background-position: -104px -36px; 73 | @include sp-size(14px, 16px); 74 | } 75 | .ple-ico-close { 76 | background-position: -128px -36px; 77 | @include sp-size(14px, 14px); 78 | } 79 | .ple-ico-resize { 80 | background-position: -152px -36px; 81 | @include sp-size(14px, 14px); 82 | } 83 | .ple-ico-reduction { 84 | background-position: -176px -36px; 85 | @include sp-size(16px, 16px); 86 | } 87 | 88 | .ple-ico-clamp { 89 | background-position: 0 -72px; 90 | @include sp-size(20px, 20px); 91 | } 92 | .ple-ico-close2 { 93 | background-position: -30px -72px; 94 | @include sp-size(24px, 24px); 95 | } -------------------------------------------------------------------------------- /lib/reducers/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var _redux = require("redux"); 11 | 12 | var core = _interopRequireWildcard(require("./core")); 13 | 14 | var body = _interopRequireWildcard(require("./body")); 15 | 16 | var _side = _interopRequireDefault(require("./side")); 17 | 18 | var cropper = _interopRequireWildcard(require("./cropper")); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 21 | 22 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 23 | 24 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 25 | 26 | var _default = (0, _redux.combineReducers)({ 27 | // settings 28 | setting: core.setting, 29 | // api 30 | api: core.api, 31 | // keyboard event 32 | keyboard: core.keyboard, 33 | // element 34 | element: core.element, 35 | // data tree 36 | tree: (0, _redux.combineReducers)({ 37 | side: _side["default"], 38 | body: (0, _redux.combineReducers)({ 39 | setting: body.setting, 40 | grid: body.grid, 41 | activeBlock: body.activeBlock 42 | }), 43 | cropper: (0, _redux.combineReducers)({ 44 | visible: cropper.visible, 45 | item: cropper.item, 46 | key: cropper.key 47 | }) 48 | }) 49 | }); 50 | 51 | exports["default"] = _default; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/lib/resamplingImage.js: -------------------------------------------------------------------------------- 1 | import * as lib from './'; 2 | 3 | // default options 4 | const defaultOptions = { 5 | image: null, 6 | width: 100, 7 | height: 100, 8 | reSampleCount: 1, 9 | bgColor: 'rgba(255,255,255,1)', 10 | cw: 0, 11 | ch: 0, 12 | cx: 0, 13 | cy: 0, 14 | dw: 0, 15 | dh: 0, 16 | dx: 0, 17 | dy: 0, 18 | }; 19 | 20 | 21 | /** 22 | * resize 23 | * 24 | * @param {Object} options 25 | * @param {Number} count 26 | * @param {Canvas} parentCanvas 27 | * @return {Promise} 28 | */ 29 | function resize(options, count, parentCanvas) 30 | { 31 | return new Promise(resolve => { 32 | function func(count, parentCanvas) 33 | { 34 | const max = Math.pow(2, count); 35 | let canvasForResize = new lib.Canvas( 36 | options.width * max, 37 | options.height * max, 38 | options.bgColor 39 | ); 40 | 41 | canvasForResize.ctx.drawImage( 42 | parentCanvas.el, 43 | 0, 44 | 0, 45 | parentCanvas.el.width * 0.5, 46 | parentCanvas.el.height * 0.5 47 | ); 48 | 49 | if (count > 0) 50 | { 51 | func(count - 1, canvasForResize); 52 | } 53 | else 54 | { 55 | resolve(canvasForResize); 56 | } 57 | } 58 | 59 | func(count - 1, parentCanvas); 60 | }); 61 | } 62 | 63 | 64 | /** 65 | * resampling image 66 | * 67 | * @param {Object} options 68 | * @return {Promise} 69 | */ 70 | export default function resamplingImage(options) 71 | { 72 | // assign options 73 | options = Object.assign({}, defaultOptions, options); 74 | 75 | return new Promise((resolve, reject) => { 76 | if (!options.image) reject('not found image'); 77 | 78 | // set reSample count 79 | options.reSampleCount = Math.min(4, options.reSampleCount); 80 | options.reSampleCount = Math.max(0, options.reSampleCount); 81 | const reSampleMax = Math.pow(2, options.reSampleCount); 82 | 83 | // set canvas 84 | const canvas = new lib.Canvas( 85 | options.width * reSampleMax, 86 | options.height * reSampleMax, 87 | options.bgColor 88 | ); 89 | 90 | canvas.ctx.drawImage( 91 | options.image, 92 | options.cx, // cx 93 | options.cy, // cy 94 | options.cw, // cw 95 | options.ch, // ch 96 | options.dx * reSampleMax, // dx 97 | options.dy * reSampleMax, // dy 98 | options.dw * reSampleMax, // dw 99 | options.dh * reSampleMax, // dh 100 | ); 101 | 102 | if (options.reSampleCount > 0) 103 | { 104 | resize(options, options.reSampleCount, canvas).then(resolve); 105 | } 106 | else 107 | { 108 | resolve(canvas); 109 | } 110 | }); 111 | 112 | } -------------------------------------------------------------------------------- /lib/actions/side.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.visible = visible; 9 | exports.toggle = toggle; 10 | exports.addFiles = addFiles; 11 | exports.removeFiles = removeFiles; 12 | exports.updateSelected = updateSelected; 13 | exports.updateProgress = updateProgress; 14 | 15 | var types = _interopRequireWildcard(require("./types")); 16 | 17 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 18 | 19 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 20 | 21 | // control visible side bar 22 | function visible(sw) { 23 | return { 24 | type: types.SIDE_VISIBLE, 25 | value: sw 26 | }; 27 | } // toggle side bar 28 | 29 | 30 | function toggle() { 31 | return { 32 | type: types.SIDE_TOGGLE 33 | }; 34 | } 35 | 36 | function addFiles() { 37 | var files = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 38 | return { 39 | type: types.SIDE_ADD_FILES, 40 | files: files 41 | }; 42 | } 43 | 44 | function removeFiles() { 45 | var keys = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 46 | return { 47 | type: types.SIDE_REMOVE_FILES, 48 | keys: keys 49 | }; 50 | } 51 | 52 | function updateSelected(value) { 53 | return { 54 | type: types.SIDE_UPDATE_SELECTED, 55 | value: value 56 | }; 57 | } 58 | 59 | function updateProgress(percent) { 60 | return { 61 | type: types.SIDE_UPDATE_PROGRESS, 62 | value: percent 63 | }; 64 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/lib/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * is touch device 3 | * 4 | * @returns {boolean} 5 | */ 6 | export function isTouchDevice() 7 | { 8 | return ( 9 | ('ontouchstart' in window) || 10 | (navigator.MaxTouchPoints > 0) || 11 | (navigator.msMaxTouchPoints > 0) 12 | ); 13 | } 14 | 15 | 16 | /** 17 | * Sleep 18 | * 19 | * @param {Number} time 20 | * @param {String} id 21 | * @return {Promise} 22 | */ 23 | export function sleep(time, id='pleTimer') 24 | { 25 | return new Promise((resolve) => { 26 | window[id] = setTimeout(resolve, time); 27 | }); 28 | } 29 | 30 | 31 | /** 32 | * Get image size 33 | * 34 | * @param {String} src 35 | * @return {Promise} 36 | */ 37 | export function getImageSize(src) 38 | { 39 | return new Promise((resolve, reject) => { 40 | if (!(src && typeof src === 'string')) reject(); 41 | 42 | let img = document.createElement('img'); 43 | 44 | img.onload = function() 45 | { 46 | resolve({ 47 | width: img.naturalWidth, 48 | height: img.naturalHeight, 49 | ratio: img.naturalHeight / img.naturalWidth, 50 | }); 51 | }; 52 | 53 | img.onerror = function() 54 | { 55 | reject(); 56 | }; 57 | 58 | img.src = src; 59 | }); 60 | } 61 | 62 | 63 | /** 64 | * get css prefix 65 | * 66 | * @variation {Object} 67 | */ 68 | export const cssPrefix = (function() 69 | { 70 | const styles = window.getComputedStyle(document.documentElement, ''); 71 | const pre = (Array.prototype.slice 72 | .call(styles) 73 | .join('') 74 | .match(/-(moz|webkit|ms)-/) || (styles.OLink === '' && ['', 'o']) 75 | )[1]; 76 | const dom = ('WebKit|Moz|MS|O').match(new RegExp('(' + pre + ')', 'i'))[1]; 77 | 78 | return { 79 | dom: dom, 80 | lowercase: pre, 81 | css: `-${pre}-`, 82 | js: pre[0].toUpperCase() + pre.substr(1) 83 | }; 84 | })(); 85 | 86 | 87 | /** 88 | * check support css 89 | * 90 | * @param {String} key 91 | * @param {String} value 92 | * @return {Boolean} 93 | */ 94 | export function checkSupportCss(key, value) 95 | { 96 | if (CSS && CSS.supports) 97 | { 98 | return CSS.supports(key, value); 99 | } 100 | } 101 | 102 | /** 103 | * load image 104 | * 105 | * @param {String} src 106 | * @return {Promise} 107 | */ 108 | export function loadImage(src) 109 | { 110 | return new Promise((resolve) => { 111 | if (!src) 112 | { 113 | resolve(null); 114 | return; 115 | } 116 | 117 | let image = new Image(); 118 | 119 | image.onload = function(e) 120 | { 121 | resolve(image); 122 | }; 123 | image.onError = function(e) 124 | { 125 | resolve(null); 126 | }; 127 | 128 | image.crossOrigin = 'Anonymous'; 129 | image.src = src; 130 | }); 131 | } -------------------------------------------------------------------------------- /lib/Container/Side/selectItems.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = void 0; 7 | var firstSelectIdx = null; 8 | /** 9 | * Change active 10 | * 11 | * @param {Number} key 12 | * @param {Object} item 13 | * @param {Number} start 14 | * @param {Number} end 15 | * @param {String} type press key name and select type 16 | * @return {Object} 17 | */ 18 | 19 | function changeActive(key, item, start, end, type) { 20 | switch (type) { 21 | case 'add': 22 | return { 23 | key: key, 24 | active: key === end ? !item.active : item.active 25 | }; 26 | 27 | case 'range': 28 | start = start || 0; 29 | 30 | if (start < end) { 31 | return { 32 | key: key, 33 | active: key >= start && key <= end 34 | }; 35 | } else { 36 | return { 37 | key: key, 38 | active: key <= start && key >= end 39 | }; 40 | } 41 | 42 | return item; 43 | } // not found type 44 | 45 | 46 | if (key === end) { 47 | return { 48 | key: key, 49 | active: !item.active 50 | }; 51 | } else { 52 | return { 53 | key: key, 54 | active: false 55 | }; 56 | } 57 | } 58 | /** 59 | * select items 60 | * 61 | * @param {Object} props 62 | * @param {Number} key 63 | * @return {Object} 64 | */ 65 | 66 | 67 | function selectItems(props, key) { 68 | var keyName = props.keyboard.keyName; 69 | var files = props.tree.side.files; 70 | var type = null; 71 | var selected = false; 72 | var items = {}; 73 | 74 | if (keyName !== 'SHIFT') { 75 | var currentItem = null; 76 | Object.keys(props.tree.side.files).forEach(function (k) { 77 | var obj = props.tree.side.files[k]; 78 | 79 | if (parseInt(k) === key) { 80 | currentItem = obj; 81 | return false; 82 | } 83 | }); 84 | firstSelectIdx = currentItem.active === true ? null : key; 85 | 86 | if (!firstSelectIdx && currentItem.active === true) { 87 | firstSelectIdx = currentItem.key; 88 | } 89 | } 90 | 91 | switch (keyName) { 92 | case 'CTRL': 93 | case 'CMD': 94 | type = 'add'; 95 | break; 96 | 97 | case 'SHIFT': 98 | type = 'range'; 99 | } 100 | 101 | Object.keys(files).forEach(function (k) { 102 | var obj = changeActive(parseInt(k), files[k], firstSelectIdx, key, type); 103 | 104 | if (obj.active) { 105 | selected = true; 106 | } 107 | 108 | items[k] = obj; 109 | }); 110 | 111 | if (!selected) { 112 | firstSelectIdx = null; 113 | } 114 | 115 | return items; 116 | } 117 | 118 | var _default = selectItems; 119 | exports["default"] = _default; -------------------------------------------------------------------------------- /lib/reducers/cropper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.visible = visible; 9 | exports.item = item; 10 | exports.key = key; 11 | 12 | var types = _interopRequireWildcard(require("../actions/types")); 13 | 14 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 15 | 16 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 17 | 18 | function visible() { 19 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; 20 | var action = arguments.length > 1 ? arguments[1] : undefined; 21 | 22 | switch (action.type) { 23 | case types.CROPPER_OPEN: 24 | return true; 25 | 26 | case types.CROPPER_CLOSE: 27 | return false; 28 | 29 | default: 30 | return state; 31 | } 32 | } 33 | 34 | function item() { 35 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; 36 | var action = arguments.length > 1 ? arguments[1] : undefined; 37 | 38 | switch (action.type) { 39 | case types.CROPPER_OPEN: 40 | return action.item; 41 | 42 | case types.CROPPER_CLOSE: 43 | return null; 44 | 45 | default: 46 | return state; 47 | } 48 | } 49 | 50 | function key() { 51 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; 52 | var action = arguments.length > 1 ? arguments[1] : undefined; 53 | 54 | switch (action.type) { 55 | case types.CROPPER_OPEN: 56 | return action.key; 57 | 58 | case types.CROPPER_CLOSE: 59 | return null; 60 | 61 | default: 62 | return state; 63 | } 64 | } -------------------------------------------------------------------------------- /lib/lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | Object.defineProperty(exports, "uploader", { 9 | enumerable: true, 10 | get: function get() { 11 | return _uploader["default"]; 12 | } 13 | }); 14 | Object.defineProperty(exports, "makingImage", { 15 | enumerable: true, 16 | get: function get() { 17 | return _makingImage["default"]; 18 | } 19 | }); 20 | Object.defineProperty(exports, "Canvas", { 21 | enumerable: true, 22 | get: function get() { 23 | return _Canvas["default"]; 24 | } 25 | }); 26 | Object.defineProperty(exports, "resamplingImage", { 27 | enumerable: true, 28 | get: function get() { 29 | return _resamplingImage["default"]; 30 | } 31 | }); 32 | exports.color = exports.object = exports.number = exports.util = void 0; 33 | 34 | var util = _interopRequireWildcard(require("./util")); 35 | 36 | exports.util = util; 37 | 38 | var number = _interopRequireWildcard(require("./number")); 39 | 40 | exports.number = number; 41 | 42 | var object = _interopRequireWildcard(require("./object")); 43 | 44 | exports.object = object; 45 | 46 | var color = _interopRequireWildcard(require("./color")); 47 | 48 | exports.color = color; 49 | 50 | var _uploader = _interopRequireDefault(require("./uploader")); 51 | 52 | var _makingImage = _interopRequireDefault(require("./makingImage")); 53 | 54 | var _Canvas = _interopRequireDefault(require("./Canvas")); 55 | 56 | var _resamplingImage = _interopRequireDefault(require("./resamplingImage")); 57 | 58 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 59 | 60 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 61 | 62 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/lib/uploader.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery/dist/jquery.slim'; 2 | 3 | 4 | /** 5 | * local upload 6 | * 7 | * @param {File} file 8 | * @return {Promise} 9 | */ 10 | function local(file) 11 | { 12 | const defer = $.Deferred(); 13 | const reader = new FileReader(); 14 | 15 | reader.addEventListener('load', function(e) { 16 | defer.resolve({ 17 | name: file.name, 18 | type: file.type, 19 | size: file.size, 20 | url: e.target.result, 21 | }); 22 | }); 23 | reader.addEventListener('error', function(e) { 24 | defer.reject(e.target); 25 | }); 26 | 27 | reader.readAsDataURL(file); 28 | 29 | return defer.promise(); 30 | } 31 | 32 | /** 33 | * external upload 34 | * 35 | * @param {String} script 36 | * @param {File} file 37 | * @return {Promise} 38 | */ 39 | function external(script, file) 40 | { 41 | const defer = $.Deferred(); 42 | const xhr = new XMLHttpRequest(); 43 | 44 | if (!(typeof FormData === 'function' || typeof FormData === 'object')) 45 | { 46 | defer.reject('not support browser', file); 47 | return null; 48 | } 49 | const formData = new FormData(); 50 | 51 | function onLoad(e) 52 | { 53 | try { 54 | const result = JSON.parse(e.target.responseText); 55 | if (result.state === 'success') 56 | { 57 | defer.resolve(result.data); 58 | } 59 | else 60 | { 61 | throw 'server error'; 62 | } 63 | } catch(e) { 64 | defer.reject(e); 65 | } 66 | } 67 | 68 | function onProgress(event) 69 | { 70 | const { loaded, total } = event; 71 | defer.notify('progress', loaded, total); 72 | } 73 | 74 | formData.append('files[]', file); 75 | xhr.open('post', script, true); 76 | 77 | xhr.addEventListener('load', onLoad); 78 | xhr.upload.addEventListener('progress', onProgress); 79 | 80 | xhr.send(formData); 81 | 82 | // start queue 83 | defer.notify('start'); 84 | 85 | return defer.promise(); 86 | } 87 | 88 | 89 | /** 90 | * upload multiple files 91 | * 92 | */ 93 | export default function uploader(files, script) 94 | { 95 | const defer = $.Deferred(); 96 | const total = files.length; 97 | let count = 0; 98 | 99 | function upload() 100 | { 101 | if (count >= total) 102 | { 103 | defer.resolve(); 104 | return; 105 | } 106 | 107 | if (script) 108 | { 109 | external(script, files[count]) 110 | .done((data) => { 111 | defer.notify('done', { count, data }); 112 | count++; 113 | upload(); 114 | }) 115 | .progress((state, loaded, total) => { 116 | defer.notify( 117 | state, 118 | state === 'progress' && { loaded, total }, 119 | ); 120 | }) 121 | .fail((error) => { 122 | defer.reject(error); 123 | }); 124 | } 125 | else 126 | { 127 | local(files[count]) 128 | .done((data) => { 129 | defer.notify('done', { count, data }); 130 | count++; 131 | upload(); 132 | }) 133 | .fail(() => {}); 134 | } 135 | } 136 | 137 | upload(0); 138 | 139 | return defer.promise(); 140 | } -------------------------------------------------------------------------------- /lib/lib/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.isTouchDevice = isTouchDevice; 7 | exports.sleep = sleep; 8 | exports.getImageSize = getImageSize; 9 | exports.checkSupportCss = checkSupportCss; 10 | exports.loadImage = loadImage; 11 | exports.cssPrefix = void 0; 12 | 13 | /** 14 | * is touch device 15 | * 16 | * @returns {boolean} 17 | */ 18 | function isTouchDevice() { 19 | return 'ontouchstart' in window || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0; 20 | } 21 | /** 22 | * Sleep 23 | * 24 | * @param {Number} time 25 | * @param {String} id 26 | * @return {Promise} 27 | */ 28 | 29 | 30 | function sleep(time) { 31 | var id = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'pleTimer'; 32 | return new Promise(function (resolve) { 33 | window[id] = setTimeout(resolve, time); 34 | }); 35 | } 36 | /** 37 | * Get image size 38 | * 39 | * @param {String} src 40 | * @return {Promise} 41 | */ 42 | 43 | 44 | function getImageSize(src) { 45 | return new Promise(function (resolve, reject) { 46 | if (!(src && typeof src === 'string')) reject(); 47 | var img = document.createElement('img'); 48 | 49 | img.onload = function () { 50 | resolve({ 51 | width: img.naturalWidth, 52 | height: img.naturalHeight, 53 | ratio: img.naturalHeight / img.naturalWidth 54 | }); 55 | }; 56 | 57 | img.onerror = function () { 58 | reject(); 59 | }; 60 | 61 | img.src = src; 62 | }); 63 | } 64 | /** 65 | * get css prefix 66 | * 67 | * @variation {Object} 68 | */ 69 | 70 | 71 | var cssPrefix = function () { 72 | var styles = window.getComputedStyle(document.documentElement, ''); 73 | var pre = (Array.prototype.slice.call(styles).join('').match(/-(moz|webkit|ms)-/) || styles.OLink === '' && ['', 'o'])[1]; 74 | var dom = 'WebKit|Moz|MS|O'.match(new RegExp('(' + pre + ')', 'i'))[1]; 75 | return { 76 | dom: dom, 77 | lowercase: pre, 78 | css: "-".concat(pre, "-"), 79 | js: pre[0].toUpperCase() + pre.substr(1) 80 | }; 81 | }(); 82 | /** 83 | * check support css 84 | * 85 | * @param {String} key 86 | * @param {String} value 87 | * @return {Boolean} 88 | */ 89 | 90 | 91 | exports.cssPrefix = cssPrefix; 92 | 93 | function checkSupportCss(key, value) { 94 | if (CSS && CSS.supports) { 95 | return CSS.supports(key, value); 96 | } 97 | } 98 | /** 99 | * load image 100 | * 101 | * @param {String} src 102 | * @return {Promise} 103 | */ 104 | 105 | 106 | function loadImage(src) { 107 | return new Promise(function (resolve) { 108 | if (!src) { 109 | resolve(null); 110 | return; 111 | } 112 | 113 | var image = new Image(); 114 | 115 | image.onload = function (e) { 116 | resolve(image); 117 | }; 118 | 119 | image.onError = function (e) { 120 | resolve(null); 121 | }; 122 | 123 | image.crossOrigin = 'Anonymous'; 124 | image.src = src; 125 | }); 126 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/components/cropper.scss: -------------------------------------------------------------------------------- 1 | /* Popup - Cropper */ 2 | .ple-cropper { 3 | position: absolute; 4 | z-index: 99999; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | bottom: 0; 9 | 10 | &__bg { 11 | display: block; 12 | position: fixed; 13 | z-index: 1; 14 | top: 0; 15 | left: 0; 16 | right: 0; 17 | bottom: 0; 18 | background: #000; 19 | background: rgba(0,0,0,.86); 20 | } 21 | &__wrap { 22 | position: absolute; 23 | z-index: 2; 24 | left: 10px; 25 | top: 10px; 26 | width: 100px; 27 | height: 100px; 28 | } 29 | &__nav { 30 | $self: &; 31 | position: absolute; 32 | right: -50px; 33 | top: 0; 34 | button { 35 | position: relative; 36 | display: block; 37 | margin: 0 0 1px; 38 | width: 40px; 39 | height: 40px; 40 | background: #888; 41 | text-align: center; 42 | text-indent: -9999px; 43 | border: none; 44 | cursor: pointer; 45 | border-radius: 0; 46 | i { 47 | position: absolute; 48 | display: block; 49 | left: 50%; 50 | top: 50%; 51 | } 52 | &#{$self}-active { 53 | background: $col-active; 54 | } 55 | } 56 | } 57 | } 58 | 59 | 60 | .ple-cropperBlock { 61 | position: relative; 62 | width: 100%; 63 | height: 100%; 64 | margin: 0; 65 | background: #fff; 66 | 67 | &__image { 68 | display: block; 69 | position: absolute; 70 | left: 0; 71 | top: 0; 72 | &-cover { 73 | right: 0; 74 | bottom: 0; 75 | background-size: cover; 76 | background-position: 50% 50%; 77 | } 78 | &-resize { 79 | width: 100%; 80 | height: 100%; 81 | overflow: hidden; 82 | img { 83 | display: block; 84 | width: 100%; 85 | } 86 | } 87 | } 88 | 89 | &__control { 90 | display: none; 91 | position: absolute; 92 | left: 0; 93 | top: 0; 94 | width: 0; 95 | height: 0; 96 | cursor: move; 97 | &:before { 98 | content: ''; 99 | display: block; 100 | position: absolute; 101 | left: 0; 102 | right: 0; 103 | top: 0; 104 | bottom: 0; 105 | border: 1px dashed #000; 106 | } 107 | &:after { 108 | content: ''; 109 | display: block; 110 | position: absolute; 111 | left: 1px; 112 | top: 1px; 113 | right: -1px; 114 | bottom: -1px; 115 | border: 1px dashed #fff; 116 | } 117 | &-active { 118 | display: block; 119 | } 120 | } 121 | &__resize { 122 | position: absolute; 123 | display: block; 124 | z-index: 2; 125 | width: 34px; 126 | height: 34px; 127 | border: none; 128 | background: none; 129 | text-indent: -9999px; 130 | outline: 0; 131 | 132 | > i { 133 | display: block; 134 | position: absolute; 135 | left: 50%; 136 | top: 50%; 137 | } 138 | &-lt { 139 | left: -10px; 140 | top: -10px; 141 | cursor: nwse-resize; 142 | } 143 | &-rt { 144 | right: -11px; 145 | top: -10px; 146 | transform: rotate(90deg); 147 | cursor: nesw-resize; 148 | } 149 | &-lb { 150 | left: -10px; 151 | bottom: -11px; 152 | transform: rotate(270deg); 153 | cursor: nesw-resize; 154 | } 155 | &-rb { 156 | right: -12px; 157 | bottom: -12px; 158 | transform: rotate(180deg); 159 | cursor: nwse-resize; 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /lib/actions/body.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.addBlock = addBlock; 9 | exports.removeBlock = removeBlock; 10 | exports.shuffleBlocks = shuffleBlocks; 11 | exports.duplicateBlock = duplicateBlock; 12 | exports.updateBlocks = updateBlocks; 13 | exports.activeBlock = activeBlock; 14 | exports.changeColorBlock = changeColorBlock; 15 | exports.updateSetting = updateSetting; 16 | exports.attachImages = attachImages; 17 | exports.attachImage = attachImage; 18 | 19 | var types = _interopRequireWildcard(require("./types")); 20 | 21 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 22 | 23 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 24 | 25 | function addBlock(value) { 26 | return { 27 | type: types.GRID_ADD_BLOCK, 28 | value: value 29 | }; 30 | } 31 | 32 | function removeBlock(keys) { 33 | return { 34 | type: types.GRID_REMOVE_BLOCK, 35 | keys: keys 36 | }; 37 | } 38 | 39 | function shuffleBlocks(options) { 40 | return { 41 | type: types.GRID_SHUFFLE_BLOCKS, 42 | value: options 43 | }; 44 | } 45 | 46 | function duplicateBlock(keys) { 47 | return { 48 | type: types.GRID_DUPLICATE_BLOCK, 49 | keys: keys 50 | }; 51 | } 52 | 53 | function updateBlocks(blocks) { 54 | return { 55 | type: types.GRID_UPDATE_BLOCKS, 56 | value: blocks 57 | }; 58 | } 59 | 60 | function activeBlock(keys) { 61 | return { 62 | type: types.GRID_ACTIVE_BLOCK, 63 | value: keys 64 | }; 65 | } 66 | 67 | function changeColorBlock(keys, color) { 68 | return { 69 | type: types.GRID_CHANGE_COLOR, 70 | keys: keys, 71 | color: color 72 | }; 73 | } 74 | 75 | function updateSetting(value) { 76 | return { 77 | type: types.GRID_SETTING_UPDATE, 78 | value: value 79 | }; 80 | } 81 | 82 | function attachImages(images, cols, activeBlocks) { 83 | return { 84 | type: types.GRID_ATTACH_IMAGES, 85 | value: images, 86 | columns: cols, 87 | activeBlocks: activeBlocks 88 | }; 89 | } 90 | 91 | function attachImage(keys, image) { 92 | return { 93 | type: types.GRID_ATTACH_IMAGE, 94 | keys: keys, 95 | image: image 96 | }; 97 | } -------------------------------------------------------------------------------- /lib/API/Cropper.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var actions = _interopRequireWildcard(require("../actions")); 11 | 12 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 13 | 14 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 15 | 16 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 17 | 18 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 19 | 20 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 21 | 22 | var Cropper = /*#__PURE__*/function () { 23 | function Cropper(store) { 24 | _classCallCheck(this, Cropper); 25 | 26 | this.store = store; 27 | } 28 | /** 29 | * open cropper 30 | * 31 | * @param {Number} key 32 | */ 33 | 34 | 35 | _createClass(Cropper, [{ 36 | key: "open", 37 | value: function open(key) { 38 | var state = this.store.getState(); 39 | var item = null; 40 | 41 | try { 42 | item = state.tree.body.grid[key]; 43 | if (!item.image) throw 'Not found image in item'; 44 | } catch (e) { 45 | alert(e); 46 | return; 47 | } 48 | 49 | this.store.dispatch(actions.cropper.open(key, item)); 50 | } 51 | }, { 52 | key: "close", 53 | value: 54 | /** 55 | * close cropper 56 | * 57 | * @param {Number} key 58 | * @param {String} position 59 | * @param {String} size 60 | */ 61 | function close(key, position, size) { 62 | this.store.dispatch(actions.cropper.close(key, position, size)); 63 | } 64 | }]); 65 | 66 | return Cropper; 67 | }(); 68 | 69 | var _default = Cropper; 70 | exports["default"] = _default; -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 4 | const TerserJSPlugin = require('terser-webpack-plugin'); 5 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 6 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 7 | 8 | const config = (env, options) => { 9 | const isDev = options.mode === 'development'; 10 | 11 | // base 12 | let out = { 13 | name: 'PhotoLayoutEditor', 14 | mode: isDev ? 'development' : 'production', 15 | resolve: { 16 | extensions: [ '.js', '.jsx' ], 17 | alias: { 18 | '~': path.resolve(__dirname, './src'), 19 | }, 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(js|jsx)$/, 25 | exclude: /node_modules/, 26 | use: { 27 | loader: 'babel-loader', 28 | } 29 | }, 30 | { 31 | test: /\.s?css$/, 32 | use: [ 33 | MiniCssExtractPlugin.loader, 34 | 'css-loader', 35 | 'sass-loader', 36 | ].filter(Boolean), 37 | }, 38 | { 39 | test: /\.(jpg|png|gif)$/, 40 | loader: 'file-loader', 41 | options: { 42 | publicPath: './', 43 | name: '[name].[ext]', 44 | limit: 10000, 45 | }, 46 | }, 47 | isDev && { 48 | test: /\.html$/, 49 | use: [ 50 | { 51 | loader: "html-loader", 52 | options: { minimize: false } 53 | } 54 | ] 55 | }, 56 | ].filter(Boolean), 57 | }, 58 | plugins: [ 59 | new MiniCssExtractPlugin({ filename: '[name].css' }), 60 | isDev && new HtmlWebpackPlugin({ template: './src/development/index.html' }), 61 | ].filter(Boolean), 62 | optimization: {}, 63 | }; 64 | 65 | if (isDev) 66 | { 67 | /** 68 | * Development 69 | */ 70 | out.entry = { 71 | // app: './src/development/index.js', 72 | app: path.join(__dirname, 'src/development/index.js'), 73 | }; 74 | out.output = { 75 | publicPath: '/', 76 | filename: '[name].js', 77 | chunkFilename: '[name].js', 78 | }; 79 | out.devtool = 'inline-source-map'; 80 | out.devServer = { 81 | hot: true, 82 | host: '0.0.0.0', 83 | port: options.port || 3000, 84 | stats: 'minimal', 85 | //before: require('./upload/script-node'), 86 | historyApiFallback: true, 87 | noInfo: true, 88 | }; 89 | } 90 | else 91 | { 92 | /** 93 | * Production 94 | */ 95 | out.entry = { 96 | PhotoLayoutEditor: './src/production/index.js', 97 | }; 98 | out.output = { 99 | path: __dirname + '/build', 100 | filename: '[name].js', 101 | publicPath: './', 102 | library: '[name]', 103 | libraryTarget: 'umd', 104 | libraryExport: 'default' 105 | }; 106 | out.externals = { 107 | 'jquery/dist/jquery.slim': 'jQuery', 108 | 'react': 'React', 109 | 'react-dom': 'ReactDOM' 110 | }; 111 | out.optimization = { 112 | minimize: true, 113 | minimizer: [ 114 | new TerserJSPlugin({}), 115 | new CssMinimizerPlugin(), 116 | ], 117 | }; 118 | } 119 | 120 | return out; 121 | }; 122 | 123 | module.exports = config; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/API/Util.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery/dist/jquery.slim'; 2 | import * as actions from '../actions'; 3 | import * as lib from '../lib'; 4 | import { isArray } from "../lib/object"; 5 | 6 | 7 | class Util { 8 | 9 | constructor(store) 10 | { 11 | this.store = store; 12 | } 13 | 14 | /** 15 | * Toggle side 16 | * 17 | * @param {Boolean|undefined} sw 18 | */ 19 | toggleSide(sw=undefined) 20 | { 21 | try { 22 | const currentSw = this.store.getState().tree.side.visible; 23 | const targetSw = (typeof sw === 'undefined') ? !currentSw : sw; 24 | 25 | this.store.dispatch(actions.side.visible(targetSw)); 26 | } catch(e) { 27 | console.warn('Error action', e) 28 | } 29 | }; 30 | 31 | /** 32 | * export 33 | * 34 | * @param {String} type 35 | * @param {Boolean} isInsertImage 36 | * @return {Array|Object} 37 | */ 38 | export(type=null, isInsertImage=true) 39 | { 40 | if (!type) return null; 41 | 42 | const state = this.store.getState(); 43 | 44 | function side() 45 | { 46 | if (!isInsertImage) return []; 47 | let keys = state.api.side.getKeys('all'); 48 | return state.api.side.getImages(keys); 49 | } 50 | function grid() 51 | { 52 | let result = state.api.grid.getBlocks('all'); 53 | return Object.keys(result).sort().map(o => { 54 | delete result[o].indexPrefix; 55 | if (!isInsertImage && result[o] && result[o].image) delete result[o].image; 56 | return result[o]; 57 | }); 58 | } 59 | function preference() 60 | { 61 | return state.api.grid.getPreference(); 62 | } 63 | 64 | switch(type) 65 | { 66 | case 'side': 67 | return side(); 68 | case 'grid': 69 | return grid(); 70 | case 'preference': 71 | return state.api.grid.getPreference(); 72 | case 'all': 73 | return { 74 | side: side(), 75 | grid: grid(), 76 | preference: preference(), 77 | }; 78 | break; 79 | } 80 | 81 | return null; 82 | } 83 | 84 | /** 85 | * import 86 | * 87 | * @param {Array|Object} value 88 | * @param {Boolean} replace 89 | */ 90 | import(value=null, replace=false) 91 | { 92 | if (!value) return; 93 | 94 | const state = this.store.getState(); 95 | 96 | function side(value=null) 97 | { 98 | if (replace) state.api.side.clear(); 99 | state.api.side.add(value); 100 | } 101 | function grid(value=null) 102 | { 103 | if (replace) 104 | { 105 | let keys = state.api.grid.getKeys('all'); 106 | state.api.grid.remove(keys); 107 | } 108 | state.api.grid.add(value); 109 | } 110 | 111 | if (value.side && isArray(value.side)) side(value.side); 112 | if (value.grid && isArray(value.grid)) grid(value.grid); 113 | if (value.preference && typeof value.preference === 'object') state.api.grid.setPreference(value.preference); 114 | } 115 | 116 | /** 117 | * make image 118 | * 119 | * @param {String} format (jpg|png) 120 | * @param {Number} quality 121 | * @param {Number} sampling 122 | * @param {String} output 123 | * @return {Promise} 124 | */ 125 | makeImage(format='jpg', quality=.75, sampling=2, output=null) 126 | { 127 | const defer = $.Deferred(); 128 | const state = this.store.getState(); 129 | const { setting, grid } = state.tree.body; 130 | 131 | lib.makingImage( 132 | state.element.querySelector('.ple-grid'), 133 | { setting, grid }, 134 | { format, quality, sampling, output } 135 | ).done((image) => { 136 | defer.resolve(image); 137 | }).progress((total, current, image) => { 138 | defer.notify(total, current, image); 139 | }).fail((error) => { 140 | defer.reject(error); 141 | }); 142 | 143 | return defer.promise(); 144 | } 145 | 146 | } 147 | 148 | 149 | export default Util; 150 | -------------------------------------------------------------------------------- /lib/lib/resamplingImage.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = resamplingImage; 9 | 10 | var lib = _interopRequireWildcard(require("./")); 11 | 12 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 13 | 14 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 15 | 16 | // default options 17 | var defaultOptions = { 18 | image: null, 19 | width: 100, 20 | height: 100, 21 | reSampleCount: 1, 22 | bgColor: 'rgba(255,255,255,1)', 23 | cw: 0, 24 | ch: 0, 25 | cx: 0, 26 | cy: 0, 27 | dw: 0, 28 | dh: 0, 29 | dx: 0, 30 | dy: 0 31 | }; 32 | /** 33 | * resize 34 | * 35 | * @param {Object} options 36 | * @param {Number} count 37 | * @param {Canvas} parentCanvas 38 | * @return {Promise} 39 | */ 40 | 41 | function resize(options, count, parentCanvas) { 42 | return new Promise(function (resolve) { 43 | function func(count, parentCanvas) { 44 | var max = Math.pow(2, count); 45 | var canvasForResize = new lib.Canvas(options.width * max, options.height * max, options.bgColor); 46 | canvasForResize.ctx.drawImage(parentCanvas.el, 0, 0, parentCanvas.el.width * 0.5, parentCanvas.el.height * 0.5); 47 | 48 | if (count > 0) { 49 | func(count - 1, canvasForResize); 50 | } else { 51 | resolve(canvasForResize); 52 | } 53 | } 54 | 55 | func(count - 1, parentCanvas); 56 | }); 57 | } 58 | /** 59 | * resampling image 60 | * 61 | * @param {Object} options 62 | * @return {Promise} 63 | */ 64 | 65 | 66 | function resamplingImage(options) { 67 | // assign options 68 | options = Object.assign({}, defaultOptions, options); 69 | return new Promise(function (resolve, reject) { 70 | if (!options.image) reject('not found image'); // set reSample count 71 | 72 | options.reSampleCount = Math.min(4, options.reSampleCount); 73 | options.reSampleCount = Math.max(0, options.reSampleCount); 74 | var reSampleMax = Math.pow(2, options.reSampleCount); // set canvas 75 | 76 | var canvas = new lib.Canvas(options.width * reSampleMax, options.height * reSampleMax, options.bgColor); 77 | canvas.ctx.drawImage(options.image, options.cx, // cx 78 | options.cy, // cy 79 | options.cw, // cw 80 | options.ch, // ch 81 | options.dx * reSampleMax, // dx 82 | options.dy * reSampleMax, // dy 83 | options.dw * reSampleMax, // dw 84 | options.dh * reSampleMax // dh 85 | ); 86 | 87 | if (options.reSampleCount > 0) { 88 | resize(options, options.reSampleCount, canvas).then(resolve); 89 | } else { 90 | resolve(canvas); 91 | } 92 | }); 93 | } -------------------------------------------------------------------------------- /lib/lib/uploader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports["default"] = uploader; 7 | 8 | var _jquery = _interopRequireDefault(require("jquery/dist/jquery.slim")); 9 | 10 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 11 | 12 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 13 | 14 | /** 15 | * local upload 16 | * 17 | * @param {File} file 18 | * @return {Promise} 19 | */ 20 | function local(file) { 21 | var defer = _jquery["default"].Deferred(); 22 | 23 | var reader = new FileReader(); 24 | reader.addEventListener('load', function (e) { 25 | defer.resolve({ 26 | name: file.name, 27 | type: file.type, 28 | size: file.size, 29 | url: e.target.result 30 | }); 31 | }); 32 | reader.addEventListener('error', function (e) { 33 | defer.reject(e.target); 34 | }); 35 | reader.readAsDataURL(file); 36 | return defer.promise(); 37 | } 38 | /** 39 | * external upload 40 | * 41 | * @param {String} script 42 | * @param {File} file 43 | * @return {Promise} 44 | */ 45 | 46 | 47 | function external(script, file) { 48 | var defer = _jquery["default"].Deferred(); 49 | 50 | var xhr = new XMLHttpRequest(); 51 | 52 | if (!(typeof FormData === 'function' || (typeof FormData === "undefined" ? "undefined" : _typeof(FormData)) === 'object')) { 53 | defer.reject('not support browser', file); 54 | return null; 55 | } 56 | 57 | var formData = new FormData(); 58 | 59 | function onLoad(e) { 60 | try { 61 | var result = JSON.parse(e.target.responseText); 62 | 63 | if (result.state === 'success') { 64 | defer.resolve(result.data); 65 | } else { 66 | throw 'server error'; 67 | } 68 | } catch (e) { 69 | defer.reject(e); 70 | } 71 | } 72 | 73 | function onProgress(event) { 74 | var loaded = event.loaded, 75 | total = event.total; 76 | defer.notify('progress', loaded, total); 77 | } 78 | 79 | formData.append('files[]', file); 80 | xhr.open('post', script, true); 81 | xhr.addEventListener('load', onLoad); 82 | xhr.upload.addEventListener('progress', onProgress); 83 | xhr.send(formData); // start queue 84 | 85 | defer.notify('start'); 86 | return defer.promise(); 87 | } 88 | /** 89 | * upload multiple files 90 | * 91 | */ 92 | 93 | 94 | function uploader(files, script) { 95 | var defer = _jquery["default"].Deferred(); 96 | 97 | var total = files.length; 98 | var count = 0; 99 | 100 | function upload() { 101 | if (count >= total) { 102 | defer.resolve(); 103 | return; 104 | } 105 | 106 | if (script) { 107 | external(script, files[count]).done(function (data) { 108 | defer.notify('done', { 109 | count: count, 110 | data: data 111 | }); 112 | count++; 113 | upload(); 114 | }).progress(function (state, loaded, total) { 115 | defer.notify(state, state === 'progress' && { 116 | loaded: loaded, 117 | total: total 118 | }); 119 | }).fail(function (error) { 120 | defer.reject(error); 121 | }); 122 | } else { 123 | local(files[count]).done(function (data) { 124 | defer.notify('done', { 125 | count: count, 126 | data: data 127 | }); 128 | count++; 129 | upload(); 130 | }).fail(function () {}); 131 | } 132 | } 133 | 134 | upload(0); 135 | return defer.promise(); 136 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Cropper/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import $ from 'jquery/dist/jquery.slim'; 4 | import classNames from 'classnames'; 5 | import Block from './Block'; 6 | import * as lib from '../../lib'; 7 | 8 | 9 | class Cropper extends React.Component { 10 | 11 | constructor(props) 12 | { 13 | super(props); 14 | const { cropper } = props.tree; 15 | 16 | this.imageMeta = null; 17 | this._block = null; 18 | this.$item = $(props.element) 19 | .find('.react-grid-item') 20 | .filter(`[data-key=${cropper.key}]`); 21 | 22 | this.state = { 23 | pending: true, 24 | position: cropper.item.image.position, 25 | size: cropper.item.image.size || 'cover', 26 | width: this.$item.width(), 27 | height: this.$item.height(), 28 | top: this.$item.position().top, 29 | left: this.$item.position().left 30 | }; 31 | } 32 | 33 | componentDidMount() 34 | { 35 | const { props } = this; 36 | const { cropper } = props.tree; 37 | 38 | lib.util.getImageSize(cropper.item.image.src).then((res) => { 39 | this.imageMeta = res; 40 | this.setState({ pending: false }); 41 | }); 42 | } 43 | 44 | /** 45 | * close cropper 46 | * `cropper`를 닫고, 변경된 이미지를 `grid`로 보낸다. 47 | */ 48 | _onClose() 49 | { 50 | const { props } = this; 51 | props.api.cropper.close( 52 | props.tree.cropper.key, 53 | this._block.state.position, 54 | this._block.state.size 55 | ); 56 | } 57 | 58 | /** 59 | * toggle image type 60 | * 직접 리사이즈를 사용하는지 기본(꽉채우는..)타입으로 사용할건지 변경하는 액션 61 | */ 62 | _toggleImageType() 63 | { 64 | const { state, props } = this; 65 | 66 | if (state.size === 'cover') 67 | { 68 | let targetSize = ''; 69 | let ratio = 0; 70 | if (state.height > state.width) 71 | { 72 | ratio = lib.number.getRatioForResize(state.height, this.imageMeta.height); 73 | targetSize = `${parseInt(this.imageMeta.width * ratio)}px ${state.height}px`; 74 | } 75 | else 76 | { 77 | ratio = lib.number.getRatioForResize(state.width, this.imageMeta.width); 78 | targetSize = `${state.width}px ${parseInt(this.imageMeta.height * ratio)}px`; 79 | } 80 | this.setState({ 81 | position: '0 0', 82 | size: targetSize, 83 | }); 84 | } 85 | else 86 | { 87 | this.setState({ 88 | position: `50% 50%`, 89 | size: 'cover', 90 | }); 91 | } 92 | } 93 | 94 | /** 95 | * on update block 96 | * 97 | * @param {object} state 98 | */ 99 | onUpdateBlock(state) 100 | { 101 | this.setState(state); 102 | } 103 | 104 | render() 105 | { 106 | const { state, props } = this; 107 | 108 | if (state.pending) return null; 109 | 110 | return ( 111 |
    112 | 113 |
    121 | { this._block = r; }} 123 | src={props.tree.cropper.item.image.src} 124 | position={state.position} 125 | size={state.size} 126 | bgColor={props.tree.cropper.item.color || props.setting.body.blockColor} 127 | onUpdateBlock={(e) => this.onUpdateBlock(e)}/> 128 | 141 |
    142 |
    143 | ); 144 | } 145 | 146 | } 147 | Cropper.displayName = 'Cropper'; 148 | 149 | 150 | export default connect((state) => Object.assign({}, state, {}))(Cropper); -------------------------------------------------------------------------------- /lib/Container/Body/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var _react = _interopRequireDefault(require("react")); 11 | 12 | var _GridLayout = _interopRequireDefault(require("./GridLayout")); 13 | 14 | var _Toolbar = _interopRequireDefault(require("./Toolbar")); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 17 | 18 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 19 | 20 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 21 | 22 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 23 | 24 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 25 | 26 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 27 | 28 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 29 | 30 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 31 | 32 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 33 | 34 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } 35 | 36 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 37 | 38 | var Body = /*#__PURE__*/function (_React$Component) { 39 | _inherits(Body, _React$Component); 40 | 41 | var _super = _createSuper(Body); 42 | 43 | function Body(props) { 44 | _classCallCheck(this, Body); 45 | 46 | return _super.call(this, props); 47 | } 48 | 49 | _createClass(Body, [{ 50 | key: "render", 51 | value: function render() { 52 | return /*#__PURE__*/_react["default"].createElement("div", { 53 | className: "ple-container" 54 | }, /*#__PURE__*/_react["default"].createElement("div", { 55 | className: "ple-body" 56 | }, /*#__PURE__*/_react["default"].createElement(_Toolbar["default"], null), /*#__PURE__*/_react["default"].createElement(_GridLayout["default"], null))); 57 | } 58 | }]); 59 | 60 | return Body; 61 | }(_react["default"].Component); 62 | 63 | Body.displayName = 'Body'; 64 | var _default = Body; 65 | exports["default"] = _default; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/components/toolbar.scss: -------------------------------------------------------------------------------- 1 | /* Toolbar */ 2 | .ple-toolbar { 3 | $toolbar: &; 4 | $height: 42px; 5 | 6 | position: relative; 7 | height: $height; 8 | &:after { 9 | content: ''; 10 | display: block; 11 | clear: both; 12 | } 13 | 14 | &__wrap { 15 | position: absolute; 16 | height: $height; 17 | z-index: 11; 18 | width: 100%; 19 | background: #fff; 20 | box-shadow: 0 1px 4px rgba(0,0,0,.2); 21 | } 22 | &__pop { 23 | position: relative; 24 | display: none; 25 | } 26 | &__block { 27 | float: left; 28 | margin-right: 1px; 29 | > button { 30 | display: block; 31 | position: relative; 32 | margin: 0; 33 | padding: 0; 34 | width: $height; 35 | height: $height; 36 | border: none; 37 | background: $col-blur; 38 | cursor: pointer; 39 | outline: 0; 40 | border-radius: 0; 41 | i { 42 | position: absolute; 43 | display: block; 44 | left: 50%; 45 | top: 50%; 46 | } 47 | &:active { 48 | background: mix($col-key, #000, 98%); 49 | } 50 | } 51 | &-key { 52 | > button { 53 | background: $col-active; 54 | &:active { 55 | background: mix($col-active, #000, 75%); 56 | } 57 | } 58 | } 59 | &-active { 60 | > button { 61 | background: mix($col-key, #000, 98%); 62 | } 63 | #{$toolbar}__pop { 64 | display: block; 65 | } 66 | &.key { 67 | > button { 68 | background: mix($col-active, #000, 75%); 69 | } 70 | } 71 | } 72 | } 73 | 74 | .ple-colorPicker { 75 | &__wrap { 76 | position: relative; 77 | } 78 | &__body { 79 | position: absolute; 80 | left: -30px; 81 | top: 0; 82 | } 83 | } 84 | } 85 | 86 | 87 | .ple-edit-setting { 88 | form { 89 | position: absolute; 90 | left: 0; 91 | top: 0; 92 | z-index: 99999; 93 | min-width: 270px; 94 | background: #fff; 95 | box-shadow: 1px 1px 8px rgba(0,0,0,.4); 96 | fieldset { 97 | margin: 0; 98 | padding: 20px; 99 | border: none; 100 | } 101 | } 102 | legend { 103 | @extend %blind; 104 | } 105 | &__title { 106 | margin: 0 0 10px; 107 | font-size: 18px; 108 | color: $col-key; 109 | } 110 | &__form { 111 | > dl { 112 | display: flex; 113 | align-items: center; 114 | margin: 1px 0; 115 | > * { 116 | padding: 3px 0; 117 | } 118 | dt { 119 | width: 82px; 120 | font-size: 12px; 121 | color: #333; 122 | letter-spacing: -.2px; 123 | label { 124 | display: inline; 125 | } 126 | } 127 | dd { 128 | flex: 1; 129 | margin: 0; 130 | } 131 | } 132 | } 133 | &__buttons { 134 | &:after { 135 | content: ''; 136 | display: block; 137 | clear: both; 138 | } 139 | span { 140 | display: block; 141 | width: 50%; 142 | float: left; 143 | &:first-child { 144 | padding-right: 1px; 145 | } 146 | } 147 | button { 148 | margin: 0; 149 | padding: 12px 0; 150 | width: 100%; 151 | font-size: 12px; 152 | color: #fff; 153 | background: $col-blur; 154 | border: none; 155 | cursor: pointer; 156 | -webkit-appearance: none; 157 | border-radius: 0; 158 | &.ple-submit { 159 | background: $col-key; 160 | } 161 | } 162 | } 163 | .ple-type-input { 164 | input[type=text], 165 | input[type=number] { 166 | box-sizing: border-box; 167 | margin: 0; 168 | padding: 6px 6px; 169 | font-size: 12px; 170 | color: #111; 171 | border: 1px solid #CACDCE; 172 | border-radius: 1px; 173 | box-shadow: inset 0 2px 3px rgba(0,0,0,.1); 174 | } 175 | span { 176 | padding-left: 5px; 177 | font-size: 12px; color: #333; 178 | } 179 | &-active { 180 | input[type=text], 181 | input[type=number] { 182 | border-color: $col-active !important; 183 | } 184 | } 185 | } 186 | .ple-type-checkbox { 187 | label { 188 | padding: 6px 12px 6px 0; 189 | display: inline-block; 190 | } 191 | input { 192 | margin: 0 0 0 4px; 193 | padding: 0; 194 | } 195 | span { 196 | padding-left: 6px; 197 | font-size: 12px; color: #333; 198 | } 199 | } 200 | } 201 | 202 | 203 | .ple-edit-bgColor { 204 | position: relative; 205 | 206 | &__inputBox { 207 | display: inline-block; 208 | width: 38px; 209 | height: 26px; 210 | border: 1px solid #CACDCE; 211 | padding: 4px; 212 | background: #fff; 213 | border-radius: 2px; 214 | } 215 | &__input { 216 | width: 100%; 217 | height: 100%; 218 | border: none; 219 | font-size: 0; 220 | margin: 0; 221 | padding: 0; 222 | border-radius: 0; 223 | vertical-align: top; 224 | cursor: pointer; 225 | } 226 | &__popup { 227 | position: relative; 228 | } 229 | &__picker { 230 | position: absolute; 231 | left: -10px; 232 | top: 0; 233 | .colorpicker { 234 | border: none; 235 | } 236 | } 237 | } -------------------------------------------------------------------------------- /lib/reducers/side.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = base; 9 | 10 | var types = _interopRequireWildcard(require("../actions/types")); 11 | 12 | var defaults = _interopRequireWildcard(require("./defaults")); 13 | 14 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 15 | 16 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 17 | 18 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } 19 | 20 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 21 | 22 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 23 | 24 | var nextFileId = 0; 25 | /** 26 | * Reducers 27 | */ 28 | // base 29 | 30 | function base() { 31 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaults.side; 32 | var action = arguments.length > 1 ? arguments[1] : undefined; 33 | var newState = Object.assign({}, state); 34 | var files = {}; 35 | 36 | switch (action.type) { 37 | case types.INIT_PLE: 38 | try { 39 | action.preference.side.files.forEach(function (o) { 40 | files[nextFileId++] = { 41 | image: o, 42 | active: false 43 | }; 44 | }); 45 | return _objectSpread(_objectSpread(_objectSpread({}, state), action.preference.side), {}, { 46 | files: _objectSpread(_objectSpread({}, state.files), files) 47 | }); 48 | } catch (e) { 49 | return state; 50 | } 51 | 52 | case types.SIDE_VISIBLE: 53 | return _objectSpread(_objectSpread({}, state), {}, { 54 | visible: action.value 55 | }); 56 | 57 | case types.SIDE_TOGGLE: 58 | return _objectSpread(_objectSpread({}, state), {}, { 59 | visible: !state.visible 60 | }); 61 | 62 | case types.SIDE_ADD_FILES: 63 | action.files.forEach(function (o) { 64 | files[nextFileId++] = { 65 | image: o, 66 | active: false 67 | }; 68 | }); 69 | return _objectSpread(_objectSpread({}, newState), {}, { 70 | files: _objectSpread(_objectSpread({}, newState.files), files) 71 | }); 72 | 73 | case types.SIDE_REMOVE_FILES: 74 | if (!action.keys.length) return state; 75 | action.keys.forEach(function (o) { 76 | delete newState.files[o]; 77 | }); 78 | return newState; 79 | 80 | case types.SIDE_UPDATE_SELECTED: 81 | if (!(action.value && Object.keys(action.value).length)) return state; 82 | Object.keys(action.value).forEach(function (k) { 83 | if (!newState.files[k]) return; 84 | newState.files[k].active = action.value[k].active; 85 | }); 86 | return newState; 87 | 88 | case types.SIDE_UPDATE_PROGRESS: 89 | return _objectSpread(_objectSpread({}, state), {}, { 90 | progressPercent: action.value 91 | }); 92 | 93 | default: 94 | return state; 95 | } 96 | } -------------------------------------------------------------------------------- /lib/lib/Keyboard.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var _jquery = _interopRequireDefault(require("jquery/dist/jquery.slim")); 11 | 12 | var libs = _interopRequireWildcard(require("../lib")); 13 | 14 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 15 | 16 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 17 | 18 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 19 | 20 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 21 | 22 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 23 | 24 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 25 | 26 | var Keyboard = /*#__PURE__*/function () { 27 | function Keyboard() { 28 | var _this = this; 29 | 30 | _classCallCheck(this, Keyboard); 31 | 32 | this.eventName = "PLE_".concat(libs.number.getTimeStamp()); 33 | this.code = null; 34 | this.keyName = null; 35 | this.names = { 36 | 17: 'CTRL', 37 | 18: 'ALT', 38 | 91: 'CMD', 39 | 93: 'CMD', 40 | 16: 'SHIFT' 41 | }; // init key down event 42 | 43 | (0, _jquery["default"])(window).on("keydown.".concat(this.eventName), function (e) { 44 | return _this._keyDown(e); 45 | }); 46 | } 47 | /** 48 | * apply 49 | * 50 | * @param {Number} code 51 | */ 52 | 53 | 54 | _createClass(Keyboard, [{ 55 | key: "apply", 56 | value: function apply(code) { 57 | this.code = code; 58 | this.keyName = this.names[this.code] || null; 59 | } 60 | /** 61 | * key down event 62 | * 63 | * @param {Event} e 64 | */ 65 | 66 | }, { 67 | key: "_keyDown", 68 | value: function _keyDown(e) { 69 | var _this2 = this; 70 | 71 | // apply keyCode 72 | this.apply(e.keyCode); // set events 73 | 74 | (0, _jquery["default"])(window).on("keyup.".concat(this.eventName), function () { 75 | return _this2._keyUp(); 76 | }).on("contextmenu.".concat(this.eventName), function () { 77 | return _this2._keyUp(); 78 | }).on("blur.".concat(this.eventName), function () { 79 | return _this2._keyUp(); 80 | }).off("keydown.".concat(this.eventName)); 81 | } 82 | /** 83 | * key up event 84 | * 85 | */ 86 | 87 | }, { 88 | key: "_keyUp", 89 | value: function _keyUp() { 90 | var _this3 = this; 91 | 92 | // apply keyCode 93 | this.apply(null); // set events 94 | 95 | (0, _jquery["default"])(window).on("keydown.".concat(this.eventName), function (e) { 96 | return _this3._keyDown(e); 97 | }).off("contextmenu.".concat(this.eventName, " keyup.").concat(this.eventName, " blur.").concat(this.eventName)); 98 | } 99 | /** 100 | * destroy event 101 | * 102 | */ 103 | 104 | }, { 105 | key: "destroy", 106 | value: function destroy() { 107 | (0, _jquery["default"])(window).off("keydown.".concat(this.eventName, " contextmenu.").concat(this.eventName, " keyup.").concat(this.eventName, " blur.").concat(this.eventName)); 108 | } 109 | }]); 110 | 111 | return Keyboard; 112 | }(); 113 | 114 | exports["default"] = Keyboard; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var _react = _interopRequireDefault(require("react")); 11 | 12 | var _redux = require("redux"); 13 | 14 | var _reactRedux = require("react-redux"); 15 | 16 | var _Container = _interopRequireDefault(require("./Container")); 17 | 18 | var _reducers = _interopRequireDefault(require("./reducers")); 19 | 20 | var _API = _interopRequireDefault(require("./API")); 21 | 22 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 23 | 24 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 25 | 26 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 27 | 28 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 29 | 30 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 31 | 32 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 33 | 34 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 35 | 36 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 37 | 38 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 39 | 40 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } 41 | 42 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 43 | 44 | var PhotoLayoutEditor = /*#__PURE__*/function (_React$Component) { 45 | _inherits(PhotoLayoutEditor, _React$Component); 46 | 47 | var _super = _createSuper(PhotoLayoutEditor); 48 | 49 | function PhotoLayoutEditor(props) { 50 | var _this; 51 | 52 | _classCallCheck(this, PhotoLayoutEditor); 53 | 54 | _this = _super.call(this, props); // set store 55 | 56 | _this.store = (0, _redux.createStore)(_reducers["default"]); // set api 57 | 58 | _this.api = new _API["default"](_this.store); 59 | return _this; 60 | } 61 | 62 | _createClass(PhotoLayoutEditor, [{ 63 | key: "render", 64 | value: function render() { 65 | var props = this.props; 66 | return /*#__PURE__*/_react["default"].createElement(_reactRedux.Provider, { 67 | store: this.store 68 | }, /*#__PURE__*/_react["default"].createElement(_Container["default"], { 69 | parent: { 70 | preference: props, 71 | api: this.api 72 | } 73 | })); 74 | } 75 | }]); 76 | 77 | return PhotoLayoutEditor; 78 | }(_react["default"].Component); 79 | 80 | PhotoLayoutEditor.displayName = 'PhotoLayoutEditor'; 81 | PhotoLayoutEditor.defaultProps = { 82 | body: { 83 | setting: {}, 84 | blockColor: '#dddddd', 85 | grid: [] 86 | }, 87 | side: { 88 | files: [], 89 | visible: true 90 | }, 91 | uploadScript: null, 92 | uploadParamsConvertFunc: null 93 | }; 94 | var _default = PhotoLayoutEditor; 95 | exports["default"] = _default; -------------------------------------------------------------------------------- /lib/reducers/core.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports.setting = setting; 9 | exports.api = api; 10 | exports.keyboard = keyboard; 11 | exports.element = element; 12 | 13 | var types = _interopRequireWildcard(require("../actions/types")); 14 | 15 | var _Keyboard = _interopRequireDefault(require("../lib/Keyboard")); 16 | 17 | var defaults = _interopRequireWildcard(require("./defaults")); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 20 | 21 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 22 | 23 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 24 | 25 | function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } 26 | 27 | function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } 28 | 29 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 30 | 31 | function setting() { 32 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; 33 | var action = arguments.length > 1 ? arguments[1] : undefined; 34 | var newState = Object.assign({}, state); 35 | 36 | switch (action.type) { 37 | case types.INIT_PLE: 38 | newState = Object.assign({}, { 39 | base: _objectSpread(_objectSpread({}, defaults.setting.base), {}, { 40 | uploadScript: action.preference.uploadScript || defaults.setting.base.uploadScript, 41 | uploadParamsConvertFunc: action.preference.uploadParamsConvertFunc || defaults.setting.base.uploadParamsConvertFunc, 42 | updateStoreFunc: action.preference.updateStoreFunc || defaults.setting.base.updateStoreFunc, 43 | callback: action.preference.callback || defaults.setting.base.callback 44 | }), 45 | side: _objectSpread(_objectSpread({}, defaults.setting.side), action.preference.side), 46 | body: _objectSpread(_objectSpread({}, defaults.setting.body), action.preference.body) 47 | }); 48 | return newState; 49 | 50 | default: 51 | return state; 52 | } 53 | } 54 | 55 | function api() { 56 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; 57 | var action = arguments.length > 1 ? arguments[1] : undefined; 58 | 59 | switch (action.type) { 60 | case types.INIT_PLE: 61 | return action.api; 62 | 63 | default: 64 | return state; 65 | } 66 | } 67 | 68 | function keyboard() { 69 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; 70 | var action = arguments.length > 1 ? arguments[1] : undefined; 71 | 72 | switch (action.type) { 73 | case types.INIT_PLE: 74 | return new _Keyboard["default"](); 75 | 76 | default: 77 | return state; 78 | } 79 | } 80 | 81 | function element() { 82 | var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null; 83 | var action = arguments.length > 1 ? arguments[1] : undefined; 84 | 85 | switch (action.type) { 86 | case types.INIT_PLE: 87 | return action.element; 88 | 89 | default: 90 | return state; 91 | } 92 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/style/components/side.scss: -------------------------------------------------------------------------------- 1 | /* Sidebar */ 2 | .ple-side { 3 | &__wrap { 4 | position: absolute; 5 | z-index: 100; 6 | right: 0; 7 | top: 0; 8 | bottom: 0; 9 | width: $sidebar-width; 10 | padding: 0; 11 | background: $col-key; 12 | -webkit-transform: translateX($sidebar-width); 13 | transform: translateX($sidebar-width); 14 | -webkit-transition: -webkit-transform 1s ease-out; 15 | transition: transform .3s ease-out; 16 | &-show { 17 | -webkit-transform: translateX(0); 18 | transform: translateX(0); 19 | } 20 | } 21 | &__toggle { 22 | position: absolute; 23 | left: -25px; 24 | top: 20px; 25 | z-index: 10; 26 | padding: 10px 5px; 27 | margin: 0; 28 | border: none; 29 | cursor: pointer; 30 | background: transparent; 31 | outline: 0; 32 | > span { 33 | display: block; 34 | width: 20px; 35 | height: 70px; 36 | background: #4e4e4e; 37 | border-top-left-radius: 3px; 38 | border-bottom-left-radius: 3px; 39 | box-shadow: inset -1px 0 6px rgba(0,0,0,.4); 40 | > i { 41 | display: block; 42 | position: absolute; 43 | left: 50%; 44 | top: 50%; 45 | } 46 | } 47 | 48 | &:active { 49 | > span { 50 | background: mix(#4e4e4e, #000, 80%); 51 | } 52 | } 53 | } 54 | &__background { 55 | position: absolute; 56 | left: 0; 57 | right: 20px; 58 | top: 0; 59 | bottom: 0; 60 | } 61 | &-active &__toggle i { 62 | width: 7px; 63 | height: 12px; 64 | background-position: -112px 0; 65 | &.ple-abs { 66 | margin-left: -4px; 67 | margin-top: -6px; 68 | } 69 | } 70 | 71 | .ple-sideNavigation { 72 | position: absolute; 73 | z-index: 2; 74 | left: 0; 75 | right: 0; 76 | top: 30px; 77 | &__wrap { 78 | margin: 0 15px; 79 | border-radius: 3px; 80 | background: #444; 81 | overflow: hidden; 82 | &:after { 83 | content: ''; 84 | display: block; 85 | clear: both; 86 | } 87 | } 88 | button, span { 89 | margin: 0; 90 | padding: 0; 91 | position: relative; 92 | float: left; 93 | width: 25%; 94 | height: 36px; 95 | border-width: 0 0 0 1px; 96 | border-style: solid; 97 | border-color: #626164; 98 | border-color: rgba(255,255,255,.3); 99 | background: none; 100 | cursor: pointer; 101 | overflow: hidden; 102 | outline: 0; 103 | border-radius: 0; 104 | &:first-child { 105 | border-width: 0; 106 | } 107 | } 108 | button { 109 | &:active { 110 | background: mix(#444, #000, 80%); 111 | } 112 | } 113 | span { 114 | &:active { 115 | background: mix(#444, #000, 80%); 116 | } 117 | } 118 | input[type=file] { 119 | position: absolute; 120 | left: 0; 121 | top: 0; 122 | z-index: 3; 123 | width: 100%; 124 | height: 100%; 125 | opacity: 0; 126 | outline: none; 127 | white-space: nowrap; 128 | cursor: pointer; 129 | -webkit-user-select: none; 130 | &::-webkit-file-upload-button { 131 | visibility: hidden; 132 | } 133 | } 134 | i { 135 | display: block; 136 | position: absolute; 137 | left: 50%; 138 | top: 50%; 139 | } 140 | } 141 | .ple-sideItems { 142 | $item-height: 63px; 143 | height: 100%; 144 | margin: 0; 145 | padding: 75px 0 0; 146 | &__wrap { 147 | height: 100%; 148 | padding: 0 10px 20px; 149 | overflow-x: hidden; 150 | overflow-y: auto; 151 | -webkit-overflow-scrolling: touch; 152 | background: $col-key; 153 | } 154 | ul { 155 | margin: 0; 156 | padding: 0; 157 | list-style: none; 158 | display: -webkit-flex; 159 | display: flex; 160 | flex-flow: row wrap; 161 | } 162 | li { 163 | position: relative; 164 | width: 33.3333%; 165 | padding: 5px; 166 | } 167 | &__loading {} 168 | &__progress { 169 | position: relative; 170 | width: 100%; 171 | height: $item-height; 172 | background: #444; 173 | } 174 | &__bar { 175 | position: absolute; 176 | left: 0; 177 | right: 0; 178 | bottom: 0; 179 | background: $col-active; 180 | } 181 | &__percent { 182 | position: absolute; 183 | left: 0; 184 | right: 0; 185 | top: 50%; 186 | margin-top: -7px; 187 | font-size: 11px; 188 | color: #fff; 189 | text-align: center; 190 | line-height: 14px; 191 | } 192 | span { 193 | position: relative; 194 | display: block; 195 | margin: 0 auto; 196 | padding: 0; 197 | border: none; 198 | width: 100%; 199 | height: $item-height; 200 | background-size: cover; 201 | background-repeat: no-repeat; 202 | background-position: 50% 50%; 203 | background-color: #fff; 204 | cursor: pointer; 205 | text-indent: -9999px; 206 | outline: none; 207 | touch-action: pan-x; 208 | &.ple-sideItems__item-active { 209 | &:after { 210 | content: ''; 211 | display: block; 212 | position: absolute; 213 | left: 0; 214 | right: 0; 215 | top: 0; 216 | bottom: 0; 217 | border: 4px solid $col-active; 218 | } 219 | } 220 | } 221 | } 222 | 223 | /* placeholder */ 224 | &__placeholder { 225 | display: block; 226 | position: absolute; 227 | z-index: 10000; 228 | margin: 0; 229 | padding: 0; 230 | left: -999px; 231 | top: -999px; 232 | opacity: .8; 233 | background-size: cover; 234 | background-repeat: no-repeat; 235 | background-position: 50% 50%; 236 | background-color: #fff; 237 | text-indent: -9999px; 238 | cursor: pointer; 239 | border: none; 240 | } 241 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Body/GridLayout/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import ReactGridLayout from 'react-grid-layout'; 4 | import classNames from 'classnames'; 5 | import Cropper from '../../Cropper'; 6 | import * as actions from '../../../actions'; 7 | import * as libs from '../../../lib'; 8 | 9 | 10 | let timeStamp = []; 11 | 12 | 13 | class GridLayout extends React.Component { 14 | 15 | constructor(props) 16 | { 17 | super(props); 18 | } 19 | 20 | /** 21 | * on select block 22 | * 23 | * @param {String} key 24 | */ 25 | _selectBlock(key=null) 26 | { 27 | const { props } = this; 28 | 29 | if (key === null) 30 | { 31 | props.api.grid.select([]); 32 | return; 33 | } 34 | 35 | switch(props.keyboard.keyName) 36 | { 37 | case 'CMD': 38 | case 'CTRL': 39 | case 'SHIFT': 40 | if (libs.object.isArray(props.tree.body.activeBlock)) 41 | { 42 | let newActiveBlock = Object.assign([], props.tree.body.activeBlock); 43 | if (newActiveBlock.indexOf(key) > -1) 44 | { 45 | newActiveBlock.splice(newActiveBlock.indexOf(key), 1); 46 | } 47 | else 48 | { 49 | newActiveBlock.push(key); 50 | } 51 | props.api.grid.select(newActiveBlock); 52 | return; 53 | } 54 | break; 55 | } 56 | 57 | props.api.grid.select([key]); 58 | } 59 | 60 | /** 61 | * On update blocks 62 | * 63 | * @param {String} type 64 | * @param {Array} layout 65 | * @param {HTMLElement} element 66 | */ 67 | _updateBlocks(type, layout=null, element=null) 68 | { 69 | const { props } = this; 70 | 71 | /** 72 | * convert layout 73 | * 74 | * @return {Object} 75 | */ 76 | function convertLayout() 77 | { 78 | let result = {}; 79 | for (let i=0; i 400) 95 | { 96 | let newLayout = convertLayout(); 97 | let newGrid = {}; 98 | Object.keys(props.tree.body.grid).forEach(k => { 99 | newGrid[k] = { 100 | ...props.tree.body.grid[k], 101 | layout: { 102 | x: newLayout[k].x, 103 | y: newLayout[k].y, 104 | w: newLayout[k].w, 105 | h: newLayout[k].h, 106 | } 107 | }; 108 | }); 109 | props.dispatch(actions.body.updateBlocks(newGrid)); 110 | } 111 | timeStamp = []; 112 | break; 113 | } 114 | } 115 | 116 | /** 117 | * Render item 118 | * 119 | * @param {String} k 120 | * @return {Component} 121 | */ 122 | renderItem(k) 123 | { 124 | const { props } = this; 125 | const { activeBlock, grid } = props.tree.body; 126 | const item = grid[k]; 127 | 128 | let key = `${item.indexPrefix}__${k}`; 129 | let active = !!(activeBlock && activeBlock.length && activeBlock.indexOf(k) > -1); 130 | 131 | return ( 132 |
    { 137 | event.stopPropagation(); 138 | this._selectBlock(k, !!item.image); 139 | }} 140 | style={{ backgroundColor: item.color || props.setting.body.blockColor }} 141 | className={classNames({ 'ple-grid__item-active': active })}> 142 | {item.image && ( 143 |
    149 | )} 150 |
    151 | ); 152 | } 153 | 154 | render() 155 | { 156 | const { props } = this; 157 | const { setting } = props.tree.body; 158 | const bodyWidth = (setting.width * setting.column) + 159 | (setting.innerMargin * (setting.column-1)) + 160 | (setting.outerMargin * 2); 161 | 162 | return ( 163 |
    this._selectBlock()}> 164 |
    165 | this._updateBlocks('start')} 173 | onDragStop={(layout, oldItem, newItem, placeholder, e, element) => this._updateBlocks('end', layout, element)} 174 | onResizeStart={() => this._updateBlocks('start')} 175 | onResizeStop={(layout, oldItem, newItem, placeholder, e, element) => this._updateBlocks('end', layout, element)} 176 | style={{ 177 | width: `100%`, 178 | backgroundColor: setting.bgColor 179 | }} 180 | className="ple-grid"> 181 | {Object.keys(props.tree.body.grid).map((k) => this.renderItem(k))} 182 | 183 | {props.tree.cropper.visible ? ( ) : null} 184 |
    185 |
    186 | ); 187 | } 188 | 189 | } 190 | GridLayout.displayName = 'GridLayout'; 191 | GridLayout.defaultProps = { 192 | tree: null, 193 | dispatch: null, 194 | }; 195 | 196 | 197 | export default connect((state) => Object.assign({}, state, {}))(GridLayout); -------------------------------------------------------------------------------- /lib/Container/Side/ToggleSideButton/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var _react = _interopRequireWildcard(require("react")); 11 | 12 | var _classnames = _interopRequireDefault(require("classnames")); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 15 | 16 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 17 | 18 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 19 | 20 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 21 | 22 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 23 | 24 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 25 | 26 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 27 | 28 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 29 | 30 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 31 | 32 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 33 | 34 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 35 | 36 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } 37 | 38 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 39 | 40 | var ToggleSideButton = /*#__PURE__*/function (_Component) { 41 | _inherits(ToggleSideButton, _Component); 42 | 43 | var _super = _createSuper(ToggleSideButton); 44 | 45 | function ToggleSideButton() { 46 | _classCallCheck(this, ToggleSideButton); 47 | 48 | return _super.apply(this, arguments); 49 | } 50 | 51 | _createClass(ToggleSideButton, [{ 52 | key: "render", 53 | value: function render() { 54 | var props = this.props; 55 | return /*#__PURE__*/_react["default"].createElement("button", { 56 | type: "button", 57 | onClick: props.onClick, 58 | className: "ple-side__toggle" 59 | }, /*#__PURE__*/_react["default"].createElement("span", null, /*#__PURE__*/_react["default"].createElement("i", { 60 | className: (0, _classnames["default"])('ple-sp-ico', 'ple-abs', { 61 | 'ple-ico-arrow-right': props.show, 62 | 'ple-ico-arrow-left': !props.show 63 | }) 64 | }, "Toggle sidebar"))); 65 | } 66 | }]); 67 | 68 | return ToggleSideButton; 69 | }(_react.Component); 70 | 71 | ToggleSideButton.displayName = 'ToggleSideButton'; 72 | ToggleSideButton.defaultProps = { 73 | show: false, 74 | onClick: function onClick() {} 75 | }; 76 | var _default = ToggleSideButton; 77 | exports["default"] = _default; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-photo-layout-editor 2 | 3 |

    4 | logo 5 |

    6 | 7 | 사진 레이아웃을 편집하는 웹 프로그램입니다. 8 | This is photo layout editor for react 9 | 10 | 예전 Instagram blog( http://blog.instagram.com/ )에 있는 정렬된 이미지의 모습에 매료되어 저런 모습을 직접 편집하여 게시물로 올렸으면 좋겠다는 생각이 들어 만들게 되었습니다. 11 | 블럭을 드래그 앤 드롭으로 위치와 크기를 편집하여 모던하게 정렬된 이미지나 레이아웃 만들 수 있습니다. 12 | 13 |

    14 | logo 15 |

    16 | 17 | 18 | ## Demo 19 | 20 | 다음 링크를 통하여 기능을 체험해볼 수 있습니다. 21 | 22 | https://redgoose-dev.github.io/react-photo-layout-editor/ 23 | 24 | 25 | ## Feature 26 | 27 | PLE는 이런 특징들을 가지고 있습니다. 28 | 29 | ### Management Images 30 | 31 | 사이드바에 이미지를 업로드하여 사진을 배치하기 전에 이미지를 담아둘 수 있습니다. 32 | 33 | ![screen](https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/master/assets/screen_1.jpg) 34 | 35 | ### Edit Blocks 36 | 37 | 블럭의 갯수나 사이즈, 여백등을 조절할 수 있습니다. 38 | 39 | ![grid editor](https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/master/assets/screen_2.jpg) 40 | 41 | ### Drag \& Drop 42 | 43 | 이미지를 드래그하여 이미지를 블럭에 넣거나 블럭의 위치를 옮기거나 수정할 수 있습니다. 44 | 45 | ### Edit image area 46 | 47 | 블럭을 선택하고 펜 모양의 툴바(edit block)를 선택하면 편집창이 뜨면서 영역을 변경할 수 있습니다. 48 | 49 | ![cropper](https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/master/assets/screen_4.jpg) 50 | 51 | ### Change color 52 | 53 | 블럭의 배경색을 바꿀 수 있습니다. 빈 블럭을 만들고 색상을 수정할 수 있습니다. 54 | 55 | ![change color](https://raw.githubusercontent.com/redgoose-dev/react-photo-layout-editor/master/assets/screen_3.jpg) 56 | 57 | 58 | ## Installation 59 | 60 | cli로 설치할 프로젝트에서 다음과 같은 명령을 실행합니다. 61 | 62 | ### npm 63 | 64 | `npm install --save react-photo-layout-editor` 65 | 66 | ### yarn 67 | 68 | `yarn add react-photo-layout-editor` 69 | 70 | 71 | ## Usage 72 | 73 | 원하는곳에 컴포넌트를 삽입합니다. 74 | 한페이지에 단독으로 사용하는것을 권장합니다. 75 | 76 | ``` 77 | import PhotoLayoutEditor from 'react-photo-layout-editor'; 78 | 79 | { ple = r }}/> 80 | ``` 81 | 82 | 83 | ## Properties 84 | 85 | 컴포넌트를 마운트할때 설정값을 정의할 수 있습니다. 86 | 87 | | Name | default | Type | Description | 88 | | ---- | ------- | ---- | ----------- | 89 | | body | {} | `object` | 툴바와 그리드를 편집하는 영역. 아래 [body 항목](https://github.com/redgoose-dev/react-photo-layout-editor#body)을 참고 | 90 | | side | {} | `object` | 이미지를 관리하는 사이드 팔레트 영역. 아래 [side 항목](https://github.com/redgoose-dev/react-photo-layout-editor#side)을 참고 | 91 | | uploadScript | null | `string` | 이미지를 서버로 업로드 처리하는 주소 | 92 | | uploadParamsConvertFunc | null | `function` | 이미지를 서버로 업로드하고 그 결과값을 받아 `side.files`에 이미지를 등록할 수 있도록 값을 변경하는 콜백함수 | 93 | | updateStoreFunc | null | `function` | store 데이터가 변경될때마다 실행하는 콜백함수 | 94 | | callback | {} | `object` | 여러가지 행동에 대한 콜백 함수들의 모음 | 95 | 96 | ### body 97 | 98 | 툴바와 그리드 편집 영역 99 | 100 | | Name | default | Type | Description | 101 | | ---- | ------- | ---- | ----------- | 102 | | setting | {} | `object` | 그리드 편집기의 설정값. [setting 섹션](https://github.com/redgoose-dev/react-photo-layout-editor#bodysetting) 참고 | 103 | | blockColor | rgba(211,211,211,1) | `string` | 블럭 하나의 기본 배경색 | 104 | | grid | [] | `array` | 블럭 데이터값 목록 | 105 | 106 | #### body.setting 107 | 108 | 그리드 편집 영역의 설정값 109 | 110 | | Name | default | Type | Description | 111 | | ---- | ------- | ---- | ----------- | 112 | | width | 100 | `number` | 기본 블럭 하나의 가로사이즈 | 113 | | height | 100 | `number` | 기본 블럭 하나의 세로사이즈 | 114 | | column | 5 | `number` | 한줄에 들어가는 기본 블럭의 갯수 | 115 | | outerMargin | 10 | `number` | 그리드 겉부분의 여백값 | 116 | | innerMargin | 10 | `number` | 블럭 사이의 여백값 | 117 | | bgColor | rgba(255,255,255,1) | `string` | 그리드 배경색 | 118 | 119 | ### side 120 | 121 | 이미지를 관리하는 사이드 팔레트 영역 122 | 123 | | Name | default | Type | Description | 124 | | ---- | ------- | ---- | ----------- | 125 | | files | [] | `array` | 이미지 목록 | 126 | | visible | true | `boolean` | 팔레트 표시유무 | 127 | 128 | ### callback 129 | 130 | 컴포넌트 콜백 함수들 131 | 132 | | Name | params | Description | 133 | | ---- | ------ | ----------- | 134 | | init | | 컴포넌트가 초기화 되었을때 호출합니다. | 135 | | sideUploadStart | | 사이드 영역 이미지를 업로드를 시작할때 호출합니다. | 136 | | sideUploadProgress | `loaded,total,percent` | 사이드 영역 이미지를 업로드중일때 호출합니다. | 137 | | sideUploadComplete | `res` | 사이드 영역 이미지 하나를 업로드가 끝나면 호출합니다. | 138 | | sideUploadCompleteAll | | 사이드 영역 모든 이미지 업로드를 완료하면 호출합니다. | 139 | | sideUploadFail | | 사이드 영역 이미지 업로드를 실패하면 호출합니다. | 140 | | sideRemove | `images` | 사이드 영역 이미지를 삭제할때 호출합니다. | 141 | 142 | 143 | ## API 144 | 145 | `PhotoLayoutEditor`를 컨트롤할 수 있습니다. 먼저 컴포넌트를 접근할 수 있도록 인스턴스 변수로 만들어줍니다. 146 | 다음 컴포넌트와 같이 `ref`를 이용하여 `ple`이름의 변수를 이용하여 API를 사용할 수있습니다. 147 | 148 | ``` 149 | let ple = null; 150 | { ple = r }}/> 151 | ``` 152 | 153 | 자세한 API의 내용은 다음 링크를 참고하세요. 154 | 155 | - [Side](https://github.com/redgoose-dev/react-photo-layout-editor/wiki/API.Side) 156 | - [Grid](https://github.com/redgoose-dev/react-photo-layout-editor/wiki/API.Grid) 157 | - [Cropper](https://github.com/redgoose-dev/react-photo-layout-editor/wiki/API.Cropper) 158 | - [Util](https://github.com/redgoose-dev/react-photo-layout-editor/wiki/API.Util) 159 | 160 | 161 | ## Development 162 | 163 | 이 프로그램을 개발하기 위하여 데모 페이지를 띄울 수 있습니다. 164 | 다음 과정을 통하여 브라우저에 개발에 사용되었던 데모 페이지를 열어볼 수 있습니다. 165 | 166 | 1. `git clone https://github.com/redgoose-dev/react-photo-layout-editor.git` 167 | 1. `cd react-photo-layout-editor` 168 | 1. `yarn install` 169 | 1. `yarn run dev` 170 | 1. in your browser, connect `http://localhost:3000` 171 | -------------------------------------------------------------------------------- /lib/Container/Side/Items/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var _react = _interopRequireDefault(require("react")); 11 | 12 | var _Item = _interopRequireDefault(require("./Item")); 13 | 14 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 15 | 16 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 17 | 18 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 19 | 20 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 21 | 22 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 23 | 24 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 25 | 26 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 27 | 28 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 29 | 30 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 31 | 32 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } 33 | 34 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 35 | 36 | var Items = /*#__PURE__*/function (_React$Component) { 37 | _inherits(Items, _React$Component); 38 | 39 | var _super = _createSuper(Items); 40 | 41 | function Items() { 42 | _classCallCheck(this, Items); 43 | 44 | return _super.apply(this, arguments); 45 | } 46 | 47 | _createClass(Items, [{ 48 | key: "render", 49 | value: function render() { 50 | var props = this.props; 51 | return /*#__PURE__*/_react["default"].createElement("div", { 52 | className: "ple-sideItems" 53 | }, /*#__PURE__*/_react["default"].createElement("div", { 54 | className: "ple-sideItems__wrap" 55 | }, /*#__PURE__*/_react["default"].createElement("ul", null, Object.keys(props.files).map(function (o) { 56 | return /*#__PURE__*/_react["default"].createElement(_Item["default"], { 57 | key: o, 58 | id: o, 59 | image: props.files[o].image, 60 | onDragStart: props.onDragStart, 61 | onDragEnd: props.onDragEnd, 62 | onTouchStart: props.onTouchStart, 63 | onTouchMove: props.onTouchMove, 64 | onTouchEnd: props.onTouchEnd, 65 | onClick: function onClick() { 66 | return props.onSelect(parseInt(o)); 67 | }, 68 | active: props.files[o].active 69 | }); 70 | }), props.progress !== null && /*#__PURE__*/_react["default"].createElement("li", { 71 | className: "ple-sideItems__loading" 72 | }, /*#__PURE__*/_react["default"].createElement("div", { 73 | className: "ple-sideItems__progress" 74 | }, /*#__PURE__*/_react["default"].createElement("span", { 75 | className: "ple-sideItems__bar", 76 | style: { 77 | height: "".concat(props.progress, "%") 78 | } 79 | }), /*#__PURE__*/_react["default"].createElement("span", { 80 | className: "ple-sideItems__percent" 81 | }, "".concat(props.progress, "%"))))))); 82 | } 83 | }]); 84 | 85 | return Items; 86 | }(_react["default"].Component); 87 | 88 | Items.displayName = 'Items'; 89 | Items.defaultProps = { 90 | files: [], 91 | // files 92 | onSelect: function onSelect(key) {}, 93 | // on select event 94 | onDragStart: null, 95 | // on drag start 96 | onDragEnd: null, 97 | // on drag end 98 | onTouchStart: null, 99 | // on touch start 100 | onTouchMove: null, 101 | // on touch move 102 | onTouchEnd: null, 103 | // on touch end 104 | progress: null // on progress number 105 | 106 | }; 107 | var _default = Items; 108 | exports["default"] = _default; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/reducers/body.js: -------------------------------------------------------------------------------- 1 | import * as types from '../actions/types'; 2 | import * as libs from '../lib'; 3 | import * as defaults from './defaults'; 4 | 5 | 6 | let lastGridId = 0; 7 | let shuffleIndex = 0; 8 | 9 | 10 | /** 11 | * setting 12 | * 13 | * @param {Object} state 14 | * @param {*} action 15 | * @return {Object} 16 | */ 17 | export function setting(state=defaults.setting.body.setting, action) 18 | { 19 | switch(action.type) 20 | { 21 | case types.INIT_PLE: 22 | try { 23 | return { 24 | ...state, 25 | ...action.preference.body.setting 26 | }; 27 | } catch(e) { 28 | return state; 29 | } 30 | 31 | case types.GRID_SETTING_UPDATE: 32 | return { 33 | ...state, 34 | ...action.value, 35 | }; 36 | } 37 | return state; 38 | } 39 | 40 | /** 41 | * grid 42 | * 43 | * @param {Object} state 44 | * @param {*} action 45 | * @return {Object} 46 | */ 47 | export function grid(state={}, action) 48 | { 49 | let newState = Object.assign({}, state); 50 | 51 | switch (action.type) 52 | { 53 | case types.INIT_PLE: 54 | let grid = {}; 55 | try { 56 | if ((action.preference.body.grid instanceof Array)) 57 | { 58 | action.preference.body.grid.forEach((o, k) => { 59 | grid[k] = o; 60 | }); 61 | } 62 | else if (typeof action.preference.body.grid === 'object') 63 | { 64 | grid = action.preference.body.grid; 65 | } 66 | else 67 | { 68 | throw 'error'; 69 | } 70 | Object.keys(grid).forEach((o) => { 71 | newState[lastGridId++] = { 72 | color: defaults.setting.body.blockColor, 73 | ...grid[o], 74 | indexPrefix: shuffleIndex, 75 | }; 76 | }); 77 | } catch(e) { 78 | newState = {}; 79 | } 80 | return newState; 81 | 82 | case types.GRID_ADD_BLOCK: 83 | newState[lastGridId++] = { 84 | color: defaults.setting.body.blockColor, 85 | layout: { x: Infinity, y: Infinity, w: 1, h: 1 }, 86 | ...action.value, 87 | indexPrefix: shuffleIndex, 88 | }; 89 | return newState; 90 | 91 | case types.GRID_REMOVE_BLOCK: 92 | if (!action.keys || !action.keys.length) return state; 93 | action.keys.forEach(o => { 94 | delete newState[o]; 95 | }); 96 | return newState; 97 | 98 | case types.GRID_SHUFFLE_BLOCKS: 99 | return Object.keys(newState).map((k) => { 100 | return { 101 | ...newState[k], 102 | layout: { 103 | x: libs.number.randomRange(0, action.value.x - 1), 104 | y: libs.number.randomRange(0, action.value.y - 1), 105 | w: libs.number.randomRange(1, action.value.w), 106 | h: libs.number.randomRange(1, action.value.h), 107 | }, 108 | indexPrefix: shuffleIndex++, 109 | }; 110 | }); 111 | 112 | case types.GRID_DUPLICATE_BLOCK: 113 | action.keys.forEach(k => { 114 | if (!newState[k]) return; 115 | newState[lastGridId++] = Object.assign({}, newState[k]); 116 | }); 117 | return newState; 118 | 119 | case types.GRID_CHANGE_COLOR: 120 | action.keys.forEach(k => { 121 | if (!newState[k]) return; 122 | newState[k].color = action.color; 123 | }); 124 | return newState; 125 | 126 | case types.GRID_ATTACH_IMAGES: 127 | if (!libs.object.isArray(action.value)) return state; 128 | 129 | if (action.activeBlocks && action.activeBlocks.length) 130 | { 131 | Object.keys(newState).forEach((k) => { 132 | if (!action.value.length) return; 133 | if (action.activeBlocks.indexOf(k) === -1) return; 134 | newState[k].image = { 135 | src: action.value.splice(0,1)[0], 136 | position: '50% 50%', 137 | size: 'cover' 138 | }; 139 | }); 140 | } 141 | else 142 | { 143 | Object.keys(newState).forEach((k) => { 144 | if (newState[k].image) return; 145 | if (!action.value || !action.value.length) return; 146 | newState[k].image = { 147 | src: action.value.splice(0,1)[0], 148 | position: '50% 50%', 149 | size: 'cover' 150 | }; 151 | }); 152 | if (action.value.length) 153 | { 154 | action.value.forEach((o, k) => { 155 | newState[lastGridId++] = { 156 | color: defaults.setting.body.blockColor, 157 | layout: { 158 | x: (Object.keys(state).length + k) % action.columns, 159 | y: Infinity, 160 | w: 1, 161 | h: 1 162 | }, 163 | image: { 164 | src: o, 165 | position: '50% 50%', 166 | size: 'cover', 167 | }, 168 | indexPrefix: shuffleIndex 169 | }; 170 | }); 171 | } 172 | } 173 | 174 | return newState; 175 | 176 | case types.GRID_ATTACH_IMAGE: 177 | if (!(action.image && typeof action.image === 'string')) return state; 178 | if (!newState[action.keys]) return state; 179 | 180 | newState[action.keys].image = { 181 | src: action.image, 182 | position: '50% 50%', 183 | size: 'cover', 184 | }; 185 | return newState; 186 | 187 | case types.GRID_UPDATE_BLOCKS: 188 | return Object.assign({}, state, action.value); 189 | 190 | case types.CROPPER_CLOSE: 191 | if (!newState[action.key]) return state; 192 | if (action.position) 193 | { 194 | newState[action.key].image.position = action.position; 195 | } 196 | if (action.size) 197 | { 198 | newState[action.key].image.size = action.size; 199 | } 200 | return newState; 201 | } 202 | 203 | return state; 204 | } 205 | 206 | /** 207 | * active block 208 | * 209 | * @param {Array} state 210 | * @param {*} action 211 | * @return {Array} 212 | */ 213 | export function activeBlock(state=[], action) 214 | { 215 | let newState = Object.assign([], state); 216 | 217 | switch (action.type) 218 | { 219 | case types.GRID_ACTIVE_BLOCK: 220 | return action.value; 221 | 222 | case types.GRID_REMOVE_BLOCK: 223 | if (action.keys && action.keys.length) 224 | { 225 | action.keys.forEach(o => { 226 | if (newState.indexOf(o) < 0) return; 227 | newState.splice(newState.indexOf(o), 1); 228 | }); 229 | return newState; 230 | } 231 | return []; 232 | } 233 | 234 | return state; 235 | } -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Body/Toolbar/EditLayoutSetting.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import $ from 'jquery/dist/jquery.slim'; 3 | import ColorPicker from 'react-simple-colorpicker'; 4 | import className from 'classnames'; 5 | 6 | 7 | class EditLayoutSetting extends React.Component { 8 | 9 | constructor(props) 10 | { 11 | super(props); 12 | this.state = { 13 | ...props.defaultSetting, 14 | ...props.setting, 15 | popup_bgColor: false, 16 | }; 17 | } 18 | 19 | activeBgColorPopup(sw, e) 20 | { 21 | const { state } = this; 22 | const cTarget = e ? e.currentTarget : null; 23 | 24 | sw = sw || !state.popup_bgColor; 25 | 26 | if (sw) 27 | { 28 | $(document).on('click.pleEditBgColor', (e) => { 29 | if ($(e.target).closest('.ple-edit-bgColor__popup').length) return; 30 | if (!(e.target === cTarget) && !(e.target.parentNode === cTarget)) 31 | { 32 | this.activeBgColorPopup(false); 33 | } 34 | }); 35 | } 36 | else 37 | { 38 | $(document).off('click.pleEditBgColor'); 39 | } 40 | 41 | this.setState({ popup_bgColor: sw }); 42 | } 43 | 44 | _submit(e) 45 | { 46 | e.preventDefault(); 47 | this.props.submit(this.state); 48 | } 49 | 50 | _reset() 51 | { 52 | this.setState({ 53 | ...this.props.defaultSetting 54 | }); 55 | } 56 | 57 | _change(e) 58 | { 59 | let value = e.target.value || ''; 60 | switch(e.target.type) { 61 | case 'text': 62 | this.setState({ [e.target.name]: value }); 63 | break; 64 | case 'number': 65 | value = value || 0; 66 | this.setState({ [e.target.name]: parseInt(value) }); 67 | break; 68 | } 69 | } 70 | 71 | _openBgColorPicker(e) 72 | { 73 | e.persist(); 74 | this.activeBgColorPopup(null, e); 75 | } 76 | 77 | render() 78 | { 79 | const { state, props } = this; 80 | 81 | return ( 82 |
    this._submit(e)} className="ple-edit-setting"> 83 |
    84 | Settings form 85 |

    Settings

    86 |
    87 |
    88 |
    89 | this._change(e)} 94 | style={{ width: '72px' }} 95 | required/> 96 | px 97 |
    98 |
    99 |
    100 |
    101 |
    102 | this._change(e)} 107 | style={{ width: '72px' }} 108 | required/> 109 | px 110 |
    111 |
    112 |
    113 |
    114 |
    115 | this._change(e)} 120 | style={{ width: '54px' }} 121 | required /> 122 | ea 123 |
    124 |
    125 |
    126 |
    127 |
    128 | this._change(e)} 133 | style={{ width: '58px' }} 134 | required /> 135 | px 136 |
    137 |
    138 |
    139 |
    140 |
    141 | this._change(e)} 146 | style={{ width: '58px' }} 147 | required /> 148 | px 149 |
    150 |
    151 |
    152 |
    153 |
    154 |
    155 | 158 | this._change(e)} 164 | onClick={(e) => this._openBgColorPicker(e)} 165 | readOnly={true} 166 | required={true} 167 | className="ple-edit-bgColor__input" 168 | style={{ backgroundColor: state.bgColor }} 169 | /> 170 | 171 | {state.popup_bgColor && ( 172 |
    173 |
    174 | this.setState({ bgColor: color })} 176 | color={state.bgColor}/> 177 |
    178 |
    179 | )} 180 |
    181 |
    182 |
    183 |
    184 | 185 | 193 |
    194 | ); 195 | } 196 | 197 | } 198 | EditLayoutSetting.displayName = 'EditLayoutSetting'; 199 | EditLayoutSetting.defaultProps = { 200 | submit: (e) => {}, 201 | setting: null, 202 | defaultSetting: { 203 | width: 100, 204 | height: 100, 205 | column: 5, 206 | outerMargin: 10, 207 | innerMargin: 10, 208 | bgColor: 'rgba(255,255,255,1)', 209 | }, 210 | }; 211 | 212 | 213 | export default EditLayoutSetting; -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | Photo Layout Editor for React 9 | 10 | 11 | 12 | 13 |
    14 |
    15 |

    Photo layut editor for react

    16 |

    17 | This is a PhotoLayoutEditor project demo page. 18 |

    19 |
    20 |
    21 | 22 |
    23 |
    24 |

    Side

    25 | 44 |
    45 |
    46 |

    Grid

    47 | 65 |
    66 |
    67 |

    Util

    68 | 86 |
    87 |
    88 |

    Make image area

    89 |
    90 |
    91 |
    92 |
    93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/API/Grid.js: -------------------------------------------------------------------------------- 1 | import * as actions from '../actions'; 2 | 3 | 4 | class Grid { 5 | 6 | constructor(store) 7 | { 8 | this.store = store; 9 | } 10 | 11 | /** 12 | * get keys in block 13 | * 14 | * @param {String} mode 15 | * @param {Array} keys 16 | * @return {Array} 17 | */ 18 | getKeys(mode=null, keys=[]) 19 | { 20 | const state = this.store.getState(); 21 | const { body } = state.tree; 22 | let result = []; 23 | 24 | switch(mode) 25 | { 26 | case 'selected': 27 | return body.activeBlock; 28 | case 'value': 29 | result = []; 30 | keys.forEach((k) => { 31 | if (body.grid[k]) 32 | { 33 | result.push(k); 34 | } 35 | }); 36 | return result; 37 | case 'all': 38 | default: 39 | return Object.keys(body.grid).map(k => k); 40 | } 41 | } 42 | 43 | /** 44 | * get blocks 45 | * 46 | * @param {String} mode 47 | * @param {Array} keys 48 | * @return {Object} 49 | */ 50 | getBlocks(mode=null, keys=[]) 51 | { 52 | const state = this.store.getState(); 53 | const { body } = state.tree; 54 | let selected = []; 55 | let result = {}; 56 | 57 | switch(mode) 58 | { 59 | case 'selected': 60 | selected = this.getKeys('selected'); 61 | break; 62 | case 'value': 63 | selected = this.getKeys('value', keys); 64 | break; 65 | case 'all': 66 | default: 67 | return body.grid; 68 | } 69 | 70 | selected.forEach(k => { 71 | if (!body.grid[k]) return; 72 | result[k] = body.grid[k]; 73 | }); 74 | 75 | return result; 76 | } 77 | 78 | /** 79 | * shuffle items 80 | * 81 | * @param {Object} options 82 | */ 83 | shuffle(options={}) 84 | { 85 | const state = this.store.getState(); 86 | const { body } = state.tree; 87 | const defaultOptions = { x: body.setting.column, y: 2, w: 2, h: 2 }; 88 | 89 | // assign options 90 | options = Object.assign({}, defaultOptions, options); 91 | 92 | this.store.dispatch(actions.body.shuffleBlocks(options)); 93 | } 94 | 95 | /** 96 | * assign image 97 | * 98 | * @param {Array} images 99 | */ 100 | assignImages(images=[]) 101 | { 102 | const state = this.store.getState(); 103 | const { body } = state.tree; 104 | 105 | this.store.dispatch(actions.body.attachImages( 106 | images, 107 | body.setting.column, 108 | body.activeBlock 109 | )); 110 | } 111 | 112 | /** 113 | * assign image 114 | * 115 | * @param {Number} key 116 | * @param {String} image 117 | */ 118 | assignImage(key=null, image=null) 119 | { 120 | if (!(key !== null && image !== null)) return; 121 | this.store.dispatch(actions.body.attachImage(key, image)); 122 | } 123 | 124 | /** 125 | * remove images 126 | * 127 | * @param {Array} keys 128 | */ 129 | removeImages(keys=[]) 130 | { 131 | const state = this.store.getState(); 132 | let newGrid = Object.assign({}, state.tree.body.grid); 133 | keys.forEach(k => { 134 | newGrid[k].image = null; 135 | }); 136 | this.update(newGrid); 137 | } 138 | 139 | /** 140 | * add blocks 141 | * 142 | * @param {Array} blocks 143 | */ 144 | add(blocks=null) 145 | { 146 | const { getState, dispatch } = this.store; 147 | const state = getState(); 148 | const { body } = state.tree; 149 | const defaultOptions = { 150 | layout: { 151 | x: Object.keys(body.grid).length % body.setting.column, 152 | y: Infinity, 153 | w: 1, 154 | h: 1 155 | }, 156 | color: null, 157 | image: null 158 | }; 159 | 160 | /** 161 | * add block 162 | * 163 | * @param {Object} options 164 | */ 165 | function block(options={}) 166 | { 167 | // assign option 168 | options = Object.assign({}, defaultOptions, options); 169 | options.layout = Object.assign({}, defaultOptions.layout, options.layout); 170 | options.image = options.image ? { 171 | src: null, 172 | position: '50% 50%', 173 | size: 'cover', 174 | ...options.image 175 | } : null; 176 | 177 | dispatch(actions.body.addBlock(options)); 178 | } 179 | 180 | // checking blocks 181 | blocks = (blocks && blocks.length) ? blocks : [null]; 182 | 183 | // play add blocks 184 | blocks.forEach(o => block(o)); 185 | } 186 | 187 | /** 188 | * update blocks 189 | * 190 | * @param {Object} blocks 191 | */ 192 | update(blocks={}) 193 | { 194 | this.store.dispatch(actions.body.updateBlocks(blocks)); 195 | } 196 | 197 | /** 198 | * remove blocks 199 | * 200 | * @param {Array} keys 201 | */ 202 | remove(keys=[]) 203 | { 204 | this.store.dispatch(actions.body.removeBlock(keys)); 205 | } 206 | 207 | /** 208 | * select blocks 209 | * 210 | * @param {Array} keys 211 | */ 212 | select(keys=[]) 213 | { 214 | keys = keys.map(k => k.toString()); 215 | this.store.dispatch(actions.body.activeBlock(keys)); 216 | } 217 | 218 | /** 219 | * un select blocks 220 | * 221 | * @param {Array} keys 222 | */ 223 | unselect(keys=[]) 224 | { 225 | const state = this.store.getState(); 226 | let newActiveBlock = Object.assign([], state.tree.body.activeBlock); 227 | 228 | keys.forEach(k => { 229 | k = k.toString(); 230 | if (newActiveBlock.indexOf(k) !== -1) 231 | { 232 | newActiveBlock.splice(newActiveBlock.indexOf(k), 1); 233 | } 234 | }); 235 | this.store.dispatch(actions.body.activeBlock(newActiveBlock)); 236 | } 237 | 238 | /** 239 | * toggle select all blocks 240 | * 241 | * @param {Boolean} isSelect 242 | */ 243 | toggleSelectAll(isSelect=null) 244 | { 245 | const state = this.store.getState(); 246 | 247 | if (typeof isSelect !== 'boolean') 248 | { 249 | isSelect = !state.tree.body.activeBlock.length; 250 | } 251 | if (isSelect) 252 | { 253 | this.select(this.getKeys('all')); 254 | } 255 | else 256 | { 257 | this.select([]); 258 | } 259 | } 260 | 261 | /** 262 | * duplicate blocks 263 | * 264 | * @param {Array} keys 265 | */ 266 | duplicate(keys=[]) 267 | { 268 | this.store.dispatch(actions.body.duplicateBlock(keys)); 269 | } 270 | 271 | /** 272 | * get preference 273 | * 274 | * @return {Object} 275 | */ 276 | getPreference() 277 | { 278 | const state = this.store.getState(); 279 | return state.tree.body.setting; 280 | } 281 | 282 | /** 283 | * set preference 284 | * 285 | * @param {Object} value 286 | */ 287 | setPreference(value={}) 288 | { 289 | let newPreference = Object.assign({}, this.getPreference(), value); 290 | this.store.dispatch(actions.body.updateSetting(newPreference)); 291 | } 292 | 293 | } 294 | 295 | 296 | export default Grid; -------------------------------------------------------------------------------- /lib/Container/Side/Navigation/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var _react = _interopRequireDefault(require("react")); 11 | 12 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 13 | 14 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 15 | 16 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 17 | 18 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 19 | 20 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 21 | 22 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 23 | 24 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 25 | 26 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 27 | 28 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 29 | 30 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } 31 | 32 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 33 | 34 | var SideNavigation = /*#__PURE__*/function (_React$Component) { 35 | _inherits(SideNavigation, _React$Component); 36 | 37 | var _super = _createSuper(SideNavigation); 38 | 39 | function SideNavigation(props) { 40 | var _this; 41 | 42 | _classCallCheck(this, SideNavigation); 43 | 44 | _this = _super.call(this, props); 45 | _this.comps = { 46 | inputFile: null 47 | }; 48 | _this.state = { 49 | timestamp: Date.now() 50 | }; 51 | return _this; 52 | } 53 | /** 54 | * Upload images 55 | * 56 | * @param {Event} e 57 | */ 58 | 59 | 60 | _createClass(SideNavigation, [{ 61 | key: "upload", 62 | value: function upload(e) { 63 | this.props.onUpload(e.target.files); 64 | this.setState({ 65 | timestamp: Date.now() 66 | }); 67 | } 68 | }, { 69 | key: "render", 70 | value: function render() { 71 | var _this2 = this; 72 | 73 | var props = this.props, 74 | state = this.state, 75 | comps = this.comps; 76 | return /*#__PURE__*/_react["default"].createElement("nav", { 77 | className: "ple-sideNavigation ple-side__navigation" 78 | }, /*#__PURE__*/_react["default"].createElement("div", { 79 | className: "ple-sideNavigation__wrap" 80 | }, /*#__PURE__*/_react["default"].createElement("button", { 81 | type: "button", 82 | title: "attach images", 83 | onClick: props.onAttach 84 | }, /*#__PURE__*/_react["default"].createElement("i", { 85 | className: "ple-sp-ico ple-ico-reply ple-abs" 86 | }, "Moving the image to grid block")), /*#__PURE__*/_react["default"].createElement("button", { 87 | type: "button", 88 | title: "toggle select", 89 | onClick: props.onToggleSelect 90 | }, /*#__PURE__*/_react["default"].createElement("i", { 91 | className: "ple-sp-ico ple-ico-select ple-abs" 92 | }, "Toggle all select")), /*#__PURE__*/_react["default"].createElement("span", { 93 | title: "upload images", 94 | key: state.timestamp 95 | }, /*#__PURE__*/_react["default"].createElement("input", { 96 | ref: function ref(r) { 97 | comps.inputFile = r; 98 | }, 99 | type: "file", 100 | onChange: function onChange(e) { 101 | return _this2.upload(e); 102 | }, 103 | multiple: true 104 | }), /*#__PURE__*/_react["default"].createElement("i", { 105 | className: "ple-sp-ico ple-ico-upload ple-abs" 106 | }, "upload images")), /*#__PURE__*/_react["default"].createElement("button", { 107 | type: "button", 108 | title: "remove images", 109 | onClick: props.onRemove 110 | }, /*#__PURE__*/_react["default"].createElement("i", { 111 | className: "ple-sp-ico ple-ico-trash ple-abs" 112 | }, "remove images")))); 113 | } 114 | }]); 115 | 116 | return SideNavigation; 117 | }(_react["default"].Component); 118 | 119 | SideNavigation.displayName = 'Navigation'; 120 | SideNavigation.defaultProps = { 121 | onRemove: function onRemove() {}, 122 | onToggleSelect: function onToggleSelect() {}, 123 | onAttach: function onAttach() {}, 124 | onUpload: function onUpload() {} 125 | }; 126 | var _default = SideNavigation; 127 | exports["default"] = _default; -------------------------------------------------------------------------------- /lib/Container/Side/Items/Item.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 4 | 5 | Object.defineProperty(exports, "__esModule", { 6 | value: true 7 | }); 8 | exports["default"] = void 0; 9 | 10 | var _react = _interopRequireDefault(require("react")); 11 | 12 | var _classnames = _interopRequireDefault(require("classnames")); 13 | 14 | var lib = _interopRequireWildcard(require("../../../lib")); 15 | 16 | function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } 17 | 18 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } 21 | 22 | function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } 23 | 24 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 25 | 26 | function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } 27 | 28 | function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } 29 | 30 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } 31 | 32 | function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } 33 | 34 | function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; } 35 | 36 | function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } 37 | 38 | function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } 39 | 40 | function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } } 41 | 42 | function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } 43 | 44 | var Item = /*#__PURE__*/function (_React$Component) { 45 | _inherits(Item, _React$Component); 46 | 47 | var _super = _createSuper(Item); 48 | 49 | function Item() { 50 | _classCallCheck(this, Item); 51 | 52 | return _super.apply(this, arguments); 53 | } 54 | 55 | _createClass(Item, [{ 56 | key: "render", 57 | value: function render() { 58 | var props = this.props; 59 | var attr = Object.assign({}, lib.util.isTouchDevice() ? { 60 | onTouchStart: props.onTouchStart, 61 | onTouchMove: props.onTouchMove, 62 | onTouchEnd: props.onTouchEnd 63 | } : { 64 | onDragStart: props.onDragStart, 65 | onDragEnd: props.onDragEnd 66 | }); 67 | return /*#__PURE__*/_react["default"].createElement("li", null, /*#__PURE__*/_react["default"].createElement("span", _extends({ 68 | type: "button", 69 | "data-id": props.id, 70 | "data-image": props.image, 71 | draggable: true, 72 | onClick: props.onClick 73 | }, attr, { 74 | style: { 75 | backgroundImage: "url('".concat(props.image, "')") 76 | }, 77 | className: (0, _classnames["default"])({ 78 | 'ple-sideItems__item-active': props.active 79 | }) 80 | }))); 81 | } 82 | }]); 83 | 84 | return Item; 85 | }(_react["default"].Component); 86 | 87 | Item.displayName = 'Item'; 88 | Item.defaultProps = { 89 | image: null, 90 | // image 91 | id: null, 92 | // id 93 | active: null, 94 | // active item 95 | onClick: null, 96 | // on click item 97 | onDragStart: null, 98 | // on drag start 99 | onDragEnd: null, 100 | // on drag end 101 | onTouchStart: null, 102 | // on touch start 103 | onTouchMove: null, 104 | // on touch move 105 | onTouchEnd: null // on touch end 106 | 107 | }; 108 | var _default = Item; 109 | exports["default"] = _default; -------------------------------------------------------------------------------- /src/PhotoLayoutEditor/Container/Side/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import classNames from 'classnames'; 4 | import $ from 'jquery/dist/jquery.slim'; 5 | import * as actions from '../../actions'; 6 | import ToggleSideButton from './ToggleSideButton'; 7 | import Navigation from './Navigation'; 8 | import Items from './Items'; 9 | import * as lib from '../../lib'; 10 | import selectItems from './selectItems'; 11 | 12 | 13 | class Side extends React.Component { 14 | 15 | constructor(props) 16 | { 17 | super(props); 18 | 19 | this.dragTarget = null; 20 | this.dragPosition = []; 21 | this.$gridItems = null; 22 | this.$dragItem = null; 23 | this.uploading = false; 24 | } 25 | 26 | /** 27 | * get gridster item 28 | * 포인트 위치에 있는 gridster블럭을 가져온다. 29 | * 30 | * @return {Object} gridster item 31 | */ 32 | getGridsterItem() 33 | { 34 | const { props } = this; 35 | let target = null; 36 | this.$gridItems = $(props.element).find('.ple-grid > div'); 37 | 38 | this.$gridItems.each((n, el) => { 39 | const $this = $(el); 40 | const pos = $this.offset(); 41 | if (pos.left < this.dragPosition[0] && 42 | (pos.left + $this.width()) > this.dragPosition[0] && 43 | pos.top < this.dragPosition[1] && 44 | (pos.top + $this.height()) > this.dragPosition[1]) 45 | { 46 | target = $this.data('key'); 47 | return false; 48 | } 49 | }); 50 | 51 | return target; 52 | } 53 | 54 | /** 55 | * On select items 56 | * 57 | * @param {Number} key 58 | */ 59 | _selectItem(key) 60 | { 61 | const { props } = this; 62 | let selected = selectItems(props, key); 63 | props.api.side.select(selected); 64 | } 65 | 66 | /** 67 | * Remove items 68 | */ 69 | _removeItems() 70 | { 71 | const { props } = this; 72 | let keys = props.api.side.getKeys('selected'); 73 | 74 | if (keys.length) 75 | { 76 | if (confirm('Do you really want to delete it?')) 77 | { 78 | props.api.side.remove(keys); 79 | } 80 | } 81 | else 82 | { 83 | if (!confirm('Delete all?')) return; 84 | keys = props.api.side.getKeys('all'); 85 | props.api.side.remove(keys); 86 | } 87 | } 88 | 89 | /** 90 | * upload 91 | * 92 | * @param {FileList} files 93 | */ 94 | _upload(files) 95 | { 96 | const { props } = this; 97 | props.api.side.upload(files); 98 | } 99 | 100 | /** 101 | * Attach images to grid 102 | */ 103 | _attach() 104 | { 105 | try 106 | { 107 | let keys = this.props.api.side.getKeys('selected'); 108 | let result = this.props.api.side.attachToGrid(keys); 109 | if (result) throw result; 110 | } 111 | catch(e) 112 | { 113 | alert(e.message); 114 | } 115 | } 116 | 117 | _dragStartItem(evt) 118 | { 119 | const { props } = this; 120 | 121 | // for firefox 122 | evt.dataTransfer.setData('text/plain', null); 123 | 124 | this.$gridItems = $(props.element).find('.ple-grid > div'); 125 | this.$gridItems.on('dragover', (e) => { 126 | e.preventDefault(); 127 | if ($(e.currentTarget).hasClass('ple-grid__item-hover')) return; 128 | $(e.currentTarget).addClass('ple-grid__item-hover'); 129 | }).on('dragleave', (e) => { 130 | e.preventDefault(); 131 | $(e.currentTarget).removeClass('ple-grid__item-hover'); 132 | }).on('drop', (e) => { 133 | e.preventDefault(); 134 | $(e.currentTarget).removeClass('ple-grid__item-hover'); 135 | this.dragTarget = $(e.currentTarget).data('key'); 136 | }); 137 | } 138 | _dragEndItem(e) 139 | { 140 | const { props } = this; 141 | 142 | this.$gridItems.off(); 143 | this.$gridItems = null; 144 | 145 | // check drag target 146 | if (this.dragTarget === null) return; 147 | 148 | // play redux 149 | props.dispatch(actions.body.attachImage( 150 | this.dragTarget, 151 | $(e.currentTarget).data('image') 152 | )); 153 | 154 | // empty dragTarget 155 | this.dragTarget = null; 156 | } 157 | _touchStartItem(e) 158 | { 159 | this.$dragItem = $(e.currentTarget) 160 | .clone() 161 | .removeAttr('draggable') 162 | .addClass('ple-side__placeholder') 163 | .width($(e.currentTarget).width()) 164 | .height($(e.currentTarget).height()); 165 | 166 | $('body').append(this.$dragItem); 167 | } 168 | _touchMoveItem(e) 169 | { 170 | if (!lib.util.checkSupportCss('touch-action', 'pan-y')) 171 | { 172 | e.preventDefault(); 173 | } 174 | 175 | let touch = e.nativeEvent.touches[0]; 176 | this.dragPosition = [touch.pageX, touch.pageY]; 177 | this.$dragItem.css({ 178 | left: touch.pageX - (this.$dragItem.width() * 0.5), 179 | top: touch.pageY - (this.$dragItem.height() * 0.5) 180 | }); 181 | } 182 | _touchEndItem(e) 183 | { 184 | const { props } = this; 185 | 186 | this.$dragItem.remove(); 187 | this.$dragItem = null; 188 | 189 | if (this.dragPosition.length > 0) 190 | { 191 | this.dragTarget = this.getGridsterItem(); 192 | 193 | // check drag target 194 | if (this.dragTarget === null) return; 195 | 196 | // play redux 197 | props.dispatch(actions.body.attachImage( 198 | this.dragTarget, 199 | $(e.currentTarget).data('image') 200 | )); 201 | this.dragPosition = []; 202 | } 203 | } 204 | 205 | render() 206 | { 207 | const { props } = this; 208 | 209 | return ( 210 | 237 | ); 238 | } 239 | 240 | } 241 | Side.displayName = 'Side'; 242 | Side.defaultProps = { 243 | tree: {}, // data tree in reduce 244 | setting: {}, // setting in reduce 245 | api: {}, // api 246 | dispatch: null, // redux dispatch 247 | }; 248 | 249 | 250 | export default connect((state) => Object.assign({}, state))(Side); 251 | --------------------------------------------------------------------------------