first version

pull/8356/head
adiguba 4 years ago
parent 70ec60c840
commit 0ddf94b934

@ -305,5 +305,9 @@ export default {
directive_conflict: (directive1, directive2) => ({
code: 'directive-conflict',
message: `Cannot use ${directive1} and ${directive2} on the same element`
})
}),
too_much_forward_event_modifiers: {
code: 'too-much-forward-event-modifiers',
message: 'Forward-event only accept one modifier (the forward alias)'
},
};

@ -198,5 +198,9 @@ export default {
invalid_rest_eachblock_binding: (rest_element_name: string) => ({
code: 'invalid-rest-eachblock-binding',
message: `...${rest_element_name} operator will create a new object and binding propogation with original object will not work`
})
}),
incorrect_forward_event_modifier: (modifiers: Set<string>) => ({
code: 'incorrect-forward-event-modifier',
message: `Forward-event only accept one modifier for the forward alias. Event modifiers should not be used here : ${modifiers}`
}),
};

@ -13,7 +13,6 @@ import { namespaces } from '../../utils/namespaces';
import map_children from './shared/map_children';
import { regex_dimensions, regex_starts_with_newline, regex_non_whitespace_character } from '../../utils/patterns';
import fuzzymatch from '../../utils/fuzzymatch';
import list from '../../utils/list';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
@ -118,24 +117,6 @@ const a11y_implicit_semantics = new Map([
const invisible_elements = new Set(['meta', 'html', 'script', 'style']);
const valid_modifiers = new Set([
'preventDefault',
'stopPropagation',
'capture',
'once',
'passive',
'nonpassive',
'self',
'trusted'
]);
const passive_events = new Set([
'wheel',
'touchstart',
'touchmove',
'touchend',
'touchcancel'
]);
const react_attributes = new Map([
['className', 'class'],
@ -943,44 +924,7 @@ export default class Element extends Node {
}
validate_event_handlers() {
const { component } = this;
this.handlers.forEach(handler => {
if (handler.modifiers.has('passive') && handler.modifiers.has('preventDefault')) {
return component.error(handler, compiler_errors.invalid_event_modifier_combination('passive', 'preventDefault'));
}
if (handler.modifiers.has('passive') && handler.modifiers.has('nonpassive')) {
return component.error(handler, compiler_errors.invalid_event_modifier_combination('passive', 'nonpassive'));
}
handler.modifiers.forEach(modifier => {
if (!valid_modifiers.has(modifier)) {
return component.error(handler, compiler_errors.invalid_event_modifier(list(Array.from(valid_modifiers))));
}
if (modifier === 'passive') {
if (passive_events.has(handler.name)) {
if (handler.can_make_passive) {
component.warn(handler, compiler_warnings.redundant_event_modifier_for_touch);
}
} else {
component.warn(handler, compiler_warnings.redundant_event_modifier_passive);
}
}
if (component.compile_options.legacy && (modifier === 'once' || modifier === 'passive')) {
// TODO this could be supported, but it would need a few changes to
// how event listeners work
return component.error(handler, compiler_errors.invalid_event_modifier_legacy(modifier));
}
});
if (passive_events.has(handler.name) && handler.can_make_passive && !handler.modifiers.has('preventDefault') && !handler.modifiers.has('nonpassive')) {
// touch/wheel events should be passive by default
handler.modifiers.add('passive');
}
});
this.handlers.forEach(h => h.validate());
}
validate_display_directive() {

@ -1,19 +1,41 @@
import Node from './shared/Node';
import Expression from './shared/Expression';
import Component from '../Component';
import { sanitize } from '../../utils/names';
import { Identifier } from 'estree';
import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces';
import compiler_errors from '../compiler_errors';
import compiler_warnings from '../compiler_warnings';
import list from '../../utils/list';
const regex_contains_term_function_expression = /FunctionExpression/;
const valid_modifiers = new Set([
'preventDefault',
'stopPropagation',
'capture',
'once',
'passive',
'nonpassive',
'self',
'trusted'
]);
const passive_events = new Set([
'wheel',
'touchstart',
'touchmove',
'touchend',
'touchcancel'
]);
export default class EventHandler extends Node {
type: 'EventHandler';
name: string;
modifiers: Set<string>;
expression: Expression;
handler_name: Identifier;
aliasName?: string;
aliasCount: 0;
uses_context = false;
can_make_passive = false;
@ -47,7 +69,56 @@ export default class EventHandler extends Node {
}
}
} else {
this.handler_name = component.get_unique_name(`${sanitize(this.name)}_handler`);
if (info.modifiers && info.modifiers.length) {
this.aliasCount = info.modifiers.length;
this.aliasName = info.modifiers[0];
}
}
}
validate() {
if (this.expression) {
if (this.modifiers.has('passive') && this.modifiers.has('preventDefault')) {
return this.component.error(this, compiler_errors.invalid_event_modifier_combination('passive', 'preventDefault'));
}
if (this.modifiers.has('passive') && this.modifiers.has('nonpassive')) {
return this.component.error(this, compiler_errors.invalid_event_modifier_combination('passive', 'nonpassive'));
}
this.modifiers.forEach(modifier => {
if (!valid_modifiers.has(modifier)) {
return this.component.error(this, compiler_errors.invalid_event_modifier(list(Array.from(valid_modifiers))));
}
if (modifier === 'passive') {
if (passive_events.has(this.name)) {
if (this.can_make_passive) {
this.component.warn(this, compiler_warnings.redundant_event_modifier_for_touch);
}
} else {
this.component.warn(this, compiler_warnings.redundant_event_modifier_passive);
}
}
if (this.component.compile_options.legacy && (modifier === 'once' || modifier === 'passive')) {
// TODO this could be supported, but it would need a few changes to
// how event listeners work
return this.component.error(this, compiler_errors.invalid_event_modifier_legacy(modifier));
}
});
if (passive_events.has(this.name) && this.can_make_passive && !this.modifiers.has('preventDefault') && !this.modifiers.has('nonpassive')) {
// touch/wheel events should be passive by default
this.modifiers.add('passive');
}
} else {
if (this.aliasCount > 1) {
return this.component.error(this, compiler_errors.too_much_forward_event_modifiers);
}
if (this.aliasName && valid_modifiers.has(this.aliasName)) {
this.component.warn(this, compiler_warnings.incorrect_forward_event_modifier(valid_modifiers));
}
}
}

@ -101,13 +101,7 @@ export default class InlineComponent extends Node {
this.scope = scope;
}
this.handlers.forEach(handler => {
handler.modifiers.forEach(modifier => {
if (modifier !== 'once') {
return component.error(handler, compiler_errors.invalid_event_modifier_component);
}
});
});
this.handlers.forEach(h => h.validate())
const children = [];
for (let i = info.children.length - 1; i >= 0; i--) {

@ -1,8 +1,9 @@
import EventHandler from '../../../nodes/EventHandler';
import Wrapper from '../shared/Wrapper';
import Block from '../../Block';
import { b, x, p } from 'code-red';
import { x, p, b } from 'code-red';
import { Expression } from 'estree';
import { sanitize } from '../../../../utils/names';
const TRUE = x`true`;
const FALSE = x`false`;
@ -15,34 +16,34 @@ export default class EventHandlerWrapper {
this.node = node;
this.parent = parent;
if (!node.expression) {
this.parent.renderer.add_to_context(node.handler_name.name);
this.parent.renderer.component.partly_hoisted.push(b`
function ${node.handler_name.name}(event) {
@bubble.call(this, $$self, event);
}
`);
}
}
get_snippet(block: Block) {
const snippet = this.node.expression ? this.node.expression.manipulate(block) : block.renderer.reference(this.node.handler_name);
return this.node.expression.manipulate(block);
}
if (this.node.reassigned) {
block.maintain_context = true;
return x`function () { if (@is_function(${snippet})) ${snippet}.apply(this, arguments); }`;
render(block: Block, target: string | Expression, is_comp: boolean = false) {
const listen = is_comp ? '@listen_comp' : '@listen';
if (!this.node.expression) {
const self = this.parent.renderer.add_to_context('$$self');
const selfvar = block.renderer.reference(self.name);
const aliasName = this.node.aliasName ? `"${this.node.aliasName}"` : null;
block.event_listeners.push(x`@bubble(${selfvar}, ${listen}, ${target}, "${this.node.name}", ${aliasName})`);
return;
}
return snippet;
}
render(block: Block, target: string | Expression) {
let snippet = this.get_snippet(block);
const snippet = this.get_snippet(block);
if (this.node.modifiers.has('preventDefault')) snippet = x`@prevent_default(${snippet})`;
if (this.node.modifiers.has('stopPropagation')) snippet = x`@stop_propagation(${snippet})`;
if (this.node.modifiers.has('self')) snippet = x`@self(${snippet})`;
if (this.node.modifiers.has('trusted')) snippet = x`@trusted(${snippet})`;
let wrappers = [];
if (this.node.modifiers.has('trusted')) wrappers.push(x`@trusted`);
if (this.node.modifiers.has('self')) wrappers.push(x`@self`);
if (this.node.modifiers.has('stopPropagation')) wrappers.push(x`@stop_propagation`);
if (this.node.modifiers.has('preventDefault')) wrappers.push(x`@prevent_default`);
// TODO : once() on component ????
const args = [];
@ -57,17 +58,30 @@ export default class EventHandlerWrapper {
: p`${opt}: true`
) } }`);
}
} else if (block.renderer.options.dev) {
} else if (wrappers.length) {
args.push(FALSE);
}
if (block.renderer.options.dev) {
args.push(this.node.modifiers.has('preventDefault') ? TRUE : FALSE);
args.push(this.node.modifiers.has('stopPropagation') ? TRUE : FALSE);
if (wrappers.length) {
args.push(x`[${wrappers}]`);
}
block.event_listeners.push(
x`@listen(${target}, "${this.node.name}", ${snippet}, ${args})`
);
if (this.node.reassigned) {
const handle = this.node.component.get_unique_name(`${sanitize(this.node.name)}_handle`);
block.add_variable(handle);
const condition = block.renderer.dirty(this.node.expression.dynamic_dependencies());
block.chunks.update.push(b`
if (${condition}) {
${handle}.swap(${snippet})
}`);
block.event_listeners.push(
x`${handle} = @listen_swap(${snippet}, (h)=> ${listen}(${target}, "${this.node.name}", h, ${args}))`
);
} else {
block.event_listeners.push(
x`${listen}(${target}, "${this.node.name}", ${snippet}, ${args})`
);
}
}
}

@ -396,13 +396,13 @@ export default class InlineComponentWrapper extends Wrapper {
return b`@binding_callbacks.push(() => @bind(${this.var}, '${binding.name}', ${id}));`;
});
const munged_handlers = this.node.handlers.map(handler => {
const event_handler = new EventHandler(handler, this);
let snippet = event_handler.get_snippet(block);
if (handler.modifiers.has('once')) snippet = x`@once(${snippet})`;
return b`${name}.$on("${handler.name}", ${snippet});`;
});
if (this.node.handlers.length > 0) {
const target = x`${name}`;
for (const handler of this.node.handlers) {
new EventHandler(handler, this)
.render(block, target, true);
}
}
const mount_target = has_css_custom_properties ? css_custom_properties_wrapper : (parent_node || '#target');
const mount_anchor = has_css_custom_properties ? 'null' : (parent_node ? 'null' : '#anchor');
@ -433,7 +433,6 @@ export default class InlineComponentWrapper extends Wrapper {
${name} = @construct_svelte_component(${switch_value}, ${switch_props}(#ctx));
${munged_bindings}
${munged_handlers}
}
`);
@ -481,7 +480,6 @@ export default class InlineComponentWrapper extends Wrapper {
${name} = @construct_svelte_component(${switch_value}, ${switch_props}(#ctx));
${munged_bindings}
${munged_handlers}
@create_component(${name}.$$.fragment);
@transition_in(${name}.$$.fragment, 1);
@ -515,7 +513,6 @@ export default class InlineComponentWrapper extends Wrapper {
${name} = new ${expression}(${component_opts});
${munged_bindings}
${munged_handlers}
`);
if (has_css_custom_properties) {

@ -11,6 +11,7 @@ export {
hasContext,
tick,
createEventDispatcher,
onEventListener,
SvelteComponentDev as SvelteComponent,
SvelteComponentTyped
// additional exports added through generate-type-definitions.js

@ -1,9 +1,9 @@
import { add_render_callback, flush, schedule_update, dirty_components } from './scheduler';
import { current_component, set_current_component } from './lifecycle';
import { current_component, set_current_component, start_callback, stop_callback } from './lifecycle';
import { blank_object, is_empty, is_function, run, run_all, noop } from './utils';
import { children, detach, start_hydrating, end_hydrating } from './dom';
import { transition_in } from './transitions';
import { T$$ } from './types';
import { Callback, T$$ } from './types';
export function bind(component, name, callback) {
const index = component.$$.props[name];
@ -95,6 +95,7 @@ export function init(component, options, instance, create_fragment, not_equal, p
// everything else
callbacks: blank_object(),
bubbles: blank_object(),
dirty,
skip_bound: false,
root: options.target || parent_component.$$.root
@ -177,17 +178,22 @@ if (typeof HTMLElement === 'function') {
this.$destroy = noop;
}
$on(type, callback) {
// TODO should this delegate to addEventListener?
$on(type: string, callback: EventListener, options?: boolean | AddEventListenerOptions | EventListenerOptions) {
if (!is_function(callback)) {
return noop;
}
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
callbacks.push(callback);
const c: Callback = {f:callback, o:options};
callbacks.push(c);
start_callback(this as any, type, c);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1) callbacks.splice(index, 1);
const index = callbacks.indexOf(c);
if (index !== -1) {
callbacks.splice(index, 1);
stop_callback(this as any, type, c);
}
};
}
@ -213,16 +219,20 @@ export class SvelteComponent {
this.$destroy = noop;
}
$on(type, callback) {
$on(type: string, callback: EventListener, options?: boolean | AddEventListenerOptions | EventListenerOptions) {
if (!is_function(callback)) {
return noop;
}
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
callbacks.push(callback);
const c: Callback = {f:callback, o:options};
callbacks.push(c);
start_callback(this as any, type, c);
return () => {
const index = callbacks.indexOf(callback);
const index = callbacks.indexOf(c);
if (index !== -1) callbacks.splice(index, 1);
stop_callback(this as any, type, c);
};
}

@ -1,6 +1,7 @@
import { custom_event, append, append_hydration, insert, insert_hydration, detach, listen, attr } from './dom';
import { custom_event, append, append_hydration, insert, insert_hydration, detach, listen, attr, prevent_default, stop_propagation, trusted, self } from './dom';
import { SvelteComponent } from './Component';
import { is_void } from '../../shared/utils/names';
import { bubble, listen_comp } from './lifecycle';
export function dispatch_dev<T=any>(type: string, detail?: T) {
document.dispatchEvent(custom_event(type, { version: '__VERSION__', ...detail }, { bubbles: true }));
@ -49,20 +50,49 @@ export function detach_after_dev(before: Node) {
}
}
export function listen_dev(node: Node, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions, has_prevent_default?: boolean, has_stop_propagation?: boolean) {
function build_modifiers(options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) {
const modifiers = options === true ? [ 'capture' ] : options ? Array.from(Object.keys(options)) : [];
if (has_prevent_default) modifiers.push('preventDefault');
if (has_stop_propagation) modifiers.push('stopPropagation');
if (wrappers) {
if (wrappers.indexOf(prevent_default) >= 0) modifiers.push('preventDefault');
if (wrappers.indexOf(stop_propagation) >= 0) modifiers.push('stopPropagation');
// ???
if (wrappers.indexOf(trusted) >= 0) modifiers.push('trusted');
if (wrappers.indexOf(self) >= 0) modifiers.push('self');
}
return modifiers;
}
export function listen_dev(node: Node, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) {
const modifiers = build_modifiers(options, wrappers);
dispatch_dev('SvelteDOMAddEventListener', { node, event, handler, modifiers });
const dispose = listen(node, event, handler, options);
const dispose = listen(node, event, handler, options, wrappers);
return () => {
dispatch_dev('SvelteDOMRemoveEventListener', { node, event, handler, modifiers });
dispose();
};
}
export function bubble_dev(component: SvelteComponent, listen_func: Function, node: EventTarget|SvelteComponent, type: string, typeName: string = type): Function {
dispatch_dev('SvelteComponentAddEventBubble', { component, listen_func, node, type, typeName });
const dispose = bubble(component, listen_func, node, type, typeName);
return () => {
dispatch_dev('SvelteComponentRemoveEventBubble', { component, listen_func, node, type, typeName });
dispose();
};
}
export function listen_comp_dev(comp: SvelteComponent, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) {
const modifiers = build_modifiers(options, wrappers);
dispatch_dev('SvelteComponentAddEventListener', { comp, event, handler, modifiers });
const dispose = listen_comp(comp, event, handler, options, wrappers);
return () => {
dispatch_dev('SvelteComponentRemoveEventListener', { comp, event, handler, modifiers });
dispose();
};
}
export function attr_dev(node: Element, attribute: string, value?: string) {
attr(node, attribute, value);
@ -144,7 +174,7 @@ export function construct_svelte_component_dev(component, props) {
type Props = Record<string, any>;
export interface SvelteComponentDev {
$set(props?: Props): void;
$on(event: string, callback: ((event: any) => void) | null | undefined): () => void;
$on(event: string, callback: ((event: any) => void) | null | undefined, options?: boolean | AddEventListenerOptions | EventListenerOptions): () => void;
$destroy(): void;
[accessor: string]: any;
}

@ -1,4 +1,4 @@
import { has_prop } from './utils';
import { has_prop, is_function, noop } from './utils';
// Track which nodes are claimed during hydration. Unclaimed nodes can then be removed from the DOM
// at the end of hydration without touching the remaining nodes.
@ -252,9 +252,38 @@ export function empty() {
return text('');
}
export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions) {
node.addEventListener(event, handler, options);
return () => node.removeEventListener(event, handler, options);
export function wrap_handler(handler: EventListenerOrEventListenerObject, wrappers?: Function[]): EventListener {
let result = is_function(handler) ? handler : handler.handleEvent.bind(handler);
if (wrappers) {
for (const fn of wrappers) {
result = fn(result);
}
}
return result;
}
export function listen(node: EventTarget, event: string, handler: EventListenerOrEventListenerObject|null|undefined, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) {
if (handler) {
handler = wrap_handler(handler, wrappers);
node.addEventListener(event, handler, options);
return () => node.removeEventListener(event, handler, options);
}
return noop;
}
export function listen_swap(handler: EventListenerOrEventListenerObject|null|undefined, factory: (handler:EventListenerOrEventListenerObject) => Function) {
let disposeHandle: Function = factory(handler);
const dispose = () => {
disposeHandle();
}
dispose.swap = (new_handler:EventListenerOrEventListenerObject) => {
if (new_handler !== handler) {
disposeHandle();
handler = new_handler;
disposeHandle = factory(handler);
}
}
return dispose;
}
export function prevent_default(fn) {

@ -1,4 +1,7 @@
import { custom_event } from './dom';
import { SvelteComponent } from './Component';
import { custom_event, wrap_handler } from './dom';
import { Bubble, Callback, CallbackFactory } from './types';
import { noop } from './utils';
export let current_component;
@ -82,14 +85,14 @@ export function createEventDispatcher<EventMap extends {} = any>(): <
const component = get_current_component();
return (type: string, detail?: any, { cancelable = false } = {}): boolean => {
const callbacks = component.$$.callbacks[type];
const callbacks: Callback[] = component.$$.callbacks[type];
if (callbacks) {
// TODO are there situations where events could be dispatched
// in a server (non-DOM) environment?
const event = custom_event(type, detail, { cancelable });
callbacks.slice().forEach(fn => {
fn.call(component, event);
callbacks.slice().forEach(callback => {
callback.f.call(component, event);
});
return !event.defaultPrevented;
}
@ -143,14 +146,93 @@ export function hasContext(key): boolean {
return get_current_component().$$.context.has(key);
}
// TODO figure out if we still want to support
// shorthand events, or if we want to implement
// a real bubbling mechanism
export function bubble(component, event) {
const callbacks = component.$$.callbacks[event.type];
function start_bubble(type: string, bubble: Bubble, callback: Callback) {
const dispose = bubble.f(type, callback.f, callback.o);
if (dispose) {
bubble.r.set(callback, dispose);
}
}
function start_bubbles(comp : SvelteComponent, bubble: Bubble) {
for (const type of Object.keys(comp.$$.callbacks)) {
comp.$$.callbacks[type].forEach( callback => { start_bubble(type, bubble, callback); })
}
}
export function start_callback(comp : SvelteComponent, type: string, callback: Callback) {
for (const bubbles of [ comp.$$.bubbles[type], comp.$$.bubbles['*'] ]) {
if (bubbles) {
for(const bubble of bubbles) {
start_bubble(type, bubble, callback);
}
}
}
}
export function stop_callback(comp : SvelteComponent, type: string, callback: Callback) {
for (const bubbles of [ comp.$$.bubbles[type], comp.$$.bubbles['*'] ]) {
if (bubbles) {
for (const bubble of bubbles) {
const dispose = bubble.r.get(callback);
if (dispose) {
dispose();
bubble.r.delete(callback);
}
}
}
}
}
function add_bubble(comp: SvelteComponent, type: string, f: CallbackFactory): Function {
const bubble : Bubble = {f, r: new Map()};
const bubbles = (comp.$$.bubbles[type] || (comp.$$.bubbles[type] = []));
bubbles.push(bubble);
start_bubbles(comp, bubble);
return () => {
const index = bubbles.indexOf(bubble);
if (index !== -1) bubbles.splice(index, 1);
for (const dispose of bubble.r.values()) {
dispose();
}
}
}
export function onEventListener(type: string, fn: CallbackFactory) {
add_bubble(get_current_component(), type, fn);
}
export function bubble(component: SvelteComponent, listen_func: Function, node: EventTarget|SvelteComponent, type: string, typeName: string = type): Function {
return add_bubble(component, type, (eventType, callback, options) => {
let typeToListen: string;
if (type === '*') {
if (typeName === '*') {
typeToListen = eventType;
} else if (typeName.startsWith('*')) {
const len = typeName.length;
if (eventType.endsWith(typeName.substring(1))) {
typeToListen = eventType.substring(0, eventType.length - (len-1));
}
} else if (typeName.endsWith('*')) {
const len = typeName.length;
if (eventType.startsWith(typeName.substring(0,len-1))) {
typeToListen = eventType.substring(len-1);
}
}
} else if (eventType === typeName) {
typeToListen = type;
}
if (typeToListen) {
return listen_func(node, typeToListen, callback, options);
}
});
}
if (callbacks) {
// @ts-ignore
callbacks.slice().forEach(fn => fn.call(this, event));
export function listen_comp(comp: SvelteComponent, event: string, handler: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | EventListenerOptions, wrappers?: Function[]) {
if (handler) {
return comp.$on(event, wrap_handler(handler, wrappers), options);
}
return noop;
}

@ -19,12 +19,27 @@ export interface Fragment {
export type FragmentFactory = (ctx: any) => Fragment;
export interface Callback {
f: EventListener;
o?: boolean | AddEventListenerOptions | EventListenerOptions;
}
export type CallbackFactory = (type: string, callback: EventListener, options: boolean | AddEventListenerOptions | EventListenerOptions | undefined) => Function|void;
export interface Bubble {
f: CallbackFactory;
r: Map<Callback,Function>;
}
export interface T$$ {
dirty: number[];
ctx: any[];
bound: any;
update: () => void;
callbacks: any;
callbacks: Record<string, Callback[]>;
bubbles: Record<string, Bubble[]>;
after_update: any[];
props: Record<string, 0 | string>;
fragment: null | false | Fragment;

Loading…
Cancel
Save