<commit_before>const fs = require('fs')
const {shell, remote} = require('electron')
const {h, Component} = require('preact')
const classNames = require('classnames')

const Drawer = require('./Drawer')

const dialog = require('../../modules/dialog')
const helper = require('../../modules/helper')
const setting = remote.require('./setting')

class PreferencesItem extends Component {
    constructor(props) {
        super(props)

        this.state = {
            checked: setting.get(props.id)
        }

        this.handleChange = evt => {
            let {onChange = helper.noop} = this.props
            let {checked} = evt.currentTarget

            setting.set(this.props.id, checked)
            onChange(Object.assign({checked}, this.props))
        }

        setting.events.on('change', ({key}) => {
            if (key === this.props.id) {
                this.setState({checked: setting.get(key)})
            }
        })
    }

    shouldComponentUpdate(_, {checked}) {
        return checked !== this.state.checked
    }

    render({text}, {checked}) {
        return h('li', {},
            h('label', {},
                h('input', {
                    type: 'checkbox',
                    checked,
                    onChange: this.handleChange
                }), ' ',

                text
            )
        )
    }
}

class GeneralTab extends Component {
    constructor() {
        super()

        this.handleSoundEnabledChange = evt => {
            sabaki.window.webContents.setAudioMuted(!evt.checked)
        }

        this.handleTreeStyleChange = evt => {
            let data = {compact: [16, 4], spacious: [22, 4], big: [26, 6]}
            let [graphGridSize, graphNodeSize] = data[evt.currentTarget.value]

            setting.set('graph.grid_size', graphGridSize)
            setting.set('graph.node_size', graphNodeSize)
        }
    }

    render({graphGridSize}) {
        return h('div', {class: 'general'},
            h('ul', {},
                h(PreferencesItem, {
                    id: 'app.startup_check_updates',
                    text: 'Check for updates at startup'
                }),
                h(PreferencesItem, {
                    id: 'sound.enable',
                    text: 'Enable sounds',
                    onChange: this.handleSoundEnabledChange
                }),
                h(PreferencesItem, {
                    id: 'game.goto_end_after_loading',
                    text: 'Jump to end after loading file'
                }),
                h(PreferencesItem, {
                    id: 'view.fuzzy_stone_placement',
                    text: 'Fuzzy stone placement'
                }),
                h(PreferencesItem, {
                    id: 'view.animated_stone_placement',
                    text: 'Animate fuzzy placement'
                }),
                h(PreferencesItem, {
                    id: 'file.show_reload_warning',
                    text: 'Offer to reload file if changed externally'
                }),
                h(PreferencesItem, {
                    id: 'gtp.start_game_after_attach',
                    text: 'Start game right after attaching engines'
                }),
                h(PreferencesItem, {
                    id: 'app.enable_hardware_acceleration',
                    text: 'Enable hardware acceleration'
                })
            ),

            h('ul', {},
                h(PreferencesItem, {
                    id: 'comments.show_move_interpretation',
                    text: 'Show automatic move titles'
                }),
                h(PreferencesItem, {
                    id: 'game.show_ko_warning',
                    text: 'Show ko warning'
                }),
                h(PreferencesItem, {
                    id: 'game.show_suicide_warning',
                    text: 'Show suicide warning'
                }),
                h(PreferencesItem, {
                    id: 'edit.show_removenode_warning',
                    text: 'Show remove node warning'
                }),
                h(PreferencesItem, {
                    id: 'edit.show_removeothervariations_warning',
                    text: 'Show remove other variations warning'
                }),
                h(PreferencesItem, {
                    id: 'edit.click_currentvertex_to_remove',
                    text: 'Click last played stone to remove'
                })
            ),

            h('p', {}, h('label', {},
                'Game Tree Style: ',

                h('select', {onChange: this.handleTreeStyleChange},
                    h('option', {
                        value: 'compact',
                        selected: graphGridSize < 22
                    }, 'Compact'),

                    h('option', {
                        value: 'spacious',
                        selected: graphGridSize === 22
                    }, 'Spacious'),

                    h('option', {
                        value: 'big',
                        selected: graphGridSize > 22
                    }, 'Big')
                )
            ))
        )
    }
}

class PathInputItem extends Component {
    constructor(props) {
        super(props)

        this.state = {
            value: setting.get(props.id)
        }

        this.handlePathChange = evt => {
            let value = evt.currentTarget.value.trim() === '' ? null : evt.currentTarget.value

            setting.set(this.props.id, value)
        }

        this.handleBrowseButtonClick = evt => {
            dialog.showOpenDialog({
                properties: ['openFile'],
            }, ({result}) => {
                if (!result || result.length === 0) return

                this.handlePathChange({currentTarget: {value: result[0]}})
            })
        }

        setting.events.on('change', ({key}) => {
            if (key === this.props.id) {
                this.setState({value: setting.get(key)})
            }
        })
    }

    shouldComponentUpdate({text}, {value}) {
        return this.props.text !== text
            || this.props.value !== value
    }

    render({text}, {value}) {
        return h('li', {}, h('label', {},
            h('span', {}, text),

            h('input', {
                type: 'search',
                placeholder: 'Path',
                value,
                onChange: this.handlePathChange
            }),

            h('a',
                {
                    class: 'browse',
                    onClick: this.handleBrowseButtonClick
                },
                h('img', {
                    src: './node_modules/octicons/build/svg/file-directory.svg',
                    title: 'Browse…',
                    height: 14
                })
            ),

            value && !fs.existsSync(value) && h('a', {class: 'invalid'},
                h('img', {
                    src: './node_modules/octicons/build/svg/alert.svg',
                    title: 'File not found',
                    height: 14
                })
            )
        ))
    }
}

class ThemesTab extends Component {
    constructor() {
        super()

        this.state = {
            currentTheme: setting.get('theme.current')
        }

        this.handleThemeChange = evt => {
            let value = evt.currentTarget.value === '' ? null : evt.currentTarget.value

            setting.set('theme.current', value)
        }

        this.handleLinkClick = evt => {
            evt.preventDefault()

            shell.openExternal(evt.currentTarget.href)
        }

        this.handleUninstallButton = evt => {
            evt.preventDefault()

            let result = dialog.showMessageBox(
                'Do you really want to uninstall this theme?',
                'warning', ['Uninstall', 'Cancel'], 1
            )

            if (result === 1) return

            let rimraf = require('rimraf')
            let {path} = setting.getThemes()[this.state.currentTheme]

            rimraf(path, err => {
                if (err) return dialog.showMessageBox('Uninstallation failed.', 'error')

                setting.loadThemes()
                setting.set('theme.current', null)
            })
        }

        this.handleInstallButton = evt => {
            evt.preventDefault()

            dialog.showOpenDialog({
                properties: ['openFile'],
                filters: [{name: 'Sabaki Themes', extensions: ['asar']}]
            }, ({result}) => {
                if (!result || result.length === 0) return

                let {join} = require('path')
                let copy = require('recursive-copy')
                let uuid = require('uuid/v1')
                let id = uuid()

                copy(result[0], join(setting.themesDirectory, id), err => {
                    if (err) return dialog.showMessageBox('Installation failed.', 'error')

                    setting.loadThemes()
                    setting.set('theme.current', id)
                })
            })
        }

        setting.events.on('change', ({key}) => {
            if (key === 'theme.current') {
                this.setState({currentTheme: setting.get(key)})
            }
        })
    }

    render() {
        let currentTheme = setting.getThemes()[this.state.currentTheme]

        return h('div', {class: 'themes'},
            h('h3', {}, 'Custom Images'),

            h('ul', {},
                h(PathInputItem, {
                    id: 'theme.custom_blackstones',
                    text: 'Black stone image:'
                }),
                h(PathInputItem, {
                    id: 'theme.custom_whitestones',
                    text: 'White stone image:'
                }),
                h(PathInputItem, {
                    id: 'theme.custom_background',
                    text: 'Background image:'
                })
            ),

            h('h3', {}, 'Current Theme'),

            h('p', {},
                h('select',
                    {
                        id: 'theme.current',
                        onChange: this.handleThemeChange
                    },

                    h('option', {value: '', selected: currentTheme == null}, 'Default'),

                    Object.keys(setting.getThemes()).map(id => h('option',
                        {
                            value: id,
                            selected: currentTheme && currentTheme.id === id
                        },

                        setting.getThemes()[id].name
                    ))
                ), ' ',

                currentTheme && h('button', {type: 'button', onClick: this.handleUninstallButton}, 'Uninstall'),

                h('div', {class: 'install'},
                    h('button', {type: 'button', onClick: this.handleInstallButton}, 'Install Theme…'),
                    ' ',
                    h('a', {
                        href: 'https://github.com/SabakiHQ/Sabaki/blob/master/docs/guides/theme-directory.md',
                        onClick: this.handleLinkClick
                    }, 'Get more themes…')
                )
            ),

            currentTheme && [
                h('p', {class: 'meta'},
                    currentTheme.author && 'by ' + currentTheme.author,
                    currentTheme.author && currentTheme.homepage && ' — ',
                    currentTheme.homepage && h('a', {
                        class: 'homepage',
                        href: currentTheme.homepage,
                        title: currentTheme.homepage,
                        onClick: this.handleLinkClick
                    }, 'Homepage')
                ),

                h('p', {class: 'description'},
                    currentTheme.version && h('span', {class: 'version'},
                        'v' + currentTheme.version
                    ), ' ',

                    currentTheme.description
                )
            ]
        )
    }
}

class EngineItem extends Component {
    constructor() {
        super()

        this.handleChange = evt => {
            let {onChange = helper.noop} = this.props
            let element = evt.currentTarget
            let data = Object.assign({}, this.props, {
                [element.name]: element.value
            })

            onChange(data)
        }

        this.handleBrowseButtonClick = () => {
            dialog.showOpenDialog({
                properties: ['openFile'],
                filters: [{name: 'All Files', extensions: ['*']}]
            }, ({result}) => {
                if (!result || result.length === 0) return

                let {id, name, args, onChange = helper.noop} = this.props
                onChange({id, name, args, path: result[0]})
            })
        }

        this.handleRemoveButtonClick = () => {
            let {onRemove = helper.noop} = this.props
            onRemove(this.props)
        }
    }

    shouldComponentUpdate({name, path, args, commands}) {
        return name !== this.props.name
            || path !== this.props.path
            || args !== this.props.args
            || commands !== this.props.commands
    }

    render({name, path, args, commands}) {
        return h('li', {},
            h('h3', {},
                h('a',
                    {
                        class: 'remove',
                        title: 'Remove',
                        onClick: this.handleRemoveButtonClick
                    },

                    h('img', {src: './node_modules/octicons/build/svg/x.svg'})
                ),
                h('input', {
                    type: 'text',
                    placeholder: '(Unnamed Engine)',
                    value: name,
                    name: 'name',
                    onChange: this.handleChange
                })
            ),
            h('p', {},
                h('a',
                    {
                        class: 'browse',
                        title: 'Browse…',
                        onClick: this.handleBrowseButtonClick
                    },

                    h('img', {src: './node_modules/octicons/build/svg/file-directory.svg'})
                ),
                h('input', {
                    type: 'text',
                    placeholder: 'Path',
                    value: path,
                    name: 'path',
                    onChange: this.handleChange
                })
            ),
            h('p', {},
                h('input', {
                    type: 'text',
                    placeholder: 'No arguments',
                    value: args,
                    name: 'args',
                    onChange: this.handleChange
                })
            ),
            h('p', {},
                h('input', {
                    type: 'text',
                    placeholder: 'Initial commands (;-separated)',
                    value: commands,
                    name: 'commands',
                    onChange: this.handleChange
                })
            )
        )
    }
}

class EnginesTab extends Component {
    constructor() {
        super()

        this.handleItemChange = ({id, name, path, args, commands}) => {
            let engines = this.props.engines.slice()

            engines[id] = {name, path, args, commands}
            setting.set('engines.list', engines)
        }

        this.handleItemRemove = ({id}) => {
            let engines = this.props.engines.slice()

            engines.splice(id, 1)
            setting.set('engines.list', engines)
        }

        this.handleAddButtonClick = evt => {
            evt.preventDefault()

            let engines = this.props.engines.slice()

            engines.unshift({name: '', path: '', args: ''})
            setting.set('engines.list', engines)

            this.setState({}, () => {
                this.element.querySelector('.engines-list li:first-child input').focus()
            })
        }
    }

    render({engines}) {
        return h('div', {ref: el => this.element = el, class: 'engines'},
            h('div', {class: 'engines-list'},
                h('ul', {}, engines.map(({name, path, args, commands}, id) =>
                    h(EngineItem, {
                        id,
                        name,
                        path,
                        args,
                        commands,

                        onChange: this.handleItemChange,
                        onRemove: this.handleItemRemove
                    })
                ))
            ),

            h('p', {},
                h('button', {type: 'button', onClick: this.handleAddButtonClick}, 'Add')
            )
        )
    }
}

class PreferencesDrawer extends Component {
    constructor() {
        super()

        this.handleCloseButtonClick = evt => {
            evt.preventDefault()
            sabaki.closeDrawer()
        }

        this.handleTabClick = evt => {
            let tabs = ['general', 'themes', 'engines']
            let tab = tabs.find(x => evt.currentTarget.classList.contains(x))

            sabaki.setState({preferencesTab: tab})
        }
    }

    shouldComponentUpdate({show}) {
        return show || show !== this.props.show
    }

    componentDidUpdate(prevProps) {
        if (prevProps.show && !this.props.show) {
            // On closing

            let natsort = require('natsort')
            let cmp = natsort({insensitive: true})

            // Sort engines.

            let engines = [...this.props.engines].sort((x, y) => cmp(x.name, y.name))

            setting.set('engines.list', engines)

            // Reset tab selection

            setTimeout(() => sabaki.setState({preferencesTab: 'general'}), 500)
        }
    }

    render({show, tab, engines, graphGridSize}) {
        return h(Drawer,
            {
                type: 'preferences',
                show
            },

            h('ul', {class: 'tabs'},
                h('li', {
                        class: classNames({general: true, current: tab === 'general'}),
                        onClick: this.handleTabClick
                    },

                    h('a', {href: '#'}, 'General')
                ),
                h('li',
                    {
                        class: classNames({themes: true, current: tab === 'themes'}),
                        onClick: this.handleTabClick
                    },

                    h('a', {href: '#'}, 'Themes')
                ),
                h('li',
                    {
                        class: classNames({engines: true, current: tab === 'engines'}),
                        onClick: this.handleTabClick
                    },

                    h('a', {href: '#'}, 'Engines')
                )
            ),

            h('form', {class: tab},
                h(GeneralTab, {graphGridSize}),
                h(ThemesTab),
                h(EnginesTab, {engines}),

                h('p', {},
                    h('button', {type: 'button', onClick: this.handleCloseButtonClick}, 'Close')
                )
            )
        )
    }
}

module.exports = PreferencesDrawer
<commit_msg>Add preferences item<commit_after>const fs = require('fs')
const {shell, remote} = require('electron')
const {h, Component} = require('preact')
const classNames = require('classnames')

const Drawer = require('./Drawer')

const dialog = require('../../modules/dialog')
const helper = require('../../modules/helper')
const setting = remote.require('./setting')

class PreferencesItem extends Component {
    constructor(props) {
        super(props)

        this.state = {
            checked: setting.get(props.id)
        }

        this.handleChange = evt => {
            let {onChange = helper.noop} = this.props
            let {checked} = evt.currentTarget

            setting.set(this.props.id, checked)
            onChange(Object.assign({checked}, this.props))
        }

        setting.events.on('change', ({key}) => {
            if (key === this.props.id) {
                this.setState({checked: setting.get(key)})
            }
        })
    }

    shouldComponentUpdate(_, {checked}) {
        return checked !== this.state.checked
    }

    render({text}, {checked}) {
        return h('li', {},
            h('label', {},
                h('input', {
                    type: 'checkbox',
                    checked,
                    onChange: this.handleChange
                }), ' ',

                text
            )
        )
    }
}

class GeneralTab extends Component {
    constructor() {
        super()

        this.handleSoundEnabledChange = evt => {
            sabaki.window.webContents.setAudioMuted(!evt.checked)
        }

        this.handleTreeStyleChange = evt => {
            let data = {compact: [16, 4], spacious: [22, 4], big: [26, 6]}
            let [graphGridSize, graphNodeSize] = data[evt.currentTarget.value]

            setting.set('graph.grid_size', graphGridSize)
            setting.set('graph.node_size', graphNodeSize)
        }
    }

    render({graphGridSize}) {
        return h('div', {class: 'general'},
            h('ul', {},
                h(PreferencesItem, {
                    id: 'app.startup_check_updates',
                    text: 'Check for updates at startup'
                }),
                h(PreferencesItem, {
                    id: 'sound.enable',
                    text: 'Enable sounds',
                    onChange: this.handleSoundEnabledChange
                }),
                h(PreferencesItem, {
                    id: 'game.goto_end_after_loading',
                    text: 'Jump to end after loading file'
                }),
                h(PreferencesItem, {
                    id: 'view.fuzzy_stone_placement',
                    text: 'Fuzzy stone placement'
                }),
                h(PreferencesItem, {
                    id: 'view.animated_stone_placement',
                    text: 'Animate fuzzy placement'
                }),
                h(PreferencesItem, {
                    id: 'file.show_reload_warning',
                    text: 'Offer to reload file if changed externally'
                }),
                h(PreferencesItem, {
                    id: 'gtp.start_game_after_attach',
                    text: 'Start game right after attaching engines'
                }),
                h(PreferencesItem, {
                    id: 'app.enable_hardware_acceleration',
                    text: 'Enable hardware acceleration'
                })
            ),

            h('ul', {},
                h(PreferencesItem, {
                    id: 'comments.show_move_interpretation',
                    text: 'Show automatic move titles'
                }),
                h(PreferencesItem, {
                    id: 'game.show_ko_warning',
                    text: 'Show ko warning'
                }),
                h(PreferencesItem, {
                    id: 'game.show_suicide_warning',
                    text: 'Show suicide warning'
                }),
                h(PreferencesItem, {
                    id: 'edit.show_removenode_warning',
                    text: 'Show remove node warning'
                }),
                h(PreferencesItem, {
                    id: 'edit.show_removeothervariations_warning',
                    text: 'Show remove other variations warning'
                }),
                h(PreferencesItem, {
                    id: 'edit.click_currentvertex_to_remove',
                    text: 'Click last played stone to remove'
                }),
                h(PreferencesItem, {
                    id: 'app.always_show_result',
                    text: 'Always show game result'
                })
            ),

            h('p', {}, h('label', {},
                'Game Tree Style: ',

                h('select', {onChange: this.handleTreeStyleChange},
                    h('option', {
                        value: 'compact',
                        selected: graphGridSize < 22
                    }, 'Compact'),

                    h('option', {
                        value: 'spacious',
                        selected: graphGridSize === 22
                    }, 'Spacious'),

                    h('option', {
                        value: 'big',
                        selected: graphGridSize > 22
                    }, 'Big')
                )
            ))
        )
    }
}

class PathInputItem extends Component {
    constructor(props) {
        super(props)

        this.state = {
            value: setting.get(props.id)
        }

        this.handlePathChange = evt => {
            let value = evt.currentTarget.value.trim() === '' ? null : evt.currentTarget.value

            setting.set(this.props.id, value)
        }

        this.handleBrowseButtonClick = evt => {
            dialog.showOpenDialog({
                properties: ['openFile'],
            }, ({result}) => {
                if (!result || result.length === 0) return

                this.handlePathChange({currentTarget: {value: result[0]}})
            })
        }

        setting.events.on('change', ({key}) => {
            if (key === this.props.id) {
                this.setState({value: setting.get(key)})
            }
        })
    }

    shouldComponentUpdate({text}, {value}) {
        return this.props.text !== text
            || this.props.value !== value
    }

    render({text}, {value}) {
        return h('li', {}, h('label', {},
            h('span', {}, text),

            h('input', {
                type: 'search',
                placeholder: 'Path',
                value,
                onChange: this.handlePathChange
            }),

            h('a',
                {
                    class: 'browse',
                    onClick: this.handleBrowseButtonClick
                },
                h('img', {
                    src: './node_modules/octicons/build/svg/file-directory.svg',
                    title: 'Browse…',
                    height: 14
                })
            ),

            value && !fs.existsSync(value) && h('a', {class: 'invalid'},
                h('img', {
                    src: './node_modules/octicons/build/svg/alert.svg',
                    title: 'File not found',
                    height: 14
                })
            )
        ))
    }
}

class ThemesTab extends Component {
    constructor() {
        super()

        this.state = {
            currentTheme: setting.get('theme.current')
        }

        this.handleThemeChange = evt => {
            let value = evt.currentTarget.value === '' ? null : evt.currentTarget.value

            setting.set('theme.current', value)
        }

        this.handleLinkClick = evt => {
            evt.preventDefault()

            shell.openExternal(evt.currentTarget.href)
        }

        this.handleUninstallButton = evt => {
            evt.preventDefault()

            let result = dialog.showMessageBox(
                'Do you really want to uninstall this theme?',
                'warning', ['Uninstall', 'Cancel'], 1
            )

            if (result === 1) return

            let rimraf = require('rimraf')
            let {path} = setting.getThemes()[this.state.currentTheme]

            rimraf(path, err => {
                if (err) return dialog.showMessageBox('Uninstallation failed.', 'error')

                setting.loadThemes()
                setting.set('theme.current', null)
            })
        }

        this.handleInstallButton = evt => {
            evt.preventDefault()

            dialog.showOpenDialog({
                properties: ['openFile'],
                filters: [{name: 'Sabaki Themes', extensions: ['asar']}]
            }, ({result}) => {
                if (!result || result.length === 0) return

                let {join} = require('path')
                let copy = require('recursive-copy')
                let uuid = require('uuid/v1')
                let id = uuid()

                copy(result[0], join(setting.themesDirectory, id), err => {
                    if (err) return dialog.showMessageBox('Installation failed.', 'error')

                    setting.loadThemes()
                    setting.set('theme.current', id)
                })
            })
        }

        setting.events.on('change', ({key}) => {
            if (key === 'theme.current') {
                this.setState({currentTheme: setting.get(key)})
            }
        })
    }

    render() {
        let currentTheme = setting.getThemes()[this.state.currentTheme]

        return h('div', {class: 'themes'},
            h('h3', {}, 'Custom Images'),

            h('ul', {},
                h(PathInputItem, {
                    id: 'theme.custom_blackstones',
                    text: 'Black stone image:'
                }),
                h(PathInputItem, {
                    id: 'theme.custom_whitestones',
                    text: 'White stone image:'
                }),
                h(PathInputItem, {
                    id: 'theme.custom_background',
                    text: 'Background image:'
                })
            ),

            h('h3', {}, 'Current Theme'),

            h('p', {},
                h('select',
                    {
                        id: 'theme.current',
                        onChange: this.handleThemeChange
                    },

                    h('option', {value: '', selected: currentTheme == null}, 'Default'),

                    Object.keys(setting.getThemes()).map(id => h('option',
                        {
                            value: id,
                            selected: currentTheme && currentTheme.id === id
                        },

                        setting.getThemes()[id].name
                    ))
                ), ' ',

                currentTheme && h('button', {type: 'button', onClick: this.handleUninstallButton}, 'Uninstall'),

                h('div', {class: 'install'},
                    h('button', {type: 'button', onClick: this.handleInstallButton}, 'Install Theme…'),
                    ' ',
                    h('a', {
                        href: 'https://github.com/SabakiHQ/Sabaki/blob/master/docs/guides/theme-directory.md',
                        onClick: this.handleLinkClick
                    }, 'Get more themes…')
                )
            ),

            currentTheme && [
                h('p', {class: 'meta'},
                    currentTheme.author && 'by ' + currentTheme.author,
                    currentTheme.author && currentTheme.homepage && ' — ',
                    currentTheme.homepage && h('a', {
                        class: 'homepage',
                        href: currentTheme.homepage,
                        title: currentTheme.homepage,
                        onClick: this.handleLinkClick
                    }, 'Homepage')
                ),

                h('p', {class: 'description'},
                    currentTheme.version && h('span', {class: 'version'},
                        'v' + currentTheme.version
                    ), ' ',

                    currentTheme.description
                )
            ]
        )
    }
}

class EngineItem extends Component {
    constructor() {
        super()

        this.handleChange = evt => {
            let {onChange = helper.noop} = this.props
            let element = evt.currentTarget
            let data = Object.assign({}, this.props, {
                [element.name]: element.value
            })

            onChange(data)
        }

        this.handleBrowseButtonClick = () => {
            dialog.showOpenDialog({
                properties: ['openFile'],
                filters: [{name: 'All Files', extensions: ['*']}]
            }, ({result}) => {
                if (!result || result.length === 0) return

                let {id, name, args, onChange = helper.noop} = this.props
                onChange({id, name, args, path: result[0]})
            })
        }

        this.handleRemoveButtonClick = () => {
            let {onRemove = helper.noop} = this.props
            onRemove(this.props)
        }
    }

    shouldComponentUpdate({name, path, args, commands}) {
        return name !== this.props.name
            || path !== this.props.path
            || args !== this.props.args
            || commands !== this.props.commands
    }

    render({name, path, args, commands}) {
        return h('li', {},
            h('h3', {},
                h('a',
                    {
                        class: 'remove',
                        title: 'Remove',
                        onClick: this.handleRemoveButtonClick
                    },

                    h('img', {src: './node_modules/octicons/build/svg/x.svg'})
                ),
                h('input', {
                    type: 'text',
                    placeholder: '(Unnamed Engine)',
                    value: name,
                    name: 'name',
                    onChange: this.handleChange
                })
            ),
            h('p', {},
                h('a',
                    {
                        class: 'browse',
                        title: 'Browse…',
                        onClick: this.handleBrowseButtonClick
                    },

                    h('img', {src: './node_modules/octicons/build/svg/file-directory.svg'})
                ),
                h('input', {
                    type: 'text',
                    placeholder: 'Path',
                    value: path,
                    name: 'path',
                    onChange: this.handleChange
                })
            ),
            h('p', {},
                h('input', {
                    type: 'text',
                    placeholder: 'No arguments',
                    value: args,
                    name: 'args',
                    onChange: this.handleChange
                })
            ),
            h('p', {},
                h('input', {
                    type: 'text',
                    placeholder: 'Initial commands (;-separated)',
                    value: commands,
                    name: 'commands',
                    onChange: this.handleChange
                })
            )
        )
    }
}

class EnginesTab extends Component {
    constructor() {
        super()

        this.handleItemChange = ({id, name, path, args, commands}) => {
            let engines = this.props.engines.slice()

            engines[id] = {name, path, args, commands}
            setting.set('engines.list', engines)
        }

        this.handleItemRemove = ({id}) => {
            let engines = this.props.engines.slice()

            engines.splice(id, 1)
            setting.set('engines.list', engines)
        }

        this.handleAddButtonClick = evt => {
            evt.preventDefault()

            let engines = this.props.engines.slice()

            engines.unshift({name: '', path: '', args: ''})
            setting.set('engines.list', engines)

            this.setState({}, () => {
                this.element.querySelector('.engines-list li:first-child input').focus()
            })
        }
    }

    render({engines}) {
        return h('div', {ref: el => this.element = el, class: 'engines'},
            h('div', {class: 'engines-list'},
                h('ul', {}, engines.map(({name, path, args, commands}, id) =>
                    h(EngineItem, {
                        id,
                        name,
                        path,
                        args,
                        commands,

                        onChange: this.handleItemChange,
                        onRemove: this.handleItemRemove
                    })
                ))
            ),

            h('p', {},
                h('button', {type: 'button', onClick: this.handleAddButtonClick}, 'Add')
            )
        )
    }
}

class PreferencesDrawer extends Component {
    constructor() {
        super()

        this.handleCloseButtonClick = evt => {
            evt.preventDefault()
            sabaki.closeDrawer()
        }

        this.handleTabClick = evt => {
            let tabs = ['general', 'themes', 'engines']
            let tab = tabs.find(x => evt.currentTarget.classList.contains(x))

            sabaki.setState({preferencesTab: tab})
        }
    }

    shouldComponentUpdate({show}) {
        return show || show !== this.props.show
    }

    componentDidUpdate(prevProps) {
        if (prevProps.show && !this.props.show) {
            // On closing

            let natsort = require('natsort')
            let cmp = natsort({insensitive: true})

            // Sort engines.

            let engines = [...this.props.engines].sort((x, y) => cmp(x.name, y.name))

            setting.set('engines.list', engines)

            // Reset tab selection

            setTimeout(() => sabaki.setState({preferencesTab: 'general'}), 500)
        }
    }

    render({show, tab, engines, graphGridSize}) {
        return h(Drawer,
            {
                type: 'preferences',
                show
            },

            h('ul', {class: 'tabs'},
                h('li', {
                        class: classNames({general: true, current: tab === 'general'}),
                        onClick: this.handleTabClick
                    },

                    h('a', {href: '#'}, 'General')
                ),
                h('li',
                    {
                        class: classNames({themes: true, current: tab === 'themes'}),
                        onClick: this.handleTabClick
                    },

                    h('a', {href: '#'}, 'Themes')
                ),
                h('li',
                    {
                        class: classNames({engines: true, current: tab === 'engines'}),
                        onClick: this.handleTabClick
                    },

                    h('a', {href: '#'}, 'Engines')
                )
            ),

            h('form', {class: tab},
                h(GeneralTab, {graphGridSize}),
                h(ThemesTab),
                h(EnginesTab, {engines}),

                h('p', {},
                    h('button', {type: 'button', onClick: this.handleCloseButtonClick}, 'Close')
                )
            )
        )
    }
}

module.exports = PreferencesDrawer
<|endoftext|>


<commit_before>var fs = require('fs');
var parallelStream = require('pelias-parallel-stream');

var maxInFlight = 10;

module.exports.create = function create_json_parse_stream(dataDirectory) {
  return parallelStream(maxInFlight, function(record, enc, next) {
    fs.readFile(dataDirectory + record.path, function(err, data) {
      if (err) {
        console.error('exception reading file ' + record.path);
        next(err);
      } else {
        try {
          var object = JSON.parse(data);
          next(null, object);
        } catch (parse_err) {
          console.error('exception parsing JSON in file %s:', record.path, parse_err);
          console.error('Inability to parse JSON usually means that WOF has been cloned ' +
                        'without using git-lfs, please see instructions here: ' +
                        'https://github.com/whosonfirst/whosonfirst-data#git-and-large-files');
          next(parse_err);
        }
      }
    });
  });
};
<commit_msg>Use full path in file reading error<commit_after>var fs = require('fs');
var parallelStream = require('pelias-parallel-stream');

var maxInFlight = 10;

module.exports.create = function create_json_parse_stream(dataDirectory) {
  return parallelStream(maxInFlight, function(record, enc, next) {
    var full_file_path = dataDirectory + record.path;
    fs.readFile(full_file_path, function(err, data) {
      if (err) {
        console.error('exception reading file ' + full_file_path);
        next(err);
      } else {
        try {
          var object = JSON.parse(data);
          next(null, object);
        } catch (parse_err) {
          console.error('exception parsing JSON in file %s:', record.path, parse_err);
          console.error('Inability to parse JSON usually means that WOF has been cloned ' +
                        'without using git-lfs, please see instructions here: ' +
                        'https://github.com/whosonfirst/whosonfirst-data#git-and-large-files');
          next(parse_err);
        }
      }
    });
  });
};
<|endoftext|>


<commit_before>/**
 * Created by tonigurski on 19.11.15.
 */

/**
 * preveneds Dropzone from autoloading
 * @type {boolean}
 */
Dropzone.autoDiscover = false;

/**
 * Helper to boot up the media bundle dropzone at the right moment
 */

(function (exports, d) {
    function MMMediaBundleDomReady(fn, context) {

        function onReady(event) {
            d.removeEventListener("DOMContentLoaded", onReady);
            fn.call(context || exports, event);
        }

        function onReadyIe(event) {
            if (d.readyState === "complete") {
                d.detachEvent("onreadystatechange", onReadyIe);
                fn.call(context || exports, event);
            }
        }

        d.addEventListener && d.addEventListener("DOMContentLoaded", onReady) ||
        d.attachEvent && d.attachEvent("onreadystatechange", onReadyIe);
    }

    exports.MMMediaBundleDomReady = MMMediaBundleDomReady;
})(window, document);

/**
 * Set ups a given dropzone to be compatible with the MMMediaBundleWidget
 *
 * @param _id
 * @param _url
 * @returns DropZone
 *
 * @constructor
 */
function MMMediaBundleFileDropzone(_id, _url, _fieldName, _multiple, _files) {

    var $this = this;

    /**
     * creates and DOM input which holds a Media-Entity ID
     * @param _file
     * @returns {Element}
     */
    this.getHiddenEntityInput = function (_file) {

        if (!_file.id)
            return;

        var reference = document.createElement('input')
        reference.type = 'hidden';
        reference.name = _fieldName;
        reference.value = _file.id;

        return reference;
    };

    /**
     * appends an DOM input to the related previewElement of the given file-element
     * @param _file
     */
    this.appendHiddenEntityInput = function (_file) {

        if (_file) {

            var $reference = $this.getHiddenEntityInput(_file)

            if ($reference)
                _file.previewElement.appendChild($reference);
        }
    }

    var $id_dropzone_preview = _id + '_previews'

    /**
     * loads the Dropzone instance
     */
    this.dropzone = new Dropzone(_id,
        { // The camelized version of the ID of the form element
            url: _url,
            // The configuration we've talked about above
            autoProcessQueue: true,
            paramName: (_multiple ? 'files' : 'files[]'),
            addRemoveLinks: true,
            previewsContainer: $id_dropzone_preview,

            uploadMultiple: _multiple,
            parallelUploads: ( (_multiple) ? 100 : 1 ),
            maxFiles: ( (_multiple) ? 100 : 1 ),

            // The setting up of the dropzone
            init: function () {

                var myDropzone = this;

                /**
                 * checks if its an mutiple field and adds the related event
                 */
                if (_multiple) {

                    /**
                     * hooks the successmultiple event to append all hidden inputs which holds all tthe entity ids
                     */
                    myDropzone.on("successmultiple", function (files, response) {

                        if (response && response.success) {

                            for (file in response.data) {

                                files[file].id = response.data[file].id;

                                $this.appendHiddenEntityInput(files[file]);

                            }

                        }

                    });
                }
                else {
                    /**
                     * hooks the sucess event to append a hidden input which holds the entity id
                     */
                    myDropzone.on("success", function (file, response) {

                        if (response && response.success) {

                            for (tFile in response.data) {

                                file.id = response.data[0].id;

                                $this.appendHiddenEntityInput(file);
                            }
                        }
                    });
                }

                /**
                 * add already stored files to the widget
                 */

                    //temporaty add this event to get the real formated dropzone-file objects
                myDropzone.on("addedfile", $this.appendHiddenEntityInput);

                //loops thought the presetted files and appends them to the dropzone_preview
                for (var i = 0; i < _files.length; i++) {
                    var mock = _files[i];
                    mock.accepted = true;

                    myDropzone.files.push(mock);
                    myDropzone.emit('addedfile', mock);
                    myDropzone.createThumbnailFromUrl(mock, mock.url);
                    myDropzone.emit('complete', mock);
                }

                //removes the temporaty added this event
                myDropzone.off("addedfile", $this.appendHiddenEntityInput);

                /**
                 * loads Dragular to enable drag and drop functionality
                 */
                if (_multiple) {

                    var $dragula = dragula([document.querySelector($id_dropzone_preview)],
                        {
                            direction: 'horizontal',
                            mirrorContainer: document.querySelector($id_dropzone_preview)

                        });
                }
            }
        }
    );
}

function MMMediaBundleFileDropzoneInitiateEvents() {

}

/**
 * Start loading the dom is rdy
 */
MMMediaBundleDomReady(function (event) {

    var elements = document.getElementsByClassName('mmmb-dropzone');

    for (var i = 0; i < elements.length; i++) {

        var dropzone = elements[i];

        var url = dropzone.getAttribute('data-url');
        var fieldName = dropzone.getAttribute('data-field-name');
        var id = dropzone.getAttribute('id');

        //fetches preloaded Files
        var files = dropzone.getAttribute('data-files');
        files = JSON.parse(files);

        /*
         * @TODO: better bool check
         */
        var multiple = dropzone.getAttribute('data-multiple');

        if (multiple == "false" || multiple == "" || multiple == "0")
            multiple = false;
        else
            multiple = true;

        //loads the UploadWidget-Dropzone
        new MMMediaBundleFileDropzone("#" + id, url, fieldName, multiple, files);
    }

    MMMediaBundleFileDropzoneInitiateEvents();

});<commit_msg>UploadWidget: added @TODO annotation<commit_after>/**
 * Created by tonigurski on 19.11.15.
 */

/**
 * preveneds Dropzone from autoloading
 * @type {boolean}
 */
Dropzone.autoDiscover = false;

/**
 * Helper to boot up the media bundle dropzone at the right moment
 */

(function (exports, d) {
    function MMMediaBundleDomReady(fn, context) {

        function onReady(event) {
            d.removeEventListener("DOMContentLoaded", onReady);
            fn.call(context || exports, event);
        }

        function onReadyIe(event) {
            if (d.readyState === "complete") {
                d.detachEvent("onreadystatechange", onReadyIe);
                fn.call(context || exports, event);
            }
        }

        d.addEventListener && d.addEventListener("DOMContentLoaded", onReady) ||
        d.attachEvent && d.attachEvent("onreadystatechange", onReadyIe);
    }

    exports.MMMediaBundleDomReady = MMMediaBundleDomReady;
})(window, document);

/**
 * Set ups a given dropzone to be compatible with the MMMediaBundleWidget
 *
 * @param _id
 * @param _url
 * @returns DropZone
 *
 * @constructor
 */
function MMMediaBundleFileDropzone(_id, _url, _fieldName, _multiple, _files) {

    var $this = this;

    /**
     * creates and DOM input which holds a Media-Entity ID
     * @param _file
     * @returns {Element}
     */
    this.getHiddenEntityInput = function (_file) {

        if (!_file.id)
            return;

        var reference = document.createElement('input')
        reference.type = 'hidden';
        reference.name = _fieldName;
        reference.value = _file.id;

        return reference;
    };

    /**
     * appends an DOM input to the related previewElement of the given file-element
     * @param _file
     */
    this.appendHiddenEntityInput = function (_file) {

        if (_file) {

            var $reference = $this.getHiddenEntityInput(_file)

            if ($reference)
                _file.previewElement.appendChild($reference);
        }
    }

    var $id_dropzone_preview = _id + '_previews'

    /**
     * loads the Dropzone instance
     */
    this.dropzone = new Dropzone(_id,
        { // The camelized version of the ID of the form element
            url: _url,
            // The configuration we've talked about above
            autoProcessQueue: true,
            paramName: (_multiple ? 'files' : 'files[]'),
            addRemoveLinks: true,
            previewsContainer: $id_dropzone_preview,

            uploadMultiple: _multiple,
            parallelUploads: ( (_multiple) ? 100 : 1 ),
            maxFiles: ( (_multiple) ? 100 : 1 ),

            // The setting up of the dropzone
            init: function () {

                var myDropzone = this;

                /**
                 * checks if its an mutiple field and adds the related event
                 */
                if (_multiple) {

                    /**
                     * hooks the successmultiple event to append all hidden inputs which holds all tthe entity ids
                     */
                    myDropzone.on("successmultiple", function (files, response) {

                        if (response && response.success) {

                            for (file in response.data) {

                                files[file].id = response.data[file].id;

                                $this.appendHiddenEntityInput(files[file]);

                            }

                        }

                    });
                }
                else {
                    /**
                     * hooks the sucess event to append a hidden input which holds the entity id
                     */
                    myDropzone.on("success", function (file, response) {

                        if (response && response.success) {

                            for (tFile in response.data) {

                                file.id = response.data[0].id;

                                $this.appendHiddenEntityInput(file);
                            }
                        }
                    });
                }

                /**
                 * add already stored files to the widget
                 */

                    //temporaty add this event to get the real formated dropzone-file objects
                myDropzone.on("addedfile", $this.appendHiddenEntityInput);

                //loops thought the presetted files and appends them to the dropzone_preview
                for (var i = 0; i < _files.length; i++) {
                    var mock = _files[i];
                    mock.accepted = true;

                    myDropzone.files.push(mock);
                    myDropzone.emit('addedfile', mock);
                    myDropzone.createThumbnailFromUrl(mock, mock.url);
                    myDropzone.emit('complete', mock);
                }

                //removes the temporaty added this event
                myDropzone.off("addedfile", $this.appendHiddenEntityInput);

                /**
                 * loads Dragular to enable drag and drop functionality
                 * @TODO: check if its possible to enable drag n drop with multiple instances of Dropzone
                 */
                if (_multiple) {

                    var $dragula = dragula([document.querySelector($id_dropzone_preview)],
                        {
                            direction: 'horizontal',
                            mirrorContainer: document.querySelector($id_dropzone_preview)

                        });
                }
            }
        }
    );
}

function MMMediaBundleFileDropzoneInitiateEvents() {

}

/**
 * Start loading the dom is rdy
 */
MMMediaBundleDomReady(function (event) {

    var elements = document.getElementsByClassName('mmmb-dropzone');

    for (var i = 0; i < elements.length; i++) {

        var dropzone = elements[i];

        var url = dropzone.getAttribute('data-url');
        var fieldName = dropzone.getAttribute('data-field-name');
        var id = dropzone.getAttribute('id');

        //fetches preloaded Files
        var files = dropzone.getAttribute('data-files');
        files = JSON.parse(files);

        /*
         * @TODO: better bool check
         */
        var multiple = dropzone.getAttribute('data-multiple');

        if (multiple == "false" || multiple == "" || multiple == "0")
            multiple = false;
        else
            multiple = true;

        //loads the UploadWidget-Dropzone
        new MMMediaBundleFileDropzone("#" + id, url, fieldName, multiple, files);
    }

    MMMediaBundleFileDropzoneInitiateEvents();

});<|endoftext|>


<commit_before>module.exports = {
    exec: async function (msg, args) {
        if (!msg.guildConfig) return msg.channel.createMessage(`Please create a config using ${config.prefix}config command before proceeding.`)
        if (args.toLowerCase() == "remove") {
            if (!msg.guildConfig.modlogChannel) return msg.channel.createMessage(`You don't have a modlog channel set.`)
            msg.guildConfig.modlogChannel = undefined
            await db.table("configs").get(msg.guild.id).update(msg.guildConfig).run()
            msg.channel.createMessage(":ok_hand: Removed the modlog channel")
            return
        }

        msg.guildConfig.modlogChannel = msg.channel.id;
        await db.table("configs").get(msg.guild.id).update(msg.guildConfig).run()
        msg.channel.createMessage(":ok_hand: I'll send modlog messages here.")
    },
    isCmd: true,
    display: true,
    category: 4,
    description: "Sets the current channel as modlog channel.",
    args: "[remove]"
};<commit_msg>attempt #idk<commit_after>module.exports = {
    exec: async function (msg, args) {
        if (!msg.guildConfig) return msg.channel.createMessage(`Please create a config using ${config.prefix}config command before proceeding.`)
        if (args.toLowerCase() == "remove") {
            if (!msg.guildConfig.modlogChannel) return msg.channel.createMessage(`You don't have a modlog channel set.`)
            msg.guildConfig.modlogChannel = undefined
            await db.table("configs").get(msg.guild.id).replace(msg.guildConfig).run()
            msg.channel.createMessage(":ok_hand: Removed the modlog channel")
            return
        }

        msg.guildConfig.modlogChannel = msg.channel.id;
        await db.table("configs").get(msg.guild.id).update(msg.guildConfig).run()
        msg.channel.createMessage(":ok_hand: I'll send modlog messages here.")
    },
    isCmd: true,
    display: true,
    category: 4,
    description: "Sets the current channel as modlog channel.",
    args: "[remove]"
};<|endoftext|>


<commit_before>/**
 * Created by quasarchimaere on 03.07.2018.
 */
import angular from "angular";
import ngAnimate from "angular-animate";
import labelledHrModule from "./labelled-hr.js";
import "ng-redux";
import { attach } from "../utils.js";
import { actionCreators } from "../actions/actions.js";
import { connect2Redux } from "../won-utils.js";
import { selectIsConnected } from "../selectors.js";
import { useCaseGroups } from "useCaseDefinitions";

const serviceDependencies = [
  "$ngRedux",
  "$scope",
  "$element" /*'$routeParams' /*injections as strings here*/,
];

function genComponentConf() {
  const template = `

          <button class="won-button--filled red ucpc_createx__button"
                  ng-disabled="self.pendingPublishing">
              Finding out what's going on…
          </button>

          <button class="won-button--filled red ucpc__createx__button"
                  ng-click="self.createWhatsAround()"
                  ng-disabled="self.pendingPublishing">
              <svg class="won-button-icon" style="--local-primary:white;">
                  <use xlink:href="#ico36_location_current" href="#ico36_location_current"></use>
              </svg>
              What's in your Area?
          </button>
          <button class="won-button--filled red ucpc_createx__button"
                  ng-click="self.createWhatsNew()"
                  ng-disabled="self.pendingPublishing">
              What's new?
          </button>

      <won-labelled-hr label="::'Or'" class="ucpc__labelledhr"></won-labelled-hr>

            <svg class="ucpc__usecasegroup__header__icon"
              ng-if="!!useCaseGroup.icon">
              <use xlink:href="{{ useCaseGroup.icon }}" href="{{ useCaseGroup.icon }}"></use>
            </svg>

                {{ useCaseGroup.label }}

              <svg class="ucpc__usecasegroup__usecases__usecase__icon"
                ng-if="!!useCase.icon">
                <use xlink:href="{{ useCase.icon }}" href="{{ useCase.icon }}"></use>
              </svg>

                  {{ useCase.label }}

    `;

  class Controller {
    constructor(/* arguments <- serviceDependencies */) {
      attach(this, serviceDependencies, arguments);

      this.pendingPublishing = false;

      window.ucpc4dbg = this;
      this.useCaseGroups = useCaseGroups;
      this.showUseCaseGroupHeaders = this.showUseCaseGroups();

      const selectFromState = state => {
        return {
          connectionHasBeenLost: !selectIsConnected(state),
        };
      };

      // Using actionCreators like this means that every action defined there is available in the template.
      connect2Redux(selectFromState, actionCreators, [], this);
    }

    createWhatsAround() {
      if (!this.pendingPublishing) {
        this.pendingPublishing = true;
        this.needs__whatsAround();
      }
    }

    createWhatsNew() {
      if (!this.pendingPublishing) {
        this.pendingPublishing = true;
        this.needs__whatsNew();
      }
    }

    startFrom(selectedUseCase) {
      const selectedUseCaseIdentifier =
        selectedUseCase && selectedUseCase.identifier;

      if (selectedUseCaseIdentifier) {
        this.router__stateGoCurrent({
          useCase: encodeURIComponent(selectedUseCaseIdentifier),
        });
      } else {
        console.log(
          "No usecase identifier found for given usecase, ",
          selectedUseCase
        );
      }
    }

    /**
     * Only display the headers of the useCaseGroups if there are multiple displayable useCaseGroups
     * @returns {boolean}
     */
    showUseCaseGroups() {
      let countDisplayedUseCaseGroups = 0;

      for (const key in this.useCaseGroups) {
        if (
          this.displayableUseCaseGroup(this.useCaseGroups[key]) &&
          ++countDisplayedUseCaseGroups > 1
        ) {
          return true;
        }
      }
      return false;
    }

    /**
     * return if the given useCaseGroup is displayable or not
     * needs to have at least one displayable UseCase
     * @param useCase
     * @returns {*}
     */
    displayableUseCaseGroup(useCaseGroup) {
      const useCaseGroupValid =
        useCaseGroup &&
        (useCaseGroup.label || useCaseGroup.icon) &&
        useCaseGroup.useCases;

      if (useCaseGroupValid) {
        for (const key in useCaseGroup.useCases) {
          if (this.displayableUseCase(useCaseGroup.useCases[key])) {
            return true;
          }
        }
      }
      return false;
    }

    /**
     * return if the given useCase is displayable or not
     * @param useCase
     * @returns {*}
     */
    displayableUseCase(useCase) {
      return useCase && useCase.identifier && (useCase.label || useCase.icon);
    }
  }

  Controller.$inject = serviceDependencies;

  return {
    restrict: "E",
    controller: Controller,
    controllerAs: "self",
    bindToController: true, //scope-bindings -> ctrl
    scope: {
      /*scope-isolation*/
    },
    template: template,
  };
}

export default //.controller('CreateNeedController', [...serviceDependencies, CreateNeedController])
angular
  .module("won.owner.components.usecasePickerContent", [
    ngAnimate,
    labelledHrModule,
  ])
  .directive("wonUsecasePickerContent", genComponentConf).name;
<commit_msg>fixed wrong css classes that made the button not style right<commit_after>/**
 * Created by quasarchimaere on 03.07.2018.
 */
import angular from "angular";
import ngAnimate from "angular-animate";
import labelledHrModule from "./labelled-hr.js";
import "ng-redux";
import { attach } from "../utils.js";
import { actionCreators } from "../actions/actions.js";
import { connect2Redux } from "../won-utils.js";
import { selectIsConnected } from "../selectors.js";
import { useCaseGroups } from "useCaseDefinitions";

const serviceDependencies = [
  "$ngRedux",
  "$scope",
  "$element" /*'$routeParams' /*injections as strings here*/,
];

function genComponentConf() {
  const template = `

          <button class="won-button--filled red ucpc__createx__button"
                  ng-disabled="self.pendingPublishing">
              Finding out what's going on…
          </button>

          <button class="won-button--filled red ucpc__createx__button"
                  ng-click="self.createWhatsAround()"
                  ng-disabled="self.pendingPublishing">
              <svg class="won-button-icon" style="--local-primary:white;">
                  <use xlink:href="#ico36_location_current" href="#ico36_location_current"></use>
              </svg>
              What's in your Area?
          </button>
          <button class="won-button--filled red ucpc__createx__button"
                  ng-click="self.createWhatsNew()"
                  ng-disabled="self.pendingPublishing">
              What's new?
          </button>

      <won-labelled-hr label="::'Or'" class="ucpc__labelledhr"></won-labelled-hr>

            <svg class="ucpc__usecasegroup__header__icon"
              ng-if="!!useCaseGroup.icon">
              <use xlink:href="{{ useCaseGroup.icon }}" href="{{ useCaseGroup.icon }}"></use>
            </svg>

                {{ useCaseGroup.label }}

              <svg class="ucpc__usecasegroup__usecases__usecase__icon"
                ng-if="!!useCase.icon">
                <use xlink:href="{{ useCase.icon }}" href="{{ useCase.icon }}"></use>
              </svg>

                  {{ useCase.label }}

    `;

  class Controller {
    constructor(/* arguments <- serviceDependencies */) {
      attach(this, serviceDependencies, arguments);

      this.pendingPublishing = false;

      window.ucpc4dbg = this;
      this.useCaseGroups = useCaseGroups;
      this.showUseCaseGroupHeaders = this.showUseCaseGroups();

      const selectFromState = state => {
        return {
          connectionHasBeenLost: !selectIsConnected(state),
        };
      };

      // Using actionCreators like this means that every action defined there is available in the template.
      connect2Redux(selectFromState, actionCreators, [], this);
    }

    createWhatsAround() {
      if (!this.pendingPublishing) {
        this.pendingPublishing = true;
        this.needs__whatsAround();
      }
    }

    createWhatsNew() {
      if (!this.pendingPublishing) {
        this.pendingPublishing = true;
        this.needs__whatsNew();
      }
    }

    startFrom(selectedUseCase) {
      const selectedUseCaseIdentifier =
        selectedUseCase && selectedUseCase.identifier;

      if (selectedUseCaseIdentifier) {
        this.router__stateGoCurrent({
          useCase: encodeURIComponent(selectedUseCaseIdentifier),
        });
      } else {
        console.log(
          "No usecase identifier found for given usecase, ",
          selectedUseCase
        );
      }
    }

    /**
     * Only display the headers of the useCaseGroups if there are multiple displayable useCaseGroups
     * @returns {boolean}
     */
    showUseCaseGroups() {
      let countDisplayedUseCaseGroups = 0;

      for (const key in this.useCaseGroups) {
        if (
          this.displayableUseCaseGroup(this.useCaseGroups[key]) &&
          ++countDisplayedUseCaseGroups > 1
        ) {
          return true;
        }
      }
      return false;
    }

    /**
     * return if the given useCaseGroup is displayable or not
     * needs to have at least one displayable UseCase
     * @param useCase
     * @returns {*}
     */
    displayableUseCaseGroup(useCaseGroup) {
      const useCaseGroupValid =
        useCaseGroup &&
        (useCaseGroup.label || useCaseGroup.icon) &&
        useCaseGroup.useCases;

      if (useCaseGroupValid) {
        for (const key in useCaseGroup.useCases) {
          if (this.displayableUseCase(useCaseGroup.useCases[key])) {
            return true;
          }
        }
      }
      return false;
    }

    /**
     * return if the given useCase is displayable or not
     * @param useCase
     * @returns {*}
     */
    displayableUseCase(useCase) {
      return useCase && useCase.identifier && (useCase.label || useCase.icon);
    }
  }

  Controller.$inject = serviceDependencies;

  return {
    restrict: "E",
    controller: Controller,
    controllerAs: "self",
    bindToController: true, //scope-bindings -> ctrl
    scope: {
      /*scope-isolation*/
    },
    template: template,
  };
}

export default //.controller('CreateNeedController', [...serviceDependencies, CreateNeedController])
angular
  .module("won.owner.components.usecasePickerContent", [
    ngAnimate,
    labelledHrModule,
  ])
  .directive("wonUsecasePickerContent", genComponentConf).name;
<|endoftext|>


<commit_before>/*
 * Copyright (c) $year, Den Norske Turistforening (DNT)
 *
 * https://github.com/Turistforeningen/turadmin
 */

module.exports = function (app, express, options) {
    "use strict";

    var upload = require('jquery-file-upload-middleware');
    var underscore = require('underscore');
    var togeojson = require('togeojson');
    var fs = require('fs');
    var jsdom = require('jsdom').jsdom; // node doesn't have xml parsing or a dom. use jsdom

    var createOptions = function (userId) {
        return {
            uploadDir: function () {
                return options.dirname + '/public/uploads/' + userId + '/tmp';
            },
            uploadUrl: function () {
                return '/uploads/' + userId + '/tmp';
            }
        };
    };

    /**
     * Register fileHandler to listen to /uploads and to store images on server
     */
    app.use('/upload/gpx', function (req, res, next) {
        var options = createOptions(req.session.userId);
        var fileHandler = upload.fileHandler(options);
        fileHandler(req, res, next);
    });

    app.use('/upload/gpx', express.bodyParser());

    // When upload is done, convert gpx file to GeoJSON using togeojson
    upload.on('end', function (fileInfo) {

        if (!!fileInfo && !!fileInfo.name && fileInfo.name.match(/\.gpx/)) {

            console.log(fileInfo);

            // var uploadDir = createOptions(req.session.userId).uploadDir();
            var uploadDir = createOptions('testUserId').uploadDir();

            var gpxFileContents = jsdom(fs.readFileSync(uploadDir + '/' + fileInfo.name, 'utf8'));
            var converted = togeojson.gpx(gpxFileContents);

            underscore.extend(fileInfo, converted);

        }

    });

};
<commit_msg>Delete GPX file after geometry has been read.<commit_after>/*
 * Copyright (c) $year, Den Norske Turistforening (DNT)
 *
 * https://github.com/Turistforeningen/turadmin
 */

module.exports = function (app, express, options) {
    "use strict";

    var upload = require('jquery-file-upload-middleware');
    var underscore = require('underscore');
    var togeojson = require('togeojson');
    var fs = require('fs');
    var jsdom = require('jsdom').jsdom; // node doesn't have xml parsing or a dom. use jsdom

    var createOptions = function (userId) {
        return {
            uploadDir: function () {
                return options.dirname + '/public/uploads/' + userId + '/tmp';
            },
            uploadUrl: function () {
                return '/uploads/' + userId + '/tmp';
            }
        };
    };

    /**
     * Register fileHandler to listen to /uploads and to store images on server
     */
    app.use('/upload/gpx', function (req, res, next) {
        var options = createOptions(req.session.userId);
        var fileHandler = upload.fileHandler(options);
        fileHandler(req, res, next);
    });

    app.use('/upload/gpx', express.bodyParser());

    // When upload is done, convert gpx file to GeoJSON using togeojson
    upload.on('end', function (fileInfo) {

        // console.log('fileInfo');
        // console.log(fileInfo);

        if (!!fileInfo && !!fileInfo.name && fileInfo.name.match(/\.gpx/)) {

            console.log(fileInfo);

            // var uploadDir = createOptions(req.session.userId).uploadDir();
            var uploadDir = createOptions('testUserId').uploadDir();

            var gpxFileContents = jsdom(fs.readFileSync(uploadDir + '/' + fileInfo.name, 'utf8'));
            var converted = togeojson.gpx(gpxFileContents);

            // NOTE: A way to confirm that the file has been deleted?
            fs.unlinkSync(uploadDir + '/' + fileInfo.name);

            underscore.extend(fileInfo, converted);

        }

    });

};
<|endoftext|>


<commit_before>'use strict';

const angular = require('angular');
import _ from 'lodash';

import { AuthenticationService } from 'core/authentication';
import { Registry } from 'core/registry';
import { SETTINGS } from 'core/config/settings';
import { AppNotificationsService } from 'core/notification/AppNotificationsService';

import { STAGE_MANUAL_COMPONENTS } from './stageManualComponents.component';
import { TRIGGER_TEMPLATE } from './triggerTemplate.component';

import './manualPipelineExecution.less';

module.exports = angular
  .module('spinnaker.core.pipeline.manualPipelineExecution.controller', [
    require('angular-ui-bootstrap'),
    TRIGGER_TEMPLATE,
    STAGE_MANUAL_COMPONENTS,
  ])
  .controller('ManualPipelineExecutionCtrl', function($scope, $uibModalInstance, pipeline, application, trigger) {
    let applicationNotifications = [];
    let pipelineNotifications = [];

    this.hiddenParameters = new Set();

    this.notificationTooltip = require('./notifications.tooltip.html');

    AppNotificationsService.getNotificationsForApplication(application.name).then(notifications => {
      Object.keys(notifications)
        .sort()
        .filter(k => Array.isArray(notifications[k]))
        .forEach(type => {
          notifications[type].forEach(notification => {
            applicationNotifications.push(notification);
          });
        });
      synchronizeNotifications();
    });

    let user = AuthenticationService.getAuthenticatedUser();

    let synchronizeNotifications = () => {
      this.notifications = applicationNotifications.concat(pipelineNotifications);
    };

    this.getNotifications = () => {
      return _.has(this.command, 'pipeline.notifications')
        ? this.command.pipeline.notifications.concat(applicationNotifications)
        : applicationNotifications;
    };

    let userEmail = user.authenticated && user.name.includes('@') ? user.name : null;

    this.command = {
      pipeline: pipeline,
      trigger: null,
      dryRun: false,
      notificationEnabled: false,
      notification: {
        type: 'email',
        address: userEmail,
        when: ['pipeline.complete', 'pipeline.failed'],
      },
    };

    this.dryRunEnabled = SETTINGS.feature.dryRunEnabled;

    // Poor react setState
    const updateCommand = () => {
      $scope.$applyAsync(() => {
        this.command = _.cloneDeep(this.command);
      });
    };

    let addTriggers = () => {
      let pipeline = this.command.pipeline;
      if (!pipeline || !pipeline.triggers || !pipeline.triggers.length) {
        this.triggers = null;
        this.command.trigger = null;
        return;
      }

      this.triggers = pipeline.triggers
        .filter(t => Registry.pipeline.hasManualExecutionComponentForTriggerType(t.type))
        .map(t => {
          let copy = _.clone(t);
          copy.description = '...'; // placeholder
          Registry.pipeline
            .getManualExecutionComponentForTriggerType(t.type)
            .formatLabel(t)
            .then(label => (copy.description = label));
          return copy;
        });

      if (trigger && trigger.type === 'manual' && this.triggers.length) {
        trigger.type = this.triggers[0].type;
      }

      const suppliedTriggerCanBeInvoked =
        trigger && Registry.pipeline.hasManualExecutionComponentForTriggerType(trigger.type);
      if (suppliedTriggerCanBeInvoked) {
        Registry.pipeline
          .getManualExecutionComponentForTriggerType(trigger.type)
          .formatLabel(trigger)
          .then(label => (trigger.description = label));
      }
      this.command.trigger = suppliedTriggerCanBeInvoked ? trigger : _.head(this.triggers);
    };

    /**
     * Controller API
     */

    this.triggerUpdated = trigger => {
      let command = this.command;

      if (trigger !== undefined) {
        command.trigger = trigger;
      }

      if (command.trigger && Registry.pipeline.hasManualExecutionComponentForTriggerType(command.trigger.type)) {
        this.triggerComponent = Registry.pipeline.getManualExecutionComponentForTriggerType(command.trigger.type);
      } else {
        this.triggerComponent = null;
      }
      updateCommand();
    };

    this.pipelineSelected = () => {
      const pipeline = this.command.pipeline,
        executions = application.executions.data || [];

      pipelineNotifications = pipeline.notifications || [];
      synchronizeNotifications();

      this.currentlyRunningExecutions = executions.filter(
        execution => execution.pipelineConfigId === pipeline.id && execution.isActive,
      );
      addTriggers();
      this.triggerUpdated();

      const additionalComponents = pipeline.stages.map(stage =>
        Registry.pipeline.getManualExecutionComponentForStage(stage),
      );
      this.stageComponents = _.uniq(_.compact(additionalComponents));

      if (pipeline.parameterConfig && pipeline.parameterConfig.length) {
        this.parameters = {};
        this.hasRequiredParameters = pipeline.parameterConfig.some(p => p.required);
        pipeline.parameterConfig.forEach(p => this.addParameter(p));
        this.updateParameters();
      }
    };

    this.addParameter = parameterConfig => {
      const { name } = parameterConfig;
      const parameters = trigger ? trigger.parameters : {};
      if (this.parameters[name] === undefined) {
        this.parameters[name] = parameters[name] !== undefined ? parameters[name] : parameterConfig.default;
      }
    };

    this.updateParameters = () => {
      this.command.pipeline.parameterConfig.forEach(p => {
        if (p.conditional) {
          const include = this.shouldInclude(p);
          if (!include) {
            delete this.parameters[p.name];
            this.hiddenParameters.add(p.name);
          } else {
            this.hiddenParameters.delete(p.name);
            this.addParameter(p);
          }
        }
      });
    };

    this.shouldInclude = p => {
      if (p.conditional) {
        const comparingTo = this.parameters[p.conditional.parameter];
        const value = p.conditional.comparatorValue;
        switch (p.conditional.comparator) {
          case '>':
            return parseFloat(comparingTo) > parseFloat(value);
          case '>=':
            return parseFloat(comparingTo) >= parseFloat(value);
          case '<':
            return parseFloat(comparingTo) < parseFloat(value);
          case '<=':
            return parseFloat(comparingTo) <= parseFloat(value);
          case '!=':
            return comparingTo !== value;
          case '=':
            return comparingTo === value;
        }
      }
      return true;
    };

    this.execute = () => {
      let selectedTrigger = this.command.trigger || {},
        command = {
          trigger: selectedTrigger,
        },
        pipeline = this.command.pipeline;

      if (this.command.notificationEnabled && this.command.notification.address) {
        selectedTrigger.notifications = [this.command.notification];
      }

      // include any extra data populated by trigger manual execution handlers
      angular.extend(selectedTrigger, this.command.extraFields);

      command.pipelineName = pipeline.name;
      selectedTrigger.type = 'manual';
      selectedTrigger.dryRun = this.command.dryRun;

      if (pipeline.parameterConfig && pipeline.parameterConfig.length) {
        selectedTrigger.parameters = this.parameters;
      }
      $uibModalInstance.close(command);
    };

    this.cancel = $uibModalInstance.dismiss;

    this.hasStageOf = stageType => {
      return this.getStagesOf(stageType).length > 0;
    };

    this.getStagesOf = stageType => {
      if (!this.command.pipeline) {
        return [];
      }
      return this.command.pipeline.stages.filter(stage => stage.type === stageType);
    };

    /**
     * Initialization
     */

    if (pipeline) {
      this.pipelineSelected();
    }

    if (!pipeline) {
      this.pipelineOptions = application.pipelineConfigs.data.filter(c => !c.disabled);
    }
  });
<commit_msg>fix(pipeline): fix invisible parameter when default is not in options (#5673)<commit_after>'use strict';

const angular = require('angular');
import _ from 'lodash';

import { AuthenticationService } from 'core/authentication';
import { Registry } from 'core/registry';
import { SETTINGS } from 'core/config/settings';
import { AppNotificationsService } from 'core/notification/AppNotificationsService';

import { STAGE_MANUAL_COMPONENTS } from './stageManualComponents.component';
import { TRIGGER_TEMPLATE } from './triggerTemplate.component';

import './manualPipelineExecution.less';

module.exports = angular
  .module('spinnaker.core.pipeline.manualPipelineExecution.controller', [
    require('angular-ui-bootstrap'),
    TRIGGER_TEMPLATE,
    STAGE_MANUAL_COMPONENTS,
  ])
  .controller('ManualPipelineExecutionCtrl', function($scope, $uibModalInstance, pipeline, application, trigger) {
    let applicationNotifications = [];
    let pipelineNotifications = [];

    this.hiddenParameters = new Set();

    this.notificationTooltip = require('./notifications.tooltip.html');

    AppNotificationsService.getNotificationsForApplication(application.name).then(notifications => {
      Object.keys(notifications)
        .sort()
        .filter(k => Array.isArray(notifications[k]))
        .forEach(type => {
          notifications[type].forEach(notification => {
            applicationNotifications.push(notification);
          });
        });
      synchronizeNotifications();
    });

    let user = AuthenticationService.getAuthenticatedUser();

    let synchronizeNotifications = () => {
      this.notifications = applicationNotifications.concat(pipelineNotifications);
    };

    this.getNotifications = () => {
      return _.has(this.command, 'pipeline.notifications')
        ? this.command.pipeline.notifications.concat(applicationNotifications)
        : applicationNotifications;
    };

    let userEmail = user.authenticated && user.name.includes('@') ? user.name : null;

    this.command = {
      pipeline: pipeline,
      trigger: null,
      dryRun: false,
      notificationEnabled: false,
      notification: {
        type: 'email',
        address: userEmail,
        when: ['pipeline.complete', 'pipeline.failed'],
      },
    };

    this.dryRunEnabled = SETTINGS.feature.dryRunEnabled;

    // Poor react setState
    const updateCommand = () => {
      $scope.$applyAsync(() => {
        this.command = _.cloneDeep(this.command);
      });
    };

    let addTriggers = () => {
      let pipeline = this.command.pipeline;
      if (!pipeline || !pipeline.triggers || !pipeline.triggers.length) {
        this.triggers = null;
        this.command.trigger = null;
        return;
      }

      this.triggers = pipeline.triggers
        .filter(t => Registry.pipeline.hasManualExecutionComponentForTriggerType(t.type))
        .map(t => {
          let copy = _.clone(t);
          copy.description = '...'; // placeholder
          Registry.pipeline
            .getManualExecutionComponentForTriggerType(t.type)
            .formatLabel(t)
            .then(label => (copy.description = label));
          return copy;
        });

      if (trigger && trigger.type === 'manual' && this.triggers.length) {
        trigger.type = this.triggers[0].type;
      }

      const suppliedTriggerCanBeInvoked =
        trigger && Registry.pipeline.hasManualExecutionComponentForTriggerType(trigger.type);
      if (suppliedTriggerCanBeInvoked) {
        Registry.pipeline
          .getManualExecutionComponentForTriggerType(trigger.type)
          .formatLabel(trigger)
          .then(label => (trigger.description = label));
      }
      this.command.trigger = suppliedTriggerCanBeInvoked ? trigger : _.head(this.triggers);
    };

    /**
     * Controller API
     */

    this.triggerUpdated = trigger => {
      let command = this.command;

      if (trigger !== undefined) {
        command.trigger = trigger;
      }

      if (command.trigger && Registry.pipeline.hasManualExecutionComponentForTriggerType(command.trigger.type)) {
        this.triggerComponent = Registry.pipeline.getManualExecutionComponentForTriggerType(command.trigger.type);
      } else {
        this.triggerComponent = null;
      }
      updateCommand();
    };

    this.pipelineSelected = () => {
      const pipeline = this.command.pipeline,
        executions = application.executions.data || [];

      pipelineNotifications = pipeline.notifications || [];
      synchronizeNotifications();

      this.currentlyRunningExecutions = executions.filter(
        execution => execution.pipelineConfigId === pipeline.id && execution.isActive,
      );
      addTriggers();
      this.triggerUpdated();

      const additionalComponents = pipeline.stages.map(stage =>
        Registry.pipeline.getManualExecutionComponentForStage(stage),
      );
      this.stageComponents = _.uniq(_.compact(additionalComponents));

      if (pipeline.parameterConfig && pipeline.parameterConfig.length) {
        this.parameters = {};
        this.hasRequiredParameters = pipeline.parameterConfig.some(p => p.required);
        pipeline.parameterConfig.forEach(p => this.addParameter(p));
        this.updateParameters();
      }
    };

    this.addParameter = parameterConfig => {
      // Inject the default value into the options list if it is absent
      if (
        parameterConfig.default &&
        parameterConfig.options &&
        !parameterConfig.options.some(option => option.value === parameterConfig.default)
      ) {
        parameterConfig.options.unshift({ value: parameterConfig.default });
      }
      const { name } = parameterConfig;
      const parameters = trigger ? trigger.parameters : {};
      if (this.parameters[name] === undefined) {
        this.parameters[name] = parameters[name] !== undefined ? parameters[name] : parameterConfig.default;
      }
    };

    this.updateParameters = () => {
      this.command.pipeline.parameterConfig.forEach(p => {
        if (p.conditional) {
          const include = this.shouldInclude(p);
          if (!include) {
            delete this.parameters[p.name];
            this.hiddenParameters.add(p.name);
          } else {
            this.hiddenParameters.delete(p.name);
            this.addParameter(p);
          }
        }
      });
    };

    this.shouldInclude = p => {
      if (p.conditional) {
        const comparingTo = this.parameters[p.conditional.parameter];
        const value = p.conditional.comparatorValue;
        switch (p.conditional.comparator) {
          case '>':
            return parseFloat(comparingTo) > parseFloat(value);
          case '>=':
            return parseFloat(comparingTo) >= parseFloat(value);
          case '<':
            return parseFloat(comparingTo) < parseFloat(value);
          case '<=':
            return parseFloat(comparingTo) <= parseFloat(value);
          case '!=':
            return comparingTo !== value;
          case '=':
            return comparingTo === value;
        }
      }
      return true;
    };

    this.execute = () => {
      let selectedTrigger = this.command.trigger || {},
        command = {
          trigger: selectedTrigger,
        },
        pipeline = this.command.pipeline;

      if (this.command.notificationEnabled && this.command.notification.address) {
        selectedTrigger.notifications = [this.command.notification];
      }

      // include any extra data populated by trigger manual execution handlers
      angular.extend(selectedTrigger, this.command.extraFields);

      command.pipelineName = pipeline.name;
      selectedTrigger.type = 'manual';
      selectedTrigger.dryRun = this.command.dryRun;

      if (pipeline.parameterConfig && pipeline.parameterConfig.length) {
        selectedTrigger.parameters = this.parameters;
      }
      $uibModalInstance.close(command);
    };

    this.cancel = $uibModalInstance.dismiss;

    this.hasStageOf = stageType => {
      return this.getStagesOf(stageType).length > 0;
    };

    this.getStagesOf = stageType => {
      if (!this.command.pipeline) {
        return [];
      }
      return this.command.pipeline.stages.filter(stage => stage.type === stageType);
    };

    /**
     * Initialization
     */

    if (pipeline) {
      this.pipelineSelected();
    }

    if (!pipeline) {
      this.pipelineOptions = application.pipelineConfigs.data.filter(c => !c.disabled);
    }
  });
<|endoftext|>


<commit_before>/*
 * HTML5 Sortable jQuery Plugin
 * http://farhadi.ir/projects/html5sortable
 *
 * Copyright 2012, Ali Farhadi
 * Released under the MIT license.
 */
(function($) {
var dragging, placeholders = $();
$.fn.sortable = function(options) {
    var method = String(options);
    options = $.extend({
        connectWith: false
    }, options);
    return this.each(function() {
        if (/^enable|disable|destroy$/.test(method)) {
            var items = $(this).children($(this).data('items')).attr('draggable', method == 'enable');
            if (method == 'destroy') {
                items.add(this).removeData('connectWith items')
                    .off('dragstart.h5s dragend.h5s selectstart.h5s dragover.h5s dragenter.h5s drop.h5s');
            }
            return;
        }
        var isHandle, index, items = $(this).children($(options.items));
        var placeholder;

        if (/^tbody$/i.test(this.tagName))
            placeholder = $("
").append($('<td colspan="100%"'));
        else
            placeholder = $('<' + (/^ul|ol$/i.test(this.tagName) ? 'li' : 'div') + ' class="sortable-placeholder">').html(" ");

        console.log(this.tagName);
        items.find(options.handle).mousedown(function() {
            isHandle = true;
        }).mouseup(function() {
            isHandle = false;
        });
        $(this).data('items', options.items)
        placeholders = placeholders.add(placeholder);
        if (options.connectWith) {
            $(options.connectWith).add(this).data('connectWith', options.connectWith);
        }
        items.attr('draggable', 'true').on('dragstart.h5s', function(e) {
            if (options.handle && !isHandle) {
                return false;
            }
            isHandle = false;
            var dt = e.originalEvent.dataTransfer;
            dt.effectAllowed = 'move';
            dt.setData('Text', 'dummy');
            index = (dragging = $(this)).addClass('sortable-dragging').index();
        }).on('dragend.h5s', function() {
            if (!dragging) {
                return;
            }
            dragging.removeClass('sortable-dragging').show();
            placeholders.detach();
            if (index != dragging.index()) {
                dragging.parent().trigger('sortupdate', {item: dragging});
            }
            dragging = null;
        }).not('a[href], img').on('selectstart.h5s', function() {
            this.dragDrop && this.dragDrop();
            return false;
        }).end().add([this, placeholder]).on('dragover.h5s dragenter.h5s drop.h5s', function(e) {
            if (!items.is(dragging) && options.connectWith !== $(dragging).parent().data('connectWith')) {
                return true;
            }
            if (e.type == 'drop') {
                e.stopPropagation();
                placeholders.filter(':visible').after(dragging);
                dragging.trigger('dragend.h5s');
                return false;
            }
            e.preventDefault();
            e.originalEvent.dataTransfer.dropEffect = 'move';
            if (items.is(this)) {
                if (options.forcePlaceholderSize) {
                    placeholder.height(dragging.outerHeight());
                }
                dragging.hide();
                $(this)[placeholder.index() < $(this).index() ? 'after' : 'before'](placeholder);
                placeholders.not(placeholder).detach();
            } else if (!placeholders.is(this) && !$(this).children(options.items).length) {
                placeholders.detach();
                $(this).append(placeholder);
            }
            return false;
        });
    });
};
})(jQuery);<commit_msg>Update jquery.sortable.js<commit_after>/*
 * HTML5 Sortable jQuery Plugin
 * http://farhadi.ir/projects/html5sortable
 *
 * Copyright 2012, Ali Farhadi
 * Released under the MIT license.
 */
(function($) {
var dragging, placeholders = $();
$.fn.sortable = function(options) {
    var method = String(options);
    options = $.extend({
        connectWith: false
    }, options);
    return this.each(function() {
        if (/^enable|disable|destroy$/.test(method)) {
            var items = $(this).children($(this).data('items')).attr('draggable', method == 'enable');
            if (method == 'destroy') {
                items.add(this).removeData('connectWith items')
                    .off('dragstart.h5s dragend.h5s selectstart.h5s dragover.h5s dragenter.h5s drop.h5s');
            }
            return;
        }
        var isHandle, index, items = $(this).children($(options.items));
        var placeholder;

        if (/^tbody$/i.test(this.tagName))
            placeholder = $("
").append($(''));
        else
            placeholder = $('<' + (/^ul|ol$/i.test(this.tagName) ? 'li' : 'div') + ' class="sortable-placeholder">').html(" ");

        console.log(this.tagName);
        items.find(options.handle).mousedown(function() {
            isHandle = true;
        }).mouseup(function() {
            isHandle = false;
        });
        $(this).data('items', options.items)
        placeholders = placeholders.add(placeholder);
        if (options.connectWith) {
            $(options.connectWith).add(this).data('connectWith', options.connectWith);
        }
        items.attr('draggable', 'true').on('dragstart.h5s', function(e) {
            if (options.handle && !isHandle) {
                return false;
            }
            isHandle = false;
            var dt = e.originalEvent.dataTransfer;
            dt.effectAllowed = 'move';
            dt.setData('Text', 'dummy');
            index = (dragging = $(this)).addClass('sortable-dragging').index();
        }).on('dragend.h5s', function() {
            if (!dragging) {
                return;
            }
            dragging.removeClass('sortable-dragging').show();
            placeholders.detach();
            if (index != dragging.index()) {
                dragging.parent().trigger('sortupdate', {item: dragging});
            }
            dragging = null;
        }).not('a[href], img').on('selectstart.h5s', function() {
            this.dragDrop && this.dragDrop();
            return false;
        }).end().add([this, placeholder]).on('dragover.h5s dragenter.h5s drop.h5s', function(e) {
            if (!items.is(dragging) && options.connectWith !== $(dragging).parent().data('connectWith')) {
                return true;
            }
            if (e.type == 'drop') {
                e.stopPropagation();
                placeholders.filter(':visible').after(dragging);
                dragging.trigger('dragend.h5s');
                return false;
            }
            e.preventDefault();
            e.originalEvent.dataTransfer.dropEffect = 'move';
            if (items.is(this)) {
                if (options.forcePlaceholderSize) {
                    placeholder.height(dragging.outerHeight());
                }
                dragging.hide();
                $(this)[placeholder.index() < $(this).index() ? 'after' : 'before'](placeholder);
                placeholders.not(placeholder).detach();
            } else if (!placeholders.is(this) && !$(this).children(options.items).length) {
                placeholders.detach();
                $(this).append(placeholder);
            }
            return false;
        });
    });
};
})(jQuery);
<|endoftext|>