hudozka app added edit from 2018.02

This commit is contained in:
tmshv committed 2019-05-29 22:29:21 +03:00
1 parent 6805e07434
commit 759f1217b5
57 files changed
+3630 -1247

No files matched your search

+722 -1246
View File
File diff suppressed because it is too large. Load diff
+10
View File
@@ -6,6 +6,7 @@
"main": "src/bin/www.js",
"scripts": {
"start": "node src/index.js",
"build-edit": "webpack -w --env.NODE_ENV=production --env.TARGET=edit",
"build-app": "webpack --env.NODE_ENV=production --env.TARGET=app",
"out": "babel src/ -d out/",
"gulp": "gulp",
@@ -19,11 +20,20 @@
"author": "Roman Timashev",
"license": "ISC",
"dependencies": {
"@mikeljames/draft-js-drag-n-drop-upload-plugin": "^2.0.1",
"async-file": "^2.0.2",
"classnames": "^2.2.5",
"co": "^4.4.0",
"draft-js": "^0.10.5",
"draft-js-alignment-plugin": "^2.0.3",
"draft-js-drag-n-drop-plugin": "^2.0.3",
"draft-js-focus-plugin": "^2.0.4",
"draft-js-image-plugin": "^2.0.5",
"draft-js-plugins-editor": "^2.0.8",
"draft-js-resizeable-plugin": "^2.0.6",
"ejson": "^2.1.2",
"handlebars": "^4.0.5",
"immutable": "^3.8.2",
"instagram-node": "^0.5.1",
"koa": "^2.5.1",
"koa-bodyparser": "^3.0.0",
+1
View File
@@ -12,6 +12,7 @@ export const index = path.join(__dirname, 'templates/main.html')
export const sitemapCacheTime = 600000// 600 sec - cache purge period
export const viewMain = path.join(__dirname, 'views/main.hbs')
export const viewEdit = path.join(__dirname, 'views/edit.hbs')
export const view404 = path.join(__dirname, 'views/404.hbs')
export const sessionConfig = {
+90
View File
@@ -0,0 +1,90 @@
// import 'babel-core/register'
// import 'babel-polyfill'
import {render} from 'react-dom'
import getEditApp from './edit'
import TextToken from './edit/features/Marker/lib/marker/TextToken'
import {Marker} from './edit/features/Marker/lib/marker/Marker'
import SplitToken from './edit/features/Marker/lib/marker/SplitToken'
import YoutubeToken from './edit/features/Marker/lib/marker/YoutubeToken'
import {TokenFactory} from './edit/features/Marker/lib/marker/TokenFactory'
import React from 'react'
import UrlToken from './edit/features/Marker/lib/marker/UrlToken'
function getMarker() {
return (new Marker())
.addTokenFactory(new TokenFactory({token: SplitToken}))
.addTokenFactory(new TokenFactory({
token: YoutubeToken,
component: ({url}) => (
<div
className='MarkerWidgetYoutube'
>
<a href={url}>
{url}
</a>
</div>
)
// component: ({url}) => (
// <div
// className='MarkerWidgetYoutube'
// >
// <div className='kazimir__video'>
// <iframe
// src={url}
// frameBorder='0'
// allowFullScreen
// />
// </div>
// </div>
// )
}))
// .addTokenFactory(new TokenFactory({token: InstagramToken}))
// .addTokenFactory(BuildTokenFactory(DocumentUrlToken, build=read_document))
// .addTokenFactory(new TokenFactory({token: ImageToken}))
// .addTokenFactory(BuildTokenFactory(DocumentToken, build=build_document))
// .addTokenFactory(BuildTokenFactory(CSVToken, build=read_csv))
// .addTokenFactory(new TokenFactory({token: FileToken}))
.addTokenFactory(new TokenFactory({
token: TextToken,
component: ({html}) => (
<div
className='MarkerWidgetHtml'
dangerouslySetInnerHTML={{__html: html}}
/>
)
}), {
blank: true,
})
// .add_tree_middleware(fix_links_quotes)
// sample_tree = await marker.create_tree(sample_text)
}
function getData() {
return `
# Edit
https://www.youtube.com/watch?v=pxtCffGya9g
Text row
`
// return [
//
// 'bla-bla',
// '### pop-pop',
// 'uiu',
// 'Url: [ya](http://ya.ru)',
// ]
}
function main() {
const mount = document.querySelector('#app')
const marker = getMarker()
const data = getData()
const app = getEditApp(marker, data)
render(app, mount)
}
main()
+3
View File
@@ -0,0 +1,3 @@
export default function effects() {
return []
}
+141
View File
@@ -0,0 +1,141 @@
import React, {Component} from 'react'
import Marker from '../Marker'
import './App.less'
import {MarkerWidgetHtml, MarkerWidgetText, MarkerWidgetUrl} from '../Marker/MarkerWidget'
import ContentEditable from '../Marker/ContentEditable'
import Editor from '../Editor/'
// import Map from '../Map'
// import Body from '../Body'
// import AppWrapper from './AppWrapper'
//
// import {bindActionCreators} from 'redux'
// import {connect} from 'react-redux'
// import * as actions from './duck'
//
// import {routerActions} from 'react-router-redux'
// import {title as defaultTitle} from '../../../config'
// import {getVariable} from '../../../lib/style'
// import {debounce} from '../../../lib/fn'
// const hit = debounce(50, path => ym('hit', path))
//
// const projectPath = project => `/projects/${project.name}`
// const frameSize = isVertical => [window.innerWidth, window.innerHeight]
// const frameSizeMultiplier = () => getVariable('--content-width-state-normal')
// const landscapeProjectBounds = (width, height, ratio=frameSizeMultiplier()) => [
// [0, 0],
// [width * ratio, 0],
// ]
// const portraitProjectBounds = (width, height, ratio=frameSizeMultiplier()) => [
// [0, 0],
// [0, width * ratio],
// ]
//
// function mapStateToProps(state) {
// const {fullPage, isVertical, halfPageAvailable} = state.app.page
//
// return {
// projects: state.app.projects.items,
// activeProject: state.app.projects.activeProject,
// fullPage: halfPageAvailable
// ? fullPage
// : true,
// showFullPageToggle: halfPageAvailable,
// isVertical,
// }
// }
//
// function mapDispatchToProps(dispatch) {
// return bindActionCreators({
// ...actions,
// push: routerActions.push
// }, dispatch)
// }
// async function build_document(data) {
// // text = data['caption'] if data['caption'] else data['file']
// // # document = await self.build()
// document = {
// 'url': data['file'],
// 'image_url': data['file'],
// 'file_url': data['file'],
// 'title': text,
// 'file_size': '0 KB',
// 'file_format': 'PDF',
// }
// return document
// }
// async function read_csv(data) {
// return '\n'.join([
// 'N,Name',
// `0,${data}`,
// '1,Pop',
// '2,Top',
// ])
// }
// async function read_document(data) {
// const doc = await Document.find_one({'id': data})
// return {
// 'url': doc['url'],
// 'image_url': doc['url'],
// 'file_url': doc['url'],
// 'title': doc['title'],
// 'file_size': '0 KB',
// 'file_format': 'PDF',
// }
// }
export default class App extends Component {
constructor(props) {
super(props)
this.onMarkerUpdate = this.onMarkerUpdate.bind(this)
this.state = {
tokens: [],
}
}
onMarkerUpdate(tokens) {
// console.log('Update')
// console.log(tokens)
this.setState({tokens})
}
componentWillMount() {
const {data, marker} = this.props
marker.evaluate(data)
.then(tokens => {
this.setState({
tokens,
})
})
// this.setState({
// data: this
// .getData()
// .reduce((acc, x) => [...acc, ...x], []),
// })
}
render() {
return (
<div className="App">
<div className={'App-Body'}>
<Editor/>
{/* <Marker */}
{/* data={this.state.tokens} */}
{/* marker={this.props.marker} */}
{/* onUpdate={this.onMarkerUpdate} */}
{/* /> */}
</div>
</div>
)
}
}
// export default connect(mapStateToProps, mapDispatchToProps)(App)
+9
View File
@@ -0,0 +1,9 @@
.App {
display: flex;
justify-content: center;
&-Body {
padding-top: 10px;
width: 800px;
}
}
+8
View File
@@ -0,0 +1,8 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});
+26
View File
@@ -0,0 +1,26 @@
.AppWrapper {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1000;
display: flex;
flex-direction: column;
justify-content: flex-start; /* align items in Main Axis */
align-items: stretch; /* align items in Cross Axis */
align-content: stretch; /* Extra space in Cross Axis */
pointer-events: none;
}
.AppWrapper footer {
background-color: rgba(34, 34, 34, 0.95);
color: #ebebeb;
padding: 1em 0.5em 0.5em;
font-weight: 100;
}
+88
View File
@@ -0,0 +1,88 @@
import React, {Component} from 'react'
import {Link} from 'react-router'
import './AppWrapper.css'
import './AppWrapperHeader.css'
import './AppWrapperBody.css'
import './AppWrapperContent.css'
import './Icon.css'
const Icon = ({children: iconName}) => (
<span className={`Icon ${iconName}`}/>
)
class Toggle extends Component {
constructor(props) {
super(props)
this.state = {
value: true
}
}
onClick = () => {
const {value} = this.state
const {onClick} = this.props
this.setState({
value: !value,
})
onClick(!value)
}
render() {
const {value} = this.state
const icon = value
? 'IconFullScreenOn'
: 'IconFullScreenOff'
return (
<button className="InvisibleButton" onClick={this.onClick}>
<Icon>{icon}</Icon>
</button>
)
}
}
const Head = ({title, onToggleFullPage, showFullPageToggle, showControls = false}) => (
<header className="AppWrapperHeader">
<div>
</div>
<div className="AppWrapperHeader-main">
<h1>{title}</h1>
</div>
<div>
{!showControls ? null : (
<div className="controls">
{!showFullPageToggle ? null: (
<Toggle onClick={onToggleFullPage}/>
)}
</div>
)}
</div>
</header>
)
const Body = ({children}) => (
<div className="AppWrapperBody">
<div className="AppWrapperContent">
{children}
</div>
</div>
)
const AppWrapper = ({title, children, showControls, showFullPageToggle, onToggleFullPage}) => (
<div className="AppWrapper">
<Head
title={title}
showControls={showControls}
onToggleFullPage={onToggleFullPage}
showFullPageToggle={showFullPageToggle}
/>
<Body>{children}</Body>
</div>
)
export default AppWrapper
+7
View File
@@ -0,0 +1,7 @@
.AppWrapperBody {
display: flex;
flex: 1;
width: 100%;
height: 100%;
}
@@ -0,0 +1,11 @@
.AppWrapperContent {
margin: 0;
padding: 0;
width: 100%;
position: relative;
/* Needed for when the area gets squished too far and there is content
that can't be displayed */
overflow: auto;
}
@@ -0,0 +1,39 @@
.AppWrapperHeader {
background-color: rgba(34, 34, 34, 0.95);
color: #ebebeb;
/*background-color: rgba(255, 255, 255, 0.95);*/
/*color: #222;*/
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.15);
pointer-events: auto;
display: flex;
align-items: center;
}
.AppWrapperHeader-main {
flex: 1;
}
.AppWrapperHeader h1 {
text-align: center;
margin: 0;
padding: .5em 0 .4em;
font-size: 1.25rem;
font-weight: 100;
}
.controls {
margin: 0 15px;
}
.controls a{
color: #bbb;
text-decoration: none;
}
.controls a:hover{
color: white;
}
+37
View File
@@ -0,0 +1,37 @@
.Icon {
display: inline-block;
width: 20px;
height: 20px;
/*background: grey;*/
background-size: 100%;
}
.IconFullScreenOn {
background-image: url('/icon-fullscreen-on.svg');
}
.IconFullScreenOff {
background-image: url('/icon-fullscreen-off.svg');
}
.InvisibleButton {
background: none;
border: none;
outline: none;
}
.InvisibleButton {
width: 24px;
height: 24px;
padding: 0;
border-radius: 0;
cursor: pointer;
}
/*.InvisibleButton:focus {*/
/*border: 2px solid #2f9fff;*/
/*}*/
+107
View File
@@ -0,0 +1,107 @@
import {combineReducers} from 'redux'
import api from '../../../api'
import {pack} from '../../../lib/fn'
import {isMobile, isLandscape} from '../../../lib/device'
const FETCH_PROJECTS = 'arch/app/FETCH_PROJECTS'
const FOCUS_PROJECT = 'arch/app/FOCUS_PROJECT'
const TOGGLE_FULL_PAGE = 'arch/app/TOGGLE_FULL_PAGE'
const CHANGE_PAGE_ORIENTATION = 'arch/app/CHANGE_PAGE_ORIENTATION'
export const PAGE_ORIENTATION_LANDSCAPE = 'landscape'
export const PAGE_ORIENTATION_PORTRAIT = 'portrait'
export function fetchProjects() {
return {
type: FETCH_PROJECTS,
async: api.projects.fetch()
.then(pack('data'))
}
}
export function focusProject(projectName) {
return {
type: FOCUS_PROJECT,
projectName
}
}
export function toggleFullPage() {
return {
type: TOGGLE_FULL_PAGE,
}
}
export function changePageOrientation(orientation) {
return {
type: CHANGE_PAGE_ORIENTATION,
orientation,
}
}
const init = {
items: [],
total: 0,
activeProject: null,
}
const initPage = {
fullPage: false,
halfPageAvailable: !isMobile(),
isVertical: !isLandscape(),
}
function projects(state = init, action) {
switch (action.type) {
case FETCH_PROJECTS: {
const {items, total} = action.data
return {...state, items, total}
}
case FOCUS_PROJECT: {
const projectName = action.projectName
const activeProject = state.items.find(p => p.name === projectName)
return {...state, activeProject}
}
default:
return state
}
}
function page(state = initPage, action) {
switch (action.type) {
case TOGGLE_FULL_PAGE: {
const fullPage = !state.fullPage
return {...state, fullPage}
}
case CHANGE_PAGE_ORIENTATION: {
const {orientation} = action
const isVertical = orientation === PAGE_ORIENTATION_PORTRAIT
return {...state, isVertical}
}
case FOCUS_PROJECT: {
let fullPage = state.fullPage
const projectName = action.projectName
fullPage = !projectName
? false
: fullPage
return {...state, fullPage}
}
default:
return state
}
}
export default combineReducers({
projects,
page,
})
+1
View File
@@ -0,0 +1 @@
export {default} from './App'
+11
View File
@@ -0,0 +1,11 @@
/**
* Draft v0.10.5
*
* Copyright (c) 2013-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
.DraftEditor-editorContainer,.DraftEditor-root,.public-DraftEditor-content{height:inherit;text-align:initial}.public-DraftEditor-content[contenteditable=true]{-webkit-user-modify:read-write-plaintext-only}.DraftEditor-root{position:relative}.DraftEditor-editorContainer{background-color:rgba(255,255,255,0);border-left:.1px solid transparent;position:relative;z-index:1}.public-DraftEditor-block{position:relative}.DraftEditor-alignLeft .public-DraftStyleDefault-block{text-align:left}.DraftEditor-alignLeft .public-DraftEditorPlaceholder-root{left:0;text-align:left}.DraftEditor-alignCenter .public-DraftStyleDefault-block{text-align:center}.DraftEditor-alignCenter .public-DraftEditorPlaceholder-root{margin:0 auto;text-align:center;width:100%}.DraftEditor-alignRight .public-DraftStyleDefault-block{text-align:right}.DraftEditor-alignRight .public-DraftEditorPlaceholder-root{right:0;text-align:right}.public-DraftEditorPlaceholder-root{color:#9197a3;position:absolute;z-index:1}.public-DraftEditorPlaceholder-hasFocus{color:#bdc1c9}.DraftEditorPlaceholder-hidden{display:none}.public-DraftStyleDefault-block{position:relative;white-space:pre-wrap}.public-DraftStyleDefault-ltr{direction:ltr;text-align:left}.public-DraftStyleDefault-rtl{direction:rtl;text-align:right}.public-DraftStyleDefault-listLTR{direction:ltr}.public-DraftStyleDefault-listRTL{direction:rtl}.public-DraftStyleDefault-ol,.public-DraftStyleDefault-ul{margin:16px 0;padding:0}.public-DraftStyleDefault-depth0.public-DraftStyleDefault-listLTR{margin-left:1.5em}.public-DraftStyleDefault-depth0.public-DraftStyleDefault-listRTL{margin-right:1.5em}.public-DraftStyleDefault-depth1.public-DraftStyleDefault-listLTR{margin-left:3em}.public-DraftStyleDefault-depth1.public-DraftStyleDefault-listRTL{margin-right:3em}.public-DraftStyleDefault-depth2.public-DraftStyleDefault-listLTR{margin-left:4.5em}.public-DraftStyleDefault-depth2.public-DraftStyleDefault-listRTL{margin-right:4.5em}.public-DraftStyleDefault-depth3.public-DraftStyleDefault-listLTR{margin-left:6em}.public-DraftStyleDefault-depth3.public-DraftStyleDefault-listRTL{margin-right:6em}.public-DraftStyleDefault-depth4.public-DraftStyleDefault-listLTR{margin-left:7.5em}.public-DraftStyleDefault-depth4.public-DraftStyleDefault-listRTL{margin-right:7.5em}.public-DraftStyleDefault-unorderedListItem{list-style-type:square;position:relative}.public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth0{list-style-type:disc}.public-DraftStyleDefault-unorderedListItem.public-DraftStyleDefault-depth1{list-style-type:circle}.public-DraftStyleDefault-orderedListItem{list-style-type:none;position:relative}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listLTR:before{left:-36px;position:absolute;text-align:right;width:30px}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-listRTL:before{position:absolute;right:-36px;text-align:left;width:30px}.public-DraftStyleDefault-orderedListItem:before{content:counter(ol0) ". ";counter-increment:ol0}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth1:before{content:counter(ol1) ". ";counter-increment:ol1}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth2:before{content:counter(ol2) ". ";counter-increment:ol2}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth3:before{content:counter(ol3) ". ";counter-increment:ol3}.public-DraftStyleDefault-orderedListItem.public-DraftStyleDefault-depth4:before{content:counter(ol4) ". ";counter-increment:ol4}.public-DraftStyleDefault-depth0.public-DraftStyleDefault-reset{counter-reset:ol0}.public-DraftStyleDefault-depth1.public-DraftStyleDefault-reset{counter-reset:ol1}.public-DraftStyleDefault-depth2.public-DraftStyleDefault-reset{counter-reset:ol2}.public-DraftStyleDefault-depth3.public-DraftStyleDefault-reset{counter-reset:ol3}.public-DraftStyleDefault-depth4.public-DraftStyleDefault-reset{counter-reset:ol4}
+313
View File
@@ -0,0 +1,313 @@
import React from 'react'
import Immutable from 'immutable'
import {
DefaultDraftBlockRenderMap,
EditorState,
RichUtils,
convertFromRaw,
getDefaultKeyBinding,
AtomicBlockUtils,
Modifier
} from 'draft-js'
import DraftEditor, {composeDecorators} from 'draft-js-plugins-editor'
import createImagePlugin from 'draft-js-image-plugin'
import createAlignmentPlugin from 'draft-js-alignment-plugin'
import createFocusPlugin from 'draft-js-focus-plugin'
import createResizeablePlugin from 'draft-js-resizeable-plugin'
import createBlockDndPlugin from 'draft-js-drag-n-drop-plugin'
// import createDragNDropUploadPlugin from 'draft-js-drag-n-drop-upload-plugin'
import createDndFileUploadPlugin from '@mikeljames/draft-js-drag-n-drop-upload-plugin'
// import './Draft.less'
import 'draft-js/dist/Draft.css'
import 'draft-js-image-plugin/lib/plugin.css'
const focusPlugin = createFocusPlugin()
const resizeablePlugin = createResizeablePlugin()
const blockDndPlugin = createBlockDndPlugin()
const alignmentPlugin = createAlignmentPlugin()
const {AlignmentTool} = alignmentPlugin
const decorator = composeDecorators(
resizeablePlugin.decorator,
alignmentPlugin.decorator,
focusPlugin.decorator,
blockDndPlugin.decorator
)
const imagePlugin = createImagePlugin({decorator})
const dndFileUploadPlugin = createDndFileUploadPlugin({
handleUpload: console.log,
addImage: imagePlugin.addImage,
})
// const dragNDropFileUploadPlugin = createDragNDropUploadPlugin({
// handleUpload: mockUpload,
// addImage: imagePlugin.addImage,
// })
const plugins = [
dndFileUploadPlugin,
blockDndPlugin,
focusPlugin,
alignmentPlugin,
resizeablePlugin,
imagePlugin
]
const initialState = {
'entityMap': {
'0': {
'type': 'IMAGE',
'mutability': 'IMMUTABLE',
'data': {
'src': 'https://static.shlisselburg.org/art/images/7c206b4e211bb8486801fd84a3bf309b-big.jpg'
}
}
},
'blocks': [
{
'key': '9gm3s',
'text': 'You can have images in your text field. This is a very rudimentary example, but you can enhance the image plugin with resizing, focus or alignment plugins.',
'type': 'unstyled',
'depth': 0,
'inlineStyleRanges': [],
'entityRanges': [],
'data': {}
},
{
'key': 'ov7r',
'text': ' ',
'type': 'atomic',
'depth': 0,
'inlineStyleRanges': [],
'entityRanges': [{
'offset': 0,
'length': 1,
'key': 0
}],
'data': {}
},
{
'key': 'e23a8',
'text': 'See advanced examples further down …',
'type': 'unstyled',
'depth': 0,
'inlineStyleRanges': [],
'entityRanges': [],
'data': {}
}
]
}
class MyCustomBlock extends React.Component {
// constructor(props) {
// super(props)
// }
render() {
return (
<div className='MyCustomBlock'>
{/* here, this.props.children contains a <section> container, as that was the matching element */}
{this.props.children}
</div>
)
}
}
class MediaComponent extends React.Component {
render() {
const {block, contentState} = this.props
const {foo} = this.props.blockProps
const data = contentState.getEntity(block.getEntityAt(0)).getData()
console.log(data)
return (
<i>
{data.text}
<img src={data.text} alt=""/>
</i>
)
// Return a <figure> or some other content using this data.
}
}
function blockRendererFn(contentBlock) {
const type = contentBlock.getType()
if (type === 'atomic') {
console.log('blockRendererFn', contentBlock.getKey(), type)
return {
component: MediaComponent,
editable: false,
props: {
foo: 'bar',
},
}
} else {
return null
}
}
const blockRenderMap = Immutable.Map({
'section': {
element: 'section',
wrapper: MyCustomBlock,
}
})
// Include 'paragraph' as a valid block and updated the unstyled element but
// keep support for other draft default block types
const extendedBlockRenderMap = DefaultDraftBlockRenderMap.merge(blockRenderMap)
export default class Editor extends React.Component {
constructor(props) {
super(props)
this.state = {
editorState: EditorState.createWithContent(convertFromRaw(initialState)),
}
this.onChange = this.onChange.bind(this)
this.onBoldClick = () => {
this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, 'BOLD'))
}
this.focus = () => {
this.editor.focus()
}
this.onZomboClick = () => {
const contentState = this.state.editorState.getCurrentContent();
const contentStateWithEntity = contentState.createEntity(
'LINK',
'MUTABLE',
{url: 'http://www.zombo.com'}
)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey();
const contentStateWithLink = Modifier.applyEntity(
contentStateWithEntity,
selectionState,
entityKey
)
this.onChange(contentStateWithLink);
// this.onChange(RichUtils.toggleInlineStyle(this.state.editorState, 'BOLD'))
}
this.handleKeyCommand = this.handleKeyCommand.bind(this)
this.keyBindingFn = this.keyBindingFn.bind(this)
this.insertBlock = this.insertBlock.bind(this)
}
insertBlock(type, data) {
const {editorState} = this.state
const contentState = editorState.getCurrentContent()
const contentStateWithEntity = contentState.createEntity(type, 'IMMUTABLE', data)
const entityKey = contentStateWithEntity.getLastCreatedEntityKey()
const newEditorState = EditorState.set(editorState, {
currentContent: contentStateWithEntity
})
this.setState({
editorState: AtomicBlockUtils.insertAtomicBlock(newEditorState, entityKey, ' '),
})
}
onChange(editorState) {
const content = editorState.getCurrentContent()
const s = editorState.getSelection()
const key = s.getStartKey()
const prevKey = content.getKeyBefore(key)
const block = content.getBlockForKey(key)
const prevBlock = content.getBlockForKey(prevKey)
const text = block.getText()
if (/\.jpg/.test(text)) {
this.insertBlock('jpg', {text})
// const newContent = Modifier.setBlockType(content, s, 'atomic')
//
// const newEditorState = EditorState.set(editorState, {
// currentContent: newContent,
// })
//
// this.setState({
// editorState: newEditorState,
// })
} else {
this.setState({editorState})
}
// const blockData = block.getData()
// console.log(block.getText(), block.getType())
// console.log(block.getEntityAt(0))
}
keyBindingFn(e) {
// console.log("K", e.key)
// if (e.key === 'Enter') {
// return 'enter'
// }
return getDefaultKeyBinding(e)
}
handleKeyCommand(command) {
console.log('CMD', command)
const newState = RichUtils.handleKeyCommand(this.state.editorState, command)
if (newState) {
this.onChange(newState)
return 'handled'
}
return 'not-handled'
}
render() {
const content = this.state.editorState.getCurrentContent()
const e = this.state.editorState
const s = e.getSelection()
const key = s.getStartKey()
const so = s.getStartOffset()
const block = content.getBlockForKey(key)
return (
<div>
<DraftEditor
editorState={this.state.editorState}
onChange={this.onChange}
blockRendererFn={blockRendererFn}
blockRenderMap={extendedBlockRenderMap}
handleKeyCommand={this.handleKeyCommand}
keyBindingFn={this.keyBindingFn}
plugins={plugins}
ref={(element) => {
this.editor = element
}}
/>
{/* <AlignmentTool/> */}
<button onClick={this.onBoldClick}>Bold</button>
<div>
<p>
block key: {key} type: {block.getType()}
</p>
<p>
start offset {so}
</p>
<pre>
{content.getPlainText('\n')}
</pre>
</div>
</div>
)
}
}
Whitespace-only changes.
+1
View File
@@ -0,0 +1 @@
export {default} from './Editor'
@@ -0,0 +1,78 @@
import React, {Component} from 'react'
export default class ContentEditable extends Component {
constructor(props) {
super(props)
this.emitChange = this.emitChange.bind(this)
this.onRef = this.onRef.bind(this)
}
getHtml() {
return this.props.children
}
onRef(element) {
this.htmlEl = element
}
emitChange() {
if (!this.htmlEl) return
const html = this.htmlEl.innerHTML
if (html !== this.lastHtml) this.props.onChange(html)
this.lastHtml = html
}
componentDidMount() {
this.htmlEl.focus()
}
shouldComponentUpdate(nextProps) {
let {props, htmlEl} = this
// We need not rerender if the change of props simply reflects the user's edits.
// Rerendering in this case would make the cursor/caret jump
// Rerender if there is no element yet... (somehow?)
if (!this.htmlEl) return true
// // ...or if html really changed... (programmatically, not by user edit)
// if (nextProps.html !== htmlEl.innerHTML && nextProps.html !== props.html) {
// return true
// }
return false
// let optional = ['style', 'className', 'disabled', 'tagName']
//
// // Handle additional properties
// return optional.some(name => props[name] !== nextProps[name])
}
componentDidUpdate() {
const html = this.getHtml()
if (this.htmlEl && this.htmlEl.innerHTML !== html) {
// Perhaps React (whose VDOM gets outdated because we often prevent
// rerendering) did not update the DOM. So we update it manually now.
this.htmlEl.innerHTML = html
}
}
render() {
const {mix} = this.props;
return <div
className={mix}
ref={this.onRef}
onInput={this.emitChange}
onChange={this.emitChange}
// onBlur={this.props.onBlur || this.emitChange}
contentEditable={true}
suppressContentEditableWarning={true}
// dangerouslySetInnerHTML={{__html: this.getHtml()}}
>
{this.getHtml()}
</div>
}
}
+214
View File
@@ -0,0 +1,214 @@
import React, {Component} from 'react'
import {MarkerTabIndex} from './MarkerTabIndex'
import {MarkerCell} from './MarkerCell'
import './Marker.less'
import {exist} from '../../lib/fn'
import ContentEditable from './ContentEditable'
import {constrain} from "./lib/fn";
export class Marker extends Component {
constructor(props) {
super(props)
this.state = {
currentCellIndex: 0,
}
this.onCellUpdate = this.onCellUpdate.bind(this)
this.onCellSelect = this.onCellSelect.bind(this)
this.onKeyDown = this.onKeyDown.bind(this)
}
onKeyDown(event) {
switch (event.key) {
case 'ArrowUp': {
console.log('Marker ArrowUp')
this.setState({
currentCellIndex: this.getPrevCellIndex(),
})
break
}
case 'ArrowDown': {
console.log('Marker ArrowDown')
this.setState({
currentCellIndex: this.getNextCellIndex(),
})
break
}
case 'Enter': {
console.log('Marker Enter')
const s = getSelection()
console.log(s, s.getRangeAt(0))
event.preventDefault()
// if (!event.shiftKey) return
// event.stopPropagation()
return this.handleEnter()
}
}
};
getCellIndex(n) {
const {data} = this.props
const {currentCellIndex} = this.state
const index = currentCellIndex + n
return constrain(index, 0, data.length - 1)
}
getPrevCellIndex(...args) {
return this.getCellIndex(-1, ...args)
}
getNextCellIndex(...args) {
return this.getCellIndex(1, ...args)
}
onCellSelect(index) {
this.setState({
currentCellIndex: index,
})
}
getDataAt(index) {
return this.props.data[index]
}
getBlankTextAt(index) {
if (index === 0) return ''
const cellBefore = this.getDataAt(index)
const textBefore = cellBefore.data
let text
const ol = (text) => {
const m = /^(\d+)\.\s/.exec(text)
if (m) {
const d = parseFloat(m[1])
return `${d + 1}.`
} else {
return null
}
}
const ul = (text) => /^-\s/.test(text)
? '- '
: null
text = ol(textBefore)
if (text) return text
text = ul(textBefore)
if (text) return text
return ''
}
async handleEnter() {
const {currentCellIndex} = this.state
const currentCell = this.getDataAt(currentCellIndex)
if (currentCell.data) {
const nextCellIndex = currentCellIndex + 1
if (nextCellIndex > this.props.data.length - 1) {
const {data, marker, onUpdate} = this.props
const blankText = this.getBlankTextAt(currentCellIndex)
const newCell = await marker.blank({text: blankText})
const newData = [
...data,
newCell,
]
onUpdate(newData)
} else {
}
this.setState({
currentCellIndex: nextCellIndex,
})
}
}
async onCellUpdate(index, text) {
console.log('UPDATE', text)
const {data, marker, onUpdate} = this.props
const cells = await marker.evaluate(text)
const before = data.slice(0, index)
const after = data.slice(index + 1)
let newData = [
...before,
...cells,
...after,
]
// const next = index + cells.length
// if (next > newData.length - 1) {
// const newCell = await marker.blank()
// newData.push(newCell)
// }
console.log(newData.map(x => x.data).join('\n'))
onUpdate(newData)
}
componentWillMount() {
// this.setState({
// currentCellIndex: 0,
// })
}
render() {
const {data} = this.props
if (!data.length) return null
const {currentCellIndex} = this.state
const tabIndex = 1
return (
<div
className='Marker'
// onKeyDownCapture={this.onKeyDown}
onKeyDown={this.onKeyDown}
>
{data.map((x, i) => {
return (i === currentCellIndex ? (
<ContentEditable
mix={'MarkerCell'}
tabIndex={tabIndex + i}
key={i}
onChange={text => this.onCellUpdate(i, text)}
>
{x.data}
</ContentEditable>
) : (
<div
className={'MarkerCell'}
tabIndex={tabIndex + i}
key={i}
onFocus={() => this.onCellSelect(i)}
>
{x.render()}
</div>
)
)
})}
</div>
)
}
}
+52
View File
@@ -0,0 +1,52 @@
.Marker {
//--active-color: #73acf6;
--active-color: #f0f0f0;
--cell-padding: 8px;
--font-size: 16px;
font-size: var(--font-size);
font-family: sans-serif;
//line-height: var(--font-size);
textarea {
width: 100%;
font-size: var(--font-size);
font-family: sans-serif;
padding: 0;
box-sizing: border-box;
height: var(--font-size);
//height: calc(2px + var(--font-size));
border: none;
background-color: var(--active-color);
outline: none;
display: block;
}
}
.MarkerCellTabIndex {
outline: none;
}
.MarkerCell {
//padding: var(--cell-padding);
padding-bottom: 8px;
position: relative;
&--active {
background-color: var(--active-color);
//background-color: #f6903b;
}
&--edit {
div {
outline: none;
}
//background-color: #00b7ec;
background-color: var(--active-color);
}
}
+72
View File
@@ -0,0 +1,72 @@
import React, {Component} from 'react'
import className from 'classnames'
import {trimNewline} from './lib/fn'
import './Marker.less'
import {MarkerEdit} from './MarkerEdit'
const Cell = ({children, active = false, edit = false}) => (
<div className={className('MarkerCell', {
'MarkerCell--active': active,
'MarkerCell--edit': edit,
})}>
{children}
</div>
)
export class MarkerCell extends Component {
constructor(props) {
super(props)
this.onSubmit = this.onSubmit.bind(this)
this.onChange = this.onChange.bind(this)
this.state = {
content: null,
}
}
getCellData() {
return this.state.content
? this.state.content
: this.props.cell.data
}
getCellComponent() {
return this.props.cell.render()
}
onSubmit(e) {
e.preventDefault()
const {onUpdate} = this.props
onUpdate(this.getCellData())
}
onChange(e) {
const content = trimNewline(e.target.value)
this.setState({
content,
})
}
render() {
const {active, edit} = this.props
return edit
? (
<Cell edit={edit}>
<MarkerEdit
onSubmit={this.onSubmit}
onChange={this.onChange}
>
{this.getCellData()}
</MarkerEdit>
</Cell>
) : (
<Cell active={active}>
{this.getCellComponent()}
</Cell>
)
}
}
+85
View File
@@ -0,0 +1,85 @@
import React, {Component, createContext} from 'react'
import className from 'classnames'
import {trim} from './lib/fn'
import {MarkerContext} from './Marker'
import './Marker.less'
import ContentEditable from './ContentEditable'
export class MarkerEdit extends Component {
constructor(props) {
super(props)
// this.onInputRef = this.onInputRef.bind(this)
this.onChange = this.onChange.bind(this)
this.onFocus = this.onFocus.bind(this)
this.onKeyDown = this.onKeyDown.bind(this)
}
onChange(event) {
console.log(event, event.target)
this.props.onChange(event)
}
onFocus(e) {
e.stopPropagation()
}
onKeyDown(event) {
switch (event.key) {
case 'ArrowUp': {
if (event.shiftKey) return
event.stopPropagation()
break
}
case 'ArrowDown': {
if (event.shiftKey) return
event.stopPropagation()
break
}
case 'Enter': {
console.log('MarkerEdit Enter')
if (event.shiftKey) return
// event.stopPropagation()
const {onSubmit} = this.props
onSubmit(event)
}
}
}
// onInputRef(e) {
// this.input = e
// }
//
// componentDidMount() {
// this.input.focus()
// }
render() {
const {children} = this.props
return (
<ContentEditable
// ref={this.onInputRef}
onChange={this.onChange}
onFocus={this.onFocus}
onKeyDown={this.onKeyDown}
>
{children}
</ContentEditable>
)
// return (
// <textarea
// value={children}
// ref={this.onInputRef}
// onChange={onChange}
// onFocus={this.onFocus}
// onKeyDown={this.onKeyDown}
// />
// )
}
}
@@ -0,0 +1,43 @@
import React, {Component, createContext} from 'react'
import './Marker.less'
export class MarkerTabIndex extends Component {
constructor(props) {
super(props)
this.onFocus = this.onFocus.bind(this)
this.onRef = this.onRef.bind(this)
}
onFocus() {
const {onFocus, data} = this.props
onFocus(data)
}
onRef(e) {
this.element = e
}
componentDidUpdate() {
const {focus} = this.props
if (focus) {
this.element.focus()
}
}
render() {
const {tabIndex, children} = this.props
return (
<div
className={'MarkerCellTabIndex'}
children={children}
tabIndex={tabIndex}
onFocus={this.onFocus}
ref={this.onRef}
role={'button'}
/>
)
}
}
+28
View File
@@ -0,0 +1,28 @@
import React from 'react'
import './Marker.less'
export const MarkerWidgetText = ({text}) => (
<div
className='MarkerWidgetText'
>
{text}
</div>
)
export const MarkerWidgetHtml = ({html}) => (
<div
className='MarkerWidgetHtml'
dangerouslySetInnerHTML={{__html: html}}
/>
)
export const MarkerWidgetUrl = ({url, text}) => (
<div
className='MarkerWidgetUrl'
>
<a href={url}>
{text}
</a>
</div>
)
+1
View File
@@ -0,0 +1 @@
export {Marker as default} from './Marker'
+31
View File
@@ -0,0 +1,31 @@
/**
* Remove trailing spaces from string
*
* f(_x_) => x
*
* @param str
*/
export function trim(str) {
return str
.replace(/^\s+/, '')
.replace(/\s+$/, '')
}
/**
* Remove trailing spaces from string
*
* f(_x_) => x
*
* @param str
*/
export function trimNewline(str) {
return str
.replace(/^\n+/, '')
.replace(/\n+$/, '')
}
export function constrain(n, min, max) {
if (n < min) return min
if (n > max) return max
return n
}
@@ -0,0 +1,34 @@
import React from 'react'
import Token from './Token'
import {trim} from '../fn'
import {getHtml} from './index'
export default class FileToken extends Token {
static test(data) {
return is_file(data, documentExtensions + imageExtensions)
}
constructor({data}) {
super({
name: 'file',
data,
})
this.joinable = false
}
async compile() {
const data = parse_file(this.getData())
const url = data['file']
const text = data.caption
? data.caption
: url
return (
<a href={url}>
{text}
</a>
)
}
}
@@ -0,0 +1,78 @@
import React from 'react'
import Token from './Token'
import {isImage, parseFile} from './index'
export class ImageToken extends Token {
static test(data) {
return isImage(data)
}
constructor({data}) {
super({
name: 'image',
data,
})
this.joinable = true
this.build = null
}
merge(token) {
const mergedToken = new ImageCollectionToken([this.data, token.data])
mergedToken.build = this.build
return mergedToken
}
async compile() {
}
}
export class ImageCollectionToken extends ImageToken {
constructor({data}) {
super({data})
this.name = 'image_collection'
this.build = null
}
merge(token) {
const data = Array.isArray(token.getData())
? token.getData()
: [token.getData()]
const mergedToken = new ImageCollectionToken({
data: [...self.getData(), ...data]
})
mergedToken.build = self.build
return mergedToken
}
async compile() {
const data = this.parseData()
let images = []
for (let x of data) {
const img = await this.build(x)
images.push(img)
}
return (
<div className='kazimir__image-collection'>
<div
className='fotorama'
data-width='100%'
data-ratio='800/600'
>
{images.map(({src}) => (
<img src={src}/>
))}
</div>
</div>
)
}
parseData() {
return this
.getData()
.map(parseFile)
}
}
@@ -0,0 +1,138 @@
import SplitToken from './SplitToken'
export class Marker {
constructor() {
this.tokenFactories = []
this.treeMiddlewares = []
this.join = true
this.blankFactory = null
}
async blank({text}) {
if (!this.blankFactory) {
throw new Error(`Blank factory is not defined`)
}
const token = await this.blankFactory.create(text)
return token.compile()
}
addTokenFactory(tokenFactory, {blank = false} = {}) {
this.tokenFactories.push(tokenFactory)
if (blank) {
this.blankFactory = tokenFactory
}
return this
}
addTreeMiddleware(callback) {
this.treeMiddlewares.push(callback)
return this
}
async evaluate(text) {
let tokens = text.split('\n')
tokens = this.markupTokens(tokens)
if (this.join) {
tokens = this.joinTokens(tokens)
}
return compileTokens(tokens)
// tree = lxml.html.fromstring(compiled)
// return await self.process_tree(tree)
}
joinTokens(tokens) {
return mergeTokens(joinTokens(tokens))
}
async processTree(tree) {
// for fn in self.treeMiddlewares:
// tree = await fn(tree)
// return tree
}
markupTokens(tokens) {
const markedTokens = []
tokens.forEach(data => {
const token = this.createToken(data)
if (!token) {
throw new Error(`Token not recognized for data ${data}`)
}
markedTokens.push(token)
})
return markedTokens
}
createToken(data) {
for (let factory of this.tokenFactories) {
if (factory.test(data)) {
return factory.create(data)
}
}
return null
}
}
function joinTokens(tokens) {
const isSplit = x => x instanceof SplitToken
let bufferTag = null
let tokensBuffer = []
let tokensList = []
for (let token of tokens) {
const tag = token.name
const changed = bufferTag !== null && tag !== bufferTag
let flush = isSplit(token)
if (changed) {
flush = true
}
if (!token.isJoinable()) {
flush = true
}
if (flush) {
if (tokensBuffer.length) {
tokensList.push(tokensBuffer)
}
if (isSplit(token)) {
tokensBuffer = []
bufferTag = null
} else {
tokensBuffer = [token]
bufferTag = tag
}
} else {
bufferTag = tag
tokensBuffer.push(token)
}
}
if (tokensBuffer.length) {
tokensList.push(tokensBuffer)
}
return tokensList
}
function mergeTokens(tokensList) {
return tokensList
.map(ts => ts
.reduce(
(token, x) => token.merge(x)
)
)
}
async function compileTokens(tokens) {
return Promise.all(
tokens.map(x => x.compile())
)
}
@@ -0,0 +1,24 @@
import Token from './Token'
import {trim} from '../fn'
export default class SplitToken extends Token {
static test(data) {
data = trim(data)
return data === ''
}
constructor(options) {
super({
...options,
name: 'split',
})
}
render() {
return null
}
merge(token) {
return this
}
}
@@ -0,0 +1,44 @@
import Token from './Token'
import {trim} from '../fn'
import {getHtml} from './index'
export default class TextToken extends Token {
static test(data) {
return true
}
constructor(options) {
super({
...options,
name: 'text',
})
}
merge(token) {
const data = [this.data, token.data].join('\n')
return new TextToken(this.getMergeOptions({
data: trim(data),
}))
}
async getData() {
const html = getHtml(this.data)
return {html}
}
// async compile() {
// // return getHtml(this.getData())
//
//
//
//
//
// return (
// <div
// className='MarkerWidgetHtml'
// dangerouslySetInnerHTML={{__html: html}}
// />
// )
// }
}
@@ -0,0 +1,51 @@
export default class Token {
static test(data) {
return true
}
constructor({name, data, component}) {
this.name = name
this.data = data
this.component = component
this.joinable = true
this.compiledData = null
}
isJoinable() {
return this.joinable
}
render() {
if (!this.compiledData) {
throw new Error('Token not compiled. Call Token::compile before.')
}
return this.component(this.compiledData)
}
async getData() {
return this.data
}
async compile() {
this.compiledData = await this.getData()
return this
}
merge(token) {
return this
}
getMergeOptions(extend) {
return {
name: this.name,
data: this.data,
component: this.component,
...extend,
}
}
toString() {
return `[Token ${this.name} ${this.data}]`
}
}
@@ -0,0 +1,35 @@
export class TokenFactory {
constructor({token, component}) {
this.token = token
this.component = component
}
test(sample) {
return this.token.test(sample)
}
create(data) {
const Token = this.token
return new Token({
data,
component: this.component,
})
}
}
export class BuildTokenFactory extends TokenFactory {
constructor({token, build, component}) {
super({token})
this.build = build
this.component = component
}
create(data) {
const Token = this.token
return new Token({
data,
component: this.component,
build: this.build,
})
}
}
@@ -0,0 +1,16 @@
import Token from './Token'
import {isUrl} from './index'
export default class UrlToken extends Token {
static test(data) {
return isUrl(data)
}
constructor(options) {
super({
...options,
name: 'url',
})
this.joinable = false
}
}
@@ -0,0 +1,24 @@
import {isUrl} from './index'
import React from 'react'
import UrlToken from './UrlToken'
export default class YoutubeToken extends UrlToken {
static test(data) {
if (!isUrl(data)) {
return false
}
const url = new URL(data)
return ['www.youtube.com', 'youtube.com']
.includes(url.hostname)
}
async getData() {
const url = new URL(this.data)
const youtubeId = url.searchParams.get('v')
return {
url: `//www.youtube.com/embed/${youtubeId}`
}
}
}
@@ -0,0 +1,76 @@
import {markdown} from 'markdown'
const {toHTML: markdownToHtml} = markdown
const splitExt = s => {
const parts = s.split('.')
const name = parts.splice(0, parts.length - 1)
const ext = parts[parts.length - 1]
return {name, ext}
}
export const imageExtensions = ['.jpg', '.jpeg', '.png']
export const documentExtensions = ['.doc', '.docx', '.pdf', '.xls', '.xlsx']
export function getHtml(text) {
return markdownToHtml(text)
}
export function isUrl(sample) {
try {
return Boolean(new URL(sample))
} catch (e) {
return false
}
}
export function isImage(sample) {
const file = parseFile(sample)['file']
return isFile(file, imageExtensions)
}
export function isDocument(sample) {
const file = parseFile(sample)['file']
return isFile(file, documentExtensions)
}
export function isFile(sample, extensions) {
let {name, ext} = splitExt(sample)
ext = ext.toLowerCase()
return extensions.include(ext)
}
export function parseFile(sample) {
const re = /![(.*)]((.*))/
const p = re.exec(sample)
let caption
let file
if (p) {
caption = p.group(1)
file = p.group(2)
} else {
sample = sample.split(' ')
file = sample[0]
caption = ' '.join(sample.substr(1))
}
return {
file,
caption,
}
}
export function parseLink(sample) {
const re = /![(.*)]((.*))/
const p = re.exec(sample)
if (!p) {
return null
}
const url = p[2]
return {
url: new URL(url),
alt: p[1],
}
}
+35
View File
@@ -0,0 +1,35 @@
:root {
--content-width-state-normal: 60%;
--content-width-state-full: 100%;
--content-width: var(--content-width-state-normal);
--module-m: .5em;
}
html, body, #root {
width: 100%;
height: 100%;
position: absolute;
background-color: #222;
margin: 0;
padding: 0;
}
body {
font-family: "Input Sans Compressed", sans-serif;
}
.leaflet-container {
height: 100%;
/*height: 100px;*/
width: 100%;
background-color: white;
}
h1 {
font-size: 1.25em;
}
+55
View File
@@ -0,0 +1,55 @@
import React from 'react'
import App from './features/App'
// import {createDevTools} from 'redux-devtools'
// import LogMonitor from 'redux-devtools-log-monitor'
// import DockMonitor from 'redux-devtools-dock-monitor'
import {Provider} from 'react-redux'
import reducer from './reducers'
import effects from './effects'
import {create} from './store'
import {Router, Route, browserHistory} from 'react-router'
import {syncHistoryWithStore} from 'react-router-redux'
const initialState = {}
// const DevTools = createDevTools(
// <DockMonitor defaultIsVisible={false}
// toggleVisibilityKey="ctrl-h"
// changePositionKey="ctrl-q"
// >
// <LogMonitor theme="tomorrow" preserveScrollTop={false}/>
// </DockMonitor>
// )
const store = create(reducer, initialState, [
// DevTools.instrument()
], effects())
// const history = syncHistoryWithStore(browserHistory, store)
export default function (marker, data) {
// return (
// <App/>
// )
return (
<Provider store={store}>
<div>
<App
marker={marker}
data={data}
/>
{/*<Router history={history}>*/}
{/*<Route path="/edit" component={App}>*/}
{/*</Route>*/}
{/*</Router>*/}
{/*<DevTools/>*/}
</div>
</Provider>
)
}
+516
View File
@@ -0,0 +1,516 @@
import L from 'leaflet'
/*
Leaflet.pattern, Provides tools to set the backgrounds of vector shapes in Leaflet to be patterns.
https://github.com/teastman/Leaflet.pattern
(c) 2015, Tyler Eastman
*/
(function (window, document) {
/*
* L.Pattern is the base class for fill patterns for leaflet Paths.
*/
L.Pattern = L.Class.extend({
// includes: [L.Mixin.Events],
includes: [L.Evented],
options: {
x: 0,
y: 0,
width: 8,
height: 8,
patternUnits: 'userSpaceOnUse',
patternContentUnits: 'userSpaceOnUse'
// angle: <0 - 360>
// patternTransform: <transform-list>
},
_addShapes: L.Util.falseFn,
_update: L.Util.falseFn,
initialize: function (options) {
this._shapes = {};
L.setOptions(this, options);
},
onAdd: function (map) {
this._map = map.target ? map.target : map;
this._map._initDefRoot();
// Create the DOM Object for the pattern.
this._initDom();
// Any shapes that were added before this was added to the map need to have their onAdd called.
// for (let i of this._shapes) {
// i.onAdd(this);
// }
Object
.values(this._shapes)
.forEach(x => x.onAdd(this))
// Call any children that want to add their own shapes.
this._addShapes();
// Add the DOM Object to the DOM Tree
this._addDom();
this.redraw();
if (this.getEvents) {
this._map.on(this.getEvents(), this);
}
this.fire('add');
this._map.fire('patternadd', {pattern: this});
},
onRemove: function () {
this._removeDom();
},
redraw: function () {
if (this._map) {
this._update();
// for (let i of this._shapes) {
// i.redraw();
// }
Object
.values(this._shapes)
.forEach(x => x.redraw())
}
return this;
},
setStyle: function (style) {
L.setOptions(this, style);
if (this._map) {
this._updateStyle();
this.redraw();
}
return this;
},
addTo: function (map) {
map.addPattern(this);
return this;
},
remove: function () {
return this.removeFrom(this._map);
},
removeFrom: function (map) {
if (map) {
map.removePattern(this);
}
return this;
}
});
L.Map.addInitHook(function () {
this._patterns = {};
});
L.Map.include({
addPattern: function (pattern) {
var id = L.stamp(pattern);
if (this._patterns[id]) { return pattern; }
this._patterns[id] = pattern;
this.whenReady(pattern.onAdd, pattern);
return this;
},
removePattern: function (pattern) {
var id = L.stamp(pattern);
if (!this._patterns[id]) { return this; }
if (this._loaded) {
pattern.onRemove(this);
}
if (pattern.getEvents) {
this.off(pattern.getEvents(), pattern);
}
delete this._patterns[id];
if (this._loaded) {
this.fire('patternremove', {pattern: pattern});
pattern.fire('remove');
}
pattern._map = null;
return this;
},
hasPattern: function (pattern) {
return !!pattern && (L.stamp(pattern) in this._patterns);
}
});
L.Pattern.SVG_NS = 'http://www.w3.org/2000/svg';
L.Pattern = L.Pattern.extend({
_createElement: function (name) {
return document.createElementNS(L.Pattern.SVG_NS, name);
},
_initDom: function () {
this._dom = this._createElement('pattern');
if (this.options.className) {
L.DomUtil.addClass(this._dom, this.options.className);
}
this._updateStyle();
},
_addDom: function () {
this._map._defRoot.appendChild(this._dom);
},
_removeDom: function () {
L.DomUtil.remove(this._dom);
},
_updateStyle: function () {
var dom = this._dom,
options = this.options;
if (!dom) { return; }
dom.setAttribute('id', L.stamp(this));
dom.setAttribute('x', options.x);
dom.setAttribute('y', options.y);
dom.setAttribute('width', options.width);
dom.setAttribute('height', options.height);
dom.setAttribute('patternUnits', options.patternUnits);
dom.setAttribute('patternContentUnits', options.patternContentUnits);
if (options.patternTransform || options.angle) {
var transform = options.patternTransform ? options.patternTransform + " " : "";
transform += options.angle ? "rotate(" + options.angle + ") " : "";
dom.setAttribute('patternTransform', transform);
}
else {
dom.removeAttribute('patternTransform');
}
// for (let i of this._shapes) {
// i._updateStyle();
// }
Object
.values(this._shapes)
.forEach(x => x._updateStyle())
}
});
L.Map.include({
_initDefRoot: function () {
if (!this._defRoot) {
if (typeof this.getRenderer === 'function') {
var renderer = this.getRenderer(this);
this._defRoot = L.Pattern.prototype._createElement('defs');
renderer._container.appendChild(this._defRoot);
} else {
if (!this._pathRoot) {
this._initPathRoot();
}
this._defRoot = L.Pattern.prototype._createElement('defs');
this._pathRoot.appendChild(this._defRoot);
}
}
}
});
if (L.SVG) {
L.SVG.include({
_superUpdateStyle: L.SVG.prototype._updateStyle,
_updateStyle: function (layer) {
this._superUpdateStyle(layer);
if (layer.options.fill && layer.options.fillPattern) {
layer._path.setAttribute('fill', 'url(#' + L.stamp(layer.options.fillPattern) + ")");
}
}
});
}
else {
L.Path.include({
_superUpdateStyle: L.Path.prototype._updateStyle,
_updateStyle: function () {
this._superUpdateStyle();
if (this.options.fill && this.options.fillPattern) {
this._path.setAttribute('fill', 'url(#' + L.stamp(this.options.fillPattern) + ")");
}
}
});
}
/*
* L.StripePattern is an implementation of Pattern that creates stripes.
*/
L.StripePattern = L.Pattern.extend({
options: {
weight: 4,
spaceWeight: 4,
color: '#000000',
spaceColor: '#ffffff',
opacity: 1.0,
spaceOpacity: 0.0
},
_addShapes: function () {
this._stripe = new L.PatternPath({
stroke: true,
weight: this.options.weight,
color: this.options.color,
opacity: this.options.opacity
});
this._space = new L.PatternPath({
stroke: true,
weight: this.options.spaceWeight,
color: this.options.spaceColor,
opacity: this.options.spaceOpacity
});
this.addShape(this._stripe);
this.addShape(this._space);
this._update();
},
_update: function () {
this._stripe.options.d = 'M0 ' + this._stripe.options.weight / 2 + ' H ' + this.options.width;
this._space.options.d = 'M0 ' + (this._stripe.options.weight + this._space.options.weight / 2) + ' H ' + this.options.width;
},
setStyle: L.Pattern.prototype.setStyle
});
L.stripePattern = function (options) {
return new L.StripePattern(options);
};
/*
* L.PatternShape is the base class that is used to define the shapes in Patterns.
*/
L.PatternShape = L.Class.extend({
options: {
stroke: true,
color: '#3388ff',
weight: 3,
opacity: 1,
lineCap: 'round',
lineJoin: 'round',
// dashArray: null
// dashOffset: null
// fill: false
// fillColor: same as color by default
fillOpacity: 0.2,
fillRule: 'evenodd',
// fillPattern: L.Pattern
},
initialize: function (options) {
L.setOptions(this, options);
},
// Called when the parent Pattern get's added to the map,
// or when added to a Pattern that is already on the map.
onAdd: function (pattern) {
this._pattern = pattern;
if (this._pattern._dom) {
this._initDom(); // This function is implemented by it's children.
this._addDom();
}
},
addTo: function (pattern) {
pattern.addShape(this);
return this;
},
redraw: function () {
if (this._pattern) {
this._updateShape(); // This function is implemented by it's children.
}
return this;
},
setStyle: function (style) {
L.setOptions(this, style);
if (this._pattern) {
this._updateStyle();
}
return this;
},
setShape: function (shape) {
this.options = L.extend({}, this.options, shape);
this._updateShape();
},
});
L.Pattern.include({
addShape: function (shape) {
var id = L.stamp(shape);
if (this._shapes[id]) { return shape; }
this._shapes[id] = shape;
shape.onAdd(this);
}
});
L.PatternShape.SVG_NS = 'http://www.w3.org/2000/svg';
L.PatternShape = L.PatternShape.extend({
_createElement: function (name) {
return document.createElementNS(L.PatternShape.SVG_NS, name);
},
_initDom: L.Util.falseFn,
_updateShape: L.Util.falseFn,
_initDomElement: function (type) {
this._dom = this._createElement(type);
if (this.options.className) {
L.DomUtil.addClass(this._dom, this.options.className);
}
this._updateStyle();
},
_addDom: function () {
this._pattern._dom.appendChild(this._dom);
},
_updateStyle: function () {
var dom = this._dom,
options = this.options;
if (!dom) { return; }
if (options.stroke) {
dom.setAttribute('stroke', options.color);
dom.setAttribute('stroke-opacity', options.opacity);
dom.setAttribute('stroke-width', options.weight);
dom.setAttribute('stroke-linecap', options.lineCap);
dom.setAttribute('stroke-linejoin', options.lineJoin);
if (options.dashArray) {
dom.setAttribute('stroke-dasharray', options.dashArray);
} else {
dom.removeAttribute('stroke-dasharray');
}
if (options.dashOffset) {
dom.setAttribute('stroke-dashoffset', options.dashOffset);
} else {
dom.removeAttribute('stroke-dashoffset');
}
} else {
dom.setAttribute('stroke', 'none');
}
if (options.fill) {
if (options.fillPattern) {
dom.setAttribute('fill', 'url(#' + L.stamp(options.fillPattern) + ")");
}
else {
dom.setAttribute('fill', options.fillColor || options.color);
}
dom.setAttribute('fill-opacity', options.fillOpacity);
dom.setAttribute('fill-rule', options.fillRule || 'evenodd');
} else {
dom.setAttribute('fill', 'none');
}
dom.setAttribute('pointer-events', options.pointerEvents || (options.interactive ? 'visiblePainted' : 'none'));
}
});
/*
* L.PatternPath is the implementation of PatternShape for adding Paths
*/
L.PatternPath = L.PatternShape.extend({
// options: {
// d: <svg path code>
// },
_initDom: function () {
this._initDomElement('path');
},
_updateShape: function () {
if (!this._dom) { return; }
this._dom.setAttribute('d', this.options.d);
}
});
/*
* L.PatternCircle is the implementation of PatternShape for adding Circles
*/
L.PatternCircle = L.PatternShape.extend({
options: {
x: 0,
y: 0,
radius: 0
},
_initDom: function () {
this._initDomElement('circle');
},
_updateShape: function () {
if (!this._dom) { return; }
this._dom.setAttribute('cx', this.options.x);
this._dom.setAttribute('cy', this.options.y);
this._dom.setAttribute('r', this.options.radius);
}
});
/*
* L.PatternRect is the implementation of PatternShape for adding Rectangles
*/
L.PatternRect = L.PatternShape.extend({
options: {
x: 0,
y: 0,
width: 10,
height: 10,
// rx: x radius for rounded corners
// ry: y radius for rounded corners
},
_initDom: function () {
this._initDomElement('rect');
},
_updateShape: function () {
if (!this._dom) { return; }
this._dom.setAttribute('x', this.options.x);
this._dom.setAttribute('y', this.options.y);
this._dom.setAttribute('width', this.options.width);
this._dom.setAttribute('height', this.options.height);
if (this.options.rx) { this._dom.setAttribute('rx', this.options.rx); }
if (this.options.ry) { this._dom.setAttribute('ry', this.options.ry); }
}
});
}(window, document));
+7
View File
@@ -0,0 +1,7 @@
export function actionRequest(type){
return type + '_REQUEST'
}
export function actionFailure(type){
return type + '_FAILURE'
}
+21
View File
@@ -0,0 +1,21 @@
import {mobileDetectionScreenSize} from '../config'
export const isLandscapeMedia = matchMedia('(orientation: landscape)')
function matchMedia(query) {
return window.matchMedia(query)
}
export function isMobile() {
const landscape = isLandscape()
const mobileWidth = landscape
? mobileDetectionScreenSize[0]
: mobileDetectionScreenSize[1]
const isMobileMedia = matchMedia(`(max-width: ${mobileWidth}px)`)
return isMobileMedia.matches
}
export function isLandscape(){
return isLandscapeMedia.matches
}
+107
View File
@@ -0,0 +1,107 @@
/**
* f([x, y, z, ...], [m, n, k, ...]) -> [[x, m], [y, n], [z, k], ...]
* @param list1
* @param list2
*/
export function zip(list1, list2) {
const [small, other] = list1.length <= list2.length
? [list1, list2]
: [list2, list1]
return small.map((s, i) => [s, other[i]])
}
/**
* f(g, [x, y, z, ...], [m, n, k, ...]) -> [[x, m], [y, n], [z, k], ...] based on G
* @param list1
* @param list2
*/
export function zipBy (key, list1, list2) {
const [small, other] = list1.length <= list2.length
? [list1, list2]
: [list2, list1]
return small.map(s => {
const k = key(s)
const o = other.find(i => key(i) === k)
return [s, o]
})
}
export function arraysIsEqual(list1, list2){
if (list1.length !== list2.length) return false
const booleans = zip(list1, list2)
.map((a, b) => a === b)
return all(booleans)
}
export function all(list){
return list
.map(Boolean)
.reduce((acc, i) => acc && i, true)
}
export function any(list){
return list
.map(Boolean)
.reduce((acc, i) => acc || i, false)
}
export function exist(value){
return all([
value !== null,
value !== undefined,
])
}
/**
* f(g) -> m(x) -> g(x) -> x
*
* @param fn
* @returns {function(*)}
*/
export function pass(fn) {
return x => {
fn(x)
return x
}
}
/**
* f(n) -> g(x) -> {[n]: x}
*
* @param name
* @returns {function(*): {}}
*/
export function pack(name) {
//return value => {[name]: value}
return value => ({[name]: value})
}
/**
* f(g, m, ...) -> g . m . ...
*
* @param functions
* @returns {function(*=): *}
*/
export function compose(...functions) {
const fs = functions.reverse()
return (...value) => fs.reduce((acc, fn) => fn(...acc), value)
}
/**
* f(t, g) -> g(x) after t milliseconds
*
* @param ms
* @param callback
* @returns {function(...[*])}
*/
export function debounce(ms, callback) {
let id = 0
return (...args) => {
clearTimeout(id)
id = setTimeout(callback, ms, ...args)
}
}
+7
View File
@@ -0,0 +1,7 @@
import Leaflet from 'leaflet'
export function getBoundsOf(feature) {
return Leaflet
.geoJSON(feature)
.getBounds()
}
+19
View File
@@ -0,0 +1,19 @@
import {actionRequest, actionFailure} from './action'
export default function promiseMiddleware() {
return next => action => {
const {async, type, ...rest} = action
if (!async) return next(action)
next({...rest, type: actionRequest(type)})
return async
.then(res => {
next({...rest, ...res, error: null, type})
return true
})
.catch(error => {
next({...rest, error, type: actionFailure(type)})
return false
})
}
}
+6
View File
@@ -0,0 +1,6 @@
export default function createEffects(effects) {
return store => next => action => {
next(action)
effects.forEach(effect => effect(store.getState(), action, store.dispatch))
}
}
+3
View File
@@ -0,0 +1,3 @@
export function sleep(ms = 0) {
return new Promise(resolve => setTimeout(resolve, ms))
}
+16
View File
@@ -0,0 +1,16 @@
export function getVariable(name, element = document.documentElement) {
const value = getComputedStyle(element).getPropertyValue(name)
return parseValue(value)
}
function parseValue(value) {
const values = /\s*(\d+)(%|px)\s*/.exec(value)
if (values) {
const v = Number(values[1])
// const v = values[1]
const w = values[2]
if (w === '%') return v / 100
return v
}
return value
}
+8
View File
@@ -0,0 +1,8 @@
import {combineReducers} from 'redux'
import {routerReducer} from 'react-router-redux'
// import app from '../features/App/duck'
export default combineReducers({
// app,
routing: routerReducer
})
+15
View File
@@ -0,0 +1,15 @@
import {createStore, applyMiddleware, compose} from 'redux'
// import thunk from 'redux-thunk'
// import async from '../lib/promiseMiddleware'
// import createEffects from '../lib/sideEffectsMiddleware'
// import {routerMiddleware} from 'react-router-redux'
// import {browserHistory} from 'react-router'
export function create(reducer, init, enhancers = [], effects = []) {
// const browser = routerMiddleware(browserHistory)
// const es = createEffects(effects)
// const enhancer = compose(applyMiddleware(browser, async, thunk, es), ...enhancers)
const enhancer = compose(applyMiddleware(), ...enhancers)
return createStore(reducer, init, enhancer)
}
+14 -1
View File
@@ -11,7 +11,7 @@ const isEqualPate = require('../lib/url').isEqualPath
const {compose, any} = require('../lib/common')
const menuModel = require('../models/menu').default
const {viewMain} = require('../config')
const {viewMain, viewEdit} = require('../config')
const defaultOptions = {
commentsEnabled: false,
@@ -105,4 +105,17 @@ async function render(path, data, meta, options = {}) {
})
}
async function renderEdit() {
const renderOptions = {
...defaultOptions,
templateFile: viewEdit,
}
const source = await readFile(renderOptions.templateFile, 'utf-8')
const template = handlebars.compile(source)
return template({})
}
exports.render = render
exports.renderEdit = renderEdit
+10
View File
@@ -0,0 +1,10 @@
const {get} = require('koa-route')
const {renderEdit} = require('../lib/render')
function getEdit() {
return get('/edit', async ctx => {
ctx.body = await renderEdit()
})
}
exports.getEdit = getEdit
+2
View File
@@ -27,6 +27,7 @@ const document = require('./routes/document')
const schedule = require('./routes/schedule')
const pages = require('./routes/pages')
const sitemap = require('./routes/sitemap')
const edit = require('./routes/edit')
const dirPublic = path.join(__dirname, '../public')
@@ -65,6 +66,7 @@ export default function (config) {
app.use(error.notFound(view404))
app.use(sitemap())
// app.use(edit.getEdit())
app.use(home.getHome(config.articlesPerPage))
app.use(gallery.getGallery())
app.use(article.getArticles(config.articlesPerPage))
+27
View File
@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<base href="/">
<title>Edit Hudozka</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, maximum-scale=1.0"/>
<meta name="referrer" content="unsafe-url">
<link rel="icon" type="image/png" href="//static.shlisselburg.org/art/graphics/favicon-32.png"/>
<link rel="apple-touch-icon" sizes="57x57" href="//static.shlisselburg.org/art/graphics/favicon-57.png"/>
<link rel="apple-touch-icon" sizes="72x72" href="//static.shlisselburg.org/art/graphics/favicon-72.png"/>
<link rel="apple-touch-icon" sizes="114x114" href="//static.shlisselburg.org/art/graphics/favicon-114.png"/>
<link rel="apple-touch-icon" sizes="144x144" href="//static.shlisselburg.org/art/graphics/favicon-144.png"/>
<!--<link href="https://fonts.googleapis.com/css?family=PT+Sans|PT+Serif" rel="stylesheet" as="font">-->
<link rel="stylesheet" href="/style.css">
</head>
<body>
<div id="app"></div>
<script src="edit.js"></script>
</body>
</html>
+13
View File
@@ -6,6 +6,7 @@ const targetDir = path.resolve(__dirname, 'public')
module.exports = {
entry: {
app: './src/public/app.js',
edit: './src/edit.entry.js',
},
output: {
path: targetDir,
@@ -56,11 +57,23 @@ module.exports = {
},
},
]
},
{
test: /\.css/,
use: [
'style-loader',
'css-loader',
]
}
],
},
plugins: [
// new HtmlWebpackPlugin({
// filename: 'edit.html',
// template: './src/views/edit.html',
// })
]
}