<commit_before>/*
Copyright 2017 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import {LayoutPropType} from "../../../settings/Layout";
import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
import sanitizeHtml from "sanitize-html";
import {UIFeature} from "../../../settings/UIFeature";
import {PERMITTED_URL_SCHEMES} from "../../../HtmlUtils";
import {replaceableComponent} from "../../../utils/replaceableComponent";

// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
// be low as each event being loaded (after the first) is triggered by an explicit user action.
@replaceableComponent("views.elements.ReplyThread")
export default class ReplyThread extends React.Component {
    static propTypes = {
        // the latest event in this chain of replies
        parentEv: PropTypes.instanceOf(MatrixEvent),
        // called when the ReplyThread contents has changed, including EventTiles thereof
        onHeightChanged: PropTypes.func.isRequired,
        permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
        // Specifies which layout to use.
        layout: LayoutPropType,
        // Whether to always show a timestamp
        alwaysShowTimestamps: PropTypes.bool,
    };

    static contextType = MatrixClientContext;

    constructor(props, context) {
        super(props, context);

        this.state = {
            // The loaded events to be rendered as linear-replies
            events: [],

            // The latest loaded event which has not yet been shown
            loadedEv: null,
            // Whether the component is still loading more events
            loading: true,

            // Whether as error was encountered fetching a replied to event.
            err: false,
        };

        this.unmounted = false;
        this.context.on("Event.replaced", this.onEventReplaced);
        this.room = this.context.getRoom(this.props.parentEv.getRoomId());
        this.room.on("Room.redaction", this.onRoomRedaction);
        this.room.on("Room.redactionCancelled", this.onRoomRedaction);

        this.onQuoteClick = this.onQuoteClick.bind(this);
        this.canCollapse = this.canCollapse.bind(this);
        this.collapse = this.collapse.bind(this);
    }

    static getParentEventId(ev) {
        if (!ev || ev.isRedacted()) return;

        // XXX: For newer relations (annotations, replacements, etc.), we now
        // have a `getRelation` helper on the event, and you might assume it
        // could be used here for replies as well... However, the helper
        // currently assumes the relation has a `rel_type`, which older replies
        // do not, so this block is left as-is for now.
        const mRelatesTo = ev.getWireContent()['m.relates_to'];
        if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
            const mInReplyTo = mRelatesTo['m.in_reply_to'];
            if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
        }
    }

    // Part of Replies fallback support
    static stripPlainReply(body) {
        // Removes lines beginning with `> ` until you reach one that doesn't.
        const lines = body.split('\n');
        while (lines.length && lines[0].startsWith('> ')) lines.shift();
        // Reply fallback has a blank line after it, so remove it to prevent leading newline
        if (lines[0] === '') lines.shift();
        return lines.join('\n');
    }

    // Part of Replies fallback support
    static stripHTMLReply(html) {
        // Sanitize the original HTML for inclusion in <mx-reply>.  We allow
        // any HTML, since the original sender could use special tags that we
        // don't recognize, but want to pass along to any recipients who do
        // recognize them -- recipients should be sanitizing before displaying
        // anyways.  However, we sanitize to 1) remove any mx-reply, so that we
        // don't generate a nested mx-reply, and 2) make sure that the HTML is
        // properly formatted (e.g. tags are closed where necessary)
        return sanitizeHtml(
            html,
            {
                allowedTags: false, // false means allow everything
                allowedAttributes: false,
                // we somehow can't allow all schemes, so we allow all that we
                // know of and mxc (for img tags)
                allowedSchemes: [...PERMITTED_URL_SCHEMES, 'mxc'],
                exclusiveFilter: (frame) => frame.tag === "mx-reply",
            },
        );
    }

    // Part of Replies fallback support
    static getNestedReplyText(ev, permalinkCreator) {
        if (!ev) return null;

        let {body, formatted_body: html} = ev.getContent();
        if (this.getParentEventId(ev)) {
            if (body) body = this.stripPlainReply(body);
        }

        if (!body) body = ""; // Always ensure we have a body, for reasons.

        if (html) {
            // sanitize the HTML before we put it in an <mx-reply>
            html = this.stripHTMLReply(html);
        } else {
            // Escape the body to use as HTML below.
            // We also run a nl2br over the result to fix the fallback representation. We do this
            // after converting the text to safe HTML to avoid user-provided BR's from being converted.
            html = escapeHtml(body).replace(/\n/g, '
');
        }

        // dev note: do not rely on `body` being safe for HTML usage below.

        const evLink = permalinkCreator.forEvent(ev.getId());
        const userLink = makeUserPermalink(ev.getSender());
        const mxid = ev.getSender();

        // This fallback contains text that is explicitly EN.
        switch (ev.getContent().msgtype) {
            case 'm.text':
            case 'm.notice': {
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>${html}</blockquote></mx-reply>`;
                const lines = body.trim().split('\n');
                if (lines.length > 0) {
                    lines[0] = `<${mxid}> ${lines[0]}`;
                    body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
                }
                break;
            }
            case 'm.image':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent an image.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent an image.\n\n`;
                break;
            case 'm.video':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent a video.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent a video.\n\n`;
                break;
            case 'm.audio':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent an audio file.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent an audio file.\n\n`;
                break;
            case 'm.file':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent a file.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent a file.\n\n`;
                break;
            case 'm.emote': {
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> * `
                    + `<a href="${userLink}">${mxid}</a><br>${html}</blockquote></mx-reply>`;
                const lines = body.trim().split('\n');
                if (lines.length > 0) {
                    lines[0] = `* <${mxid}> ${lines[0]}`;
                    body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
                }
                break;
            }
            default:
                return null;
        }

        return {body, html};
    }

    static makeReplyMixIn(ev) {
        if (!ev) return {};
        return {
            'm.relates_to': {
                'm.in_reply_to': {
                    'event_id': ev.getId(),
                },
            },
        };
    }

    static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) {
        if (!ReplyThread.getParentEventId(parentEv)) {
            return null;
        }
        return <ReplyThread
            parentEv={parentEv}
            onHeightChanged={onHeightChanged}
            ref={ref}
            permalinkCreator={permalinkCreator}
            layout={layout}
            alwaysShowTimestamps={alwaysShowTimestamps}
        />;
    }

    componentDidMount() {
        this.initialize();
    }

    componentDidUpdate() {
        this.props.onHeightChanged();
    }

    componentWillUnmount() {
        this.unmounted = true;
        this.context.removeListener("Event.replaced", this.onEventReplaced);
        if (this.room) {
            this.room.removeListener("Room.redaction", this.onRoomRedaction);
            this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction);
        }
    }

    updateForEventId = (eventId) => {
        if (this.state.events.some(event => event.getId() === eventId)) {
            this.forceUpdate();
        }
    };

    onEventReplaced = (ev) => {
        if (this.unmounted) return;

        // If one of the events we are rendering gets replaced, force a re-render
        this.updateForEventId(ev.getId());
    };

    onRoomRedaction = (ev) => {
        if (this.unmounted) return;

        const eventId = ev.getAssociatedId();
        if (!eventId) return;

        // If one of the events we are rendering gets redacted, force a re-render
        this.updateForEventId(eventId);
    };

    async initialize() {
        const {parentEv} = this.props;
        // at time of making this component we checked that props.parentEv has a parentEventId
        const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));

        if (this.unmounted) return;

        if (ev) {
            const loadedEv = await this.getNextEvent(ev);
            this.setState({
                events: [ev],
                loadedEv,
                loading: false,
            });
        } else {
            this.setState({err: true});
        }
    }

    async getNextEvent(ev) {
        try {
            const inReplyToEventId = ReplyThread.getParentEventId(ev);
            return await this.getEvent(inReplyToEventId);
        } catch (e) {
            return null;
        }
    }

    async getEvent(eventId) {
        if (!eventId) return null;
        const event = this.room.findEventById(eventId);
        if (event) return event;

        try {
            // ask the client to fetch the event we want using the context API, only interface to do so is to ask
            // for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map
            await this.context.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
        } catch (e) {
            // if it fails catch the error and return early, there's no point trying to find the event in this case.
            // Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
            return null;
        }
        return this.room.findEventById(eventId);
    }

    canCollapse() {
        return this.state.events.length > 1;
    }

    collapse() {
        this.initialize();
    }

    async onQuoteClick() {
        const events = [this.state.loadedEv, ...this.state.events];

        let loadedEv = null;
        if (events.length > 0) {
            loadedEv = await this.getNextEvent(events[0]);
        }

        this.setState({
            loadedEv,
            events,
        });

        dis.fire(Action.FocusComposer);
    }

    render() {
        let header = null;

        if (this.state.err) {
            header =

                {
                    _t('Unable to load event that was replied to, ' +
                        'it either does not exist or you do not have permission to view it.')
                }

;
        } else if (this.state.loadedEv) {
            const ev = this.state.loadedEv;
            const Pill = sdk.getComponent('elements.Pill');
            const room = this.context.getRoom(ev.getRoomId());
            header =

                {
                    _t('In reply to <pill>', {}, {
                        'a': (sub) => { sub },
                        'pill': (
                            <Pill
                                type={Pill.TYPE_USER_MENTION}
                                room={room}
                                url={makeUserPermalink(ev.getSender())}
                                shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
                            />
                        ),
                    })
                }

;
        } else if (this.state.loading) {
            const Spinner = sdk.getComponent("elements.Spinner");
            header = <Spinner w={16} h={16} />;
        }

        const EventTile = sdk.getComponent('views.rooms.EventTile');
        const DateSeparator = sdk.getComponent('messages.DateSeparator');
        const evTiles = this.state.events.map((ev) => {
            let dateSep = null;

            if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
                dateSep = <DateSeparator ts={ev.getTs()} />;
            }

            return

                { dateSep }
                <EventTile
                    mxEvent={ev}
                    tileShape="reply"
                    onHeightChanged={this.props.onHeightChanged}
                    permalinkCreator={this.props.permalinkCreator}
                    isRedacted={ev.isRedacted()}
                    isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
                    layout={this.props.layout}
                    alwaysShowTimestamps={this.props.alwaysShowTimestamps}
                    enableFlair={SettingsStore.getValue(UIFeature.Flair)}
                    replacingEventId={ev.replacingEventId()}
                />

;
        });

        return

{ header }

{ evTiles }

;
    }
}
<commit_msg>remove stray bullet point in reply preview<commit_after>/*
Copyright 2017 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2019 The Matrix.org Foundation C.I.C.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import {_t} from '../../../languageHandler';
import PropTypes from 'prop-types';
import dis from '../../../dispatcher/dispatcher';
import {wantsDateSeparator} from '../../../DateUtils';
import {MatrixEvent} from 'matrix-js-sdk/src/models/event';
import {makeUserPermalink, RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks";
import SettingsStore from "../../../settings/SettingsStore";
import {LayoutPropType} from "../../../settings/Layout";
import escapeHtml from "escape-html";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions";
import sanitizeHtml from "sanitize-html";
import {UIFeature} from "../../../settings/UIFeature";
import {PERMITTED_URL_SCHEMES} from "../../../HtmlUtils";
import {replaceableComponent} from "../../../utils/replaceableComponent";

// This component does no cycle detection, simply because the only way to make such a cycle would be to
// craft event_id's, using a homeserver that generates predictable event IDs; even then the impact would
// be low as each event being loaded (after the first) is triggered by an explicit user action.
@replaceableComponent("views.elements.ReplyThread")
export default class ReplyThread extends React.Component {
    static propTypes = {
        // the latest event in this chain of replies
        parentEv: PropTypes.instanceOf(MatrixEvent),
        // called when the ReplyThread contents has changed, including EventTiles thereof
        onHeightChanged: PropTypes.func.isRequired,
        permalinkCreator: PropTypes.instanceOf(RoomPermalinkCreator).isRequired,
        // Specifies which layout to use.
        layout: LayoutPropType,
        // Whether to always show a timestamp
        alwaysShowTimestamps: PropTypes.bool,
    };

    static contextType = MatrixClientContext;

    constructor(props, context) {
        super(props, context);

        this.state = {
            // The loaded events to be rendered as linear-replies
            events: [],

            // The latest loaded event which has not yet been shown
            loadedEv: null,
            // Whether the component is still loading more events
            loading: true,

            // Whether as error was encountered fetching a replied to event.
            err: false,
        };

        this.unmounted = false;
        this.context.on("Event.replaced", this.onEventReplaced);
        this.room = this.context.getRoom(this.props.parentEv.getRoomId());
        this.room.on("Room.redaction", this.onRoomRedaction);
        this.room.on("Room.redactionCancelled", this.onRoomRedaction);

        this.onQuoteClick = this.onQuoteClick.bind(this);
        this.canCollapse = this.canCollapse.bind(this);
        this.collapse = this.collapse.bind(this);
    }

    static getParentEventId(ev) {
        if (!ev || ev.isRedacted()) return;

        // XXX: For newer relations (annotations, replacements, etc.), we now
        // have a `getRelation` helper on the event, and you might assume it
        // could be used here for replies as well... However, the helper
        // currently assumes the relation has a `rel_type`, which older replies
        // do not, so this block is left as-is for now.
        const mRelatesTo = ev.getWireContent()['m.relates_to'];
        if (mRelatesTo && mRelatesTo['m.in_reply_to']) {
            const mInReplyTo = mRelatesTo['m.in_reply_to'];
            if (mInReplyTo && mInReplyTo['event_id']) return mInReplyTo['event_id'];
        }
    }

    // Part of Replies fallback support
    static stripPlainReply(body) {
        // Removes lines beginning with `> ` until you reach one that doesn't.
        const lines = body.split('\n');
        while (lines.length && lines[0].startsWith('> ')) lines.shift();
        // Reply fallback has a blank line after it, so remove it to prevent leading newline
        if (lines[0] === '') lines.shift();
        return lines.join('\n');
    }

    // Part of Replies fallback support
    static stripHTMLReply(html) {
        // Sanitize the original HTML for inclusion in <mx-reply>.  We allow
        // any HTML, since the original sender could use special tags that we
        // don't recognize, but want to pass along to any recipients who do
        // recognize them -- recipients should be sanitizing before displaying
        // anyways.  However, we sanitize to 1) remove any mx-reply, so that we
        // don't generate a nested mx-reply, and 2) make sure that the HTML is
        // properly formatted (e.g. tags are closed where necessary)
        return sanitizeHtml(
            html,
            {
                allowedTags: false, // false means allow everything
                allowedAttributes: false,
                // we somehow can't allow all schemes, so we allow all that we
                // know of and mxc (for img tags)
                allowedSchemes: [...PERMITTED_URL_SCHEMES, 'mxc'],
                exclusiveFilter: (frame) => frame.tag === "mx-reply",
            },
        );
    }

    // Part of Replies fallback support
    static getNestedReplyText(ev, permalinkCreator) {
        if (!ev) return null;

        let {body, formatted_body: html} = ev.getContent();
        if (this.getParentEventId(ev)) {
            if (body) body = this.stripPlainReply(body);
        }

        if (!body) body = ""; // Always ensure we have a body, for reasons.

        if (html) {
            // sanitize the HTML before we put it in an <mx-reply>
            html = this.stripHTMLReply(html);
        } else {
            // Escape the body to use as HTML below.
            // We also run a nl2br over the result to fix the fallback representation. We do this
            // after converting the text to safe HTML to avoid user-provided BR's from being converted.
            html = escapeHtml(body).replace(/\n/g, '
');
        }

        // dev note: do not rely on `body` being safe for HTML usage below.

        const evLink = permalinkCreator.forEvent(ev.getId());
        const userLink = makeUserPermalink(ev.getSender());
        const mxid = ev.getSender();

        // This fallback contains text that is explicitly EN.
        switch (ev.getContent().msgtype) {
            case 'm.text':
            case 'm.notice': {
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>${html}</blockquote></mx-reply>`;
                const lines = body.trim().split('\n');
                if (lines.length > 0) {
                    lines[0] = `<${mxid}> ${lines[0]}`;
                    body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
                }
                break;
            }
            case 'm.image':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent an image.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent an image.\n\n`;
                break;
            case 'm.video':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent a video.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent a video.\n\n`;
                break;
            case 'm.audio':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent an audio file.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent an audio file.\n\n`;
                break;
            case 'm.file':
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> <a href="${userLink}">${mxid}</a>`
                    + `<br>sent a file.</blockquote></mx-reply>`;
                body = `> <${mxid}> sent a file.\n\n`;
                break;
            case 'm.emote': {
                html = `<mx-reply><blockquote><a href="${evLink}">In reply to</a> * `
                    + `<a href="${userLink}">${mxid}</a><br>${html}</blockquote></mx-reply>`;
                const lines = body.trim().split('\n');
                if (lines.length > 0) {
                    lines[0] = `* <${mxid}> ${lines[0]}`;
                    body = lines.map((line) => `> ${line}`).join('\n') + '\n\n';
                }
                break;
            }
            default:
                return null;
        }

        return {body, html};
    }

    static makeReplyMixIn(ev) {
        if (!ev) return {};
        return {
            'm.relates_to': {
                'm.in_reply_to': {
                    'event_id': ev.getId(),
                },
            },
        };
    }

    static makeThread(parentEv, onHeightChanged, permalinkCreator, ref, layout, alwaysShowTimestamps) {
        if (!ReplyThread.getParentEventId(parentEv)) {
            return null;
        }
        return <ReplyThread
            parentEv={parentEv}
            onHeightChanged={onHeightChanged}
            ref={ref}
            permalinkCreator={permalinkCreator}
            layout={layout}
            alwaysShowTimestamps={alwaysShowTimestamps}
        />;
    }

    componentDidMount() {
        this.initialize();
    }

    componentDidUpdate() {
        this.props.onHeightChanged();
    }

    componentWillUnmount() {
        this.unmounted = true;
        this.context.removeListener("Event.replaced", this.onEventReplaced);
        if (this.room) {
            this.room.removeListener("Room.redaction", this.onRoomRedaction);
            this.room.removeListener("Room.redactionCancelled", this.onRoomRedaction);
        }
    }

    updateForEventId = (eventId) => {
        if (this.state.events.some(event => event.getId() === eventId)) {
            this.forceUpdate();
        }
    };

    onEventReplaced = (ev) => {
        if (this.unmounted) return;

        // If one of the events we are rendering gets replaced, force a re-render
        this.updateForEventId(ev.getId());
    };

    onRoomRedaction = (ev) => {
        if (this.unmounted) return;

        const eventId = ev.getAssociatedId();
        if (!eventId) return;

        // If one of the events we are rendering gets redacted, force a re-render
        this.updateForEventId(eventId);
    };

    async initialize() {
        const {parentEv} = this.props;
        // at time of making this component we checked that props.parentEv has a parentEventId
        const ev = await this.getEvent(ReplyThread.getParentEventId(parentEv));

        if (this.unmounted) return;

        if (ev) {
            const loadedEv = await this.getNextEvent(ev);
            this.setState({
                events: [ev],
                loadedEv,
                loading: false,
            });
        } else {
            this.setState({err: true});
        }
    }

    async getNextEvent(ev) {
        try {
            const inReplyToEventId = ReplyThread.getParentEventId(ev);
            return await this.getEvent(inReplyToEventId);
        } catch (e) {
            return null;
        }
    }

    async getEvent(eventId) {
        if (!eventId) return null;
        const event = this.room.findEventById(eventId);
        if (event) return event;

        try {
            // ask the client to fetch the event we want using the context API, only interface to do so is to ask
            // for a timeline with that event, but once it is loaded we can use findEventById to look up the ev map
            await this.context.getEventTimeline(this.room.getUnfilteredTimelineSet(), eventId);
        } catch (e) {
            // if it fails catch the error and return early, there's no point trying to find the event in this case.
            // Return null as it is falsey and thus should be treated as an error (as the event cannot be resolved).
            return null;
        }
        return this.room.findEventById(eventId);
    }

    canCollapse() {
        return this.state.events.length > 1;
    }

    collapse() {
        this.initialize();
    }

    async onQuoteClick() {
        const events = [this.state.loadedEv, ...this.state.events];

        let loadedEv = null;
        if (events.length > 0) {
            loadedEv = await this.getNextEvent(events[0]);
        }

        this.setState({
            loadedEv,
            events,
        });

        dis.fire(Action.FocusComposer);
    }

    render() {
        let header = null;

        if (this.state.err) {
            header =

                {
                    _t('Unable to load event that was replied to, ' +
                        'it either does not exist or you do not have permission to view it.')
                }

;
        } else if (this.state.loadedEv) {
            const ev = this.state.loadedEv;
            const Pill = sdk.getComponent('elements.Pill');
            const room = this.context.getRoom(ev.getRoomId());
            header =

                {
                    _t('In reply to <pill>', {}, {
                        'a': (sub) => { sub },
                        'pill': (
                            <Pill
                                type={Pill.TYPE_USER_MENTION}
                                room={room}
                                url={makeUserPermalink(ev.getSender())}
                                shouldShowPillAvatar={SettingsStore.getValue("Pill.shouldShowPillAvatar")}
                            />
                        ),
                    })
                }

;
        } else if (this.state.loading) {
            const Spinner = sdk.getComponent("elements.Spinner");
            header = <Spinner w={16} h={16} />;
        }

        const EventTile = sdk.getComponent('views.rooms.EventTile');
        const DateSeparator = sdk.getComponent('messages.DateSeparator');
        const evTiles = this.state.events.map((ev) => {
            let dateSep = null;

            if (wantsDateSeparator(this.props.parentEv.getDate(), ev.getDate())) {
                dateSep = <DateSeparator ts={ev.getTs()} />;
            }

            return

                { dateSep }
                <EventTile
                    mxEvent={ev}
                    tileShape="reply"
                    onHeightChanged={this.props.onHeightChanged}
                    permalinkCreator={this.props.permalinkCreator}
                    isRedacted={ev.isRedacted()}
                    isTwelveHour={SettingsStore.getValue("showTwelveHourTimestamps")}
                    layout={this.props.layout}
                    alwaysShowTimestamps={this.props.alwaysShowTimestamps}
                    enableFlair={SettingsStore.getValue(UIFeature.Flair)}
                    replacingEventId={ev.replacingEventId()}
                    as="div"
                />

;
        });

        return

{ header }

{ evTiles }

;
    }
}
<|endoftext|>


<commit_before>var dom = require('@nymag/dom'),
  _ = require('lodash'),
  AddController = require('./add-component-controller'),
  references = require('references'),
  utils = require('../services/utils'),
  removeService = require('../services/remove-service'),
  createService = require('../services/create-service'),
  logicReadoutService = require('../services/logic-readout-service'),
  statusService = require('../services/status-service'),
  proto = BrowseController.prototype;

/**
 * @param {Element} el
 * @param {Object} callbacks
 */
function BrowseController(el, callbacks) {
  /**
   * The Clay Space component data
   * @type {Object}
   */
  this.el = el;

  this.spaceRef = el.getAttribute('data-uri');

  this.callbacks = callbacks;

  /**
   * Children Space Logic componens
   * @type {Array}
   */
  this.childComponents = [];

  /**
   * The list of components in the Space in
   * organized to be used in a filterable list
   * @type {Array}
   */
  this.componentList = [];

  this.findChildrenMakeList(this.el);
  // Launch the pane
  this.launchPane();
}

/**
 * Open the settins pane
 */
proto.launchPane = function () {
  var paneContent = this.markActiveInList(utils.createFilterableList(this.componentList, {
    click: this.listItemClick.bind(this),
    reorder: this.reorder.bind(this),
    settings: this.settings.bind(this),
    remove: this.remove.bind(this),
    add: this.addComponent.bind(this),
    addTitle: 'Add Component To Space'
  }));

  references.pane.open([{ header: this.el.getAttribute(references.dataPaneTitle) || 'Browse Space', content: paneContent }]);
};

/**
 * Launch the AddController
 */
proto.addComponent = function () {
  AddController(this.el, this.callbacks.add);
};

/**
 * [findChildrenMakeList description]
 * @param  {Element} el
 */
proto.findChildrenMakeList = function (el) {
  this.childComponents = utils.findAllLogic(el);
  this.componentList = this.makeList(this.childComponents);
};

/**
 * Apply active styling to the proper item in the filterable list
 * @param  {element} listHtml
 * @returns {element}
 */
proto.markActiveInList = function (listHtml) {
  _.each(this.childComponents, function (logicComponent) {
    if (statusService.isEditing(logicComponent)) {
      dom.find(listHtml, '[data-item-id="' + logicComponent.getAttribute('data-uri') + '"]').classList.add('active');
    }
  });

  return listHtml;
};

/**
 * Delete a component from a space
 *
 * @param  {string} id The `id` value of the item in the filterable list that was clicked
 */
proto.remove = function (id) {
  removeService.removeLogic(id, this.el)
    .then(() => {
      // Make new component list from the returned HTML
      this.findChildrenMakeList(this.el);

      // Invoke the callback
      this.callbacks.remove(this.el);
      // Close the old pane
      references.pane.close();
      // Launch new pane with updated components
      this.launchPane();
    });
};

/**
 * [makeList description]
 * @param  {Array} components
 * @return {Object}
 */
proto.makeList = function (components) {
  return _.map(components, function (item) {
    var childComponent = dom.find(item, '[data-uri]'),
      componentType = references.getComponentNameFromReference(childComponent.getAttribute('data-uri')),
      componentTitle = references.label(componentType),
      readouts = logicReadoutService(item);

    componentTitle = readouts ? componentTitle + readouts : componentTitle;

    return {
      title: componentTitle,
      id: item.getAttribute('data-uri')
    };
  });
};

/**
 * [reorder description]
 * @param  {string} id
 * @param  {number} newIndex
 * @param  {number} oldIndex
 */
proto.reorder = function (id, newIndex, oldIndex) {
  var data = { _ref: this.spaceRef },
    content = _.map(this.componentList, function (item) {
      return { _ref: item.id };
    });

  content.splice(newIndex, 0, content.splice(oldIndex, 1)[0]); // reorder the array
  data.content = content;

  // Save the space
  references.edit.savePartial(data)
    .then((newHtml) => {
      var newChildHtmlPromises;

      // Make new component list from the returned HTML
      this.findChildrenMakeList(newHtml);

      // Make an array of promises for the updated children HTML
      newChildHtmlPromises = _.map(this.childComponents, function (logicComponent) {
        var query = { currentUrl: window.location.href };

        return references.edit.getHTMLWithQuery(logicComponent.getAttribute('data-uri'), query);
      });

      return Promise.all(newChildHtmlPromises)
        .then(this.renderUpdatedSpace.bind(this))
        .then(createService.attachHandlersAndFocus);

    });
};

/**
 * [renderUpdatedSpace description]
 * @param  {Element} resp
 * @return {Element}
 */
proto.renderUpdatedSpace = function (resp) {
  var space = this.el,
    spaceChildren = utils.findAllLogic(this.el),
    spaceOnPage = dom.find(document, '[data-uri="' + this.spaceRef + '"]');

  _.forEach(spaceChildren, function (child) {
    child.parentNode.removeChild(child);
  });

  _.forEach(resp, function (logicComponent) {
    space.appendChild(logicComponent);
  });

  dom.replaceElement(spaceOnPage, space);
  this.findChildrenMakeList(space);

  return space;
};

/**
 * [listItemClick description]
 * @param {string} id
 */
proto.listItemClick = function (id) {
  var newActive = dom.find(this.el, `[data-uri="${id}"]`);

  _.each(this.childComponents, function (el) {
    statusService.removeEditing(el);
  });

  statusService.setEditing(newActive);

  references.pane.close();
};

/**
 * @param  {string} id
 */
proto.settings = function (id) {
  references.focus.unfocus().then(function () {
    return references.forms.open(id, document.body);
  }).catch(_.noop);
};

/**
 * [spaceSettings description]
 * @param  {Object} parent
 * @param  {Object} callbacks
 * @return {BrowseController}
 */
function spaceSettings(parent, callbacks) {
  return new BrowseController(parent, callbacks);
};

module.exports = spaceSettings;

<commit_msg>Focusing on the selected Logic/component after click<commit_after>var dom = require('@nymag/dom'),
  _ = require('lodash'),
  AddController = require('./add-component-controller'),
  references = require('references'),
  utils = require('../services/utils'),
  removeService = require('../services/remove-service'),
  createService = require('../services/create-service'),
  logicReadoutService = require('../services/logic-readout-service'),
  statusService = require('../services/status-service'),
  proto = BrowseController.prototype;

/**
 * @param {Element} el
 * @param {Object} callbacks
 */
function BrowseController(el, callbacks) {
  /**
   * The Clay Space component data
   * @type {Object}
   */
  this.el = el;

  this.spaceRef = el.getAttribute('data-uri');

  this.callbacks = callbacks;

  /**
   * Children Space Logic componens
   * @type {Array}
   */
  this.childComponents = [];

  /**
   * The list of components in the Space in
   * organized to be used in a filterable list
   * @type {Array}
   */
  this.componentList = [];

  this.findChildrenMakeList(this.el);
  // Launch the pane
  this.launchPane();
}

/**
 * Open the settins pane
 */
proto.launchPane = function () {
  var paneContent = this.markActiveInList(utils.createFilterableList(this.componentList, {
    click: this.listItemClick.bind(this),
    reorder: this.reorder.bind(this),
    settings: this.settings.bind(this),
    remove: this.remove.bind(this),
    add: this.addComponent.bind(this),
    addTitle: 'Add Component To Space'
  }));

  references.pane.open([{ header: this.el.getAttribute(references.dataPaneTitle) || 'Browse Space', content: paneContent }]);
};

/**
 * Launch the AddController
 */
proto.addComponent = function () {
  AddController(this.el, this.callbacks.add);
};

/**
 * [findChildrenMakeList description]
 * @param  {Element} el
 */
proto.findChildrenMakeList = function (el) {
  this.childComponents = utils.findAllLogic(el);
  this.componentList = this.makeList(this.childComponents);
};

/**
 * Apply active styling to the proper item in the filterable list
 * @param  {element} listHtml
 * @returns {element}
 */
proto.markActiveInList = function (listHtml) {
  _.each(this.childComponents, function (logicComponent) {
    if (statusService.isEditing(logicComponent)) {
      dom.find(listHtml, '[data-item-id="' + logicComponent.getAttribute('data-uri') + '"]').classList.add('active');
    }
  });

  return listHtml;
};

/**
 * Delete a component from a space
 *
 * @param  {string} id The `id` value of the item in the filterable list that was clicked
 */
proto.remove = function (id) {
  removeService.removeLogic(id, this.el)
    .then(() => {
      // Make new component list from the returned HTML
      this.findChildrenMakeList(this.el);

      // Invoke the callback
      this.callbacks.remove(this.el);
      // Close the old pane
      references.pane.close();
      // Launch new pane with updated components
      this.launchPane();
    });
};

/**
 * [makeList description]
 * @param  {Array} components
 * @return {Object}
 */
proto.makeList = function (components) {
  return _.map(components, function (item) {
    var childComponent = dom.find(item, '[data-uri]'),
      componentType = references.getComponentNameFromReference(childComponent.getAttribute('data-uri')),
      componentTitle = references.label(componentType),
      readouts = logicReadoutService(item);

    componentTitle = readouts ? componentTitle + readouts : componentTitle;

    return {
      title: componentTitle,
      id: item.getAttribute('data-uri')
    };
  });
};

/**
 * [reorder description]
 * @param  {string} id
 * @param  {number} newIndex
 * @param  {number} oldIndex
 */
proto.reorder = function (id, newIndex, oldIndex) {
  var data = { _ref: this.spaceRef },
    content = _.map(this.componentList, function (item) {
      return { _ref: item.id };
    });

  content.splice(newIndex, 0, content.splice(oldIndex, 1)[0]); // reorder the array
  data.content = content;

  // Save the space
  references.edit.savePartial(data)
    .then((newHtml) => {
      var newChildHtmlPromises;

      // Make new component list from the returned HTML
      this.findChildrenMakeList(newHtml);

      // Make an array of promises for the updated children HTML
      newChildHtmlPromises = _.map(this.childComponents, function (logicComponent) {
        var query = { currentUrl: window.location.href };

        return references.edit.getHTMLWithQuery(logicComponent.getAttribute('data-uri'), query);
      });

      return Promise.all(newChildHtmlPromises)
        .then(this.renderUpdatedSpace.bind(this))
        .then(createService.attachHandlersAndFocus);

    });
};

/**
 * [renderUpdatedSpace description]
 * @param  {Element} resp
 * @return {Element}
 */
proto.renderUpdatedSpace = function (resp) {
  var space = this.el,
    spaceChildren = utils.findAllLogic(this.el),
    spaceOnPage = dom.find(document, '[data-uri="' + this.spaceRef + '"]');

  _.forEach(spaceChildren, function (child) {
    child.parentNode.removeChild(child);
  });

  _.forEach(resp, function (logicComponent) {
    space.appendChild(logicComponent);
  });

  dom.replaceElement(spaceOnPage, space);
  this.findChildrenMakeList(space);

  return space;
};

/**
 * When an item in the browse pane is clicked we want to be able
 * to edit that selected component. Remove the editing attribute
 * from all Logic's and then add it back to the one that was
 * selected. Let's then close the pane and focus on that selected
 * component.
 *
 * @param {string} id
 */
proto.listItemClick = function (id) {
  var newActive = dom.find(this.el, `[data-uri="${id}"]`);

  // Remove editing status from each Logic
  _.each(this.childComponents, function (el) {
    statusService.removeEditing(el);
  });
  // Set the selected Logic to active
  statusService.setEditing(newActive);
  // Close pane
  references.pane.close();
  // Focus on the Logic's embedded component
  references.focus.focus(dom.find(newActive, '[data-uri]'));
};

/**
 * Open the settings window for the logic
 *
 * @param  {string} id
 */
proto.settings = function (id) {
  references.focus.unfocus().then(function () {
    return references.forms.open(id, document.body);
  }).catch(_.noop);
};

/**
 * Make a new Space Settings instance
 *
 * @param  {Object} parent
 * @param  {Object} callbacks
 * @return {BrowseController}
 */
function spaceSettings(parent, callbacks) {
  return new BrowseController(parent, callbacks);
};

module.exports = spaceSettings;

<|endoftext|>


<commit_before>import $ from 'dom7';
import Utils from '../../utils/utils';
import Support from '../../utils/support';

function resizablePanel(panel) {
  const app = panel.app;
  if (panel.resizableInitialized) return;
  Utils.extend(panel, {
    resizable: true,
    resizableWidth: null,
    resizableInitialized: true,
  });
  const $htmlEl = $('html');
  const { $el, $backdropEl, side, effect } = panel;
  if (!$el) return;

  let isTouched;
  let isMoved;
  const touchesStart = {};
  let touchesDiff;
  let panelWidth;

  let $viewEl;

  let panelMinWidth;
  let panelMaxWidth;
  let visibleByBreakpoint;

  function transformCSSWidth(v) {
    if (!v) return null;
    if (v.indexOf('%') >= 0 || v.indexOf('vw') >= 0) {
      return parseInt(v, 10) / 100 * app.width;
    }
    const newV = parseInt(v, 10);
    if (Number.isNaN(newV)) return null;
    return newV;
  }

  function isResizable() {
    return panel.resizable && $el.hasClass('panel-resizable');
  }

  function handleTouchStart(e) {
    if (!isResizable()) return;
    touchesStart.x = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX;
    touchesStart.y = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY;
    isMoved = false;
    isTouched = true;
    panelMinWidth = transformCSSWidth($el.css('min-width'));
    panelMaxWidth = transformCSSWidth($el.css('max-width'));
    visibleByBreakpoint = $el.hasClass('panel-in-breakpoint');
  }
  function handleTouchMove(e) {
    if (!isTouched) return;
    const pageX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX;

    if (!isMoved) {
      panelWidth = $el[0].offsetWidth;
      $el.transition(0);
      $el.addClass('panel-resizing');
      $htmlEl.css('cursor', 'col-resize');
      if (effect === 'reveal' || visibleByBreakpoint) {
        $viewEl = $(panel.getViewEl());
      }
      if (effect === 'reveal' && !visibleByBreakpoint) {
        $backdropEl.transition(0);
        $viewEl.transition(0);
      }
    }

    isMoved = true;

    e.preventDefault();

    touchesDiff = (pageX - touchesStart.x);

    let newPanelWidth = side === 'left' ? panelWidth + touchesDiff : panelWidth - touchesDiff;
    if (panelMinWidth && !Number.isNaN(panelMinWidth)) {
      newPanelWidth = Math.max(newPanelWidth, panelMinWidth);
    }
    if (panelMaxWidth && !Number.isNaN(panelMaxWidth)) {
      newPanelWidth = Math.min(newPanelWidth, panelMaxWidth);
    }
    newPanelWidth = Math.min(Math.max(newPanelWidth, 0), app.width);

    panel.resizableWidth = newPanelWidth;
    $el[0].style.width = `${newPanelWidth}px`;
    if (effect === 'reveal' && !visibleByBreakpoint) {
      if ($viewEl) {
        $viewEl.transform(`translate3d(${side === 'left' ? newPanelWidth : -newPanelWidth}px, 0, 0)`);
      }
      if ($backdropEl) {
        $backdropEl.transform(`translate3d(${side === 'left' ? newPanelWidth : -newPanelWidth}px, 0, 0)`);
      }
    } else if (visibleByBreakpoint && $viewEl) {
      $viewEl.css(`margin-${side}`, `${newPanelWidth}px`);
    }

    $el.trigger('panel:resize', newPanelWidth);
    panel.emit('local::resize panelResize', panel, newPanelWidth);
  }
  function handleTouchEnd() {
    $('html').css('cursor', '');
    if (!isTouched || !isMoved) {
      isTouched = false;
      isMoved = false;
      return;
    }
    isTouched = false;
    isMoved = false;

    $htmlEl[0].style.setProperty(`--f7-panel-${side}-width`, `${panel.resizableWidth}px`);
    $el[0].style.width = '';
    if (effect === 'reveal' && !visibleByBreakpoint) {
      $viewEl.transform('');
      $backdropEl.transform('');
    }
    $el.removeClass('panel-resizing');
    Utils.nextFrame(() => {
      if (visibleByBreakpoint) return;
      $el.transition('');
      if (effect === 'reveal') {
        $backdropEl.transition('');
        if ($viewEl) $viewEl.transition('');
      }
    });
  }

  function handleResize() {
    if (!panel.opened || !panel.resizableWidth) return;
    panelMinWidth = transformCSSWidth($el.css('min-width'));
    panelMaxWidth = transformCSSWidth($el.css('max-width'));

    if (panelMinWidth && !Number.isNaN(panelMinWidth) && panel.resizableWidth < panelMinWidth) {
      panel.resizableWidth = Math.max(panel.resizableWidth, panelMinWidth);
    }
    if (panelMaxWidth && !Number.isNaN(panelMaxWidth) && panel.resizableWidth > panelMaxWidth) {
      panel.resizableWidth = Math.min(panel.resizableWidth, panelMaxWidth);
    }
    panel.resizableWidth = Math.min(Math.max(panel.resizableWidth, 0), app.width);

    $htmlEl[0].style.setProperty(`--f7-panel-${side}-width`, `${panel.resizableWidth}px`);
  }

  if (panel.$el.find('.panel-resize-handler').length === 0) {
    panel.$el.append('

');
  }
  panel.$resizeHandlerEl = panel.$el.children('.panel-resize-handler');

  $el.addClass('panel-resizable');

  // Add Events
  const passive = Support.passiveListener ? { passive: true } : false;

  panel.$el.on(app.touchEvents.start, '.panel-resize-handler', handleTouchStart, passive);
  app.on('touchmove:active', handleTouchMove);
  app.on('touchend:passive', handleTouchEnd);
  app.on('resize', handleResize);
  panel.on('beforeOpen', handleResize);

  panel.once('panelDestroy', () => {
    $el.removeClass('panel-resizable');
    panel.$resizeHandlerEl.remove();
    panel.$el.off(app.touchEvents.start, '.panel-resize-handler', handleTouchStart, passive);
    app.off('touchmove:active', handleTouchMove);
    app.off('touchend:passive', handleTouchEnd);
    app.off('resize', handleResize);
    panel.off('beforeOpen', handleResize);
  });
}

export default resizablePanel;
<commit_msg>Core: Panel - fix visible breakpoint transition<commit_after>import $ from 'dom7';
import Utils from '../../utils/utils';
import Support from '../../utils/support';

function resizablePanel(panel) {
  const app = panel.app;
  if (panel.resizableInitialized) return;
  Utils.extend(panel, {
    resizable: true,
    resizableWidth: null,
    resizableInitialized: true,
  });
  const $htmlEl = $('html');
  const { $el, $backdropEl, side, effect } = panel;
  if (!$el) return;

  let isTouched;
  let isMoved;
  const touchesStart = {};
  let touchesDiff;
  let panelWidth;

  let $viewEl;

  let panelMinWidth;
  let panelMaxWidth;
  let visibleByBreakpoint;

  function transformCSSWidth(v) {
    if (!v) return null;
    if (v.indexOf('%') >= 0 || v.indexOf('vw') >= 0) {
      return parseInt(v, 10) / 100 * app.width;
    }
    const newV = parseInt(v, 10);
    if (Number.isNaN(newV)) return null;
    return newV;
  }

  function isResizable() {
    return panel.resizable && $el.hasClass('panel-resizable');
  }

  function handleTouchStart(e) {
    if (!isResizable()) return;
    touchesStart.x = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX;
    touchesStart.y = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY;
    isMoved = false;
    isTouched = true;
    panelMinWidth = transformCSSWidth($el.css('min-width'));
    panelMaxWidth = transformCSSWidth($el.css('max-width'));
    visibleByBreakpoint = $el.hasClass('panel-in-breakpoint');
  }
  function handleTouchMove(e) {
    if (!isTouched) return;
    const pageX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX;

    if (!isMoved) {
      panelWidth = $el[0].offsetWidth;
      $el.transition(0);
      $el.addClass('panel-resizing');
      $htmlEl.css('cursor', 'col-resize');
      if (effect === 'reveal' || visibleByBreakpoint) {
        $viewEl = $(panel.getViewEl());
      }
      if (effect === 'reveal' && !visibleByBreakpoint) {
        $backdropEl.transition(0);
        $viewEl.transition(0);
      }
    }

    isMoved = true;

    e.preventDefault();

    touchesDiff = (pageX - touchesStart.x);

    let newPanelWidth = side === 'left' ? panelWidth + touchesDiff : panelWidth - touchesDiff;
    if (panelMinWidth && !Number.isNaN(panelMinWidth)) {
      newPanelWidth = Math.max(newPanelWidth, panelMinWidth);
    }
    if (panelMaxWidth && !Number.isNaN(panelMaxWidth)) {
      newPanelWidth = Math.min(newPanelWidth, panelMaxWidth);
    }
    newPanelWidth = Math.min(Math.max(newPanelWidth, 0), app.width);

    panel.resizableWidth = newPanelWidth;
    $el[0].style.width = `${newPanelWidth}px`;
    if (effect === 'reveal' && !visibleByBreakpoint) {
      if ($viewEl) {
        $viewEl.transform(`translate3d(${side === 'left' ? newPanelWidth : -newPanelWidth}px, 0, 0)`);
      }
      if ($backdropEl) {
        $backdropEl.transform(`translate3d(${side === 'left' ? newPanelWidth : -newPanelWidth}px, 0, 0)`);
      }
    } else if (visibleByBreakpoint && $viewEl) {
      $viewEl.css(`margin-${side}`, `${newPanelWidth}px`);
    }

    $el.trigger('panel:resize', newPanelWidth);
    panel.emit('local::resize panelResize', panel, newPanelWidth);
  }
  function handleTouchEnd() {
    $('html').css('cursor', '');
    if (!isTouched || !isMoved) {
      isTouched = false;
      isMoved = false;
      return;
    }
    isTouched = false;
    isMoved = false;

    $htmlEl[0].style.setProperty(`--f7-panel-${side}-width`, `${panel.resizableWidth}px`);
    $el[0].style.width = '';
    if (effect === 'reveal' && !visibleByBreakpoint) {
      $viewEl.transform('');
      $backdropEl.transform('');
    }
    $el.removeClass('panel-resizing');
    Utils.nextFrame(() => {
      $el.transition('');
      if (effect === 'reveal') {
        $backdropEl.transition('');
        if ($viewEl) $viewEl.transition('');
      }
    });
  }

  function handleResize() {
    if (!panel.opened || !panel.resizableWidth) return;
    panelMinWidth = transformCSSWidth($el.css('min-width'));
    panelMaxWidth = transformCSSWidth($el.css('max-width'));

    if (panelMinWidth && !Number.isNaN(panelMinWidth) && panel.resizableWidth < panelMinWidth) {
      panel.resizableWidth = Math.max(panel.resizableWidth, panelMinWidth);
    }
    if (panelMaxWidth && !Number.isNaN(panelMaxWidth) && panel.resizableWidth > panelMaxWidth) {
      panel.resizableWidth = Math.min(panel.resizableWidth, panelMaxWidth);
    }
    panel.resizableWidth = Math.min(Math.max(panel.resizableWidth, 0), app.width);

    $htmlEl[0].style.setProperty(`--f7-panel-${side}-width`, `${panel.resizableWidth}px`);
  }

  if (panel.$el.find('.panel-resize-handler').length === 0) {
    panel.$el.append('

');
  }
  panel.$resizeHandlerEl = panel.$el.children('.panel-resize-handler');

  $el.addClass('panel-resizable');

  // Add Events
  const passive = Support.passiveListener ? { passive: true } : false;

  panel.$el.on(app.touchEvents.start, '.panel-resize-handler', handleTouchStart, passive);
  app.on('touchmove:active', handleTouchMove);
  app.on('touchend:passive', handleTouchEnd);
  app.on('resize', handleResize);
  panel.on('beforeOpen', handleResize);

  panel.once('panelDestroy', () => {
    $el.removeClass('panel-resizable');
    panel.$resizeHandlerEl.remove();
    panel.$el.off(app.touchEvents.start, '.panel-resize-handler', handleTouchStart, passive);
    app.off('touchmove:active', handleTouchMove);
    app.off('touchend:passive', handleTouchEnd);
    app.off('resize', handleResize);
    panel.off('beforeOpen', handleResize);
  });
}

export default resizablePanel;
<|endoftext|>


<commit_before>import $ from 'dom7';
import Utils from '../../utils/utils';
import Framework7Class from '../../utils/class';

class Stepper extends Framework7Class {
  constructor(app, params) {
    super(params, [app]);
    const stepper = this;

    const defaults = {
      el: null,
      inputEl: null,
      valueEl: null,
      value: 0,
      formatValue: null,
      step: 1,
      min: 0,
      max: 100,
      watchInput: true,
      autorepeat: false,
      autorepeatDynamic: false,
      wraps: false,
    };

    // Extend defaults with modules params
    stepper.useModulesParams(defaults);

    stepper.params = Utils.extend(defaults, params);
    if (stepper.params.value < stepper.params.min) {
      stepper.params.value = stepper.params.min;
    }
    if (stepper.params.value > stepper.params.max) {
      stepper.params.value = stepper.params.max;
    }

    const el = stepper.params.el;
    if (!el) return stepper;

    const $el = $(el);
    if ($el.length === 0) return stepper;

    if ($el[0].f7Stepper) return $el[0].f7Stepper;

    let $inputEl;
    if (stepper.params.inputEl) {
      $inputEl = $(stepper.params.inputEl);
    } else if ($el.find('.stepper-input-wrap').find('input, textarea').length) {
      $inputEl = $el.find('.stepper-input-wrap').find('input, textarea').eq(0);
    }

    if ($inputEl && $inputEl.length) {
      ('step min max').split(' ').forEach((paramName) => {
        if (!params[paramName] && $inputEl.attr(paramName)) {
          stepper.params[paramName] = parseFloat($inputEl.attr(paramName));
        }
      });

      const inputValue = parseFloat($inputEl.val());
      if (typeof params.value === 'undefined' && !Number.isNaN(inputValue) && (inputValue || inputValue === 0)) {
        stepper.params.value = inputValue;
      }
    }

    let $valueEl;
    if (stepper.params.valueEl) {
      $valueEl = $(stepper.params.valueEl);
    } else if ($el.find('.stepper-value').length) {
      $valueEl = $el.find('.stepper-value').eq(0);
    }

    const $buttonPlusEl = $el.find('.stepper-button-plus');
    const $buttonMinusEl = $el.find('.stepper-button-minus');

    const { step, min, max, value } = stepper.params;

    Utils.extend(stepper, {
      app,
      $el,
      el: $el[0],
      $buttonPlusEl,
      buttonPlusEl: $buttonPlusEl[0],
      $buttonMinusEl,
      buttonMinusEl: $buttonMinusEl[0],
      $inputEl,
      inputEl: $inputEl ? $inputEl[0] : undefined,
      $valueEl,
      valueEl: $valueEl ? $valueEl[0] : undefined,
      step,
      min,
      max,
      value,
    });

    $el[0].f7Stepper = stepper;

    // Handle Events
    const touchesStart = {};
    let isTouched;
    let isScrolling;
    let preventButtonClick;
    let intervalId;
    let timeoutId;
    let autorepeatAction = null;
    let autorepeatInAction = false;

    function dynamicRepeat(current, progressions, startsIn, progressionStep, repeatEvery, action) {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        if (current === 1) {
          preventButtonClick = true;
          autorepeatInAction = true;
        }
        clearInterval(intervalId);
        action();
        intervalId = setInterval(() => {
          action();
        }, repeatEvery);
        if (current < progressions) {
          dynamicRepeat(current + 1, progressions, startsIn, progressionStep, repeatEvery / 2, action);
        }
      }, current === 1 ? startsIn : progressionStep);
    }

    function onTouchStart(e) {
      if (isTouched) return;
      if ($(e.target).closest($buttonPlusEl).length) {
        autorepeatAction = 'increment';
      } else if ($(e.target).closest($buttonMinusEl).length) {
        autorepeatAction = 'decrement';
      }
      if (!autorepeatAction) return;

      touchesStart.x = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX;
      touchesStart.y = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY;
      isTouched = true;
      isScrolling = undefined;

      const progressions = stepper.params.autorepeatDynamic ? 4 : 1;
      dynamicRepeat(1, progressions, 500, 1000, 300, () => {
        stepper[autorepeatAction]();
      });
    }
    function onTouchMove(e) {
      if (!isTouched) return;
      const pageX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX;
      const pageY = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY;

      if (typeof isScrolling === 'undefined' && !autorepeatInAction) {
        isScrolling = !!(isScrolling || Math.abs(pageY - touchesStart.y) > Math.abs(pageX - touchesStart.x));
      }
      const distance = (((pageX - touchesStart.x) ** 2) + ((pageY - touchesStart.y) ** 2)) ** 0.5;

      if (isScrolling || distance > 20) {
        isTouched = false;
        clearTimeout(timeoutId);
        clearInterval(intervalId);
      }
    }
    function onTouchEnd() {
      clearTimeout(timeoutId);
      clearInterval(intervalId);
      autorepeatAction = null;
      autorepeatInAction = false;
      isTouched = false;
    }

    function onMinusClick() {
      if (preventButtonClick) {
        preventButtonClick = false;
        return;
      }
      stepper.decrement();
    }
    function onPlusClick() {
      if (preventButtonClick) {
        preventButtonClick = false;
        return;
      }
      stepper.increment();
    }
    function onInput(e) {
      if (e.detail && e.detail.sentByF7Stepper) return;
      stepper.setValue(e.target.value, true);
    }
    stepper.attachEvents = function attachEvents() {
      $buttonMinusEl.on('click', onMinusClick);
      $buttonPlusEl.on('click', onPlusClick);
      if (stepper.params.watchInput && $inputEl && $inputEl.length) {
        $inputEl.on('input', onInput);
      }
      if (stepper.params.autorepeat) {
        app.on('touchstart:passive', onTouchStart);
        app.on('touchmove:active', onTouchMove);
        app.on('touchend:passive', onTouchEnd);
      }
    };
    stepper.detachEvents = function detachEvents() {
      $buttonMinusEl.off('click', onMinusClick);
      $buttonPlusEl.off('click', onPlusClick);
      if (stepper.params.watchInput && $inputEl && $inputEl.length) {
        $inputEl.off('input', onInput);
      }
    };

    // Install Modules
    stepper.useModules();

    // Init
    stepper.init();

    return stepper;
  }

  minus() {
    return this.decrement();
  }

  plus() {
    return this.increment();
  }

  decrement() {
    const stepper = this;
    return stepper.setValue(stepper.value - stepper.step);
  }

  increment() {
    const stepper = this;
    return stepper.setValue(stepper.value + stepper.step);
  }

  setValue(newValue, forceUpdate) {
    const stepper = this;
    const { step, min, max } = stepper;

    const oldValue = stepper.value;

    let value = Math.round(newValue / step) * step;
    if (!stepper.params.wraps) {
      value = Math.max(Math.min(value, max), min);
    } else {
      if (value > max) value = min;
      if (value < min) value = max;
    }
    if (Number.isNaN(value)) {
      value = oldValue;
    }
    stepper.value = value;

    const valueChanged = oldValue !== value;

    // Events
    if (!valueChanged && !forceUpdate) return stepper;
    stepper.$el.trigger('stepper:change', stepper, stepper.value);
    const formattedValue = stepper.formatValue(stepper.value);
    if (stepper.$inputEl && stepper.$inputEl.length) {
      stepper.$inputEl.val(formattedValue);
      stepper.$inputEl.trigger('input change', { sentByF7Stepper: true });
    }
    if (stepper.$valueEl && stepper.$valueEl.length) {
      stepper.$valueEl.html(formattedValue);
    }
    stepper.emit('local::change stepperChange', stepper, stepper.value);
    return stepper;
  }

  getValue() {
    return this.value;
  }

  formatValue(value) {
    const stepper = this;
    if (!stepper.params.formatValue) return value;
    return stepper.params.formatValue.call(stepper, value);
  }

  init() {
    const stepper = this;
    stepper.attachEvents();
    if (stepper.$valueEl && stepper.$valueEl.length) {
      const formattedValue = stepper.formatValue(stepper.value);
      stepper.$valueEl.html(formattedValue);
    }
    return stepper;
  }

  destroy() {
    let stepper = this;
    stepper.$el.trigger('stepper:beforedestroy', stepper);
    stepper.emit('local::beforeDestroy stepperBeforeDestroy', stepper);
    delete stepper.$el[0].f7Stepper;
    stepper.detachEvents();
    Utils.deleteProps(stepper);
    stepper = null;
  }
}

export default Stepper;
<commit_msg>Add manual input with fractions<commit_after>import $ from 'dom7';
import Utils from '../../utils/utils';
import Framework7Class from '../../utils/class';

class Stepper extends Framework7Class {
  constructor(app, params) {
    super(params, [app]);
    const stepper = this;

    const defaults = {
      el: null,
      inputEl: null,
      valueEl: null,
      value: 0,
      formatValue: null,
      step: 1,
      min: 0,
      max: 100,
      watchInput: true,
      autorepeat: false,
      autorepeatDynamic: false,
      wraps: false,
      decimalPoint: 4,
	  buttonsEndInputMode: true,
    };

    // Extend defaults with modules params
    stepper.useModulesParams(defaults);

    stepper.params = Utils.extend(defaults, params);
    if (stepper.params.value < stepper.params.min) {
      stepper.params.value = stepper.params.min;
    }
    if (stepper.params.value > stepper.params.max) {
      stepper.params.value = stepper.params.max;
    }

    const el = stepper.params.el;
    if (!el) return stepper;

    const $el = $(el);
    if ($el.length === 0) return stepper;

    if ($el[0].f7Stepper) return $el[0].f7Stepper;

    let $inputEl;
    if (stepper.params.inputEl) {
      $inputEl = $(stepper.params.inputEl);
    } else if ($el.find('.stepper-input-wrap').find('input, textarea').length) {
      $inputEl = $el.find('.stepper-input-wrap').find('input, textarea').eq(0);
    }

    if ($inputEl && $inputEl.length) {
      ('step min max').split(' ').forEach((paramName) => {
        if (!params[paramName] && $inputEl.attr(paramName)) {
          stepper.params[paramName] = parseFloat($inputEl.attr(paramName));
        }
      });

	  const decimalPoint = parseInt(stepper.params.decimalPoint);
      if (Number.isNaN(decimalPoint)) {
        stepper.params.decimalPoint = 0;
      } else {
        stepper.params.decimalPoint = decimalPoint;
      }

      const inputValue = parseFloat($inputEl.val());
      if (typeof params.value === 'undefined' && !Number.isNaN(inputValue) && (inputValue || inputValue === 0)) {
        stepper.params.value = inputValue;
      }
    }

    let $valueEl;
    if (stepper.params.valueEl) {
      $valueEl = $(stepper.params.valueEl);
    } else if ($el.find('.stepper-value').length) {
      $valueEl = $el.find('.stepper-value').eq(0);
    }

    const $buttonPlusEl = $el.find('.stepper-button-plus');
    const $buttonMinusEl = $el.find('.stepper-button-minus');

    const { step, min, max, value, decimalPoint } = stepper.params;

    Utils.extend(stepper, {
      app,
      $el,
      el: $el[0],
      $buttonPlusEl,
      buttonPlusEl: $buttonPlusEl[0],
      $buttonMinusEl,
      buttonMinusEl: $buttonMinusEl[0],
      $inputEl,
      inputEl: $inputEl ? $inputEl[0] : undefined,
      $valueEl,
      valueEl: $valueEl ? $valueEl[0] : undefined,
      step,
      min,
      max,
      value,
	  decimalPoint,
    });

    $el[0].f7Stepper = stepper;

    // Handle Events
    const touchesStart = {};
    let isTouched;
    let isScrolling;
    let preventButtonClick;
    let intervalId;
    let timeoutId;
    let autorepeatAction = null;
    let autorepeatInAction = false;
	let manualInput = false;

    function dynamicRepeat(current, progressions, startsIn, progressionStep, repeatEvery, action) {
      clearTimeout(timeoutId);
      timeoutId = setTimeout(() => {
        if (current === 1) {
          preventButtonClick = true;
          autorepeatInAction = true;
        }
        clearInterval(intervalId);
        action();
        intervalId = setInterval(() => {
          action();
        }, repeatEvery);
        if (current < progressions) {
          dynamicRepeat(current + 1, progressions, startsIn, progressionStep, repeatEvery / 2, action);
        }
      }, current === 1 ? startsIn : progressionStep);
    }

    function onTouchStart(e) {
      if (isTouched) return;
	  if (manualInput) { return; }
      if ($(e.target).closest($buttonPlusEl).length) {
        autorepeatAction = 'increment';
      } else if ($(e.target).closest($buttonMinusEl).length) {
        autorepeatAction = 'decrement';
      }
      if (!autorepeatAction) return;

      touchesStart.x = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX;
      touchesStart.y = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY;
      isTouched = true;
      isScrolling = undefined;

      const progressions = stepper.params.autorepeatDynamic ? 4 : 1;
      dynamicRepeat(1, progressions, 500, 1000, 300, () => {
        stepper[autorepeatAction]();
      });
    }
    function onTouchMove(e) {
      if (!isTouched) return;
	  if (manualInput) { return; }
      const pageX = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX;
      const pageY = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY;

      if (typeof isScrolling === 'undefined' && !autorepeatInAction) {
        isScrolling = !!(isScrolling || Math.abs(pageY - touchesStart.y) > Math.abs(pageX - touchesStart.x));
      }
      const distance = (((pageX - touchesStart.x) ** 2) + ((pageY - touchesStart.y) ** 2)) ** 0.5;

      if (isScrolling || distance > 20) {
        isTouched = false;
        clearTimeout(timeoutId);
        clearInterval(intervalId);
      }
    }
    function onTouchEnd() {
      clearTimeout(timeoutId);
      clearInterval(intervalId);
      autorepeatAction = null;
      autorepeatInAction = false;
      isTouched = false;
    }

    function onMinusClick() {
	  if (manualInput) {
        if (stepper.params.buttonsEndInputMode) {
          manualInput = false;
          stepper.endTypeMode();
        };
        return;
      }
      if (preventButtonClick) {
        preventButtonClick = false;
        return;
      }
      stepper.decrement();
    }
    function onPlusClick() {
	  if (manualInput) {
        if (stepper.params.buttonsEndInputMode) {
          manualInput = false;
          stepper.endTypeMode();
        };
        return;
      }
      if (preventButtonClick) {
        preventButtonClick = false;
        return;
      }
      stepper.increment();
    }
    function onInputClick(e) {
      if (!e.target.readOnly) {
        manualInput = true;
        if (typeof e.target.selectionStart == "number") {
          e.target.selectionStart = e.target.selectionEnd = e.target.value.length;
        }
      }
    }
    function onInputKey(e) {
      if (e.keyCode == 13 || e.which == 13) {
        e.preventDefault();
        manualInput = false;
        stepper.endTypeMode();
      }
    }
    function onInputBlur() {
      manualInput = false;
      stepper.endTypeMode();
    }
    function onInput(e) {
      if (e.detail && e.detail.sentByF7Stepper) return;
      stepper.setValue(e.target.value, true);
    }
    stepper.attachEvents = function attachEvents() {
      $buttonMinusEl.on('click', onMinusClick);
      $buttonPlusEl.on('click', onPlusClick);
      if (stepper.params.watchInput && $inputEl && $inputEl.length) {
        $inputEl.on('input', onInput);
        $inputEl.on('click', onInputClick);
        $inputEl.on('blur', onInputBlur);
        $inputEl.on('keyup', onInputKey);
      }
      if (stepper.params.autorepeat) {
        app.on('touchstart:passive', onTouchStart);
        app.on('touchmove:active', onTouchMove);
        app.on('touchend:passive', onTouchEnd);
      }
    };
    stepper.detachEvents = function detachEvents() {
      $buttonMinusEl.off('click', onMinusClick);
      $buttonPlusEl.off('click', onPlusClick);
      if (stepper.params.watchInput && $inputEl && $inputEl.length) {
        $inputEl.off('input', onInput);
        $inputEl.off('click', onInputClick);
        $inputEl.off('blur', onInputBlur);
        $inputEl.off('keyup', onInputKey);
      }
    };

    // Install Modules
    stepper.useModules();

    // Init
    stepper.init();

    return stepper;
  }

  minus() {
    return this.decrement();
  }

  plus() {
    return this.increment();
  }

  decrement() {
    const stepper = this;
    return stepper.setValue(stepper.value - stepper.step);
  }

  increment() {
    const stepper = this;
    return stepper.setValue(stepper.value + stepper.step);
  }

  setValue(newValue, forceUpdate) {
    const stepper = this;
    const { step, min, max } = stepper;

    const oldValue = stepper.value;

    let value = Math.round(newValue / step) * step;
    if (!stepper.params.wraps) {
      value = Math.max(Math.min(value, max), min);
    } else {
      if (value > max) value = min;
      if (value < min) value = max;
    }
    if (Number.isNaN(value)) {
      value = oldValue;
    }
    stepper.value = value;

    const valueChanged = oldValue !== value;

    // Events
    if (!valueChanged && !forceUpdate) return stepper;
    stepper.$el.trigger('stepper:change', stepper, stepper.value);
    const formattedValue = stepper.formatValue(stepper.value);
    if (stepper.$inputEl && stepper.$inputEl.length) {
      stepper.$inputEl.val(formattedValue);
      stepper.$inputEl.trigger('input change', { sentByF7Stepper: true });
    }
    if (stepper.$valueEl && stepper.$valueEl.length) {
      stepper.$valueEl.html(formattedValue);
    }
    stepper.emit('local::change stepperChange', stepper, stepper.value);
    return stepper;
  }

  endTypeMode() {
    const stepper = this;
    stepper.value = parseFloat(stepper.value);

    stepper.$el.trigger('stepper:change', stepper, stepper.value);
    const formattedValue = stepper.formatValue(stepper.value);
    if (stepper.$inputEl && stepper.$inputEl.length) {
      stepper.$inputEl.val(formattedValue);
      stepper.$inputEl.trigger('input change', { sentByF7Stepper: true });
      stepper.$inputEl.blur();
    }
    if (stepper.$valueEl && stepper.$valueEl.length) {
      stepper.$valueEl.html(formattedValue);
    }
    stepper.emit('local::change stepperChange', stepper, stepper.value);
    return stepper;
  };

  typeValue(value) {
    const stepper = this;
    const { step, min, max } = stepper;

    let _inputTxt = String(value);
    if (_inputTxt.lastIndexOf('.') + 1 == _inputTxt.length || _inputTxt.lastIndexOf(',') + 1 == _inputTxt.length) {
      if (_inputTxt.lastIndexOf('.') != _inputTxt.indexOf('.') || _inputTxt.lastIndexOf(',') != _inputTxt.indexOf(',')) {
        _inputTxt = _inputTxt.slice(0, -1);
        stepper.value = _inputTxt;
        stepper.$inputEl.val(stepper.value);
        return;
      }
    } else {
      let value = parseFloat(_inputTxt.replace(',', '.'));
      if (value === 0) {
        stepper.value = _inputTxt.replace(',', '.');
        stepper.$inputEl.val(stepper.value);
        return;
      }
      if (isNaN(value)) {
        stepper.value = 0;
        stepper.$inputEl.val(stepper.value);
        return;
      }
      else {
        const powVal = Math.pow(10, stepper.params.decimalPoint);
        value = (Math.round((value) * powVal)).toFixed(stepper.params.decimalPoint + 1) / powVal;
        stepper.value = parseFloat(String(value).replace(',', '.'));
        stepper.$inputEl.val(stepper.value);
        return;
      }
    }
    stepper.value = _inputTxt;
    stepper.$inputEl.val(_inputTxt);
    return stepper;
  };

  getValue() {
    return this.value;
  }

  formatValue(value) {
    const stepper = this;
    if (!stepper.params.formatValue) return value;
    return stepper.params.formatValue.call(stepper, value);
  }

  init() {
    const stepper = this;
    stepper.attachEvents();
    if (stepper.$valueEl && stepper.$valueEl.length) {
      const formattedValue = stepper.formatValue(stepper.value);
      stepper.$valueEl.html(formattedValue);
    }
    return stepper;
  }

  destroy() {
    let stepper = this;
    stepper.$el.trigger('stepper:beforedestroy', stepper);
    stepper.emit('local::beforeDestroy stepperBeforeDestroy', stepper);
    delete stepper.$el[0].f7Stepper;
    stepper.detachEvents();
    Utils.deleteProps(stepper);
    stepper = null;
  }
}

export default Stepper;
<|endoftext|>


<commit_before>import $ from 'dom7';
import Utils from '../../utils/utils';
import Support from '../../utils/support';
import Framework7Class from '../../utils/class';

class Tooltip extends Framework7Class {
  constructor(app, params = {}) {
    // Extends with open/close Modal methods;
    super(app, params);

    const tooltip = this;

    const defaults = {};

    // Extend defaults with modules params
    tooltip.useModulesParams(defaults);

    tooltip.params = Utils.extend(defaults, params);

    const { el } = tooltip.params;
    if (!el) return tooltip;

    const $el = $(el);
    if ($el.length === 0) return tooltip;

    const $tooltipEl = $(tooltip.render()).eq(0);

    Utils.extend(tooltip, {
      app,
      $el,
      el: $el && $el[0],
      $tooltipEl,
      tooltipEl: $tooltipEl && $tooltipEl[0],
      text: tooltip.params.text || '',
      visible: false,
      opened: false,
    });

    $el[0].f7Tooltip = tooltip;

    const touchesStart = {};
    let isTouched;
    function handleTouchStart(e) {
      if (isTouched) return;
      isTouched = true;
      touchesStart.x = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX;
      touchesStart.y = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY;
      tooltip.show(this);
    }
    function handleTouchMove(e) {
      if (!isTouched) return;
      const x = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX;
      const y = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY;
      const distance = (
        ((x - touchesStart.x) ** 2) +
        ((y - touchesStart.y) ** 2)
      ) ** 0.5;
      if (distance > 50) {
        isTouched = false;
        tooltip.hide();
      }
    }
    function handleTouchEnd() {
      if (!isTouched) return;
      isTouched = false;
      tooltip.hide();
    }
    function handleMouseEnter() {
      tooltip.show(this);
    }
    function handleMouseLeave() {
      tooltip.hide();
    }
    function handleTransitionEnd() {
      if (!$tooltipEl.hasClass('tooltip-in')) {
        $tooltipEl.removeClass('tooltip-out').remove();
      }
    }

    tooltip.attachEvents = function attachEvents() {
      if (Support.touch) {
        const passive = Support.passiveListener ? { passive: true } : false;
        $el.on(app.touchEvents.start, handleTouchStart, passive);
        app.on('touchmove', handleTouchMove);
        app.on('touchend:passive', handleTouchEnd);
        return;
      }
      $tooltipEl.on('transitionend webkitTransitionEnd', handleTransitionEnd);
      $el.on('mouseenter', handleMouseEnter);
      $el.on('mouseleave', handleMouseLeave);
    };
    tooltip.detachEvents = function detachEvents() {
      if (Support.touch) {
        const passive = Support.passiveListener ? { passive: true } : false;
        $el.off(app.touchEvents.start, handleTouchStart, passive);
        app.off('touchmove', handleTouchMove);
        app.off('touchend:passive', handleTouchEnd);
        return;
      }
      $tooltipEl.off('transitionend webkitTransitionEnd', handleTransitionEnd);
      $el.off('mouseenter', handleMouseEnter);
      $el.off('mouseleave', handleMouseLeave);
    };

    // Install Modules
    tooltip.useModules();

    tooltip.init();

    return tooltip;
  }
  position(targetEl) {
    const tooltip = this;
    const { $tooltipEl, app } = tooltip;
    $tooltipEl.css({ left: '', top: '' });
    const $targetEl = $(targetEl || tooltip.el);
    const [width, height] = [$tooltipEl.width(), $tooltipEl.height()];

    $tooltipEl.css({ left: '', top: '' });

    let targetWidth;
    let targetHeight;
    let targetOffsetLeft;
    let targetOffsetTop;
    if ($targetEl && $targetEl.length > 0) {
      targetWidth = $targetEl.outerWidth();
      targetHeight = $targetEl.outerHeight();

      const targetOffset = $targetEl.offset();
      targetOffsetLeft = targetOffset.left - app.left;
      targetOffsetTop = targetOffset.top - app.top;

      const targetParentPage = $targetEl.parents('.page');
      if (targetParentPage.length > 0) {
        targetOffsetTop -= targetParentPage[0].scrollTop;
      }
    }
    let [left, top] = [0, 0, 0];

    // Top Position
    let position = 'top';

    if (height < targetOffsetTop) {
      // On top
      top = targetOffsetTop - height;
    } else if (height < app.height - targetOffsetTop - targetHeight) {
      // On bottom
      position = 'bottom';
      top = targetOffsetTop + targetHeight;
    } else {
      // On middle
      position = 'middle';
      top = ((targetHeight / 2) + targetOffsetTop) - (height / 2);
      if (top <= 0) {
        top = 8;
      } else if (top + height >= app.height) {
        top = app.height - height - 8;
      }
    }

    // Horizontal Position
    if (position === 'top' || position === 'bottom') {
      left = ((targetWidth / 2) + targetOffsetLeft) - (width / 2);
      if (left < 8) left = 8;
      if (left + width > app.width) left = app.width - width - 8;
      if (left < 0) left = 0;
    } else if (position === 'middle') {
      left = targetOffsetLeft - width;
      if (left < 8 || (left + width > app.width)) {
        if (left < 8) left = targetOffsetLeft + targetWidth;
        if (left + width > app.width) left = app.width - width - 8;
      }
    }

    // Apply Styles
    $tooltipEl.css({ top: `${top}px`, left: `${left}px` });
  }
  show(aroundEl) {
    const tooltip = this;
    const { app, $tooltipEl, $el } = tooltip;
    app.root.append($tooltipEl);
    tooltip.position(aroundEl);
    const $aroundEl = $(aroundEl);
    tooltip.visible = true;
    tooltip.opened = true;
    $el.trigger('tooltip:show', tooltip);
    $tooltipEl.trigger('tooltip:show', tooltip);
    if ($aroundEl.length && $aroundEl[0] !== $el[0]) {
      $aroundEl.trigger('tooltip:show', tooltip);
    }
    tooltip.emit('local::show tooltipShow', tooltip);
    $tooltipEl.removeClass('tooltip-out').addClass('tooltip-in');
    return tooltip;
  }
  hide() {
    const tooltip = this;
    const { $tooltipEl, $el } = tooltip;
    tooltip.visible = false;
    tooltip.opened = false;
    $el.trigger('tooltip:hide', tooltip);
    $tooltipEl.trigger('tooltip:hide', tooltip);
    tooltip.emit('local::hide tooltipHide', tooltip);
    $tooltipEl.addClass('tooltip-out').removeClass('tooltip-in');
    return tooltip;
  }
  render() {
    const tooltip = this;
    if (tooltip.params.render) return tooltip.params.render.call(tooltip, tooltip);
    const { cssClass, text } = tooltip.params;
    return `

${text || ''}

    `.trim();
  }
  setText(newText) {
    const tooltip = this;
    if (typeof newText === 'undefined') {
      return tooltip;
    }
    tooltip.params.text = newText;
    tooltip.text = newText;
    if (tooltip.$tooltipEl) {
      tooltip.$tooltipEl.children('.tooltip-content').html(newText);
    }
    if (tooltip.opened) {
      tooltip.position();
    }
    return tooltip;
  }
  init() {
    const tooltip = this;
    tooltip.attachEvents();
  }
  destroy() {
    const tooltip = this;
    if (!tooltip.$el || tooltip.destroyed) return;
    tooltip.$el.trigger('tooltip:beforedestroy', tooltip);
    tooltip.emit('local::beforeDestroy tooltipBeforeDestroy', tooltip);
    tooltip.$tooltipEl.remove();
    delete tooltip.$el[0].f7Tooltip;
    tooltip.detachEvents();
    Utils.deleteProps(tooltip);
    tooltip.destroyed = true;
  }
}
export default Tooltip;
<commit_msg>Core: Tooltip - use app defaults<commit_after>import $ from 'dom7';
import Utils from '../../utils/utils';
import Support from '../../utils/support';
import Framework7Class from '../../utils/class';

class Tooltip extends Framework7Class {
  constructor(app, params = {}) {
    super(app, params);

    const tooltip = this;

    const defaults = Utils.extend({}, app.params.tooltip);

    // Extend defaults with modules params
    tooltip.useModulesParams(defaults);

    tooltip.params = Utils.extend(defaults, params);

    const { el } = tooltip.params;
    if (!el) return tooltip;

    const $el = $(el);
    if ($el.length === 0) return tooltip;

    const $tooltipEl = $(tooltip.render()).eq(0);

    Utils.extend(tooltip, {
      app,
      $el,
      el: $el && $el[0],
      $tooltipEl,
      tooltipEl: $tooltipEl && $tooltipEl[0],
      text: tooltip.params.text || '',
      visible: false,
      opened: false,
    });

    $el[0].f7Tooltip = tooltip;

    const touchesStart = {};
    let isTouched;
    function handleTouchStart(e) {
      if (isTouched) return;
      isTouched = true;
      touchesStart.x = e.type === 'touchstart' ? e.targetTouches[0].pageX : e.pageX;
      touchesStart.y = e.type === 'touchstart' ? e.targetTouches[0].pageY : e.pageY;
      tooltip.show(this);
    }
    function handleTouchMove(e) {
      if (!isTouched) return;
      const x = e.type === 'touchmove' ? e.targetTouches[0].pageX : e.pageX;
      const y = e.type === 'touchmove' ? e.targetTouches[0].pageY : e.pageY;
      const distance = (
        ((x - touchesStart.x) ** 2) +
        ((y - touchesStart.y) ** 2)
      ) ** 0.5;
      if (distance > 50) {
        isTouched = false;
        tooltip.hide();
      }
    }
    function handleTouchEnd() {
      if (!isTouched) return;
      isTouched = false;
      tooltip.hide();
    }
    function handleMouseEnter() {
      tooltip.show(this);
    }
    function handleMouseLeave() {
      tooltip.hide();
    }
    function handleTransitionEnd() {
      if (!$tooltipEl.hasClass('tooltip-in')) {
        $tooltipEl.removeClass('tooltip-out').remove();
      }
    }

    tooltip.attachEvents = function attachEvents() {
      if (Support.touch) {
        const passive = Support.passiveListener ? { passive: true } : false;
        $el.on(app.touchEvents.start, handleTouchStart, passive);
        app.on('touchmove', handleTouchMove);
        app.on('touchend:passive', handleTouchEnd);
        return;
      }
      $tooltipEl.on('transitionend webkitTransitionEnd', handleTransitionEnd);
      $el.on('mouseenter', handleMouseEnter);
      $el.on('mouseleave', handleMouseLeave);
    };
    tooltip.detachEvents = function detachEvents() {
      if (Support.touch) {
        const passive = Support.passiveListener ? { passive: true } : false;
        $el.off(app.touchEvents.start, handleTouchStart, passive);
        app.off('touchmove', handleTouchMove);
        app.off('touchend:passive', handleTouchEnd);
        return;
      }
      $tooltipEl.off('transitionend webkitTransitionEnd', handleTransitionEnd);
      $el.off('mouseenter', handleMouseEnter);
      $el.off('mouseleave', handleMouseLeave);
    };

    // Install Modules
    tooltip.useModules();

    tooltip.init();

    return tooltip;
  }
  position(targetEl) {
    const tooltip = this;
    const { $tooltipEl, app } = tooltip;
    $tooltipEl.css({ left: '', top: '' });
    const $targetEl = $(targetEl || tooltip.el);
    const [width, height] = [$tooltipEl.width(), $tooltipEl.height()];

    $tooltipEl.css({ left: '', top: '' });

    let targetWidth;
    let targetHeight;
    let targetOffsetLeft;
    let targetOffsetTop;
    if ($targetEl && $targetEl.length > 0) {
      targetWidth = $targetEl.outerWidth();
      targetHeight = $targetEl.outerHeight();

      const targetOffset = $targetEl.offset();
      targetOffsetLeft = targetOffset.left - app.left;
      targetOffsetTop = targetOffset.top - app.top;

      const targetParentPage = $targetEl.parents('.page');
      if (targetParentPage.length > 0) {
        targetOffsetTop -= targetParentPage[0].scrollTop;
      }
    }
    let [left, top] = [0, 0, 0];

    // Top Position
    let position = 'top';

    if (height < targetOffsetTop) {
      // On top
      top = targetOffsetTop - height;
    } else if (height < app.height - targetOffsetTop - targetHeight) {
      // On bottom
      position = 'bottom';
      top = targetOffsetTop + targetHeight;
    } else {
      // On middle
      position = 'middle';
      top = ((targetHeight / 2) + targetOffsetTop) - (height / 2);
      if (top <= 0) {
        top = 8;
      } else if (top + height >= app.height) {
        top = app.height - height - 8;
      }
    }

    // Horizontal Position
    if (position === 'top' || position === 'bottom') {
      left = ((targetWidth / 2) + targetOffsetLeft) - (width / 2);
      if (left < 8) left = 8;
      if (left + width > app.width) left = app.width - width - 8;
      if (left < 0) left = 0;
    } else if (position === 'middle') {
      left = targetOffsetLeft - width;
      if (left < 8 || (left + width > app.width)) {
        if (left < 8) left = targetOffsetLeft + targetWidth;
        if (left + width > app.width) left = app.width - width - 8;
      }
    }

    // Apply Styles
    $tooltipEl.css({ top: `${top}px`, left: `${left}px` });
  }
  show(aroundEl) {
    const tooltip = this;
    const { app, $tooltipEl, $el } = tooltip;
    app.root.append($tooltipEl);
    tooltip.position(aroundEl);
    const $aroundEl = $(aroundEl);
    tooltip.visible = true;
    tooltip.opened = true;
    $el.trigger('tooltip:show', tooltip);
    $tooltipEl.trigger('tooltip:show', tooltip);
    if ($aroundEl.length && $aroundEl[0] !== $el[0]) {
      $aroundEl.trigger('tooltip:show', tooltip);
    }
    tooltip.emit('local::show tooltipShow', tooltip);
    $tooltipEl.removeClass('tooltip-out').addClass('tooltip-in');
    return tooltip;
  }
  hide() {
    const tooltip = this;
    const { $tooltipEl, $el } = tooltip;
    tooltip.visible = false;
    tooltip.opened = false;
    $el.trigger('tooltip:hide', tooltip);
    $tooltipEl.trigger('tooltip:hide', tooltip);
    tooltip.emit('local::hide tooltipHide', tooltip);
    $tooltipEl.addClass('tooltip-out').removeClass('tooltip-in');
    return tooltip;
  }
  render() {
    const tooltip = this;
    if (tooltip.params.render) return tooltip.params.render.call(tooltip, tooltip);
    const { cssClass, text } = tooltip.params;
    return `

${text || ''}

    `.trim();
  }
  setText(newText) {
    const tooltip = this;
    if (typeof newText === 'undefined') {
      return tooltip;
    }
    tooltip.params.text = newText;
    tooltip.text = newText;
    if (tooltip.$tooltipEl) {
      tooltip.$tooltipEl.children('.tooltip-content').html(newText);
    }
    if (tooltip.opened) {
      tooltip.position();
    }
    return tooltip;
  }
  init() {
    const tooltip = this;
    tooltip.attachEvents();
  }
  destroy() {
    const tooltip = this;
    if (!tooltip.$el || tooltip.destroyed) return;
    tooltip.$el.trigger('tooltip:beforedestroy', tooltip);
    tooltip.emit('local::beforeDestroy tooltipBeforeDestroy', tooltip);
    tooltip.$tooltipEl.remove();
    delete tooltip.$el[0].f7Tooltip;
    tooltip.detachEvents();
    Utils.deleteProps(tooltip);
    tooltip.destroyed = true;
  }
}
export default Tooltip;
<|endoftext|>


<commit_before>//=============================================================================
// SkipPartyCommand.js
//=============================================================================

/*:
 * @plugindesc This plugin skip a Party Command Window after a battle start.
 * @author masami
 *
 * @help This plugin does not provide plugin commands.
 */

/*:ja
 * @plugindesc 戦闘開始時、PartyCommandを飛ばします。(「戦う」を選んだ時と同じ扱いです。)
 * @author masami
 *
 * @help このプラグインには、プラグインコマンドはありません。
 */

(function() {
    Scene_Battle.prototype.startPartyCommandSelection = function() {
        this.refreshStatus();
        this._statusWindow.deselect();
        this._statusWindow.open();
        this._actorCommandWindow.close();
        this.selectNextCommand();
    };
})();
<commit_msg>fix japanese desc;<commit_after>//=============================================================================
// SkipPartyCommand.js
//=============================================================================

/*:
 * @plugindesc This plugin skip a Party Command Window after a battle start.
 * @author masami
 *
 * @help This plugin does not provide plugin commands.
 */

/*:ja
 * @plugindesc 戦闘開始時、PartyCommandを飛ばします。(「戦う」を選んだ時と同じ扱いになります。)
 * @author masami
 *
 * @help このプラグインには、プラグインコマンドはありません。
 */

(function() {
    Scene_Battle.prototype.startPartyCommandSelection = function() {
        this.refreshStatus();
        this._statusWindow.deselect();
        this._statusWindow.open();
        this._actorCommandWindow.close();
        this.selectNextCommand();
    };
})();
<|endoftext|>


<commit_before>var artical=require('../../modules/blog/artical');
var agree=require('../../modules/blog/agree');
var comment=require('../../modules/blog/comment');
var User=require('../../modules/resume/user');
var category=require('../../modules/blog/category')
var notifyFans=require('../../controler/blog/notifyFans')
var async = require('asyncawait/async');
var await = require('asyncawait/await');
module.exports=(async(function(method,req,res){
	var result;
	if(method==="get"){
		User.hasMany(artical);
		artical.belongsTo(User);
		var id=req.query.id
		var detail=await(artical.findOne({
			where:{
				id:id
			},
			include:[User]
		}))
		if(detail){
			//type:为1时表示来自博客前台的请求，执行增加阅读量的逻辑操作
			var type=req.query.type;
			var reading=detail.dataValues.reading+1;
			if(req.session.artical){
				//将文章id存入session中，没有读过该文章则给reading字段加1
				if(!req.session.artical[id]){
					req.session.artical[id]=id;
					artical.update({
						reading:reading
					},{
						where:{
							id:id
						}
					})
				}else{
					//已读则不做操作
				}
			}else{
				req.session.artical=[];
					artical.update({
						reading:reading
					},{
						where:{
							id:id
						}
					})
					req.session.artical[id]=id;
			}
			var categoryData=await(category.findOne({
				where:{
					id:detail.dataValues.categoryId
				}
			}))
			var agreeData=await(agree.findAndCountAll({
				where:{
					articalId:id
				}
			}))
			result={
				status:0,
				msg:'查询成功',
				data:{
					agree:agreeData.count,
					reading:detail.dataValues.reading,
					categoryId:detail.dataValues.categoryId,
					articalName:detail.dataValues.articalName,
					articalContent:detail.dataValues.articalContent,
					createAt:detail.dataValues.createAt,
					updateAt:detail.dataValues.updateAt,
					author:detail.dataValues.user.dataValues.userName,
					menuId:categoryData.dataValues.menuId,
					categoryName:categoryData.dataValues.categoryName
				}
			}
		}
		else{
			result={status:1,msg:"没有查到这篇文章"}
		}

	}
	else if(method==='post'){
		var uid=req.session.uid;
		var id=req.body.id;
		var categoryId=req.body.categoryId;
		var articalName=req.body.articalName;
		var articalContent=req.body.articalContent;
		var notifyFans=req.body.notifyFans
		if(id){
			var nowStamp=new Date().valueOf();
			var item=await(artical.update({
				categoryId:categoryId,
				articalName:articalName,
				articalContent:articalContent,
				updateAt:nowStamp
			},{
				where:{
					userId:uid,
					id:id
				}
			}))
			if(item){
				result={
					status:0,
					msg:"修改文章成功",
					data:{
						updateAt:nowStamp
					}
				}
			}
			if(notifyFans){
				notifyFans(2,uid,articalName)
			}

		}else{
			var nowStamp=new Date().valueOf();
			var item=await(artical.create({
				userId:uid,
				categoryId:categoryId,
				articalName:articalName,
				articalContent:articalContent,
				reading:0,
				createAt:nowStamp,
				updateAt:nowStamp
			}))
			if(item){
				result={
					status:0,
					msg:"新增文章成功",
					data:{
						createAt:nowStamp,
						updateAt:nowStamp,
						id:item.dataValues.id
					}
				}
			}
			if(notifyFans(2,uid,articalName)){
				notifyFans(1,uid,articalName)
			}

		}
	}
	else if(method==='delete'){
		var id=req.query.id
		var uid=req.session.uid;
		//删除该用户下的articalId文章
		var deleteArt=await(artical.destroy({
			where:{
				id:id,
				userId:uid
			}
		}))
		if(deleteArt){
			result={
				status:0,
				msg:"删除成功"
			}
		}else{
			result={
				status:-1,
				msg:"删除失败"
			}
		}
	}else{
		result={status:1,msg:"系统异常"}
	}
	res.writeHead(200,{'Content-Type':'text/json;charset=utf-8'})
	res.end(JSON.stringify(result))
}))<commit_msg>isNotify Signed-off-by: wade <wade@hyx.com><commit_after>var artical=require('../../modules/blog/artical');
var agree=require('../../modules/blog/agree');
var comment=require('../../modules/blog/comment');
var User=require('../../modules/resume/user');
var category=require('../../modules/blog/category')
var notifyFans=require('../../controler/blog/notifyFans')
var async = require('asyncawait/async');
var await = require('asyncawait/await');
module.exports=(async(function(method,req,res){
	var result;
	if(method==="get"){
		User.hasMany(artical);
		artical.belongsTo(User);
		var id=req.query.id
		var detail=await(artical.findOne({
			where:{
				id:id
			},
			include:[User]
		}))
		if(detail){
			//type:为1时表示来自博客前台的请求，执行增加阅读量的逻辑操作
			var type=req.query.type;
			var reading=detail.dataValues.reading+1;
			if(req.session.artical){
				//将文章id存入session中，没有读过该文章则给reading字段加1
				if(!req.session.artical[id]){
					req.session.artical[id]=id;
					artical.update({
						reading:reading
					},{
						where:{
							id:id
						}
					})
				}else{
					//已读则不做操作
				}
			}else{
				req.session.artical=[];
					artical.update({
						reading:reading
					},{
						where:{
							id:id
						}
					})
					req.session.artical[id]=id;
			}
			var categoryData=await(category.findOne({
				where:{
					id:detail.dataValues.categoryId
				}
			}))
			var agreeData=await(agree.findAndCountAll({
				where:{
					articalId:id
				}
			}))
			result={
				status:0,
				msg:'查询成功',
				data:{
					agree:agreeData.count,
					reading:detail.dataValues.reading,
					categoryId:detail.dataValues.categoryId,
					articalName:detail.dataValues.articalName,
					articalContent:detail.dataValues.articalContent,
					createAt:detail.dataValues.createAt,
					updateAt:detail.dataValues.updateAt,
					author:detail.dataValues.user.dataValues.userName,
					menuId:categoryData.dataValues.menuId,
					categoryName:categoryData.dataValues.categoryName
				}
			}
		}
		else{
			result={status:1,msg:"没有查到这篇文章"}
		}

	}
	else if(method==='post'){
		var uid=req.session.uid;
		var id=req.body.id;
		var categoryId=req.body.categoryId;
		var articalName=req.body.articalName;
		var articalContent=req.body.articalContent;
		var isNotifyFans=req.body.notifyFans
		if(id){
			var nowStamp=new Date().valueOf();
			var item=await(artical.update({
				categoryId:categoryId,
				articalName:articalName,
				articalContent:articalContent,
				updateAt:nowStamp
			},{
				where:{
					userId:uid,
					id:id
				}
			}))
			if(item){
				result={
					status:0,
					msg:"修改文章成功",
					data:{
						updateAt:nowStamp
					}
				}
			}
			if(isNotifyFans){
				notifyFans(2,uid,articalName)
			}

		}else{
			var nowStamp=new Date().valueOf();
			var item=await(artical.create({
				userId:uid,
				categoryId:categoryId,
				articalName:articalName,
				articalContent:articalContent,
				reading:0,
				createAt:nowStamp,
				updateAt:nowStamp
			}))
			if(item){
				result={
					status:0,
					msg:"新增文章成功",
					data:{
						createAt:nowStamp,
						updateAt:nowStamp,
						id:item.dataValues.id
					}
				}
			}
			if(isNotifyFans){
				notifyFans(1,uid,articalName)
			}

		}
	}
	else if(method==='delete'){
		var id=req.query.id
		var uid=req.session.uid;
		//删除该用户下的articalId文章
		var deleteArt=await(artical.destroy({
			where:{
				id:id,
				userId:uid
			}
		}))
		if(deleteArt){
			result={
				status:0,
				msg:"删除成功"
			}
		}else{
			result={
				status:-1,
				msg:"删除失败"
			}
		}
	}else{
		result={status:1,msg:"系统异常"}
	}
	res.writeHead(200,{'Content-Type':'text/json;charset=utf-8'})
	res.end(JSON.stringify(result))
}))<|endoftext|>