keep keep replace replace keep keep keep

: As extends keyof JSX.IntrinsicElements

import * as React from 'react';

export type Primitive = string | boolean | number | symbol | null | undefined;

export type FieldValues = Record<string, any>;

export type FieldName<FormValues extends FieldValues> =
  | (keyof FormValues & string)
  | string;

export type FieldValue<FormValues extends FieldValues> = FormValues[FieldName<
  FormValues
>];

export type Ref =
  | HTMLInputElement
  | HTMLSelectElement
  | HTMLTextAreaElement
  | any;

export type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends Array<infer U>
    ? Array<DeepPartial<U>>
    : T[P] extends ReadonlyArray<infer U>
    ? ReadonlyArray<DeepPartial<U>>
    : DeepPartial<T[P]>;
};

export type ValidationMode = {
  onBlur: 'onBlur';
  onChange: 'onChange';
  onSubmit: 'onSubmit';
};

export type Mode = keyof ValidationMode;

export type OnSubmit<FormValues extends FieldValues> = (
  data: FormValues,
  event?: React.BaseSyntheticEvent,
) => void | Promise<void>;

export type SchemaValidateOptions = Partial<{
  strict: boolean;
  abortEarly: boolean;
  stripUnknown: boolean;
  recursive: boolean;
  context: object;
}>;

export type UseFormOptions<
  FormValues extends FieldValues = FieldValues
> = Partial<{
  mode: Mode;
  reValidateMode: Mode;
  defaultValues: DeepPartial<FormValues>;
  validationSchema: any;
  submitFocusError: boolean;
  validateCriteriaMode: 'firstError' | 'all';
}>;

export type MutationWatcher = {
  disconnect: VoidFunction;
  observe?: any;
};

type ValidationOptionObject<Value> = Value | { value: Value; message: string };

export type ValidationValue = boolean | number | string | RegExp;

export type ValidateResult = string | boolean | undefined;

export type Validate = (data: any) => ValidateResult | Promise<ValidateResult>;

export type ValidationOptions = Partial<{
  required: boolean | string | ValidationOptionObject<boolean>;
  min: ValidationOptionObject<number | string>;
  max: ValidationOptionObject<number | string>;
  maxLength: ValidationOptionObject<number | string>;
  minLength: ValidationOptionObject<number | string>;
  pattern: ValidationOptionObject<RegExp>;
  validate: Validate | Record<string, Validate>;
}>;

export type MultipleFieldErrors = Record<string, ValidateResult>;

export type FieldError = {
  type: string;
  ref?: Ref;
  types?: MultipleFieldErrors;
  message?: string;
  isManual?: boolean;
};

export type ManualFieldError<FormValues> = {
  name: FieldName<FormValues>;
  type: string;
  types?: MultipleFieldErrors;
  message?: string;
};

export type Field = {
  ref: Ref;
  mutationWatcher?: MutationWatcher;
  options?: {
    ref: Ref;
    mutationWatcher?: MutationWatcher;
  }[];
} & ValidationOptions;

export type FieldRefs<FormValues extends FieldValues> = Partial<
  Record<FieldName<FormValues>, Field>
>;

export type NestDataObject<FormValues> = {
  [Key in keyof FormValues]?: FormValues[Key] extends any[]
    ? FormValues[Key][number] extends object
      ? FieldErrors<FormValues[Key][number]>[]
      : FormValues[Key][number] extends string | number
      ? FieldError[]
      : FieldError
    : FormValues[Key] extends Date
    ? FieldError
    : FormValues[Key] extends object
    ? FieldErrors<FormValues[Key]>
    : FieldError;
};

export type FieldErrors<FormValues> = NestDataObject<FormValues>;

export type Touched<FormValues> = NestDataObject<FormValues>;

export type SubmitPromiseResult<FormValues extends FieldValues> = {
  errors: FieldErrors<FormValues>;
  values: FormValues;
};

export type FormStateProxy<FormValues extends FieldValues = FieldValues> = {
  dirty: boolean;
  dirtyFields: Set<FieldName<FormValues>>;
  isSubmitted: boolean;
  submitCount: number;
  touched: Touched<FormValues>;
  isSubmitting: boolean;
  isValid: boolean;
};

export type ReadFormState = { [P in keyof FormStateProxy]: boolean };

export type RadioOrCheckboxOption = {
  ref?: Ref;
  mutationWatcher?: MutationWatcher;
};

export type ElementLike = {
  name: string;
  type?: string;
  value?: string;
  checked?: boolean;
  options?: any;
};

export type HandleChange = ({
  type,
  target,
}: MouseEvent) => Promise<void | boolean>;

export type FormValuesFromErrors<Errors> = Errors extends FieldErrors<
  infer FormValues
>
  ? FormValues
  : never;

export type EventFunction = (args: any) => any;

export type Control<FormValues extends FieldValues = FieldValues> = {
  register<Element extends ElementLike = ElementLike>(): (
    ref: Element | null,
  ) => void;
  register<Element extends ElementLike = ElementLike>(
    validationOptions: ValidationOptions,
  ): (ref: Element | null) => void;
  register<Element extends ElementLike = ElementLike>(
    name: FieldName<FormValues>,
    validationOptions?: ValidationOptions,
  ): void;
  register<Element extends ElementLike = ElementLike>(
    namesWithValidationOptions: Record<
      FieldName<FormValues>,
      ValidationOptions
    >,
  ): void;
  register<Element extends ElementLike = ElementLike>(
    ref: Element,
    validationOptions?: ValidationOptions,
  ): void;
  register<Element extends ElementLike = ElementLike>(
    refOrValidationOptions: ValidationOptions | Element | null,
    validationOptions?: ValidationOptions,
  ): ((ref: Element | null) => void) | void;
  triggerValidation: (
    payload?: FieldName<FormValues> | FieldName<FormValues>[] | string,
    shouldRender?: boolean,
  ) => Promise<boolean>;
  removeFieldEventListener: (field: Field, forceDelete?: boolean) => void;
  unregister(name: FieldName<FormValues>): void;
  unregister(names: FieldName<FormValues>[]): void;
  unregister(names: FieldName<FormValues> | FieldName<FormValues>[]): void;
  getValues: (payload?: { nest: boolean }) => any;
  setValue: <Name extends FieldName<FormValues>>(
    name: Name,
    value: FormValues[Name],
    shouldValidate?: boolean,
  ) => void | Promise<boolean>;
  formState: FormStateProxy<FormValues>;
  mode: {
    isOnBlur: boolean;
    isOnSubmit: boolean;
  };
  reValidateMode: {
    isReValidateOnBlur: boolean;
    isReValidateOnSubmit: boolean;
  };
  touchedFieldsRef: React.MutableRefObject<Touched<FormValues>>;
  watchFieldArrayRef: React.MutableRefObject<any>;
  errorsRef: React.MutableRefObject<FieldErrors<FormValues>>;
  fieldsRef: React.MutableRefObject<FieldRefs<FormValues>>;
  resetFieldArrayFunctionRef: React.MutableRefObject<
    Record<string, (values: any) => void>
  >;
  fieldArrayNamesRef: React.MutableRefObject<Set<string>>;
  isDirtyRef: React.MutableRefObject<boolean>;
  readFormStateRef: React.MutableRefObject<{
    dirty: boolean;
    isSubmitted: boolean;
    submitCount: boolean;
    touched: boolean;
    isSubmitting: boolean;
    isValid: boolean;
  }>;
  defaultValuesRef: React.MutableRefObject<
    DeepPartial<FormValues> | FormValues[FieldName<FormValues>]
  >;
};

export type AsProps<
  As extends
    | undefined
    | React.ReactElement
    | keyof JSX.IntrinsicElements = undefined
> = As extends keyof JSX.IntrinsicElements
  ? JSX.IntrinsicElements[As]
  : As extends React.ReactElement
  ? { [key: string]: any }
  : As extends undefined
  ? {}
  : never;

export type ControllerProps<ControlProp extends Control = Control> = {
  name: string;
  as: React.ReactElement | React.ElementType | string;
  rules?: ValidationOptions;
  onChange?: EventFunction;
  onBlur?: EventFunction;
  mode?: Mode;
  onChangeName?: string;
  onBlurName?: string;
  valueName?: string;
  defaultValue?: any;
  control?: ControlProp;
  [key: string]: any;
};

export type ErrorMessageProps<
  Errors extends FieldErrors<any>,
  Name extends FieldName<FormValuesFromErrors<Errors>>,
  As extends
    | undefined
    | React.ReactElement
    | keyof JSX.IntrinsicElements = undefined
> = {
  as?: As;
  errors?: Errors;
  name: Name;
  message?: string;
  children?: (data: {
    message: string;
    messages: MultipleFieldErrors;
  }) => React.ReactNode;
} & AsProps<As>;

export type UseFieldArrayProps<ControlProp extends Control = Control> = {
  control?: ControlProp;
  name: string;
};

export type WithFieldId<Value> = {
  id?: string;
} & Value;


import isArray from './isArray';

export default (fields: any, indexA: number, indexB: number) =>
  isArray(fields) &&
  ([fields[indexA], fields[indexB]] = [fields[indexB], fields[indexA]]);


context('re-validate mode', () => {
  it('should re-validate the form only onSubmit with mode onSubmit and reValidateMode onSubmit', () => {
    cy.visit('http://localhost:3000/re-validate-mode/onSubmit/onSubmit');

    cy.get('button#submit').click();

    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="lastName"] + p').contains('lastName error');

    cy.get('input[name="firstName"]').type('luo123456');
    cy.get('input[name="lastName"]').type('luo12');

    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="lastName"] + p').contains('lastName error');

    cy.get('button#submit').click();

    cy.get('p').should('have.length', 0);
    cy.get('#renderCount').contains('3');
  });

  it('should re-validate the form only onBlur with mode onSubmit and reValidateMode onBlur', () => {
    cy.visit('http://localhost:3000/re-validate-mode/onSubmit/onBlur');
    cy.get('input[name="firstName"]').focus();
    cy.get('input[name="firstName"]').blur();

    cy.get('input[name="lastName"]').focus();
    cy.get('input[name="lastName"]').blur();
    cy.get('p').should('have.length', 0);

    cy.get('button#submit').click();

    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="lastName"] + p').contains('lastName error');

    cy.get('input[name="firstName"]').type('luo123456');
    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="firstName"]').blur();
    cy.get('input[name="lastName"]').type('luo12');
    cy.get('input[name="lastName"] + p').contains('lastName error');
    cy.get('input[name="lastName"]').blur();

    cy.get('p').should('have.length', 0);
    cy.get('#renderCount').contains('4');
  });

  it('should re-validate the form only onSubmit with mode onBlur and reValidateMode onSubmit', () => {
    cy.visit('http://localhost:3000/re-validate-mode/onBlur/onSubmit');

    cy.get('button#submit').click();

    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="lastName"] + p').contains('lastName error');

    cy.get('input[name="firstName"]').type('luo123456');
    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="firstName"]').blur();
    cy.get('input[name="lastName"]').type('luo12');
    cy.get('input[name="lastName"] + p').contains('lastName error');
    cy.get('input[name="lastName"]').blur();

    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="lastName"] + p').contains('lastName error');

    cy.get('button#submit').click();

    cy.get('p').should('have.length', 0);
    cy.get('#renderCount').contains('3');
  });

  it('should re-validate the form only onSubmit with mode onChange and reValidateMode onSubmit', () => {
    cy.visit('http://localhost:3000/re-validate-mode/onChange/onSubmit');

    cy.get('button#submit').click();

    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="lastName"] + p').contains('lastName error');

    cy.get('input[name="firstName"]').type('luo123456');
    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="lastName"]').type('luo12');
    cy.get('input[name="lastName"] + p').contains('lastName error');

    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="lastName"] + p').contains('lastName error');

    cy.get('button#submit').click();

    cy.get('p').should('have.length', 0);
    cy.get('#renderCount').contains('3');
  });

  it('should re-validate the form onBlur only with mode onBlur and reValidateMode onBlur', () => {
    cy.visit('http://localhost:3000/re-validate-mode/onBlur/onBlur');

    cy.get('input[name="firstName"]').focus();
    cy.get('input[name="firstName"]').blur();
    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="lastName"]').focus();
    cy.get('input[name="lastName"]').blur();
    cy.get('input[name="lastName"] + p').contains('lastName error');

    cy.get('input[name="firstName"]').type('luo123456');
    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="firstName"]').blur();
    cy.get('input[name="lastName"]').type('luo12');
    cy.get('input[name="lastName"] + p').contains('lastName error');
    cy.get('input[name="lastName"]').blur();

    cy.get('p').should('have.length', 0);
    cy.get('#renderCount').contains('5');
  });

  it('should re-validate the form onChange with mode onBlur and reValidateMode onChange', () => {
    cy.visit('http://localhost:3000/re-validate-mode/onBlur/onChange');

    cy.get('input[name="firstName"]').focus();
    cy.get('input[name="firstName"]').blur();
    cy.get('input[name="firstName"] + p').contains('firstName error');
    cy.get('input[name="lastName"]').focus();
    cy.get('input[name="lastName"]').blur();
    cy.get('input[name="lastName"] + p').contains('lastName error');

    cy.get('input[name="firstName"]').type('luo123456');
    cy.get('input[name="lastName"]').type('luo12');

    cy.get('p').should('have.length', 0);
    cy.get('#renderCount').contains('5');
  });
});


import focusErrorField from './focusErrorField';

test('should focus on the first error it encounter', () => {
  const focus = jest.fn();
  focusErrorField(
    {
      test: {
        ref: {
          focus,
        },
      },
    },
    {
      // @ts-ignore
      test: 'test',
    },
  );

  expect(focus).toBeCalled();
});

test('should focus on first option when options input error encounters', () => {
  const focus = jest.fn();
  focusErrorField(
    {
      test: {
        ref: {},
        options: [
          {
            ref: {
              focus,
            },
          },
        ],
      },
    },
    {
      // @ts-ignore
      test: 'test',
    },
  );

  expect(focus).toBeCalled();
});


'clone', 'extend', 'defaults', 'omit', 'values'

'isElement', 'isEqual', 'isFunction', 'isNumber', 'isObject', 'isString'

keep keep keep keep keep replace keep keep keep

'all', 'each', 'find', 'invoke', 'map', 'reduce', 'partition',

Quill  = require('../quill')
_      = Quill.require('lodash')
dom    = Quill.require('dom')
Delta  = Quill.require('delta')

class Keyboard
  @hotkeys:
    BOLD:       { key: 'B',          metaKey: true }
    INDENT:     { key: dom.KEYS.TAB }
    ITALIC:     { key: 'I',          metaKey: true }
    OUTDENT:    { key: dom.KEYS.TAB, shiftKey: true }
    UNDERLINE:  { key: 'U',          metaKey: true }

  constructor: (@quill, options) ->
    @hotkeys = {}
    this._initListeners()
    this._initHotkeys()
    @quill.onModuleLoad('toolbar', (toolbar) =>
      @toolbar = toolbar
    )

  addHotkey: (hotkeys, callback) ->
    hotkeys = [hotkeys] unless Array.isArray(hotkeys)
    _.each(hotkeys, (hotkey) =>
      hotkey = if _.isObject(hotkey) then _.clone(hotkey) else { key: hotkey }
      hotkey.callback = callback
      which = if _.isNumber(hotkey.key) then hotkey.key else hotkey.key.toUpperCase().charCodeAt(0)
      @hotkeys[which] ?= []
      @hotkeys[which].push(hotkey)
    )

  removeHotkey: (hotkey) ->
    hotkey = if Keyboard.hotkeys[hotkey] then Keyboard.hotkeys[hotkey] else hotkey
    hotkey = if _.isObject(hotkey) then hotkey else { key: hotkey }
    which = if _.isNumber(hotkey.key) then hotkey.key else hotkey.key.toUpperCase().charCodeAt(0)
    for handler in @hotkeys[which] || []
      return handler.callback if _.isEqual(hotkey, _.omit(handler, 'callback'))

  toggleFormat: (range, format) ->
    if range.isCollapsed()
      delta = @quill.getContents(Math.max(0, range.start-1), range.end)
    else
      delta = @quill.getContents(range)
    value = delta.ops.length == 0 or !_.all(delta.ops, (op) ->
      return op.attributes?[format]
    )
    if range.isCollapsed()
      @quill.prepareFormat(format, value, Quill.sources.USER)
    else
      @quill.formatText(range, format, value, Quill.sources.USER)
    @toolbar.setActive(format, value) if @toolbar?

  _initEnter: ->
    keys = [
      { key: dom.KEYS.ENTER }
      { key: dom.KEYS.ENTER, shiftKey: true }
    ]
    this.addHotkey(keys, (range, hotkey) =>
      return true unless range?
      [line, offset] = @quill.editor.doc.findLineAt(range.start)
      [leaf, offset] = line.findLeafAt(offset)
      delta = new Delta().retain(range.start).insert('\n', line.formats).delete(range.end - range.start)
      @quill.updateContents(delta, Quill.sources.USER)
      _.each(leaf.formats, (value, format) =>
        @quill.prepareFormat(format, value)
        @toolbar.setActive(format, value) if @toolbar?
        return
      )
      return false
    )

  _initDeletes: ->
    this.addHotkey([dom.KEYS.DELETE, dom.KEYS.BACKSPACE], (range, hotkey) =>
      if range? and @quill.getLength() > 0
        if range.start != range.end
          @quill.deleteText(range.start, range.end, Quill.sources.USER)
        else
          if hotkey.key == dom.KEYS.BACKSPACE
            [line, offset] = @quill.editor.doc.findLineAt(range.start)
            if offset == 0 and (line.formats.bullet or line.formats.list)
              format = if line.formats.bullet then 'bullet' else 'list'
              @quill.formatLine(range.start, range.start, format, false)
            else if range.start > 0
              @quill.deleteText(range.start - 1, range.start, Quill.sources.USER)
          else if range.start < @quill.getLength() - 1
            @quill.deleteText(range.start, range.start + 1, Quill.sources.USER)
      return false
    )

  _initHotkeys: ->
    this.addHotkey(Keyboard.hotkeys.INDENT, (range) =>
      this._onTab(range, false)
      return false
    )
    this.addHotkey(Keyboard.hotkeys.OUTDENT, (range) =>
      # TODO implement when we implement multiline tabs
      return false
    )
    _.each(['bold', 'italic', 'underline'], (format) =>
      this.addHotkey(Keyboard.hotkeys[format.toUpperCase()], (range) =>
        if (@quill.options.formats.indexOf(format) > -1)
          this.toggleFormat(range, format)
        return false
      )
    )
    this._initDeletes()
    this._initEnter()

  _initListeners: ->
    dom(@quill.root).on('keydown', (event) =>
      prevent = false
      _.each(@hotkeys[event.which], (hotkey) =>
        metaKey = if dom.isMac() then event.metaKey else event.metaKey or event.ctrlKey
        return if !!hotkey.metaKey != !!metaKey
        return if !!hotkey.shiftKey != !!event.shiftKey
        return if !!hotkey.altKey != !!event.altKey
        prevent = hotkey.callback(@quill.getSelection(), hotkey, event) == false or prevent
        return true
      )
      return !prevent
    )

  _onTab: (range, shift = false) ->
    # TODO implement multiline tab behavior
    # Behavior according to Google Docs + Word
    # When tab on one line, regardless if shift is down, delete selection and insert a tab
    # When tab on multiple lines, indent each line if possible, outdent if shift is down
    delta = new Delta().retain(range.start)
                       .insert("\t")
                       .delete(range.end - range.start)
                       .retain(@quill.getLength() - range.end)
    @quill.updateContents(delta, Quill.sources.USER)
    @quill.setSelection(range.start + 1, range.start + 1)

Quill.registerModule('keyboard', Keyboard)
module.exports = Keyboard


@import '../base/base'

// Make sure relative modules folder is included in both base and custom theme
@import '../snow/modules/*'

.ql-snow
  a
    color: #06c


async = require('async')
fs = require('fs')
glob = require('glob')
istanbul = require('istanbul')
collector = new istanbul.Collector()
reporter = new istanbul.Reporter(null, '.coverage/merged')

module.exports = (grunt) ->
  grunt.registerTask('istanbul:instrument', ->
    grunt.util.spawn(
      cmd: './node_modules/.bin/istanbul'
      args: ['instrument', 'lib/', '-o', 'src/']
      opts:
        stdio: 'inherit'
    , this.async())
  )

  grunt.registerTask('istanbul:report', ->
    done = this.async()
    glob('.coverage/**/*.json', (er, files) ->
      async.each(files, (file, callback) ->
        fs.readFile(file, 'utf8', (err, data) ->
          collector.add(JSON.parse(data))
          callback()
        )
      , (err) ->
        reporter.addAll(['html', 'text'])
        reporter.write(collector, false, done)
      )
    )
  )


quill

Byron Milligan <byronner@gmail.com>
Keegan Poppen <keegan.poppen@gmail.com>

Cross browser rich text editor


hotkey = if Keyboard.hotkeys[hotkey] then Keyboard.hotkeys[hotkey] else hotkey

hotkey = if _.isObject(hotkey) then hotkey else { key: hotkey }

hotkey = if _.isString(hotkey) then hotkey.toUpperCase() else hotkey

derequire = require('derequire/plugin')
fs = require('fs')
through = require('through')

versionify = (file) ->
  data = ''
  write = (buf) ->
    data += buf
  end = ->
    if file.indexOf('package.json') > -1
      version = JSON.parse(data).version
      this.queue(JSON.stringify({ version: version }))
    else
      this.queue(data)
    this.queue(null)
  return through(write, end)

module.exports = (grunt) ->
  grunt.config('browserify',
    quill:
      options:
        browserifyOptions:
          extensions: ['.js', '.coffee']
          standalone: 'Quill'
        transform: ['coffeeify', 'stylify', versionify]
        plugin: [derequire]
      files:
        'dist/quill.js': ['src/index.coffee']
  )

  grunt.config('clean',
    all: ['.build', 'dist']
    coverage: ['lib', 'src/**/*.js']
  )

  grunt.config('coffee',
    quill:
      options:
        bare: true
      cwd: 'src/'
      dest: 'lib/'
      expand: true
      src: ['**/*.coffee']
      ext: '.js'
  )

  grunt.config('concat',
    options:
      banner:
        '/*! Quill Editor v<%= pkg.version %>\n' +
        ' *  https://quilljs.com/\n' +
        ' *  Copyright (c) 2014, Jason Chen\n' +
        ' *  Copyright (c) 2013, salesforce.com\n' +
        ' */\n'
    quill:
      files:
        'dist/quill.js': ['dist/quill.js']
        'dist/quill.min.js': ['dist/quill.min.js']
        'dist/quill.base.css': ['dist/quill.base.css']
        'dist/quill.snow.css': ['dist/quill.snow.css']
  )

  grunt.config('lodash',
    options:
      modifier: 'modern'
      include: [
        'difference', 'intersection', 'last'
        'all', 'each', 'find', 'invoke', 'map', 'reduce'
        'bind', 'defer', 'partial'
        'clone', 'extend', 'defaults', 'omit', 'values'
        'isElement', 'isEqual', 'isFunction', 'isNumber', 'isObject', 'isString'
        'uniqueId'
      ]
      flags: ['development']
    target:
      dest: '.build/lodash.js'
  )

  grunt.config('stylus',
    options:
      compress: false
    themes:
      options:
        urlfunc: 'url'
      files: [{
        expand: true
        ext: '.css'
        flatten: true
        src: 'src/themes/*/*.styl'
        rename: (dest, src) ->
          return "dist/quill.#{src}"
      }]
  )

  grunt.config('uglify',
    quill:
      files: { 'dist/quill.min.js': ['dist/quill.js'] }
  )


_          = require('lodash')
dom        = require('../lib/dom')
Format     = require('./format')
LinkedList = require('../lib/linked-list')

class Leaf extends LinkedList.Node
  @DATA_KEY: 'leaf'

  @isLeafNode: (node) ->
    return dom(node).isTextNode() or !node.firstChild?

  constructor: (@node, formats) ->
    @formats = _.clone(formats)
    @text = dom(@node).text()
    @length = @text.length
    dom(@node).data(Leaf.DATA_KEY, this)

  deleteText: (offset, length) ->
    return unless length > 0
    @text = @text.slice(0, offset) + @text.slice(offset + length)
    @length = @text.length
    if dom.EMBED_TAGS[@node.tagName]?
      textNode = document.createTextNode(@text)
      dom(textNode).data(Leaf.DATA_KEY, this)
      @node = dom(@node).replace(textNode).get()
    else
      dom(@node).text(@text)

  insertText: (offset, text) ->
    @text = @text.slice(0, offset) + text + @text.slice(offset)
    if dom(@node).isTextNode()
      dom(@node).text(@text)
    else
      textNode = document.createTextNode(text)
      dom(textNode).data(Leaf.DATA_KEY, this)
      if @node.tagName == dom.DEFAULT_BREAK_TAG
        @node = dom(@node).replace(textNode).get()
      else
        @node.appendChild(textNode)
        @node = textNode
    @length = @text.length

module.exports = Leaf


_             = require('lodash')
pkg           = require('../package.json')
Delta         = require('rich-text/lib/delta')
EventEmitter2 = require('eventemitter2').EventEmitter2
dom           = require('./lib/dom')
Document      = require('./core/document')
Editor        = require('./core/editor')
Format        = require('./core/format')
Normalizer    = require('./core/normalizer')
Range         = require('./lib/range')

class Quill extends EventEmitter2
  @version: pkg.version
  @editors: []

  @modules: []
  @themes: []

  @DEFAULTS:
    formats: ['align', 'bold', 'italic', 'strike', 'underline', 'color', 'background', 'font', 'size', 'link', 'image', 'bullet', 'list']
    modules:
      'keyboard': true
      'paste-manager': true
      'undo-manager': true
    pollInterval: 100
    readOnly: false
    styles: {}
    theme: 'base'

  @events:
    FORMAT_INIT      : 'format-init'
    MODULE_INIT      : 'module-init'
    POST_EVENT       : 'post-event'
    PRE_EVENT        : 'pre-event'
    SELECTION_CHANGE : 'selection-change'
    TEXT_CHANGE      : 'text-change'

  @sources: Editor.sources

  @registerModule: (name, module) ->
    console.warn("Overwriting #{name} module") if Quill.modules[name]?
    Quill.modules[name] = module

  @registerTheme: (name, theme) ->
    console.warn("Overwriting #{name} theme") if Quill.themes[name]?
    Quill.themes[name] = theme

  @require: (name) ->
    switch name
      when 'lodash'     then return _
      when 'delta'      then return Delta
      when 'format'     then return Format
      when 'normalizer' then return Normalizer
      when 'dom'        then return dom
      when 'document'   then return Document
      when 'range'      then return Range
      else return null

  constructor: (@container, options = {}) ->
    @container = document.querySelector(@container) if _.isString(@container)
    throw new Error('Invalid Quill container') unless @container?
    moduleOptions = _.defaults(options.modules or {}, Quill.DEFAULTS.modules)
    html = @container.innerHTML
    @container.innerHTML = ''
    @options = _.defaults(options, Quill.DEFAULTS)
    @options.modules = moduleOptions
    @options.id = @id = "ql-editor-#{Quill.editors.length + 1}"
    @modules = {}
    @root = this.addContainer('ql-editor')
    @editor = new Editor(@root, this, @options)
    Quill.editors.push(this)
    this.setHTML(html, Quill.sources.SILENT)
    themeClass = Quill.themes[@options.theme]
    throw new Error("Cannot load #{@options.theme} theme. Are you sure you registered it?") unless themeClass?
    @theme = new themeClass(this, @options)
    _.each(@options.modules, (option, name) =>
      this.addModule(name, option)
    )

  destroy: ->
    html = this.getHTML()
    _.each(@modules, (module, name) ->
      module.destroy() if _.isFunction(module.destroy)
    )
    @editor.destroy()
    this.removeAllListeners()
    Quill.editors.splice(_.indexOf(Quill.editors, this), 1)
    @container.innerHTML = html

  addContainer: (className, before = false) ->
    refNode = if before then @root else null
    container = document.createElement('div')
    dom(container).addClass(className)
    @container.insertBefore(container, refNode)
    return container

  addFormat: (name, config) ->
    @editor.doc.addFormat(name, config)
    this.emit(Quill.events.FORMAT_INIT, name)

  addModule: (name, options) ->
    moduleClass = Quill.modules[name]
    throw new Error("Cannot load #{name} module. Are you sure you registered it?") unless moduleClass?
    options = {} if options == true   # Allow for addModule('module', true)
    options = _.defaults(options, @theme.constructor.OPTIONS[name] or {}, moduleClass.DEFAULTS or {})
    @modules[name] = new moduleClass(this, options)
    this.emit(Quill.events.MODULE_INIT, name, @modules[name])
    return @modules[name]

  deleteText: (start, end, source = Quill.sources.API) ->
    [start, end, formats, source] = this._buildParams(start, end, {}, source)
    return unless end > start
    delta = new Delta().retain(start).delete(end - start)
    @editor.applyDelta(delta, source)

  emit: (eventName, args...) ->
    super(Quill.events.PRE_EVENT, eventName, args...)
    super(eventName, args...)
    super(Quill.events.POST_EVENT, eventName, args...)

  focus: ->
    @editor.focus()

  formatLine: (start, end, name, value, source) ->
    [start, end, formats, source] = this._buildParams(start, end, name, value, source)
    [line, offset] = @editor.doc.findLineAt(end)
    end += (line.length - offset) if line?
    this.formatText(start, end, formats, source)

  formatText: (start, end, name, value, source) ->
    [start, end, formats, source] = this._buildParams(start, end, name, value, source)
    formats = _.reduce(formats, (formats, value, name) =>
      format = @editor.doc.formats[name]
      # TODO warn if no format
      formats[name] = null unless value and value != format.config.default     # false will be composed and kept in attributes
      return formats
    , formats)
    delta = new Delta().retain(start).retain(end - start, formats)
    @editor.applyDelta(delta, source)

  getBounds: (index) ->
    return @editor.getBounds(index)

  getContents: (start = 0, end = null) ->
    if _.isObject(start)
      end = start.end
      start = start.start
    return @editor.delta.slice(start, end)

  getHTML: ->
    @editor.doc.getHTML()

  getLength: ->
    return @editor.length

  getModule: (name) ->
    return @modules[name]

  getSelection: ->
    @editor.checkUpdate()   # Make sure we access getRange with editor in consistent state
    return @editor.selection.getRange()

  getText: (start = 0, end = null) ->
    return _.map(this.getContents(start, end).ops, (op) ->
      return if _.isString(op.insert) then op.insert else ''
    ).join('')

  insertEmbed: (index, type, url, source) ->
    [index, end, formats, source] = this._buildParams(index, 0, type, url, source)
    delta = new Delta().retain(index).insert(1, formats)
    @editor.applyDelta(delta, source)

  insertText: (index, text, name, value, source) ->
    [index, end, formats, source] = this._buildParams(index, 0, name, value, source)
    return unless text.length > 0
    delta = new Delta().retain(index).insert(text, formats)
    @editor.applyDelta(delta, source)

  onModuleLoad: (name, callback) ->
    if (@modules[name]) then return callback(@modules[name])
    this.on(Quill.events.MODULE_INIT, (moduleName, module) ->
      callback(module) if moduleName == name
    )

  prepareFormat: (name, value, source = Quill.sources.API) ->
    format = @editor.doc.formats[name]
    return unless format?     # TODO warn
    range = this.getSelection()
    return unless range?.isCollapsed()
    if format.isType(Format.types.LINE)
      this.formatLine(range, name, value, source)
    else
      format.prepare(value)

  setContents: (delta, source = Quill.sources.API) ->
    if Array.isArray(delta)
      delta = new Delta(delta.slice())
    else
      delta = new Delta(delta.ops.slice())
    # Retain trailing newline unless inserting one
    lastOp = _.last(delta.slice(delta.length() - 1).ops)
    delta.delete(this.getLength() - 1)
    if lastOp? and _.isString(lastOp.insert) and _.last(lastOp.insert) == '\n'
      delta.delete(1)
    this.updateContents(delta, source)

  setHTML: (html, source = Quill.sources.API) ->
    html = "<#{dom.DEFAULT_BLOCK_TAG}><#{dom.DEFAULT_BREAK_TAG}></#{dom.DEFAULT_BLOCK_TAG}>" unless html.trim()
    @editor.doc.setHTML(html)
    @editor.checkUpdate(source)

  setSelection: (start, end, source = Quill.sources.API) ->
    if _.isNumber(start) and _.isNumber(end)
      range = new Range(start, end)
    else
      range = start
      source = end or source
    @editor.selection.setRange(range, source)

  setText: (text, source = Quill.sources.API) ->
    delta = new Delta().insert(text)
    this.setContents(delta, source)

  updateContents: (delta, source = Quill.sources.API) ->
    delta = { ops: delta } if Array.isArray(delta)
    @editor.applyDelta(delta, source)

  # fn(Number start, Number end, String name, String value, String source)
  # fn(Number start, Number end, Object formats, String source)
  # fn(Object range, String name, String value, String source)
  # fn(Object range, Object formats, String source)
  _buildParams: (params...) ->
    if _.isObject(params[0])
      params.splice(0, 1, params[0].start, params[0].end)
    if _.isString(params[2])
      formats = {}
      formats[params[2]] = params[3]
      params.splice(2, 2, formats)
    params[3] ?= Quill.sources.API
    return params

Quill.registerTheme('base', require('./themes/base'))
Quill.registerTheme('snow', require('./themes/snow'))

module.exports = Quill


Quill  = require('../quill')
_      = Quill.require('lodash')
dom    = Quill.require('dom')
Delta  = Quill.require('delta')

class Authorship
  @DEFAULTS:
    authorId: null
    color: 'transparent'
    enabled: false

  constructor: (@quill, @options) ->
    this.attachButton(@options.button) if @options.button?
    this.enable() if @options.enabled
    @quill.addFormat('author', { class: 'author-' })
    return unless @options.authorId?
    @quill.on(@quill.constructor.events.PRE_EVENT, (eventName, delta, origin) =>
      if eventName == @quill.constructor.events.TEXT_CHANGE and origin == 'user'
        authorDelta = new Delta()
        authorFormat = { author: @options.authorId }
        _.each(delta.ops, (op) =>
          return if op.delete?
          if op.insert? or (op.retain? and op.attributes?)
            # Add authorship to insert/format
            op.attributes or= {}
            op.attributes.author = @options.authorId
            # Apply authorship to our own editor
            authorDelta.retain(op.retain or op.insert.length or 1, authorFormat)
          else
            authorDelta.retain(op.retain)
        )
        @quill.updateContents(authorDelta, Quill.sources.SILENT)
    )
    this.addAuthor(@options.authorId, @options.color)

  addAuthor: (id, color) ->
    styles = {}
    styles[".authorship .author-#{id}"] = { "background-color": "#{color}" }
    @quill.theme.addStyles(styles)

  attachButton: (button) ->
    $button = dom(button)
    $button.on('click', =>
      $button.toggleClass('ql-on')
      this.enable($dom.hasClass('ql-on'))
    )

  enable: (enabled = true) ->
    dom(@quill.root).toggleClass('authorship', enabled)

  disable: ->
    this.enable(false)

Quill.registerModule('authorship', Authorship)
module.exports = Authorship


which = if _.isNumber(hotkey.key) then hotkey.key else hotkey.key.toUpperCase().charCodeAt(0)

return handler.callback if _.isEqual(hotkey, _.omit(handler, 'callback'))

keep keep replace replace replace keep keep keep

which = if _.isNumber(hotkey.key) then hotkey.key else hotkey.key.charCodeAt(0)

[removed, kept] = _.partition(@hotkeys[which], (handler) ->

_.isEqual(hotkey, _.omit(handler, 'callback')) and

(!callback or callback == handler.callback)

dom = Quill.Lib.DOM

describe('Keyboard', ->
  beforeEach( ->
    @container = $('#editor-container').get(0)
  )

  describe('toggleFormat()', ->
    beforeEach( ->
      @container.innerHTML = '

<s>01</s>2345

'
      @quill = new Quill(@container.firstChild)
      @keyboard = @quill.getModule('keyboard')
    )

    it('set if all unset', ->
      @keyboard.toggleFormat(new Quill.Lib.Range(3, 6), 'strike')
      expect(@quill.root.firstChild).toEqualHTML('<s>01</s>2<s>3</s><s>45</s>')
    )

    it('unset if all set', ->
      @keyboard.toggleFormat(new Quill.Lib.Range(2, 4), 'bold')
      expect(@quill.root.firstChild).toEqualHTML('<s>01</s>2345')
    )

    it('set if partially set', ->
      @keyboard.toggleFormat(new Quill.Lib.Range(3, 5), 'bold')
      expect(@quill.root.firstChild).toEqualHTML('<s>01</s>2345')
    )
  )

  describe('hotkeys', ->
    beforeEach( ->
      @container.innerHTML = '

0123

'
      @quill = new Quill(@container.firstChild)
      @keyboard = @quill.getModule('keyboard')
    )

    it('trigger', (done) ->
      hotkey = { key: 'B', metaKey: true }
      @keyboard.addHotkey(hotkey, (range) ->
        expect(range.start).toEqual(1)
        expect(range.end).toEqual(2)
        done()
      )
      @quill.setSelection(1, 2)
      dom(@quill.root).trigger('keydown', hotkey)
    )

    it('format', ->
      @quill.setSelection(0, 4)
      dom(@quill.root).trigger('keydown', Quill.Module.Keyboard.hotkeys.BOLD)
      expect(@quill.root).toEqualHTML('
0123
', true)
    )

    it('tab', ->
      @quill.setSelection(1, 3)
      dom(@quill.root).trigger('keydown', Quill.Module.Keyboard.hotkeys.INDENT)
      expect(@quill.root).toEqualHTML('
0\t3
', true)
    )

    it('shift + tab', ->
      @quill.setSelection(0, 2)
      dom(@quill.root).trigger('keydown', Quill.Module.Keyboard.hotkeys.OUTDENT)
      expect(@quill.root).toEqualHTML('
0123
', true)
    )

    it('retain formatting', ->
      @quill.addModule('toolbar', { container: $('#toolbar-container').get(0) })
      size = '18px'

      @quill.setText('foo bar baz')
      @quill.formatText(0, @quill.getLength(), { 'bold': true, 'size': size })

      @quill.setSelection(@quill.getLength(), @quill.getLength())
      dom(@quill.root).trigger('keydown', { key: dom.KEYS.ENTER })

      expect(dom($('.ql-bold').get(0)).hasClass('ql-active')).toBe(true)
      expect(dom($('.ql-size').get(0)).value()).toBe(size)
    )

    it('removeHotkey by name', ->
      fn = @quill.getModule('keyboard').removeHotkey('BOLD')
      expect(typeof fn).toBe('function');
    )

    it('removeHotkey by number', ->
      fn = @quill.getModule('keyboard').removeHotkey(13)
      expect(typeof fn).toBe('function');
    )

    it('removeHotkey by object', ->
      fn = @quill.getModule('keyboard').removeHotkey({ key: 'B', metaKey: true })
      expect(typeof fn).toBe('function');
    )
  )
)


.ql-snow
  .ql-tooltip
    border: 1px solid #ccc
    box-shadow: 0px 0px 5px #ddd
    color: #222
    a
      color: #06c
    .input
      border: 1px solid #ccc
      margin: 0px
      padding: 5px


require('./modules/authorship')
require('./modules/image-tooltip')
require('./modules/keyboard')
require('./modules/link-tooltip')
require('./modules/multi-cursor')
require('./modules/paste-manager')
require('./modules/toolbar')
require('./modules/tooltip')
require('./modules/undo-manager')

module.exports = require('./quill')


keep keep keep replace replace replace keep keep

dom(@quill.root).trigger('keydown', { key: 'S' })

result = keyboard.removeHotkeys('S', fn)


describe('Normalizer', ->
  beforeEach( ->
    @container = $('#test-container').html('').get(0)
  )

  describe('handleBreaks()', ->
    tests =
      'Break in middle of line':
        initial:  '
One
Two
'
        expected: '
One

Two
'
      'Break preceding line':
        initial:  '

One
'
        expected: '

One
'
        ieOmit: true
      'Break after line':
        initial:  '
One

'
        expected: '
One

'

    _.each(tests, (test, name) ->
      return if test.ieOmit and Quill.Lib.DOM.isIE(10)
      it(name, ->
        @container.innerHTML = test.initial
        lineNode = Quill.Normalizer.handleBreaks(@container.firstChild)
        expect(@container).toEqualHTML(test.expected)
        expect(lineNode).toEqual(@container.firstChild)
      )
    )
  )

  describe('normalizeLine()', ->
    tests =
      'inline with text':
        initial:  'WhatTest'
        expected: '
WhatTest
'
      'whitelist line node':
        initial:  '
Test
'
        expected: '
Test
'
      'pull text node':
        initial:  '

AB
C

'
        expected: '
AB

C
'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        @normalizer = new Quill.Normalizer()
        lineNode = @normalizer.normalizeLine(@container.firstChild)
        expect(@container).toEqualHTML(test.expected)
        expect(lineNode).toEqual(@container.firstChild)
      )
    )
  )

  describe('normalizeNode()', ->
    it('whitelist style and tag', ->
      @normalizer = new Quill.Normalizer()
      @normalizer.whitelist.tags.B = true
      @normalizer.whitelist.styles.color = true
      @container.innerHTML = 'Test'
      @normalizer.normalizeNode(@container.firstChild)
      expect(@container).toEqualHTML('Test')
    )

    it('convert size attribute', ->
      @normalizer = new Quill.Normalizer()
      @normalizer.whitelist.styles.fontSize = true
      @container.innerHTML = 'Test'
      @normalizer.normalizeNode(@container.firstChild)
      expect(@container).toEqualHTML('Test')
    )

    it('text node', ->
      @normalizer = new Quill.Normalizer()
      @container.innerHTML = 'Test'
      @normalizer.normalizeNode(@container.firstChild)
      expect(@container).toEqualHTML('Test')
    )
  )

  describe('optimizeLine()', ->
    tests =
      'remove unneeded break':
        initial:  'Test
'
        expected: 'Test'
      'preserve span with attributes':
        initial:  'Test'
        expected: 'Test'
      'unwrap zero length nodes':
        initial:  ''
        expected: ''
      'unwrap eventually':
        initial:  '<s></s>'
        expected: ''
      'unwrap break on empty line':
        initial:  '
'
        expected: '
'
      'preserve break on empty line':
        initial:  '
'
        expected: '
'
      'unwrap eventually with break':
        initial:  '<s>
</s>'
        expected: '
'
      'merge similar nodes':
        initial:  'AB'
        expected: 'AB'
      'merge similar span nodes':
        initial:  'AB'
        expected: 'AB'
      'merge multiple criteria similar nodes':
        initial:  'AB'
        expected: 'AB'
      'merge recursive':
        initial:  '<s>A</s><s>B</s>'
        expected: '<s>AB</s>'
      'preserve close but not same nodes':
        initial:  'AB'
        expected: 'AB'
      'preserve similar images':
        initial:  ''
        expected: ''
      'wrap orphaned text node':
        initial:  '<s>0</s><s>1</s>'
        expected: '<s>01</s>'
      'merge text nodes':
        initial:  'A  B.'
        expected:  'A  B.'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = "
#{test.initial}
"
        Quill.Normalizer.optimizeLine(@container.firstChild)
        expect(@container.firstChild).toEqualHTML(test.expected)
      )
    )
  )

  describe('pullBlocks()', ->
    tests =
      'No children':
        initial:  '

'
        expected: '

'
      'After block':
        initial:  '
One
Two

'
        expected: '
One

Two
'
      'Before inner block':
        initial:  '

One
Two

'
        expected: '
One

Two
'
      'After inner block':
        initial:  '

One
Two

'
        expected: '
One

Two
'
      'Middle block':
        initial:  '
One
Two
Three
'
        expected: '
One

Two

Three
'
      'Inner block':
        initial:  '

Test

Another

'
        expected: '
Test

Another

'
      'Inner deep block':
        initial:  '

Test

'
        expected: '
Test
'
      'Inner deep recursive':
        initial:  '

Test
Test

'
        expected: '
Test

Test
'
      'Continuous inlines':
        initial:  '
A
B
Inner

'
        expected: '
A
B

Inner
'
      'Bullets':
        initial:  '

One

Two

'
        expected: '

One

Two

'
      'Inner list':
        initial:  '

One

Two

'
        expected: '

One

Two

'
      'Middle list':
        initial:  '
Test

One

Two

Test
'
        expected: '
Test

One

Two

Test
'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        firstChild = @container.firstChild
        Quill.Normalizer.pullBlocks(firstChild)
        expect(@container).toEqualHTML(test.expected)
      )
    )
  )

  describe('stripComments()', ->
    it('single line', ->
      html = '
Test

Test
'
      expect(Quill.Normalizer.stripComments(html)).toEqual('
Test

Test
')
    )

    it('multiple lines', ->
      html = "
Test
\n\n
Test
"
      expect(Quill.Normalizer.stripComments(html)).toEqual("
Test
\n\n
Test
")
    )
  )

  describe('stripWhitespace()', ->
    tests =
      'newline between tags':
        initial:
         '
Test

'
        expected: '
Test

'
      'newlines in text':
        initial: "
A\nB\r\nC\rD
"
        expected: "
A B C D
"
      'consecutive newlines in text':
        initial: "
A\n\nB\r\n\rC
"
        expected: "
A B C
"
      'preceding and trailing spaces':
        initial:  '

  '
        expected: '

'
      'inner spaces':
        initial:  '

'
        expected: '

'

    _.each(tests, (test, name) ->
      it(name, ->
        strippedHTML = Quill.Normalizer.stripWhitespace(test.initial)
        expect(strippedHTML).toEqual(test.expected)
      )
    )
  )

  describe('whitelistStyles()', ->
    beforeEach( ->
      @normalizer = new Quill.Normalizer()
      @normalizer.whitelist.styles.color = true
    )

    tests =
      'no styles':
        initial:  '

'
        expected: '

'
      'no removal':
        initial:  '

'
        expected: '

'
      'removal':
        initial:  '

'
        expected: '

'
      'complete removal':
        initial:  '

'
        expected: '

'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        @normalizer.whitelistStyles(@container.firstChild)
        expect(@container).toEqualHTML(test.expected)
      )
    )
  )

  describe('whitelistTags()', ->
    beforeEach( ->
      @normalizer = new Quill.Normalizer()
      @normalizer.whitelist.tags.B = true
    )

    tests =
      'not element':
        initial:  'Test'
        expected: 'Test'
      'no switch needed':
        initial:  'Bold'
        expected: 'Bold'
      'alias':
        initial:  'Bold'
        expected: 'Bold'
      'unwrap inline':
        initial:  'A'
        expected:  'A'
      'switch block':
        initial:  '
Test
'
        expected: '
Test
'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        @normalizer.whitelistTags(@container.firstChild)
        expect(@container).toEqualHTML(test.expected)
      )
    )
  )

  describe('wrapInline()', ->
    tests =
      'Wrap newline':
        initial:  ['
']
        expected: ['

']
      'Wrap text':
        initial:  ['One']
        expected: ['
One
']
      'Wrap multiple':
        initial: [
          'One'
          '<s>Two</s>'
        ]
        expected: [
          '
One<s>Two</s>
'
        ]
      'Wrap break and text':
        initial:  ['
One']
        expected: ['

One
']

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial.join('')
        Quill.Normalizer.wrapInline(@container.firstChild)
        expect(@container).toEqualHTML(test.expected)
      )
    )
  )

  describe('unwrapText()', ->
    tests =
      'inner text':
        initial:  'InnerTest'
        expected: 'InnerTest'
      'multiple inner':
        initial:  'TestTestTest'
        expected: 'TestTestTest'
      'dont unwrap':
        initial:  'Test'
        expected: 'Test'

    _.each(tests, (test, name) ->
      it(name, ->
        @container.innerHTML = test.initial
        Quill.Normalizer.unwrapText(@container)
        expect(@container).toEqualHTML(test.expected)
      )
    )
  )
)


_ = require('lodash')
browserify = require('browserify')
coffeeify = require('coffeeify')
fs = require('fs')
harp = require('harp')
proxy = require('http-proxy')
stylify = require('stylify')
stylus = require('stylus')
watchify = require('watchify')

FAVICON = new Buffer('iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACf0lEQVR42r2XS2gTURSG04K2VReilorEECVKiJk8EYuurIgPEFddKW4El1J3FbRUEOzKKuhKdy4Uql0H0UVxoYIKkoWCrxaKz1qKTayNYv0O3IEhzNzecSYz8HNnJpPz3XPm3HPuxGIRHNlstqdQKBwul8tDpVLpDprg/BV63hJgPB7vAngU0HX0BtCSh76FCs7n89sBjqJZDfS343whFHCxWNyEsZvojwb8jok9YKw77tUDwzF6CtW8wPw2zwQvMN51+f3jf4MzmcwaDIxpPBb4S8Zd6JHHM9UgIa/q4OgqObFDQq+Z4G3fcLJ77TLwBSZ4gueSACaXmeRZv2FfidGHGo9+MO7N5XJbDOBLRKjoN+Eu69Y0Xu80haO3mGzzAz+I/np4Pk3YMwLnesoALv8ZMIYnk8lOTTLNCNyyrK2mcPQerTKeAA8PenhRQ70+4T95Vbv9rvcZF0MNPD/EmNDBmeB3qYDSF7geAb7fb+KdcTMM/CTjBtXVnMAv6BY6ThfcHLjUYvS1i1ejKjJPm+7PomP8rT2UJiPvygVekXbL+X3Ne37BcwfCaDRXmuCT6XR6vWwqDJdaRVZQkAl8cPZxIrKHe9cM4Z9RX5DwF5qMnlcygY+TpN1Bwz/sMPpEst6rEjqTUBpRKAmIscfK6C/G07LuNfCG5AsrY10ocGr6ahsoPZtxzsPjRcYbUglD3VwSxn12b0efXMBfVWdMtGRbLXs4j7o/Ltttrle07CNCdT57xyNldkSWUyqV6ojiI6YN2D17wyi5EIvyIPTnFHyOUG+LFA60X9a50pGo4ZZ8QCjvL0Ud9m675kvzCK2V+qh4F9Ez+Xqhkm2MRXz8AzAAXszjgRshAAAAAElFTkSuQmCC', 'base64')

browserifyOps =
  cache: {}
  extensions: ['.js', '.coffee']
  fullPaths: true
  packageCache: {}
  standalone: 'Quill'

bundle = (watcher) ->
  return watcher.bundle().on('error', (err) ->
    console.error(err.name, err.message)
  )

serve = (connect, req, res, next) ->
  watchers = connect.watchers
  if req.url.indexOf('/karma') == 0 or req.url.indexOf('/base') == 0
    return connect.karmaProxy.web(req, res)
  url = if req.url.indexOf('/develop') == 0 then req.url.slice('/develop'.length) else req.url
  switch url
    when '/quill.js'
      res.setHeader('Content-Type', 'application/javascript')
      bundle(watchers['src']).pipe(res)
    when '/test/quill.js'
      res.setHeader('Content-Type', 'application/javascript')
      bundle(watchers['test']).pipe(res)
    when '/quill.snow.css', '/quill.base.css'
      theme = url.slice(7, 11)
      res.setHeader('Content-Type', 'text/css')
      fs.readFile("./src/themes/#{theme}/#{theme}.styl", (err, data) ->
        s = stylus(data.toString())
        s.include("./src/themes/#{theme}")
        s.define('url', stylus.url())
        s.render((err, css) ->
          console.error(err.name, err.message) if err?
          res.end(css)
        )
      )
    when '/favicon.ico'
      res.setHeader('Content-Type', 'image/png')
      res.end(FAVICON)
    else
      next()

module.exports = (grunt) ->
  grunt.config('connect',
    options:
      onCreateServer: (server, connect, options) ->
        connect.watchers = _.reduce(['src', 'test'], (watchers, type) ->
          file = if type == 'src' then './src/index.coffee' else './test/quill.coffee'
          b = browserify(file, browserifyOps)
          watchers[type] = watchify(b)
          watchers[type].transform(coffeeify)
          watchers[type].transform(stylify)
          watchers[type].on('update', _.bind(bundle, watchers[type], watchers[type]))
          bundle(watchers[type])
          return watchers
        , {})
        connect.karmaProxy = proxy.createProxyServer({ target: "http://localhost:#{grunt.config('karmaPort')}" })
      middleware: (connect, options, middlewares) ->
        middlewares.push(serve.bind(this, connect))
        middlewares.push(harp.mount(__dirname + '/../..'))
        return middlewares
      debug: true
    server:
      options:
        port: grunt.config('port')
        useAvailablePort: true
  )

  grunt.event.once('connect.server.listening', (host, port) ->
    grunt.config('port', port)
  )


dom    = require('./dom')
Picker = require('./picker')

class ColorPicker extends Picker
  constructor: ->
    super
    dom(@container).addClass('ql-color-picker')

  buildItem: (picker, option, index) ->
    item = super(picker, option, index)
    item.style.backgroundColor = option.value
    return item

module.exports = ColorPicker


keep keep replace replace replace keep keep

keyboard.addHotkey({ key: 'S', metaKey: true }, fn)

dom(@quill.root).trigger('keydown', { key: 'S', metaKey: true })

result = keyboard.removeHotkeys({ key: 'S', metaKey: true })


var _  = require('lodash');
var browsers = require('./browsers');
var sauce = require('./sauce');

var customLaunchers = _.reduce(browsers, function(memo, browser, name) {
  memo[name] = {
    base: 'SauceLabs',
    platform: browser[0],
    browserName: browser[1],
    version: browser[2]
  };
  return memo;
}, {});

module.exports = function(config) {
  config.set({
    basePath: '../',
    frameworks: ['jasmine'],
    coverageReporter: {
      type: 'json',
      dir: '.coverage/karma/'
    },
    reporters: ['progress'],
    preprocessors: {
      '**/*.coffee': ['coffee'],
      '**/*.html': ['html2js']
    },
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: false,
    singleRun: true,
    sauceLabs: {
      testName: 'quill-unit',
      options: {
        'public': 'public',
        'record-screenshots': false
      },
      build: sauce.build,
      username: sauce.username,
      accessKey: sauce.accessKey,
      tunnelIdentifier: sauce.tunnel
    },
    customLaunchers: customLaunchers
  })

  if (process.env.TRAVIS) {
    config.transports = ['xhr-polling'];
  }
}


_          = require('lodash')
Delta      = require('rich-text/lib/delta')
dom        = require('../lib/dom')
Format     = require('./format')
Leaf       = require('./leaf')
Line       = require('./line')
LinkedList = require('../lib/linked-list')
Normalizer = require('./normalizer')

class Line extends LinkedList.Node
  @DATA_KEY  : 'line'

  constructor: (@doc, @node) ->
    @formats = {}
    this.rebuild()
    super(@node)

  buildLeaves: (node, formats) ->
    _.each(dom(node).childNodes(), (node) =>
      node = @doc.normalizer.normalizeNode(node)
      nodeFormats = _.clone(formats)
      # TODO: optimize
      _.each(@doc.formats, (format, name) ->
        # format.value() also checks match() but existing bug in tandem-core requires check anyways
        nodeFormats[name] = format.value(node) if !format.isType(Format.types.LINE) and format.match(node)
      )
      if Leaf.isLeafNode(node)
        @leaves.append(new Leaf(node, nodeFormats))
      else
        this.buildLeaves(node, nodeFormats)
    )

  deleteText: (offset, length) ->
    return unless length > 0
    [leaf, offset] = this.findLeafAt(offset)
    while leaf? and length > 0
      deleteLength = Math.min(length, leaf.length - offset)
      leaf.deleteText(offset, deleteLength)
      length -= deleteLength
      leaf = leaf.next
      offset = 0
    this.rebuild()

  findLeaf: (leafNode) ->
    return if leafNode? then dom(leafNode).data(Leaf.DATA_KEY) else undefined

  findLeafAt: (offset, inclusive = false) ->
    # TODO exact same code as findLineAt
    return [@leaves.last, @leaves.last.length] if offset >= @length - 1
    leaf = @leaves.first
    while leaf?
      if offset < leaf.length or (offset == leaf.length and inclusive)
        return [leaf, offset]
      offset -= leaf.length
      leaf = leaf.next
    return [@leaves.last, offset - @leaves.last.length]   # Should never occur unless length calculation is off

  format: (name, value) ->
    if _.isObject(name)
      formats = name
    else
      formats = {}
      formats[name] = value
    _.each(formats, (value, name) =>
      format = @doc.formats[name]
      return unless format?
      # TODO reassigning @node might be dangerous...
      if format.isType(Format.types.LINE)
        if format.config.exclude and @formats[format.config.exclude]
          excludeFormat = @doc.formats[format.config.exclude]
          if excludeFormat?
            @node = excludeFormat.remove(@node)
            delete @formats[format.config.exclude]
        @node = format.add(@node, value)
      if value
        @formats[name] = value
      else
        delete @formats[name]
    )
    this.resetContent()

  formatText: (offset, length, name, value) ->
    [leaf, leafOffset] = this.findLeafAt(offset)
    format = @doc.formats[name]
    return unless format? and format.config.type != Format.types.LINE
    while leaf? and length > 0
      nextLeaf = leaf.next
      # Make sure we need to change leaf format
      if (value and leaf.formats[name] != value) or (!value and leaf.formats[name]?)
        targetNode = leaf.node
        # Identify node to modify
        if leaf.formats[name]?
          dom(targetNode).splitBefore(@node)
          while !format.match(targetNode)
            targetNode = targetNode.parentNode
          dom(targetNode).split(leaf.length)
        # Isolate target node
        if leafOffset > 0
          [leftNode, targetNode] = dom(targetNode).split(leafOffset)
        if leaf.length > leafOffset + length  # leaf.length does not update with split()
          [targetNode, rightNode] = dom(targetNode).split(length)
        format.add(targetNode, value)
      length -= leaf.length - leafOffset
      leafOffset = 0
      leaf = nextLeaf
    this.rebuild()

  _insert: (offset, node, formats) ->
    [leaf, leafOffset] = this.findLeafAt(offset)
    node = _.reduce(formats, (node, value, name) =>
      format = @doc.formats[name]
      if format? and !format.isType(Format.types.LINE)
        node = format.add(node, value)
      return node
    , node)
    [prevNode, nextNode] = dom(leaf.node).split(leafOffset)
    nextNode = dom(nextNode).splitBefore(@node).get() if nextNode
    @node.insertBefore(node, nextNode)
    this.rebuild()

  insertEmbed: (offset, attributes) ->
    [leaf, leafOffset] = this.findLeafAt(offset)
    [prevNode, nextNode] = dom(leaf.node).split(leafOffset)
    formatName = _.find(Object.keys(attributes), (name) =>
      return @doc.formats[name].isType(Format.types.EMBED)
    )
    node = @doc.formats[formatName].add({}, attributes[formatName])  # TODO fix {} hack
    attributes = _.clone(attributes)
    delete attributes[formatName]
    this._insert(offset, node, attributes)

  insertText: (offset, text, formats = {}) ->
    return unless text.length > 0
    [leaf, leafOffset] = this.findLeafAt(offset)
    if _.isEqual(leaf.formats, formats)
      leaf.insertText(leafOffset, text)
      this.resetContent()
    else
      this._insert(offset, document.createTextNode(text), formats)

  optimize: ->
    Normalizer.optimizeLine(@node)
    this.rebuild()

  rebuild: (force = false) ->
    if !force and @outerHTML? and @outerHTML == @node.outerHTML
      if _.all(@leaves.toArray(), (leaf) =>
        return dom(leaf.node).isAncestor(@node)
      )
        return false
    @node = @doc.normalizer.normalizeNode(@node)
    if dom(@node).length() == 0 and !@node.querySelector(dom.DEFAULT_BREAK_TAG)
      @node.appendChild(document.createElement(dom.DEFAULT_BREAK_TAG))
    @leaves = new LinkedList()
    @formats = _.reduce(@doc.formats, (formats, format, name) =>
      if format.isType(Format.types.LINE)
        if format.match(@node)
          formats[name] = format.value(@node)
        else
          delete formats[name]
      return formats
    , @formats)
    this.buildLeaves(@node, {})
    this.resetContent()
    return true

  resetContent: ->
    dom(@node).data(Line.DATA_KEY, this)
    @outerHTML = @node.outerHTML
    @length = 1
    @delta = new Delta()
    _.each(@leaves.toArray(), (leaf) =>
      @length += leaf.length
      # TODO use constant for embed type
      if dom.EMBED_TAGS[leaf.node.tagName]?
        @delta.insert(1, leaf.formats)
      else
        @delta.insert(leaf.text, leaf.formats)
    )
    @delta.insert('\n', @formats)

module.exports = Line


_  = require('lodash')
browsers = require('../browsers')

remoteReporters = ['dots']
remoteReporters.push('saucelabs') if process.env.TRAVIS_BRANCH == 'master'

remoteKarma = _.reduce(browsers, (memo, config, browser) ->
  memo[browser] =
    browsers: [browser]
    browserDisconnectTimeout: 10000
    browserDisconnectTolerance: 4
    browserNoActivityTimeout: 60000
    reporters: remoteReporters
  return memo
, {})

module.exports = (grunt) ->
  grunt.config('karma', _.extend(remoteKarma,
    options:
      configFile: 'config/karma.js'
      files: [
        'node_modules/jquery/dist/jquery.js'
        'node_modules/lodash/index.js'

        grunt.config('baseUrl') + 'quill.base.css'
        grunt.config('baseUrl') + 'test/quill.js'

        'test/fixtures/unit.html'
        'test/helpers/inject.coffee'
        'test/helpers/matchers.coffee'

        { pattern: 'test/fixtures/*.css', included: false }

        # We dont do **/*.coffee to control order of tests
        'test/unit/lib/*.coffee'
        'test/unit/core/*.coffee'
        'test/unit/modules/*.coffee'
        'test/unit/themes/*.coffee'
      ]
      port: grunt.config('karmaPort')
    coverage:
      browserNoActivityTimeout: 30000
      browsers: ['Chrome']
      reporters: ['coverage']
    local:
      browsers: ['Chrome', 'Firefox', 'Safari']
    server:
      autoWatch: true
      browsers: []
      urlRoot: '/karma/'    # TODO move this back into karma.js as soon as urlRoot works for socket.io again
      singleRun: false
    test:
      browsers: ['Chrome']
  ))


fn = @quill.getModule('keyboard').removeHotkey({ key: 'B', metaKey: true })

it('removeHotKeys only the specified callback', ->

keyboard.addHotkey({ key: 'S', metaKey: true }, anotherFn)

result = keyboard.removeHotkeys({ key: 'S', metaKey: true }, fn)


.ql-multi-cursor
  position: absolute
  left: 0
  top: 0
  z-index: 1000
  .cursor
    margin-left: -1px
    position: absolute
  .cursor-flag
    bottom: 100%
    position: absolute
    white-space: nowrap
  .cursor-name
    display: inline-block
    color: white
    padding: 2px 8px
  .cursor-caret
    height: 100%
    position: absolute
    width: 2px
  .cursor.hidden .cursor-flag
    display: none
  .cursor.top .cursor-flag
    bottom: auto
    top: 100%
  .cursor.right .cursor-flag
    right: -2px


Cross browser rich text editor


doctype html
html
  head
    title Quill Editor Demo
    link(rel='stylesheet', type='text/css', href='styles/style.css')
    meta(charset='utf-8')

  body
    #content-container
      #editor-wrapper
        #formatting-container
          select.ql-font(title='Font')
            option(value='sans-serif', selected) Sans Serif
            option(value='Georgia, serif') Serif
            option(value='Monaco, \'Courier New\', monospace') Monospace
          select.ql-size(title='Size')
            option(value='10px') Small
            option(value='13px', selected) Normal
            option(value='18px') Large
            option(value='32px') Huge
          select.ql-color(title='Text Color')
            option(value='rgb(255, 255, 255)') White
            option(value='rgb(0, 0, 0)', selected) Black
            option(value='rgb(255, 0, 0)') Red
            option(value='rgb(0, 0, 255)') Blue
            option(value='rgb(0, 255, 0)') Lime
            option(value='rgb(0, 128, 128)') Teal
            option(value='rgb(255, 0, 255)') Magenta
            option(value='rgb(255, 255, 0)') Yellow
          select.ql-background(title='Background Color')
            option(value='rgb(255, 255, 255)', selected) White
            option(value='rgb(0, 0, 0)') Black
            option(value='rgb(255, 0, 0)') Red
            option(value='rgb(0, 0, 255)') Blue
            option(value='rgb(0, 255, 0)') Lime
            option(value='rgb(0, 128, 128)') Teal
            option(value='rgb(255, 0, 255)') Magenta
            option(value='rgb(255, 255, 0)') Yellow
          select.ql-align(title='Text Alignment')
            option(value='left', selected) Left
            option(value='center') Center
            option(value='right') Right
            option(value='justify') Justify
          button.ql-format-button.ql-bold(title='Bold') Bold
          button.ql-format-button.ql-italic(title='Italic') Italic
          button.ql-format-button.ql-underline(title='Underline') Under
          button.ql-format-button.ql-strike(title='Strikethrough') Strike
          button.ql-format-button.ql-link(title='Link') Link
          button.ql-format-button.ql-image(title='Image') Image
          button.ql-format-button.ql-bullet(title='Bullet') Bullet
          button.ql-format-button.ql-list(title='List') List
        #editor-container

    script(type='text/javascript', src='../quill.js')
    script(type='text/javascript').
      var editor = new Quill('#editor-container', {
        modules: {
          'toolbar': { container: '#formatting-container' },
          'link-tooltip': true,
          'image-tooltip': true
        }
      });
      editor.on('selection-change', function(range) {
        console.log('selection-change', range)
      });
      editor.on('text-change', function(delta, source) {
        console.log('text-change', delta, source)
      });


active: 1,
bio: 1,
reason: 1,
statusText: 1,
avatarETag: 1,
};
const fullFields = {

type DefaultUserFields = {
	[k: string]: number;
};

export const getDefaultUserFields = (): DefaultUserFields => ({
	'name': 1,
	'username': 1,
	'nickname': 1,
	'emails': 1,
	'status': 1,
	'statusDefault': 1,
	'statusText': 1,
	'statusConnection': 1,
	'bio': 1,
	'avatarOrigin': 1,
	'utcOffset': 1,
	'language': 1,
	'settings': 1,
	'enableAutoAway': 1,
	'idleTimeLimit': 1,
	'roles': 1,
	'active': 1,
	'defaultRoom': 1,
	'customFields': 1,
	'requirePasswordChange': 1,
	'requirePasswordChangeReason': 1,
	'services.github': 1,
	'services.gitlab': 1,
	'services.tokenpass': 1,
	'services.blockstack': 1,
	'services.password.bcrypt': 1,
	'services.totp.enabled': 1,
	'services.email2fa.enabled': 1,
	'statusLivechat': 1,
	'banners': 1,
	'oauth.authorizedClients': 1,
	'_updatedAt': 1,
	'avatarETag': 1,
});


# Rocket.chat bot helpers

This package provides some extra shortcut Meteor methods intended to help Hubot scripts.

## Approach

This was an experiment in how to extend Hubot and Rocket.chat integration.

Keeping controller logic out of Hubot scripts made sense to me, but its not necessarily the best approach.

For bots to use these methods they obviously need this package added to Rocket.chat as well as the [rocketchat adapter](https://github.com/RocketChat/hubot-rocketchat).

## Usage

In Hubot scripts, call bot helper methods using Meteor.call with the first argument as the helper type, then the method name as second argument.

At present, there's only one type of helper (`botRequest`) for simple getters, more utility methods may be added in future.

e.g. Below uses `onlineNames` method, which returns array of names of online users (not including bots).

Listens for *"who is online"* and replies with something like *"Robert, Desmond and Billie are currently online"*.

```
# Use Bot Helpers class to check who's online
robot.hear /who is online/i, (res) ->
  promise = robot.adapter.callMethod('botRequest', 'onlineNames')
  promise.then (result) ->
    if result.length > 0
      names = result.join(', ').replace(/,(?=[^,]*$)/, ' and ') # convert last comma to 'and'
      res.send "#{ names } #{ if result.length == 1 then 'is' else 'are' } currently online"
    else
      res.send "Nobody is currently online... \*cricket sound\*"
    return
  , (error) ->
    res.send "Uh, sorry I don't know, something's not working"
    return
```

Method names that include fields like `allUsernames` return an 1D array of just that property.

First two methods return 2D array with properties defined in a defaults object (currently id, name, username, status, emails).

- `allUsers`
- `onlineUsers`
- `allUsernames`
- `onlineUsernames`
- `allNames`
- `onlineNames`
- `allIDs`
- `onlineIDs`

## Tests

None yet, PR adding tests would be much apreciated.


import { request } from '../data/api-data.js';

const methods = ['get', 'post', 'put', 'del'];

let lastUrl;
let lastResponse;

methods.forEach((method) => {
	const original = request[method];
	request[method] = function (url, fn) {
		lastUrl = url;
		return original(url, fn).expect((res) => {
			lastResponse = res;
		});
	};
});

afterEach(async function () {
	if (this.currentTest.state === 'failed') {
		console.log({
			lastUrl,
			lastResponse: lastResponse.text,
		});
	}
});


import { EmojiCustom } from '../../../models/server/raw';

export async function findEmojisCustom({ query = {}, pagination: { offset, count, sort } }) {
	const cursor = EmojiCustom.find(query, {
		sort: sort || { name: 1 },
		skip: offset,
		limit: count,
	});

	const total = await cursor.count();

	const emojis = await cursor.toArray();

	return {
		emojis,
		count: emojis.length,
		offset,
		total,
	};
}


requirePasswordChange: 1,
requirePasswordChangeReason: 1,
roles: 1,
extension: 1,
};
let publicCustomFields = {};
let customFields = {};

keep keep keep replace keep keep keep keep


import { Button } from '@rocket.chat/fuselage';
import React, { forwardRef, memo } from 'react';

import { TranslationKey, useTranslation } from '../../../../../../client/contexts/TranslationContext';

type TextButtonProps = {
	text: TranslationKey;
	action: () => void;
};

const TextButton = forwardRef<HTMLElement, TextButtonProps>(function TextButton({ text, action }, ref) {
	const t = useTranslation();

	return (
		<Button
			nude
			small
			display='flex'
			justifyContent='center'
			alignItems='center'
			onClick={(e): void => {
				e.stopPropagation();
				e.preventDefault();
				action();
			}}
			ref={ref}
		>
			{t(text)}
		</Button>
	);
});
export default memo(TextButton);


import './methods/addWebdavAccount';
import './methods/removeWebdavAccount';
import './methods/getWebdavFileList';
import './methods/getWebdavFilePreview';
import './methods/getFileFromWebdav';
import './methods/uploadFileToWebdav';
import './startup/settings';


'banners': 1,
'oauth.authorizedClients': 1,
'_updatedAt': 1,
'avatarETag': 1,
});


import React, { FC } from 'react';

import AttachmentProvider from '../components/Message/Attachments/providers/AttachmentProvider';
import AuthorizationProvider from './AuthorizationProvider';
import AvatarUrlProvider from './AvatarUrlProvider';
import { CallProvider } from './CallProvider';
import ConnectionStatusProvider from './ConnectionStatusProvider';
import CustomSoundProvider from './CustomSoundProvider';
import LayoutProvider from './LayoutProvider';
import ModalProvider from './ModalProvider';
import OmnichannelProvider from './OmnichannelProvider';
import RouterProvider from './RouterProvider';
import ServerProvider from './ServerProvider';
import SessionProvider from './SessionProvider';
import SettingsProvider from './SettingsProvider';
import SidebarProvider from './SidebarProvider';
import ToastMessagesProvider from './ToastMessagesProvider';
import TranslationProvider from './TranslationProvider';
import UserProvider from './UserProvider';

const MeteorProvider: FC = ({ children }) => (
	<ConnectionStatusProvider>
		<ServerProvider>
			<RouterProvider>
				<TranslationProvider>
					<SessionProvider>
						<SidebarProvider>
							<ToastMessagesProvider>
								<SettingsProvider>
									<LayoutProvider>
										<AvatarUrlProvider>
											<CustomSoundProvider>
												<UserProvider>
													<AuthorizationProvider>
														<CallProvider>
															<OmnichannelProvider>
																<ModalProvider>
																	<AttachmentProvider>{children}</AttachmentProvider>
																</ModalProvider>
															</OmnichannelProvider>
														</CallProvider>
													</AuthorizationProvider>
												</UserProvider>
											</CustomSoundProvider>
										</AvatarUrlProvider>
									</LayoutProvider>
								</SettingsProvider>
							</ToastMessagesProvider>
						</SidebarProvider>
					</SessionProvider>
				</TranslationProvider>
			</RouterProvider>
		</ServerProvider>
	</ConnectionStatusProvider>
);

export default MeteorProvider;


<template name="emojiPicker">

							{{> icon block="rc-input__icon-svg" icon="magnifier" }}

						<input name="name" type="text" class="rc-input__element js-emojipicker-search" placeholder="{{_ "Search"}}" autofocus autocomplete="off">

				{{#each category in emojiCategories}}

				{{/each}}

			{{#if searching}}
				{{{searchResults}}}
			{{else}}
				{{#each category in emojiCategories}}

{{_ category.i18n}}

						{{{emojiList category.key}}}

				{{/each}}
			{{/if}}

			{{#if hasPermission 'manage-emoji'}}{{_"Add_custom_emoji"}}{{/if}}
			{{{_"Emoji_provided_by_JoyPixels"}}}

</template>


import { CachedCollectionManager } from '../../../app/ui-cached-collection/client';
import { call } from '../../../client/lib/utils/call';

export const fetchFeatures = (): Promise<string[]> =>
	new Promise((resolve, reject) => {
		CachedCollectionManager.onLogin(() => {
			call('license:getModules').then(resolve, reject);
		});
	});


if (!user?.extension) {
return {
enabled: false,
ready: false,
};
}

import { Logger } from '../../../logger';
import { settings } from '../../../settings/server';
import { Users } from '../../../models/server';
import { hasPermission } from '../../../authorization';

const logger = new Logger('getFullUserData');

const defaultFields = {
	name: 1,
	username: 1,
	nickname: 1,
	status: 1,
	utcOffset: 1,
	type: 1,
	active: 1,
	bio: 1,
	reason: 1,
	statusText: 1,
	avatarETag: 1,
};

const fullFields = {
	emails: 1,
	phone: 1,
	statusConnection: 1,
	bio: 1,
	createdAt: 1,
	lastLogin: 1,
	services: 1,
	requirePasswordChange: 1,
	requirePasswordChangeReason: 1,
	roles: 1,
	extension: 1,
};

let publicCustomFields = {};
let customFields = {};

settings.watch('Accounts_CustomFields', (value) => {
	publicCustomFields = {};
	customFields = {};

	if (!value.trim()) {
		return;
	}

	try {
		const customFieldsOnServer = JSON.parse(value.trim());
		Object.keys(customFieldsOnServer).forEach((key) => {
			const element = customFieldsOnServer[key];
			if (element.public) {
				publicCustomFields[`customFields.${key}`] = 1;
			}
			customFields[`customFields.${key}`] = 1;
		});
	} catch (e) {
		logger.warn(`The JSON specified for "Accounts_CustomFields" is invalid. The following error was thrown: ${e}`);
	}
});

const getCustomFields = (canViewAllInfo) => (canViewAllInfo ? customFields : publicCustomFields);

const getFields = (canViewAllInfo) => ({
	...defaultFields,
	...(canViewAllInfo && fullFields),
	...getCustomFields(canViewAllInfo),
});

const removePasswordInfo = (user) => {
	if (user && user.services) {
		delete user.services.password;
		delete user.services.email;
		delete user.services.resume;
		delete user.services.emailCode;
		delete user.services.cloud;
		delete user.services.email2fa;
		delete user.services.totp;
	}

	return user;
};

export function getFullUserDataByIdOrUsername({ userId, filterId, filterUsername }) {
	const caller = Users.findOneById(userId, { fields: { username: 1 } });
	const targetUser = filterId || filterUsername;
	const myself = (filterId && targetUser === userId) || (filterUsername && targetUser === caller.username);
	const canViewAllInfo = !!myself || hasPermission(userId, 'view-full-other-user-info');

	const fields = getFields(canViewAllInfo);

	const options = {
		fields,
	};
	const user = Users.findOneByIdOrUsername(targetUser, options);
	if (!user) {
		return null;
	}

	user.canViewAllInfo = canViewAllInfo;

	return myself ? user : removePasswordInfo(user);
}


export { default } from './KeyboardShortcutsWithClose';


অভ্যন্তরীণ সার্ভার ত্রুটি

__count__ খালি ঘরগুলি স্বয়ংক্রিয়ভাবে সরানো হবে।

__count__ খালি ঘরগুলি স্বয়ংক্রিয়ভাবে সরানো হবে: <br/> __rooms__।

এই ঘরের এনক্রিপশনটি __username__ দ্বারা বন্ধ করা হয়েছে

@ব্যবহারকারীর নাম

@ ব্যবহারকারী নাম <message>

0 - শুধুমাত্র ত্রুটি

1 - ত্রুটি এবং তথ্য

2 - ত্রুটি, তথ্য এবং ডিবাগ

12-ঘন্টার ঘড়ি

24-ঘন্টার ঘড়ি

একজন নতুন মালিককে __count__ কক্ষে স্বয়ংক্রিয়ভাবে নিয়োগ দেওয়া হবে।

একজন নতুন মালিককে __roomName__ ঘরে স্বয়ংক্রিয়ভাবে নিয়োগ দেওয়া হবে।

একজন নতুন মালিককে সেই __count__ কক্ষে স্বয়ংক্রিয়ভাবে নিয়োগ দেওয়া হবে:
__rooms__।

কোনও এজেন্ট online এ না থাকলেও ইনকামিং ওমনিচ্যানেল অনুরোধগুলি গ্রহণ করুন

এজেন্ট যখন নিষ্ক্রিয় থাকে তখন নতুন ওমনিচ্যানেল অনুরোধগুলি গ্রহণ করুন

কোনও অনলাইন এজেন্টের সাথে গ্রহণ করুন

প্রবেশ অনুমোদিত নয়

অ্যাক্সেস টোকেন URL টি

সকল ব্যবহারকারীর কাছে গণ ইমেল প্রেরণের অনুমতি।

বিভিন্ন ভূমিকা জন্য অনুমতি পরিবর্তন করুন।

সেটিং-ভিত্তিক অনুমতিগুলি পরিবর্তন করুন

সেটিং-ভিত্তিক অনুমতিগুলি সংশোধন করার অনুমতি

অ্যাকাউন্ট এসআইডি

ব্যবহারকারীদের নিজস্ব অ্যাকাউন্ট মুছতে অনুমতি দিন

ইমেল পরিবর্তনের অনুমতি দিন

পাসওয়ার্ড পরিবর্তনের অনুমতি দিন

নাম পরিবর্তনের অনুমতি দিন

ব্যবহারকারীর প্রোফাইল পরিবর্তনের অনুমতি দিন

ডিফল্ট ব্যবহারকারী পছন্দসমূহ

অ্যাকাউন্ট নিষ্ক্রিয় করা হয়েছে

গুগল সিক্রেট

Tokenpass কলব্যাক ইউআরএল

ব্যবহারকারী যুক্ত করুন

ব্যবহারকারী যুক্ত করুন

সিস্টেম দ্বারা স্বয়ংক্রিয়ভাবে বন্ধ

কাস্টমাইজ করুন


const [result, setResult] = useSafely(useState<UseVoipClientResult>({}));

if (!user || !user?._id || !voipEnabled) {

if (!user || !user?._id || !user?.extension || !voipEnabled) {

import { Random } from 'meteor/random';
import React, { useMemo, FC, useRef, useCallback, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { OutgoingByeRequest } from 'sip.js/lib/core';

import { CustomSounds } from '../../../app/custom-sounds/client';
import { getUserPreference } from '../../../app/utils/client';
import { IVoipRoom } from '../../../definition/IRoom';
import { IUser } from '../../../definition/IUser';
import { ICallerInfo } from '../../../definition/voip/ICallerInfo';
import { WrapUpCallModal } from '../../components/voip/modal/WrapUpCallModal';
import { CallContext, CallContextValue } from '../../contexts/CallContext';
import { useSetModal } from '../../contexts/ModalContext';
import { useRoute } from '../../contexts/RouterContext';
import { useEndpoint, useStream } from '../../contexts/ServerContext';
import { useSetting } from '../../contexts/SettingsContext';
import { useUser } from '../../contexts/UserContext';
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
import { isUseVoipClientResultError, isUseVoipClientResultLoading, useVoipClient } from './hooks/useVoipClient';

const startRingback = (user: IUser): void => {
	const audioVolume = getUserPreference(user, 'notificationsSoundVolume');
	CustomSounds.play('telephone', {
		volume: Number((audioVolume / 100).toPrecision(2)),
		loop: true,
	});
};

const stopRingback = (): void => {
	CustomSounds.pause('telephone');
	CustomSounds.remove('telephone');
};

export const CallProvider: FC = ({ children }) => {
	const voipEnabled = useSetting('VoIP_Enabled');
	const subscribeToNotifyUser = useStream('notify-user');

	const result = useVoipClient();

	const user = useUser();
	const homeRoute = useRoute('home');

	const remoteAudioMediaRef = useRef<HTMLAudioElement>(null); // TODO: Create a dedicated file for the AUDIO and make the controls accessible

	const AudioTagPortal: FC = ({ children }) => useMemo(() => createPortal(children, document.body), [children]);

	const [queueName, setQueueName] = useState('');
	const [queueCounter, setQueueCounter] = useState(0);

	const setModal = useSetModal();

	const openWrapUpModal = useCallback((): void => {
		setModal(<WrapUpCallModal />);
	}, [setModal]);

	useEffect(() => {
		if (!voipEnabled || !user) {
			return;
		}

		if (isUseVoipClientResultError(result) || isUseVoipClientResultLoading(result)) {
			return;
		}

		const queueAggregator = result.voipClient.getAggregator();
		if (!queueAggregator) {
			return;
		}

		const handleQueueJoined = async (joiningDetails: {
			queuename: string;
			callerid: { id: string };
			queuedcalls: string;
		}): Promise<void> => {
			queueAggregator.queueJoined(joiningDetails);
			setQueueName(joiningDetails.queuename);
			setQueueCounter(queueAggregator.getCallWaitingCount());
		};

		return subscribeToNotifyUser(`${user._id}/callerjoined`, handleQueueJoined);
	}, [result, subscribeToNotifyUser, user, voipEnabled]);

	useEffect(() => {
		if (!voipEnabled || !user) {
			return;
		}

		if (isUseVoipClientResultError(result) || isUseVoipClientResultLoading(result)) {
			return;
		}

		const queueAggregator = result.voipClient.getAggregator();
		if (!queueAggregator) {
			return;
		}

		const handleAgentConnected = (queue: { queuename: string; queuedcalls: string; waittimeinqueue: string }): void => {
			queueAggregator.callPickedup(queue);
			setQueueCounter(queueAggregator.getCallWaitingCount());
		};

		return subscribeToNotifyUser(`${user._id}/agentconnected`, handleAgentConnected);
	}, [result, subscribeToNotifyUser, user, voipEnabled]);

	useEffect(() => {
		if (!voipEnabled || !user) {
			return;
		}

		if (isUseVoipClientResultError(result) || isUseVoipClientResultLoading(result)) {
			return;
		}

		const queueAggregator = result.voipClient.getAggregator();
		if (!queueAggregator) {
			return;
		}

		const handleMemberAdded = (queue: { queuename: string; queuedcalls: string }): void => {
			queueAggregator.memberAdded(queue);
			setQueueName(queue.queuename);
			setQueueCounter(queueAggregator.getCallWaitingCount());
		};

		return subscribeToNotifyUser(`${user._id}/queuememberadded`, handleMemberAdded);
	}, [result, subscribeToNotifyUser, user, voipEnabled]);

	useEffect(() => {
		if (!voipEnabled || !user) {
			return;
		}

		if (isUseVoipClientResultError(result) || isUseVoipClientResultLoading(result)) {
			return;
		}

		const queueAggregator = result.voipClient.getAggregator();
		if (!queueAggregator) {
			return;
		}

		const handleMemberRemoved = (queue: { queuename: string; queuedcalls: string }): void => {
			queueAggregator.memberRemoved(queue);
			setQueueCounter(queueAggregator.getCallWaitingCount());
		};

		return subscribeToNotifyUser(`${user._id}/queuememberremoved`, handleMemberRemoved);
	}, [result, subscribeToNotifyUser, user, voipEnabled]);

	useEffect(() => {
		if (!voipEnabled || !user) {
			return;
		}

		if (isUseVoipClientResultError(result) || isUseVoipClientResultLoading(result)) {
			return;
		}

		const queueAggregator = result.voipClient.getAggregator();
		if (!queueAggregator) {
			return;
		}

		const handleCallAbandon = (queue: { queuename: string; queuedcallafterabandon: string }): void => {
			queueAggregator.queueAbandoned(queue);
			setQueueCounter(queueAggregator.getCallWaitingCount());
		};

		return subscribeToNotifyUser(`${user._id}/callabandoned`, handleCallAbandon);
	}, [result, subscribeToNotifyUser, user, voipEnabled]);

	useEffect(() => {
		if (!voipEnabled || !user) {
			return;
		}

		const handleCallHangup = (_event: { roomId: string }): void => {
			openWrapUpModal();
		};

		return subscribeToNotifyUser(`${user._id}/call.callerhangup`, handleCallHangup);
	}, [openWrapUpModal, result, subscribeToNotifyUser, user, voipEnabled]);

	useEffect(() => {
		if (isUseVoipClientResultError(result)) {
			return;
		}

		if (isUseVoipClientResultLoading(result)) {
			return;
		}

		/*
		 * This code may need a revisit when we handle callinqueue differently.
		 * Check clickup taks for more details
		 * https://app.clickup.com/t/22hy1k4
		 * When customer called a queue (Either using skype or using internal number), call would get established
		 * customer would hear agent's voice but agent would not hear anything from customer.
		 * This issue was observed on unstable. It was found to be incosistent to reproduce.
		 * On some developer env, it would happen randomly. On Safari it did not happen if
		 * user refreshes before taking every call.
		 *
		 * The reason behind this was as soon as agent accepts a call, queueCounter would change.
		 * This change will trigger re-rendering of media and creation of audio element.
		 * This audio element gets used by voipClient to render the remote audio.
		 * Because the re-render happend, it would hold a stale reference.
		 *
		 * If the dom is inspected, audio element just before body is usually created by this class.
		 * this audio element.srcObject contains null value. In working case, it should display
		 * valid stream object.
		 *
		 * Reason for inconsistecies :
		 * This element is utilised in VoIPUser::setupRemoteMedia
		 * This function is called when webRTC receives a remote track event. i.e when the webrtc's peer connection
		 * starts receiving media. This event call back depends on several factors. How does asterisk setup streams.
		 * How does it creates a bridge which patches up the agent and customer (Media is flowing thru asterisk).
		 * When it works in de-environment, it was observed that the audio element in dom and the audio element hold
		 * by VoIPUser is different. Nonetheless, this stale audio element holds valid media stream, which is being played.
		 * Hence sometimes the audio is heard.
		 *
		 * Ideally call component once gets stable, should not get rerendered. Queue, Room creation are the parameters
		 * which should be independent and should not control the call component.
		 *
		 * Solution :
		 * Either make the audio elemenent rendered independent of rest of the DOM.
		 * or implement useEffect. This useEffect will reset the rendering elements with the latest audio tag.
		 *
		 * Note : If this code gets refactor, revisit the line below to check if this call is needed.
		 *
		 */
		remoteAudioMediaRef.current && result.voipClient.switchMediaRenderer({ remoteMediaElement: remoteAudioMediaRef.current });
	});

	const visitorEndpoint = useEndpoint('POST', 'livechat/visitor');
	const voipEndpoint = useEndpoint('GET', 'voip/room');
	const voipCloseRoomEndpoint = useEndpoint('POST', 'voip/room.close');

	const [roomInfo, setRoomInfo] = useState<{ v: { token?: string }; rid: string }>();

	const openRoom = (rid: IVoipRoom['_id']): void => {
		roomCoordinator.openRouteLink('v', { rid });
	};

	const contextValue: CallContextValue = useMemo(() => {
		if (!voipEnabled) {
			return {
				enabled: false,
				ready: false,
			};
		}

		if (isUseVoipClientResultError(result)) {
			return {
				enabled: true,
				ready: false,
				error: result.error,
			};
		}

		if (isUseVoipClientResultLoading(result)) {
			return {
				enabled: true,
				ready: false,
			};
		}

		const { registrationInfo, voipClient } = result;

		voipClient.on('incomingcall', () => user && startRingback(user));
		voipClient.on('callestablished', () => stopRingback());
		voipClient.on('callterminated', () => stopRingback());

		return {
			enabled: true,
			ready: true,
			openedRoomInfo: roomInfo,
			registrationInfo,
			voipClient,
			queueCounter,
			queueName,
			actions: {
				mute: (): Promise<void> => voipClient.muteCall(true), // voipClient.mute(),
				unmute: (): Promise<void> => voipClient.muteCall(false), // voipClient.unmute()
				pause: (): Promise<void> => voipClient.holdCall(true), // voipClient.pause()
				resume: (): Promise<void> => voipClient.holdCall(false), // voipClient.resume()
				end: (): Promise<OutgoingByeRequest | void> => voipClient.endCall(),
				pickUp: async (): Promise<void | null> =>
					remoteAudioMediaRef.current && voipClient.acceptCall({ remoteMediaElement: remoteAudioMediaRef.current }),
				reject: (): Promise<void> => voipClient.rejectCall(),
			},
			openRoom,
			createRoom: async (caller: ICallerInfo): Promise<IVoipRoom['_id']> => {
				if (user) {
					const { visitor } = await visitorEndpoint({
						visitor: {
							token: Random.id(),
							phone: caller.callerId,
							name: caller.callerName || caller.callerId,
						},
					});
					const voipRoom = visitor && (await voipEndpoint({ token: visitor.token, agentId: user._id }));
					openRoom(voipRoom.room._id);
					voipRoom.room && setRoomInfo({ v: { token: voipRoom.room.v.token }, rid: voipRoom.room._id });
					const queueAggregator = result.voipClient.getAggregator();
					if (queueAggregator) {
						queueAggregator.callStarted();
					}
					return voipRoom.room._id;
				}
				return '';
			},
			closeRoom: async ({ comment, tags }: { comment: string; tags: string[] }): Promise<void> => {
				roomInfo && (await voipCloseRoomEndpoint({ rid: roomInfo.rid, token: roomInfo.v.token || '', comment: comment || '', tags }));
				homeRoute.push({});
				const queueAggregator = result.voipClient.getAggregator();
				if (queueAggregator) {
					queueAggregator.callEnded();
				}
			},
			openWrapUpModal,
		};
	}, [
		voipEnabled,
		result,
		roomInfo,
		queueCounter,
		queueName,
		openWrapUpModal,
		user,
		visitorEndpoint,
		voipEndpoint,
		voipCloseRoomEndpoint,
		homeRoute,
	]);

	return (
		<CallContext.Provider value={contextValue}>
			{children}
			{contextValue.enabled && (
				<AudioTagPortal>
					<audio ref={remoteAudioMediaRef} />
				</AudioTagPortal>
			)}
		</CallContext.Provider>
	);
};


import { createElement, useEffect, Dispatch, SetStateAction, ReactNode } from 'react';

import { imperativeModal } from '../../lib/imperativeModal';

export const useImperativeModal = (setModal: Dispatch<SetStateAction<ReactNode>>): void => {
	useEffect(() => {
		const unsub = imperativeModal.on('update', (descriptor) => {
			if (descriptor === null) {
				return setModal(null);
			}
			if ('component' in descriptor) {
				setModal(
					createElement(descriptor.component, {
						key: Math.random(),
						...descriptor.props,
					}),
				);
			}
		});
		return unsub;
	}, [setModal]);
};


import { clientLogger } from '../lib/logger';
import { FederationRoomEvents, Subscriptions } from '../../../models/server';
import { normalizers } from '../normalizers';
import { deleteRoom } from '../../../lib/server/functions';
import { getFederationDomain } from '../lib/getFederationDomain';
import { dispatchEvents } from '../handler';
import { isFullyQualified } from '../functions/helpers';

async function afterCreateDirectRoom(room, extras) {
	clientLogger.debug({ msg: 'afterCreateDirectRoom', room, extras });

	// If the room is federated, ignore
	if (room.federation) {
		return room;
	}

	// Check if there is a federated user on this direct room
	const hasFederatedUser = room.usernames.some(isFullyQualified);

	// If there are not federated users on this room, ignore it
	if (!hasFederatedUser) {
		return room;
	}

	try {
		//
		// Genesis
		//

		// Normalize room
		const normalizedRoom = normalizers.normalizeRoom(room);

		// Ensure a genesis event for this room
		const genesisEvent = await FederationRoomEvents.createGenesisEvent(getFederationDomain(), normalizedRoom);

		const events = await Promise.all(
			extras.members.map((member) => {
				const normalizedMember = normalizers.normalizeUser(member);

				const sourceSubscription = Subscriptions.findOne({
					'rid': normalizedRoom._id,
					'u._id': normalizedMember._id,
				});
				const normalizedSourceSubscription = normalizers.normalizeSubscription(sourceSubscription);

				// Build the user event
				return FederationRoomEvents.createAddUserEvent(
					getFederationDomain(),
					normalizedRoom._id,
					normalizedMember,
					normalizedSourceSubscription,
				);
			}),
		);

		// Dispatch the events
		await dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...events]);
	} catch (err) {
		await deleteRoom(room._id);

		clientLogger.error({ msg: 'afterCreateDirectRoom => Could not create federated room:', err });
	}

	return room;
}

export const definition = {
	hook: 'afterCreateDirectRoom',
	callback: (room, extras) => Promise.await(afterCreateDirectRoom(room, extras)),
	id: 'federation-after-create-direct-room',
};


<template name="loginForm">
	<form id="login-card" class="content-background-color color-primary-font-color" method='/' novalidate>
		{{#if state 'wait-activation'}}

{{{_ "Registration_Succeeded"}}}

{{_ "Wait_activation_warning"}}

{{_ "Please_wait_activation"}}

		{{else if state 'wait-email-activation'}}

{{{_ "Registration_Succeeded"}}}

{{_ "We_have_sent_registration_email"}}

		{{else}}
			{{> loginServices }}
			{{#if needsValidateEmail}}

					{{_ "You_need_confirm_email"}}

			{{/if}}

				{{#if state 'login'}}
					{{#if showFormLogin}}

									<input name="emailOrUsername" id="emailOrUsername" type="text" class="rc-input__element"
										autocapitalize="off" autocorrect="off"
										placeholder="{{emailOrUsernamePlaceholder}}" autofocus>

									<input name="pass" id="pass" type="password" class="rc-input__element"
										autocapitalize="off" autocorrect="off"
										placeholder="{{passwordPlaceholder}}" autofocus>

					{{/if}}
				{{/if}}
				{{#if state 'register'}}

								<input name="name" id="name" type="text" class="rc-input__element"
									autocapitalize="off" autocorrect="off"
									placeholder="{{namePlaceholder}}" autofocus>

								<input name="email" id="email" type="text" class="rc-input__element"
									autocapitalize="off" autocorrect="off"
									placeholder="{{_ "Email"}}" autofocus>

								<input name="pass" id="pass" type="password" class="rc-input__element"
									autocapitalize="off" autocorrect="off"
									placeholder="{{passwordPlaceholder}}" autofocus>

					{{#if requirePasswordConfirmation}}

									<input name="confirm-pass" id="confirm-pass" type="password" class="rc-input__element"
										autocapitalize="off" autocorrect="off"
										placeholder="{{confirmPasswordPlaceholder}}" autofocus>

					{{/if}}
					{{#if manuallyApproveNewUsers}}

									<input name="reason" id="reason" type="text" class="rc-input__element"
										autocapitalize="off" autocorrect="off"
										placeholder="{{_ 'Reason_To_Join'}}" autofocus>

					{{/if}}
				{{/if}}
				{{#if state 'forgot-password' 'email-verification'}}

								<input name="email" id="email" type="text" class="rc-input__element"
									autocapitalize="off" autocorrect="off"
									placeholder="{{_ "Email"}}" autofocus value="{{#if state 'email-verification'}}{{typedEmail}}{{/if}}">

				{{/if}}

				{{#if state 'login'}}
					{{#if showFormLogin}}
						<button class='rc-button rc-button--primary login'>{{btnLoginSave}}</button>

						{{#if passwordResetAllowed}}
							<button type="button" class="rc-button rc-button--nude forgot-password">{{_ 'Forgot_password'}}</button>
						{{/if}}
					{{/if}}

					{{#if registrationAllowed}}
						<button type="button" class="rc-button rc-button--nude register">{{_ 'Register_new_account'}}</button>
					{{else}}
						{{#if linkReplacementText}}

								{{{linkReplacementText}}}

						{{/if}}
					{{/if}}
				{{else}}

						<button class='rc-button rc-button--primary login'>{{btnLoginSave}}</button>

				{{/if}}

		{{/if}}
		{{#unless state 'login'}}

				<button type="button" class="rc-button rc-button--nude back-to-login">{{_ 'Back_to_login'}}</button>

		{{/unless}}
	</form>

		{{{loginTerms}}}

			Powered by Open Source Chat Platform Rocket.Chat.

</template>


'description' => 'The requested image was not found.',

keep keep keep keep add keep keep keep keep

'description' => 'The requested image could not be parsed.',

<?php

namespace Appwrite\Extend;

use Utopia\Config\Config;

class Exception extends \Exception
{
    /**
     * Error Codes
     *
     * Naming the error types based on the following convention
     * <ENTITY>_<ERROR_TYPE>
     *
     * Appwrite has the follwing entities:
     * - Users
     * - Projects
     * - Sessions
     * - Teams
     * - Memberships
     * - Files
     * - Functions
     * - Deployments
     * - Executions
     */

    /** Users */
    const USER_COUNT_EXCEEDED               = 'user_count_exceeded';
    const USER_ALREADY_EXISTS               = 'user_already_exists';
    const USER_BLOCKED                      = 'user_blocked';
    const USER_CREATION_FAILED              = 'user_creation_failed';
    const USER_INVALID_TOKEN                = 'user_invalid_token';
    const USER_NOT_FOUND                    = 'user_not_found';
    const USER_INVALID_CREDENTIALS          = 'user_invalid_credentials';
    const USER_EMAIL_ALREADY_EXISTS         = 'user_email_already_exists';
    const USER_PASSWORD_MISMATCH            = 'user_password_mismatch';
    const USER_AUTH_METHOD_UNSUPPORTED      = 'user_auth_method_unsupported';
    const USER_PASSWORD_RESET_REQUIRED      = 'user_password_reset_required';
    const USER_EMAIL_NOT_WHITELISTED        = 'user_email_not_whitelisted';
    const USER_IP_NOT_WHITELISTED           = 'user_ip_not_whitelisted';
    const USER_SESSION_ALREADY_EXISTS       = 'user_session_already_exists';
    const USER_SESSION_NOT_FOUND            = 'user_session_not_found';
    const USER_ANONYMOUS_CONSOLE_PROHIBITED = 'user_anonymous_console_prohibited';

    /** OAuth **/
    const OAUTH_PROVIDER_DISABLED          = 'oauth_provider_disabled';
    const OAUTH_PROVIDER_UNSUPPORTED       = 'oauth_provider_unsupported';
    const OAUTH_INVALID_LOGIN_STATE_PARAMS = 'oauth_invalid_login_state_params';
    const OAUTH_INVALID_SUCCESS_URL        = 'oauth_invalid_success_url';
    const OAUTH_INVALID_FAILURE_URL        = 'oauth_invalid_failure_url';
    const OAUTH_ACCESS_TOKEN_FAILED        = 'oauth_access_token_failed';
    const OAUTH_MISSING_USER_ID            = 'oauth_missing_user_id';

    /** Avatars */
    const AVATAR_SET_NOT_FOUND             = 'avatar_set_not_found';
    const AVATAR_NOT_FOUND                 = 'avatar_not_found';
    const IMAGIC_EXTENSION_MISSING         = 'imagic_extension_missing';
    const AVATAR_IMAGE_NOT_FOUND           = 'avatar_image_not_found';

    /** Files */
    const FILE_NOT_FOUND                   = 'file_not_found';
    const FILE_NOT_READABLE                = 'file_not_readable';

    /** Projects */
    const PROJECT_NOT_FOUND       = 'project_not_found';
    const PROJECT_UNKNOWN         = 'project_unknown';

    /** API */
    const UNKNOWN                 = 'unknown';
    const UNKNOWN_ORIGIN          = 'unknown_origin';
    const SERVICE_DISABLED        = 'service_disabled';
    const UNAUTHORIZED_SCOPE      = 'unauthorized_scope';
    const STORAGE_ERROR           = 'storage_error';
    const RATE_LIMIT_EXCEEDED     = 'rate_limit_exceeded';
    const SMTP_DISABLED           = 'smtp_disabled';

    private $errorCode = '';

    public function __construct(string $message, int $code = 0, string $errorCode = Exception::UNKNOWN, \Throwable $previous = null)
    {
        $this->errorCode = $errorCode;

        parent::__construct($message, $code, $previous);
    }

    /**
     * @return string
     */
    public function getErrorCode(): string
    {
        return $this->errorCode;
    }
}


import Appwrite

func main() {
    let client = Client()
      .setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
      .setProject("5df5acd0d48c2") // Your project ID

    let locale = Locale(client)
    locale.getCountries() { result in
        switch result {
        case .failure(let error):
            print(error.message)
        case .success(let countryList):
            print(String(describing: countryList)
        }
    }
}


import io.appwrite.Client
import io.appwrite.services.Users

suspend fun main() {
    val client = Client(context)
      .setEndpoint("https://[HOSTNAME_OR_IP]/v1") // Your API Endpoint
      .setProject("5df5acd0d48c2") // Your project ID
      .setKey("919c2d18fb5d4...a2ae413da83346ad2") // Your secret API key

    val users = Users(client)
    val response = users.getSessions(
        userId = "[USER_ID]"
    )
    val json = response.body?.string()
}


<?php

use Appwrite\Client;
use Appwrite\Services\Functions;

$client = new Client();

$client
    ->setEndpoint('https://[HOSTNAME_OR_IP]/v1') // Your API Endpoint
    ->setProject('5df5acd0d48c2') // Your project ID
    ->setKey('919c2d18fb5d4...a2ae413da83346ad2') // Your secret API key
;

$functions = new Functions($client);

$result = $functions->delete('[FUNCTION_ID]');